From 1ef46bd36050f9ea75a63cae97f9ba17837122c5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 10 Sep 2026 19:06:45 +0200 Subject: [PATCH 1/8] feat(vscode): post inline comments to GitHub pull requests --- .changeset/pr-comments-from-changes.md | 5 + .../plans/diff-comment-simplification-plan.md | 133 +++++ .../diff-viewer-github-comment-creation.md | 205 ++++++++ .../src/agent-manager/pr/review-actions.ts | 11 + .../src/agent-manager/pr/review-context.ts | 1 + .../src/diff/DiffViewerProvider.ts | 51 +- .../src/shared/pr-comment-actions.ts | 2 + packages/kilo-vscode/src/shared/pr-patch.ts | 5 +- .../tests/fixtures/diff-comment-form.tsx | 114 ++++ .../tests/fixtures/inline-comment-form.tsx | 98 ++++ .../tests/fixtures/send-all-button.tsx | 51 ++ .../tests/unit/agent-manager-arch.test.ts | 1 + .../tests/unit/annotation-lifecycle.test.ts | 30 ++ .../tests/unit/comments-github.test.ts | 115 ++++ .../tests/unit/composer-action-order.test.ts | 60 +++ .../tests/unit/diff-comment-form.test.ts | 4 + .../tests/unit/diff-preview-request.test.ts | 6 + .../tests/unit/diff-viewer-provider.test.ts | 78 +++ .../tests/unit/inline-comment-form.test.ts | 8 + .../kilo-vscode/tests/unit/pr-diff.test.ts | 54 ++ .../tests/unit/pr-review-actions.test.ts | 38 +- .../tests/unit/review-annotations.test.ts | 202 +++++++ .../tests/unit/send-all-button.test.ts | 4 + .../agent-manager/AgentManagerApp.tsx | 19 + .../webview-ui/agent-manager/DiffPanel.tsx | 64 ++- .../agent-manager/DiffPanelCache.tsx | 9 + .../agent-manager/agent-manager.css | 64 ++- .../webview-ui/agent-manager/i18n/ar.ts | 2 + .../webview-ui/agent-manager/i18n/br.ts | 2 + .../webview-ui/agent-manager/i18n/bs.ts | 2 + .../webview-ui/agent-manager/i18n/da.ts | 2 + .../webview-ui/agent-manager/i18n/de.ts | 2 + .../webview-ui/agent-manager/i18n/en.ts | 2 + .../webview-ui/agent-manager/i18n/es.ts | 2 + .../webview-ui/agent-manager/i18n/fa.ts | 2 + .../webview-ui/agent-manager/i18n/fr.ts | 2 + .../webview-ui/agent-manager/i18n/it.ts | 2 + .../webview-ui/agent-manager/i18n/ja.ts | 2 + .../webview-ui/agent-manager/i18n/ko.ts | 2 + .../webview-ui/agent-manager/i18n/nl.ts | 2 + .../webview-ui/agent-manager/i18n/no.ts | 2 + .../webview-ui/agent-manager/i18n/pl.ts | 2 + .../webview-ui/agent-manager/i18n/ru.ts | 2 + .../webview-ui/agent-manager/i18n/th.ts | 2 + .../webview-ui/agent-manager/i18n/tr.ts | 2 + .../webview-ui/agent-manager/i18n/uk.ts | 2 + .../webview-ui/agent-manager/i18n/zh.ts | 2 + .../webview-ui/agent-manager/i18n/zht.ts | 2 + .../agent-manager/pr/PRCommentForm.tsx | 497 +++++++++++++++--- .../agent-manager/pr/diff-comment-forms.tsx | 86 +++ .../agent-manager/pr/diff-comment-state.ts | 75 +++ .../webview-ui/agent-manager/pr/pr-panel.css | 70 +++ .../webview-ui/diff-viewer/DiffViewerApp.tsx | 162 +++++- .../diff-viewer/FullScreenDiffView.tsx | 62 ++- .../webview-ui/diff-viewer/SendAllButton.tsx | 70 +++ .../diff-viewer/annotation-lifecycle.ts | 32 ++ .../webview-ui/diff-viewer/comments-github.ts | 136 +++++ .../webview-ui/diff-viewer/pr-diff.ts | 61 +++ .../diff-viewer/review-annotations.ts | 120 ++++- .../diff-viewer/review-controller.ts | 95 +++- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/fa.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/it.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 12 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 12 + .../webview-ui/src/styles/banners.css | 18 + 82 files changed, 3079 insertions(+), 131 deletions(-) create mode 100644 .changeset/pr-comments-from-changes.md create mode 100644 .kilo/plans/diff-comment-simplification-plan.md create mode 100644 .kilo/plans/diff-viewer-github-comment-creation.md create mode 100644 packages/kilo-vscode/tests/fixtures/diff-comment-form.tsx create mode 100644 packages/kilo-vscode/tests/fixtures/inline-comment-form.tsx create mode 100644 packages/kilo-vscode/tests/fixtures/send-all-button.tsx create mode 100644 packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts create mode 100644 packages/kilo-vscode/tests/unit/comments-github.test.ts create mode 100644 packages/kilo-vscode/tests/unit/composer-action-order.test.ts create mode 100644 packages/kilo-vscode/tests/unit/diff-comment-form.test.ts create mode 100644 packages/kilo-vscode/tests/unit/inline-comment-form.test.ts create mode 100644 packages/kilo-vscode/tests/unit/pr-diff.test.ts create mode 100644 packages/kilo-vscode/tests/unit/review-annotations.test.ts create mode 100644 packages/kilo-vscode/tests/unit/send-all-button.test.ts create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/diff-comment-forms.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/diff-comment-state.ts create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/SendAllButton.tsx create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/annotation-lifecycle.ts create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/comments-github.ts create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/pr-diff.ts diff --git a/.changeset/pr-comments-from-changes.md b/.changeset/pr-comments-from-changes.md new file mode 100644 index 00000000000..3ef64c5ce98 --- /dev/null +++ b/.changeset/pr-comments-from-changes.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Post inline comments to a checked-out GitHub pull request from Changes and Agent Manager diff views. The compact composer saves locally, sends to Kilo, or posts to GitHub with an explicit destination choice. Cmd/Ctrl+Enter saves a comment, and pressing it again in the review view sends all comments to Kilo without publishing to GitHub. The toolbar shows separate send-all-to-Kilo and send-all-to-GitHub actions, and only the Kilo action carries the keyboard shortcut. GitHub posting stops on the first error so unpublished comments are kept. diff --git a/.kilo/plans/diff-comment-simplification-plan.md b/.kilo/plans/diff-comment-simplification-plan.md new file mode 100644 index 00000000000..74985024780 --- /dev/null +++ b/.kilo/plans/diff-comment-simplification-plan.md @@ -0,0 +1,133 @@ +# Simplify the Diff Comment Implementation + +## Goal + +Reduce the diff size and structural complexity of the PR comment work without changing behavior. Optimize for reviewability and for keeping shared existing files close to their original shape. + +## Current Size + +| Area | Added | +|---|---| +| Existing tracked files | ~1124 insertions, 101 deletions across 18 files | +| New source modules | `pr-diff.ts`, `annotation-lifecycle.ts`, `diff-comment-forms.tsx`, `diff-comment-state.ts` (~285 lines) | +| New tests | 5 unit/fixture files | +| Untracked scratch | `.kilo/plans/diff-viewer-github-comment-creation.md` (remove from the shipped diff) | + +The largest existing-file growth is `review-annotations.ts` (+310), `PRCommentForm.tsx` (+245), and `DiffViewerApp.tsx` (+165). + +## Principles + +- One owner for PR snapshot state and comment form creation. No parallel implementations. +- Production always mounts `PRCommentForm`. Do not keep imperative fallbacks that only tests use. +- Prefer collapsing props into one object over adding a prop per field. +- Keep keyboard, focus, and safety behavior exactly as tested. +- Do not edit shared upstream opencode files. This package is Kilo-owned. + +## Proposals + +### 1. Create comment forms inside `createReviewView` (high impact, medium risk) + +`createDiffCommentForms` is instantiated in three places: `DiffPanel.tsx`, `FullScreenDiffView.tsx`, and `DiffViewerApp.tsx`. Two of them then pass it back into `createReviewView` through `localForm`/`remote`/`remoteAccessor` plus a `ReviewViewOverrides` layer. + +Change: + +- Create the forms once in `createReviewView` from `diffs`, `worktreeId`, and a single PR context. +- Delete the `createDiffCommentForms` blocks in `DiffPanel` and `FullScreenDiffView`. +- Remove `ReviewViewOverrides`, `ReviewViewProps.remote`, `ReviewViewProps.localForm`, and `ReviewViewProps.remoteAccessor`. +- `DiffViewerApp` keeps computing PR diffs and the snapshot for PR mode, but stops creating forms. + +Estimated: −60 to −80 lines and one less indirection layer. + +### 2. Collapse the four PR props into one `pr` context (high impact, low risk) + +`prTarget`, `prSnapshot`, `prLoading`, and `prError` are threaded `AgentManagerApp` → `DiffPanelCache` → `DiffPanel`, plus three of them through `FullScreenDiffView`. + +Change: + +- Introduce `pr?: { target: PRTarget; snapshot?: PRDiffSnapshot; loading?: boolean; error?: string }`. +- Pass one prop per layer. `DiffPanelCache` drops four prop definitions and four call-sites to one. + +Estimated: −15 to −20 lines and a smaller public surface. + +### 3. Delete the unused non-mounted remote fallback (high impact, low risk) + +Production always supplies `remote.mount`. `remote.submit`, `publish()`, `githubSubmitButton`, the non-mounted `hint`, and the `remoteMounted` branches in `update()` exist only for tests. + +Change: + +- Make `mount` required in `RemoteCommentConfig`. +- Delete `submit`, `publish()`, `githubSubmitButton`, the non-mounted hint, and the `handlers.remote && !handlers.localMount` branch. +- Update `tests/unit/review-annotations.test.ts` to always pass a mount. + +Estimated: −60 to −80 lines in `review-annotations.ts`. + +### 4. Evaluate removing `annotation-lifecycle.ts` (medium impact, needs a check first) + +This module (32 lines + tests + `track` plumbing) exists because Pierre may replace annotation DOM without invoking button handlers. + +Change: + +- Add a focused test that rebuilds the same open draft twice and checks whether the mounted root is disposed. +- If Pierre reuses the wrapper for an open draft, dispose only on draft change, cancel, and complete (already partly handled in `review-controller.ts`), and delete the module plus the `track` handler. +- If Pierre does detach, keep it but reuse the existing mounted-registry pattern from `remote-comment-renderer.tsx` instead of adding a second `MutationObserver` implementation. + +Estimated: −40 to −60 lines if removable. + +### 5. Collapse the new `PRCommentForm` props into one variant (medium impact, low risk) + +`submitOnEnter`, `onEscape`, `replaceBody`, and `inline` are four new props that are always set together by the diff composers. + +Change: + +- Replace with `variant?: "inline"`. +- `inline` implies submit-on-Enter, Escape-to-cancel, and body replacement on destination switch. +- Move the render-time `untrack` patch to a mount effect so it does not write during component creation. + +Estimated: −15 to −25 lines and a clearer API. + +### 6. Deduplicate PR context label CSS and strings (small, low risk) + +Three near-identical rules for the same label: `.diff-pr-context` in `banners.css`, `.am-diff-pr-context` and `.am-review-pr-context` in `agent-manager.css`, including duplicated `svg` rules. + +Change: + +- Use `.diff-pr-context` in all three components and delete the other two rule sets. +- Reuse existing i18n keys where semantics match and drop duplicates such as `diffViewer.comment.postToGithub` if an equivalent `agentManager.pr.*` key exists. + +Estimated: −20 CSS lines, −3 to −4 i18n keys. + +### 7. Small host-side cleanup (small, low risk) + +- `PRReviewActions.handle()` calls `checkBranch` and then `load()` calls it again. Keep one check per action. +- Reuse an existing git helper in `DiffViewerProvider` instead of the inline `execWithShellEnv` closure if one fits. + +Estimated: −5 lines and one fewer `git rev-parse` per load. + +### 8. Optional: use GitHub `additions`/`deletions` (neutral) + +`review-actions.parse()` already reads `additions`/`deletions` but drops them, so `pr-diff.ts` re-counts patch lines. Adding the two fields to `PRFile` and using them removes the `counts()` helper. + +Estimated: roughly neutral diff, one fewer parser. + +## Recommended Order + +| Order | Item | Why | +|---|---|---| +| 1 | 3, delete fallback | Largest reduction, no behavior risk | +| 2 | 5, one form variant | Unblocks reading the rest of `PRCommentForm` | +| 3 | 1 + 2, single form owner and `pr` object | Biggest structural win | +| 4 | 6 + 7, small dedupe | Cheap cleanup | +| 5 | 4, lifecycle decision | Needs a test first, keep if unproven | +| 6 | 8, optional | Neutral, do only if touching `parse()` anyway | + +## Verification + +- Keep the focused suites green: `inline-comment-form`, `review-annotations`, `annotation-lifecycle` (or its replacement), `pr-diff`, `pr-review-actions`, `pr-review-render`, `diff-comment-*`, `remote-comments`, `agent-manager-arch`. +- Re-run `bun run compile`, `bun run lint`, `bun run knip`, and `bun run check-kilocode-change`. +- Re-run the isolated VS Code checks for focus, Enter, Shift+Enter, Escape, preview, destination switch, local save, and GitHub post on the disposable test repo only. + +## Non-Goals + +- No behavior changes to keyboard, focus, or publication safety. +- No new features or UX changes beyond what is already merged in this branch. +- No changes to `packages/opencode/` or other shared upstream files. diff --git a/.kilo/plans/diff-viewer-github-comment-creation.md b/.kilo/plans/diff-viewer-github-comment-creation.md new file mode 100644 index 00000000000..17c2cb5ab32 --- /dev/null +++ b/.kilo/plans/diff-viewer-github-comment-creation.md @@ -0,0 +1,205 @@ +# Create GitHub Comments From the Diff Viewer + +## Goal + +After checking out another person's PR in the current worktree, let the user select a line in the diff viewer and post a new inline comment to that PR. Keep local comments for agent feedback clearly separate from comments published to GitHub. + +This is an implementation plan only. It does not authorize checkout operations or posting comments during planning. + +## Existing Foundation + +Source inspection shows that Agent Manager's PR Files view already creates GitHub inline comments, including multiline comments. The standalone Changes viewer loads GitHub threads and supports replies and other existing thread actions, but does not route new-comment creation. This work is primarily an integration and UX change, not a new GitHub API feature. + +All paths below are relative to `packages/kilo-vscode/`. + +| Area | Existing Implementation | +|---|---| +| Changes viewer and local comments | `webview-ui/diff-viewer/DiffViewerApp.tsx`, `review-controller.ts`, `review-annotations.ts` | +| Changes host and thread actions | `src/diff/DiffViewerProvider.ts`, `src/diff/comment-actions.ts` | +| GitHub thread rendering and safe display mapping | `webview-ui/diff-viewer/remote-comments.ts`, `remote-comment-renderer.tsx` | +| Snapshot-backed PR diff and composer | `webview-ui/agent-manager/pr/PRFiles.tsx`, `PRCommentForm.tsx` | +| Shared creation and snapshot contracts | `src/shared/pr-comment-actions.ts`, `src/shared/pr-patch.ts` | +| Validated GitHub writes | `src/agent-manager/pr/review-actions.ts` (`PRReviewActions`) | +| Existing host integration example | `src/agent-manager/pr-status-bridge.ts` | +| Worktree-scoped PR discovery | `src/diff/pr-poller.ts`, `src/agent-manager/PRStatusPoller.ts` | + +Reuse `loadPRFiles`, `createReviewComment`, `PRDiffSnapshot`, and `PRCommentForm` with `action="line"`. The existing write handler validates the patch and fresh base/head revisions before posting through `gh`. No CLI endpoint, SDK regeneration, or new authentication system should be needed. + +## Recommended UX + +Use one inline composer with an explicit destination, not a global setting that silently changes what the existing comment action does. + +| Element | Local Comment | GitHub Comment | +|---|---|---| +| Destination label | `Local` | `GitHub` | +| Helper text | `Saved locally. Not posted to GitHub. Send to the agent when ready.` | `Posts immediately to owner/repo#123. Visible to people with access to this PR.` | +| Submit action | `Save local comment` | `Post to GitHub` | +| Saved appearance | Local badge and existing edit/remove actions | GitHub badge, author, timestamp, and link | +| Agent submission | Included in the existing local-comment flow | Not included automatically | +| Publication | Never automatic | Only after the user selects the GitHub destination and posts | + +- Keep the existing gutter action. Open the composer with `Local` selected by default, including on PR branches. +- Show the `Local | GitHub` destination control inside the composer. Do not persist a GitHub default across composers, sessions, or worktrees in the first version. +- Show a compact PR context label in the viewer header, for example `GitHub: owner/repo#123`, with an open-in-browser action. +- Show the target PR and authenticated GitHub account before publication. Use the same account and authentication path as existing replies. +- When GitHub commenting is unavailable, explain why beside the disabled destination. Keep local commenting available. +- In a local diff, show `Open PR changes to comment on GitHub` rather than pretending local coordinates are publishable. Switch to the snapshot-backed PR source and require a new line selection. Direct posting from arbitrary local diffs is deferred. +- Switching the destination preserves the typed text but does not save or publish it. Once saved or posted, a comment's destination does not change. +- Label existing GitHub reply buttons `Reply on GitHub` so replies and new comments share the same publication model. +- Keep local counts and the action to send comments to the agent separate from GitHub thread counts. Do not let a generic `Send comments` action publish GitHub drafts. +- Use text labels and existing icons, not color alone. Preserve the current visual style, keyboard flow, and narrow-panel layout. + +### Example Composer + +```text +src/example.ts:42 +[ Local ] [ GitHub ] + +GitHub: owner/repo#123 | Posting as @reviewer +Posts immediately. Visible to people with access to this PR. + +[ Comment text ] + +[ Cancel ] [ Post to GitHub ] +``` + +## First-Version Scope + +- Create one published inline PR comment at a time, using the existing GitHub thread display and reply flow afterward. +- Support additions, deletions, valid context lines, and same-side multiline ranges through the existing PR patch validator. Reject cross-side or invalid hunk selections. +- Support PRs from forks, not only branches in the base repository. +- Work in the current worktree. The user checks out the PR through existing tools; a new checkout UI is not required. +- Keep local comments and GitHub composer drafts in separate state. Unposted GitHub text must never enter the local agent-feedback payload. +- Retain failed drafts while the viewer stays open, including across diff refreshes. Warn before an explicit action discards text. Cross-restart draft persistence is not required for this version. + +Out of scope: pending GitHub reviews, batch submission, approve/request-changes actions, editing/deleting published comments, thread resolution, automatic conversion of local comments, file-level comments, and comments on arbitrary uncommitted lines. + +Existing edit/delete/resolve actions remain unchanged; they are not new work in this plan. Preserve the current GitHub.com-only write support. GitHub Enterprise support and detached-HEAD PR discovery are separate follow-ups. + +## Correct PR and Line Targeting + +The main correctness requirement is that the displayed source and line match the PR snapshot sent to GitHub. A local branch diff is not automatically the GitHub PR diff. + +1. Resolve the checked-out PR using the existing worktree-scoped GitHub integration. Capture the host, base repository, PR number and URL, base/head SHAs, and state. Use the base repository for API writes, including fork PRs. +2. Reuse the current discovery order: bare `gh pr view`, branch lookup, then an exact local-HEAD SHA match. Preserve host-side branch and panel-generation checks. If discovery is ambiguous or unsupported, disable publication with a clear reason rather than introducing a PR picker in this first version. +3. Provide a clearly labeled `PR changes` source in the existing viewer using the existing `PRFiles` snapshot loader and GitHub patches. Do not use a worktree creation base or working-copy contents. Prefer a small extraction of shared rendering where needed over copying PR Files into a second implementation. +4. Bind the displayed patch and composer to a host-owned snapshot identity. Keep PR review content separate from uncommitted changes, so a dirty worktree does not invalidate a correctly loaded PR snapshot. Never reset, stash, or overwrite local files to enable commenting. +5. Validate the path, side, and line against a complete PR patch. Use `LEFT` for deleted lines and `RIGHT` for added lines. Map context lines to verified coordinates. Do not guess when a patch is truncated, a file is binary, or a rename cannot be mapped reliably. +6. Before publication, verify that the PR context and base/head revisions still match the snapshot. If they changed, preserve the text, refresh the diff, and require the user to select a valid line again. Do not silently retarget a draft. +7. GitHub can still change between validation and publication. Send the captured commit SHA, handle API rejection, and keep any successfully published comment attached to its actual commit, even if it becomes outdated immediately afterward. + +Keep draft identity scoped to the worktree, PR, snapshot, path, side, and line. Ignore responses for a different viewer context; do not attach old drafts or responses to a newly checked-out PR. + +## GitHub Write Contract + +Reuse the extension's existing `gh` execution and authentication path. Do not introduce a token store or send credentials to the webview. + +Use the review-comment endpoint, not a PR timeline comment: + +```text +POST /repos/{owner}/{repo}/pulls/{pull_number}/comments +``` + +Single-line request body: + +```json +{ + "body": "The user's comment", + "commit_id": "", + "path": "src/example.ts", + "line": 42, + "side": "RIGHT" +} +``` + +- Use `line` and `side`, not the deprecated `position` field. Reuse the existing multiline support, which also sends `start_line` and `start_side`. +- Resolve the repository and commit from host-owned context. Validate all webview input, including nonempty text, integer line numbers, allowed sides, and membership in the loaded patch. +- Pass JSON safely through the existing process helper, with no shell interpolation of comment text. +- Correlate requests and results with a request ID. Disable repeat submission while the request is pending. +- On success, clear only the submitted draft and refresh the existing thread list. If thread refresh fails, report that publication succeeded and offer refresh, not another post. +- On authentication, permission, rate-limit, or invalid-line errors, retain the draft and show a specific next action. A network timeout can mean the write succeeded: show `Publication status unknown`, reload comments to check, and do not automatically retry the POST. +- Reuse existing logging conventions without recording comment bodies or credentials. + +## Implementation Steps + +### 1. Connect the Existing Host Actions + +- Extend the standalone diff integration to route `loadPRFiles` and `createReviewComment` to `PRReviewActions`, following `pr-status-bridge.ts`. +- Supply the host-owned directory, validated PR context, result callback, and poll-refresh callback. Preserve the panel instance/open-generation/branch/PR target checks in `comment-actions.ts`. +- New-thread creation must work when a PR has zero existing threads. Do not apply the existing-thread membership requirement to creation. +- Enable only the required operations. Do not expose review submission or suggestion application as a side effect. + +### 2. Add the PR Diff Source + +- Add `PR changes` to the Changes viewer's source controls when a supported PR is detected. +- Reuse `PRFiles.tsx` and its snapshot-backed selection behavior. Adapt or extract only the shared pieces needed to fit the existing viewer layout and local-comment annotations. +- Keep the current local source and its comparison-base controls unchanged. Label the immutable PR source so it is not confused with working-copy changes. +- Preserve existing snapshot limits: complete patches only, at most 3,000 files, 4 MiB of snapshot data, and bounded retained snapshots. Surface unsupported files and expired snapshots as unavailable targets. +- Do not use `remote-comments.ts` as an inverse line mapper. Its safe display checks do not prove that a local selection is publishable. + +### 3. Make the Destination Explicit + +- Add the destination control and exact-action button labels to the inline composer. Reuse `PRCommentForm action="line"` for the GitHub path rather than duplicating its pending, error, and correlation behavior. +- Keep the existing local `ReviewComment[]` format and agent submission route unchanged. Local records do not retain range endpoints, so never reconstruct a GitHub range from a saved local comment's selected text. +- Preserve range endpoints in the remote draft and bind it to the snapshot. Keep local and GitHub annotations distinct when both are displayed at the same line. +- Review current source/context-switch clearing behavior. Retain remote drafts during refresh, and warn before explicit navigation discards them. Never transfer a draft silently to another snapshot. +- Add localized copy, accessible destination labels, and existing-theme styling. Update any shared composer callers so Agent Manager PR Files keeps working. + +### 4. Verify and Release + +- Extend the focused tests below rather than duplicating existing patch-validation coverage. +- Run the extension checks and isolated VS Code flow before considering the implementation complete. +- Add one patch changeset for the extension, for example: `Post GitHub PR comments from the Changes viewer with explicit local and GitHub destinations.` No changeset is needed for this plan-only change. + +## Verification Plan + +### Focused Automated Coverage + +| Existing Test | Add or Verify | +|---|---| +| `tests/unit/diff-comment-actions.test.ts` | New-operation routing, zero-thread creation, branch and panel checks, result correlation, refresh | +| `tests/unit/diff-comment-target.test.ts` | Stale generations, wrong worktrees, historical contexts without live write targets | +| `tests/unit/pr-review-actions.test.ts` | Reuse payload/range/revision coverage; add only missing adapter-specific and uncertain-write cases | +| `tests/unit/pr-review-render.test.ts` | Reused PR Files/form behavior remains intact | +| `tests/unit/diff-comment-render.test.ts` | Explicit destinations, correct action labels, pending/error state, drafts, no accidental agent submission | +| `tests/unit/remote-comments.test.ts` | New threads integrate with existing rendering without weakening anchor validation | +| `tests/unit/pr-comment-context.test.ts` | Dirty worktree, different HEAD/index, renames, missing objects, snapshot-pinned content | + +Use real temporary Git repositories and existing implementation tests where possible. Use controlled GitHub boundary fixtures only where network writes or failure injection require them. + +From `packages/kilo-vscode/`, run the affected tests with `bun test tests/unit/.test.ts`, then `bun run typecheck`, `bun run lint`, `bun run compile`, `bun run knip`, and `bun run check-kilocode-change`. Run the broader `bun run test:unit` suite before release. If implementation adds or changes source URLs, run the repository's source-link extraction guard. + +### Isolated UI Verification + +Load `self-testing` and `vscode-self-test` and use the isolated VS Code harness with a disposable fixture workspace. Do not use Storybook as a substitute or real credentials in the automated harness. + +1. Open Changes for a fixture PR with zero threads, select `PR changes`, create a GitHub comment, and verify the correct request, success state, thread display, and reply action through a controlled GitHub boundary. +2. Create a local comment on the same file. Verify the badge, local-only count, and agent submission route. Confirm that no GitHub write occurs. +3. Check additions, deletions, multiline ranges, renames, unsupported patches, a fork PR, and dirty/unpushed local changes. +4. Change the PR revisions or worktree while a form or request is active. Verify safe blocking, retained text, and no result appearing in the wrong context. +5. Exercise permission failure, authentication failure, timeout, double-click submission, and successful publication followed by failed refresh. +6. Inspect screenshots for keyboard access, narrow width, readable destination labels, and mixed local/GitHub threads. Check that Agent Manager's existing PR Files flow has no regression. + +Separately, an authorized human smoke test on a disposable GitHub PR should confirm that a new comment appears on the correct line under the expected account, including a fork PR. Do not post to production discussions as an automated test. Report fixture-only verification as such if this live check is unavailable. + +## Delivery Size + +This is a medium-sized integration with an existing API foundation. Most work is in composing the two diff/comment UIs, preserving draft state, and keeping write targets safe. A new API client is unnecessary. + +Prefer two focused implementation increments if needed: first connect the existing snapshot-backed PR view and creation handler to Changes; then add the explicit destination UX and regression coverage. Both are required for the user-facing feature to be complete. Avoid expanding the work into arbitrary local-to-PR line translation or a full review workflow. + +## Acceptance Criteria + +1. A user can check out another person's PR, open its diff, select an added or deleted line, choose `GitHub`, and post a new thread visible at the same location on GitHub. +2. The target repository, PR number, account, and immediate publication behavior are clear before posting. +3. Saving local comments and sending them to the agent work as before and never write to GitHub. +4. New GitHub comments use the existing thread display and can be replied to through the existing flow. +5. A fork PR posts to the base repository, not the fork's repository or another open worktree's PR. +6. Local-only lines cannot be published as if they were PR lines. A changed snapshot blocks submission and preserves the draft. +7. Failed or uncertain writes do not lose text, automatically retry, or claim success without evidence. +8. Switching branches, PRs, worktrees, or diff sources does not mix comment destinations, line anchors, or pending results. + +## References + +- [GitHub REST: Create a review comment](https://docs.github.com/en/rest/pulls/comments#create-a-review-comment-for-a-pull-request) +- [GitHub CLI: PR metadata](https://cli.github.com/manual/gh_pr_view) diff --git a/packages/kilo-vscode/src/agent-manager/pr/review-actions.ts b/packages/kilo-vscode/src/agent-manager/pr/review-actions.ts index c33ec95e1ee..e75c0177079 100644 --- a/packages/kilo-vscode/src/agent-manager/pr/review-actions.ts +++ b/packages/kilo-vscode/src/agent-manager/pr/review-actions.ts @@ -150,6 +150,7 @@ export class PRReviewActions { if (!result.requestId) throw new Error("Missing request identity.") const initial = this.host.context(message) const context = { ...initial, pr: { ...initial.pr } } + await this.checkBranch(context) if (message.type === "agentManager.loadPRFiles") { const snapshot = await this.load(context, message) this.host.post({ ...result, type: "agentManager.loadPRFilesResult", success: true, snapshot }) @@ -180,6 +181,13 @@ export class PRReviewActions { } } + private async checkBranch(context: PRReviewContext) { + if (!this.host.checkBranch) return + const branch = await this.host.checkBranch(context.directory) + if (!branch || branch === "HEAD" || branch !== context.branch) + throw new Error("Diff branch changed. Refresh and try again.") + } + private current(context: PRReviewContext, message: Record) { if (identity(this.host.context(message)) !== identity(context)) throw new Error("Pull request context changed. Reload the review.") @@ -203,6 +211,7 @@ export class PRReviewActions { } } const after = await metadata(context) + await this.checkBranch(context) this.current(context, message) if ( before.head !== after.head || @@ -229,6 +238,7 @@ export class PRReviewActions { const snapshot = this.snapshot(context, message) const { file, start, end, body } = selection(snapshot, message) const fresh = await metadata(context) + await this.checkBranch(context) this.current(context, message) if (fresh.head !== snapshot.data.head || fresh.base !== snapshot.base) throw new Error("Pull request changed. Reload the review before posting.") @@ -263,6 +273,7 @@ export class PRReviewActions { if (message.head !== snapshot.data.head) throw new Error("Pull request changed. Reload the review before submitting.") const fresh = await metadata(context) + await this.checkBranch(context) this.current(context, message) if (fresh.head !== snapshot.data.head || fresh.base !== snapshot.base) throw new Error("Pull request changed. Reload the review before submitting.") diff --git a/packages/kilo-vscode/src/agent-manager/pr/review-context.ts b/packages/kilo-vscode/src/agent-manager/pr/review-context.ts index 2c613515ade..74a36bb7247 100644 --- a/packages/kilo-vscode/src/agent-manager/pr/review-context.ts +++ b/packages/kilo-vscode/src/agent-manager/pr/review-context.ts @@ -18,4 +18,5 @@ export interface PRReviewHost { conflicts?: (context: PRReviewContext, base: string, head: string) => Promise getPRMergeMethod?: (repo: string) => PRMergeMethod | undefined savePRMergeMethod?: (repo: string, method: PRMergeMethod) => Promise + checkBranch?: (directory: string) => Promise } diff --git a/packages/kilo-vscode/src/diff/DiffViewerProvider.ts b/packages/kilo-vscode/src/diff/DiffViewerProvider.ts index daf06bd8a6b..e68c33a18cf 100644 --- a/packages/kilo-vscode/src/diff/DiffViewerProvider.ts +++ b/packages/kilo-vscode/src/diff/DiffViewerProvider.ts @@ -17,6 +17,9 @@ import { addCommentReaction, isPRReactionContent, removeCommentReaction } from " import type { PRStatus } from "../agent-manager/types" import { ghErrorReason } from "../agent-manager/pr/am-pr-utils" import { createDiffCommentActions } from "./comment-actions" +import { PRReviewActions } from "../agent-manager/pr/review-actions" +import type { PRReviewContext } from "../agent-manager/pr/review-context" +import { execWithShellEnv } from "../agent-manager/shell-env" type CommentHandler = (comments: unknown[], autoSend: boolean) => void type OpenArgs = { @@ -120,6 +123,7 @@ export class DiffViewerProvider implements vscode.Disposable { private baseBranchOverride: string | undefined private target: CommentHandler | undefined private readonly prPolling: ReturnType + private readonly reviews: PRReviewActions private focusPending = false private openGeneration = 0 private readonly identity = randomUUID() @@ -149,6 +153,23 @@ export class DiffViewerProvider implements vscode.Disposable { onStatus: () => this.sendComments(), log: (...args) => this.log(...args), }) + this.reviews = new PRReviewActions({ + context: (message) => this.reviewContext(message), + post: (message) => { + void this.panel?.webview.postMessage(message) + }, + refresh: (review) => { + if (this.commentContext()?.token === review.projectId) this.prPolling.refresh() + }, + dirtyFiles: () => [], + checkBranch: async (directory) => { + const result = await execWithShellEnv("git", ["rev-parse", "--abbrev-ref", "HEAD"], { + cwd: directory, + timeout: 5_000, + }) + return result.stdout.trim() + }, + }) } setCommentHandler(handler: CommentHandler): void { @@ -279,7 +300,7 @@ export class DiffViewerProvider implements vscode.Disposable { } private onMessage(msg: Record): void { - if (this.actions.handle(msg)) return + if (this.actions.handle(msg) || this.reviews.handle(msg)) return const handler = this.messageHandlers[msg.type as string] handler?.(msg) } @@ -483,7 +504,14 @@ export class DiffViewerProvider implements vscode.Disposable { const comments = selected && !match ? [...live, { ...selected, outdated: true }] : live const ctx = this.commentContext() const target = ctx - ? { projectId: ctx.token, worktreeId: "diff", prNumber: ctx.pr.number, prUrl: ctx.pr.url } + ? { + projectId: ctx.token, + worktreeId: "diff", + prNumber: ctx.pr.number, + prUrl: ctx.pr.url, + baseRefOid: ctx.pr.baseRefOid, + headRefOid: ctx.pr.headRefOid, + } : undefined void this.panel.webview.postMessage({ type: "diffViewer.prComments", @@ -522,6 +550,25 @@ export class DiffViewerProvider implements vscode.Disposable { } } + private reviewContext(message: Record): PRReviewContext { + const ctx = this.commentContext() + if ( + !ctx || + message.projectId !== ctx.token || + message.worktreeId !== "diff" || + message.prNumber !== ctx.pr.number || + message.prUrl !== ctx.pr.url + ) + throw new Error("Pull request context changed. Refresh and try again.") + return { + pr: ctx.pr, + directory: ctx.directory, + branch: ctx.branch, + worktreeId: "diff", + projectId: ctx.token, + } + } + private getHtml(webview: vscode.Webview): string { return buildWebviewHtml(webview, { scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-viewer.js")), diff --git a/packages/kilo-vscode/src/shared/pr-comment-actions.ts b/packages/kilo-vscode/src/shared/pr-comment-actions.ts index d7de0a23a79..a34f0e59ac5 100644 --- a/packages/kilo-vscode/src/shared/pr-comment-actions.ts +++ b/packages/kilo-vscode/src/shared/pr-comment-actions.ts @@ -3,6 +3,8 @@ export interface PRTarget { worktreeId: string prNumber: number prUrl: string + baseRefOid?: string + headRefOid?: string } export interface PRFile { diff --git a/packages/kilo-vscode/src/shared/pr-patch.ts b/packages/kilo-vscode/src/shared/pr-patch.ts index 02ea588fac4..45bc4239f8f 100644 --- a/packages/kilo-vscode/src/shared/pr-patch.ts +++ b/packages/kilo-vscode/src/shared/pr-patch.ts @@ -45,9 +45,12 @@ export function parsePatch(patch: string, totals?: { additions: unknown; deletio function hunks(patch: string, selection?: Range) { const lines = patch.split("\n") if (lines.at(-1) === "") lines.pop() + // Patches may include file headers (`diff --git`, `---`, `+++`) before the first hunk. + const start = lines.findIndex((line) => line.startsWith("@@")) + if (start < 0) return const result: Range[] = [] const selected: string[] = [] - let index = 0 + let index = start let added = 0 let removed = 0 let left = 0 diff --git a/packages/kilo-vscode/tests/fixtures/diff-comment-form.tsx b/packages/kilo-vscode/tests/fixtures/diff-comment-form.tsx new file mode 100644 index 00000000000..08dde1970a0 --- /dev/null +++ b/packages/kilo-vscode/tests/fixtures/diff-comment-form.tsx @@ -0,0 +1,114 @@ +import assert from "node:assert/strict" +import { harness } from "./comment-harness" +import type { PRReviewRequest } from "../../src/shared/pr-comment-actions" + +const { window, root, messages, node, button, input, type, last, respond, wait, mount } = + await harness() +const { PRCommentForm } = await import("../../webview-ui/agent-manager/pr/PRCommentForm") +const saved: string[] = [] +const sent: string[] = [] +let cancelled = 0 +let completed = 0 +const release = mount(() => ( + <> +
+ saved.push(body)} + onSendKilo={(body) => sent.push(body)} + onGithubSuccess={() => completed++} + onCancel={() => cancelled++} + onDestinationChange={() => {}} + /> +
+
+ {}} + onSendKilo={() => {}} + onGithubSuccess={() => completed++} + onCancel={() => cancelled++} + onDestinationChange={() => {}} + /> +
+ +)) +await wait() +const local = node("#local") +const remote = node("#remote") + +// Local-only destination exposes Kilo actions, never the GitHub split button. +assert.equal(button("send-kilo", local).textContent, "Send to Kilo") +assert.equal(button("save", local).textContent, "Save") +assert.equal(button("cancel", local).textContent, "Cancel") +assert.equal(local.querySelector('[data-action="send-primary"]'), null, "no split button without a PR") +assert.equal(messages.length, 0) + +type(local, "Keep this") +button("save", local).click() +assert.deepEqual(saved, ["Keep this"]) +assert.equal(input(local).value, "", "saving clears the composer") +type(local, "Send this") +button("send-kilo", local).click() +assert.deepEqual(sent, ["Send this"]) +assert.equal(input(local).value, "", "sending to Kilo clears the composer") + +// Plain Enter sends to Kilo and never posts to GitHub. +type(local, "Keyboard send") +input(local).dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true })) +assert.deepEqual(sent, ["Send this", "Keyboard send"]) +assert.equal(messages.length, 0, "local actions never request a GitHub write") + +// Cmd/Ctrl+Enter saves the comment locally instead of sending it. +type(local, "Keyboard save") +input(local).dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", metaKey: true, bubbles: true })) +assert.deepEqual(saved, ["Keep this", "Keyboard save"], "Cmd+Enter saves the comment") +assert.deepEqual(sent, ["Send this", "Keyboard send"], "Cmd+Enter does not send to Kilo") +assert.equal(messages.length, 0, "Cmd+Enter never requests a GitHub write") + +// The remembered GitHub destination drives the split primary label. +assert.equal(button("send-primary", remote).textContent, "Send to GitHub #1") +node('[aria-label="Choose destination"]', remote) + +// Enter is not bound to the GitHub destination, so it cannot publish by accident. +type(remote, "Do not post") +input(remote).dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true })) +assert.equal(messages.length, 0, "Enter never posts to GitHub") + +type(remote, "Post me") +button("send-primary", remote).click() +const request = last() +assert.equal(request.type, "agentManager.createReviewComment") +assert.equal(input(remote).disabled, true) +button("send-primary", remote).click() +assert.equal(messages.length, 1, "double submission cannot publish twice") +respond(request, {}) +assert.equal(completed, 1) + +button("cancel", local).click() +assert.equal(cancelled, 1) +release() +await window.happyDOM.close() diff --git a/packages/kilo-vscode/tests/fixtures/inline-comment-form.tsx b/packages/kilo-vscode/tests/fixtures/inline-comment-form.tsx new file mode 100644 index 00000000000..5e93fad090f --- /dev/null +++ b/packages/kilo-vscode/tests/fixtures/inline-comment-form.tsx @@ -0,0 +1,98 @@ +import assert from "node:assert/strict" +import { harness } from "./comment-harness" +import type { PRReviewRequest } from "../../src/shared/pr-comment-actions" + +const { window, root, messages, node, button, input, type, last, respond, wait, mount } = + await harness() +const { PRCommentForm } = await import("../../webview-ui/agent-manager/pr/PRCommentForm") +const saved: string[] = [] +const sent: string[] = [] +let cancelled = 0 +let completed = 0 +let reads = 0 +const initial = () => { + reads++ + return "" +} +const release = mount(() => ( + <> +
+ saved.push(body)} + onSend={(body) => sent.push(body)} + onCancel={() => cancelled++} + onEscape={() => cancelled++} + /> +
+
+ completed++} + onCancel={() => cancelled++} + /> +
+ +)) +await wait() +const local = node("#local") +const remote = node("#remote") +assert.equal(root.querySelector('[data-slot="comment-toolbar"]'), null, "no second toolbar in inline forms") +assert.equal(button("submit", local).textContent, "Save local") +assert.equal(button("send", local).textContent, "Send") +assert.equal(button("send", local).getAttribute("aria-label"), "Send to agent") +assert.equal(button("submit", remote).textContent, "Post to GitHub") +assert.equal(button("discard", remote).textContent, "Cancel") +const before = reads +type(local, "Preview **this**") +assert.equal(reads, before, "typing in one form does not invalidate unrelated drafts") +button("preview", local).click() +await wait() +assert.match(node('[data-slot="comment-preview"]', local).textContent ?? "", /Preview this/) +button("write", local).click() +assert.equal(document.activeElement, input(local)) +input(local).dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", shiftKey: true, bubbles: true })) +assert.equal(saved.length, 0, "Shift+Enter does not submit") +input(local).dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", isComposing: true, bubbles: true })) +assert.equal(saved.length, 0, "IME confirmation does not submit") +input(local).dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true })) +assert.deepEqual(saved, ["Preview **this**"]) +assert.equal(messages.length, 0, "local save never requests a GitHub write") +type(local, "Send this") +button("send", local).click() +assert.deepEqual(sent, ["Send this"]) +type(remote, "Review this line") +button("submit", remote).click() +const request = last() +assert.equal(request.type, "agentManager.createReviewComment") +assert.equal(input(remote).disabled, true) +button("submit", remote).click() +assert.equal(messages.length, 1, "double submission cannot publish twice") +respond(request, { success: false, error: "Snapshot changed" }) +assert.equal(input(remote).value, "Review this line") +assert.match(remote.textContent ?? "", /Snapshot changed/) +button("submit", remote).click() +respond(last(), {}) +assert.equal(completed, 1) +button("cancel", local).click() +assert.equal(cancelled, 1) +release() +await window.happyDOM.close() diff --git a/packages/kilo-vscode/tests/fixtures/send-all-button.tsx b/packages/kilo-vscode/tests/fixtures/send-all-button.tsx new file mode 100644 index 00000000000..27e5c36be1b --- /dev/null +++ b/packages/kilo-vscode/tests/fixtures/send-all-button.tsx @@ -0,0 +1,51 @@ +import assert from "node:assert/strict" +import { createSignal } from "solid-js" +import { harness } from "./comment-harness" +import type { PRCommentRequest } from "../../src/shared/pr-comment-actions" + +const { window, root, button, wait, mount } = await harness() +const { SendAllButton } = await import("../../webview-ui/diff-viewer/SendAllButton") + +const chat: string[] = [] +const github: string[] = [] +const [number, setNumber] = createSignal(undefined) +const [pending, setPending] = createSignal(false) + +const release = mount(() => ( + chat.push("chat")} + onSendGithub={() => github.push("github")} + keybind="Ctrl+Enter" + /> +)) +await wait() + +// Without a PR only the plain chat button is shown. +assert.equal(button("send-all-chat", root).textContent, "Send all to chat (2)") +assert.equal(root.querySelector('[data-action="send-all-github"]'), null) +button("send-all-chat", root).click() +assert.deepEqual(chat, ["chat"]) +assert.deepEqual(github, []) + +// With a PR both explicit actions appear, and the chat action keeps working. +setNumber(7) +await wait() +assert.equal(button("send-all-chat", root).textContent, "Send all to chat (2)") +assert.equal(button("send-all-github", root).textContent, "Send 2 to GitHub #7") +button("send-all-chat", root).click() +assert.deepEqual(chat, ["chat", "chat"]) +assert.deepEqual(github, []) +button("send-all-github", root).click() +assert.deepEqual(github, ["github"]) + +// A pending send disables both actions. +setPending(true) +await wait() +assert.equal(button("send-all-chat", root).disabled, true) +assert.equal(button("send-all-github", root).disabled, true) +release() +await window.happyDOM.close() diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index 6e035725143..5f6c7d18fd0 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -83,6 +83,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/src/components/shared/BranchSelect.tsx"), path.join(ROOT, "webview-ui/src/components/chat/TabDnd.tsx"), path.join(ROOT, "webview-ui/diff-viewer/BaseBranchPicker.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/SendAllButton.tsx"), ] const SHARED_CSS = path.join(ROOT, "webview-ui/src/styles/session-tabs.css") const TSX_FILE = TSX_FILES[0]! diff --git a/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts b/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts new file mode 100644 index 00000000000..aacc0f40a8c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts @@ -0,0 +1,30 @@ +import { afterEach, expect, it } from "bun:test" +import { Window } from "happy-dom" +import { createAnnotationLifecycle } from "../../webview-ui/diff-viewer/annotation-lifecycle" +import type { AnnotationMeta } from "../../webview-ui/diff-viewer/review-annotations" + +const previous = { document: globalThis.document, MutationObserver: globalThis.MutationObserver } +afterEach(() => Object.assign(globalThis, previous)) + +it("disposes detached and replaced annotation roots exactly once", async () => { + const window = new Window() + Object.assign(globalThis, { document: window.document, MutationObserver: window.MutationObserver }) + const lifecycle = createAnnotationLifecycle() + const meta: AnnotationMeta = { type: "draft", comment: null, file: "test.ts", side: "additions", line: 1 } + const host = document.createElement("div") + let released = 0 + lifecycle.track(meta, host, () => released++) + document.body.append(host) + await window.happyDOM.waitUntilComplete() + expect(released).toBe(0) + host.remove() + await window.happyDOM.waitUntilComplete() + expect(released).toBe(1) + lifecycle.track(meta, host, () => released++) + lifecycle.track(meta, document.createElement("div"), () => released++) + expect(released).toBe(2) + lifecycle.clear() + lifecycle.clear() + expect(released).toBe(3) + await window.happyDOM.close() +}) diff --git a/packages/kilo-vscode/tests/unit/comments-github.test.ts b/packages/kilo-vscode/tests/unit/comments-github.test.ts new file mode 100644 index 00000000000..8395294bf61 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/comments-github.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from "bun:test" +import { postAllGithub, resolveGithubContext, type CommentsGithub } from "../../webview-ui/diff-viewer/comments-github" +import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" +import type { ReviewComment } from "../../webview-ui/diff-viewer/review-comments" + +const patch = "@@ -1,2 +1,2 @@\n context\n-old\n+new\n" + +const target: PRTarget = { + worktreeId: "wt-1", + prNumber: 7, + prUrl: "https://github.com/example/repo/pull/7", +} + +const snapshot: PRDiffSnapshot = { + id: "snap-1", + head: "a".repeat(40), + files: [{ path: "src/file.ts", status: "modified", patch }], +} + +function comment(id: string, line: number): ReviewComment { + return { id, file: "src/file.ts", side: "additions", line, comment: id, selectedText: "" } +} + +function fake(handler: (comment: ReviewComment) => { success: boolean; error?: string }): CommentsGithub { + return { + available: () => true, + resolve: () => undefined, + send: async (item) => handler(item), + } +} + +describe("resolveGithubContext", () => { + it("accepts a line inside the PR hunk", () => { + const result = resolveGithubContext({ + target, + snapshot, + file: "src/file.ts", + side: "additions", + start: 2, + end: 2, + patch, + }) + expect(result).toEqual({ + prNumber: 7, + prUrl: target.prUrl, + snapshotId: "snap-1", + label: "GitHub #7", + closed: false, + }) + }) + + it("marks a line outside the hunk as closed", () => { + const result = resolveGithubContext({ + target, + snapshot, + file: "src/file.ts", + side: "additions", + start: 9, + end: 9, + patch, + }) + expect(result?.closed).toBe(true) + }) + + it("marks a missing patch as closed", () => { + const result = resolveGithubContext({ + target, + snapshot, + file: "src/file.ts", + side: "additions", + start: 2, + end: 2, + }) + expect(result?.closed).toBe(true) + }) + + it("returns undefined without a target or snapshot", () => { + expect( + resolveGithubContext({ snapshot, file: "src/file.ts", side: "additions", start: 2, end: 2, patch }), + ).toBeUndefined() + expect( + resolveGithubContext({ target, file: "src/file.ts", side: "additions", start: 2, end: 2, patch }), + ).toBeUndefined() + }) +}) + +describe("postAllGithub", () => { + it("posts every comment in order when each request succeeds", async () => { + const sent: string[] = [] + const result = await postAllGithub( + [comment("first", 2), comment("second", 1)], + fake((item) => { + sent.push(item.id) + return { success: true } + }), + ) + expect(sent).toEqual(["first", "second"]) + expect(result.posted.map((item) => item.id)).toEqual(["first", "second"]) + expect(result.failure).toBeUndefined() + }) + + it("stops at the first failure and keeps the unposted comments", async () => { + const sent: string[] = [] + const result = await postAllGithub( + [comment("first", 2), comment("second", 1), comment("third", 1)], + fake((item) => { + sent.push(item.id) + return item.id === "second" ? { success: false, error: "boom" } : { success: true } + }), + ) + expect(sent).toEqual(["first", "second"]) + expect(result.posted.map((item) => item.id)).toEqual(["first"]) + expect(result.failure).toBe("boom") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/composer-action-order.test.ts b/packages/kilo-vscode/tests/unit/composer-action-order.test.ts new file mode 100644 index 00000000000..5305522bf70 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/composer-action-order.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "bun:test" +import fs from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "../..") +const CSS = fs + .readFileSync(path.join(ROOT, "webview-ui/agent-manager/pr/pr-panel.css"), "utf-8") + .replace(/\/\*[\s\S]*?\*\//g, "") + +function blocks(source: string) { + return source + .split("}") + .map((chunk) => { + const open = chunk.lastIndexOf("{") + if (open === -1) return undefined + return { + selectors: chunk + .slice(0, open) + .split(",") + .map((value) => value.trim()) + .filter(Boolean), + body: chunk.slice(open + 1), + } + }) + .filter((value): value is { selectors: string[]; body: string } => value !== undefined) +} + +describe("diff composer action ordering", () => { + it("orders the split-button wrapper, not the inner primary button", () => { + const rules = blocks(CSS) + const ordered = rules.filter( + (rule) => + rule.selectors.some((selector) => selector.startsWith('.am-pr-comment-composer[data-action="diff"]')) && + /(^|[;\s])order\s*:/.test(rule.body), + ) + const targets = ordered.flatMap((rule) => rule.selectors.map((selector) => selector)) + const inner = targets.filter((selector) => selector.includes('[data-action="send-primary"]')) + expect(inner, "the inner send-primary keeps DOM order; the wrapper carries flex order").toEqual([]) + expect( + targets.some((selector) => selector.includes(".am-split-button")), + "the split-button wrapper must carry the flex order", + ).toBe(true) + }) + + it("keeps preview before the send group and save/cancel on the left", () => { + const rules = blocks(CSS) + const order = (needle: string) => { + const rule = rules.find((item) => + item.selectors.some( + (selector) => selector.startsWith('.am-pr-comment-composer[data-action="diff"]') && selector.includes(needle), + ), + ) + const match = rule?.body.match(/(?:^|[;\s])order\s*:\s*(\d+)/) + return match ? Number(match[1]) : 0 + } + expect(order('[data-action="save"]')).toBeLessThan(order(".am-split-button")) + expect(order('[data-action="preview"]')).toBeLessThan(order(".am-split-button")) + expect(order('[data-action="cancel"]')).toBeLessThan(order('[data-slot="comment-actions-gap"]')) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/diff-comment-form.test.ts b/packages/kilo-vscode/tests/unit/diff-comment-form.test.ts new file mode 100644 index 00000000000..676c3d0b041 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/diff-comment-form.test.ts @@ -0,0 +1,4 @@ +import { it } from "bun:test" +import { fixture } from "../fixtures/run" + +it("routes the unified diff composer to Kilo, GitHub, save, and cancel", () => fixture("diff-comment-form"), 30_000) diff --git a/packages/kilo-vscode/tests/unit/diff-preview-request.test.ts b/packages/kilo-vscode/tests/unit/diff-preview-request.test.ts index 1a3e9d4cadb..2c99b863e4a 100644 --- a/packages/kilo-vscode/tests/unit/diff-preview-request.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-preview-request.test.ts @@ -285,6 +285,9 @@ describe("diff preview detail requests", () => { export const useServer = () => ({}) export const FullScreenDiffView = (props) => { state.view = props; return "" } export const Toast = { Region: () => "" } + export const reviewRequest = () => {} + export const createPRDiffs = () => [] + export const createDiffCommentForms = () => ({ mount: () => () => {} }) ${[ "DialogProvider", "CodeComponentProvider", @@ -303,6 +306,9 @@ describe("diff preview detail requests", () => { "Diff", "File", "Icon", + "IconButton", + "Button", + "Spinner", "DiffPickerHeader", "BaseBranchPicker", ] diff --git a/packages/kilo-vscode/tests/unit/diff-viewer-provider.test.ts b/packages/kilo-vscode/tests/unit/diff-viewer-provider.test.ts index 5c36b051bf8..f93ec45271c 100644 --- a/packages/kilo-vscode/tests/unit/diff-viewer-provider.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-viewer-provider.test.ts @@ -1,19 +1,24 @@ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" import * as vscode from "vscode" import { DiffViewerProvider } from "../../src/diff/DiffViewerProvider" +import * as gh from "../../src/agent-manager/gh" +import * as shell from "../../src/agent-manager/shell-env" import type { DiffPRPoller, DiffPRPollerOptions } from "../../src/diff/pr-poller" import type { PRComment, PRStatus } from "../../src/agent-manager/types" import type { PRReviewCommentData } from "../../src/shared/review-comments" import type { PanelContext } from "../../src/diff/types" +import type { PRTarget } from "../../src/shared/pr-comment-actions" const addCommentReaction = mock(async (_commentId: string, _reaction: string, _cwd: string) => {}) const removeCommentReaction = mock(async (_commentId: string, _reaction: string, _cwd: string) => {}) +const execGhInput = mock(async () => ({ stdout: "{}", stderr: "" })) const isPRReactionContent = (value: unknown): value is string => typeof value === "string" && ["THUMBS_UP", "THUMBS_DOWN", "LAUGH", "HOORAY", "CONFUSED", "HEART", "ROCKET", "EYES"].includes(value) mock.module("../../src/agent-manager/pr/PRActions", () => ({ addCommentReaction, + execGhInput, isPRReactionContent, removeCommentReaction, })) @@ -39,6 +44,7 @@ afterEach(() => { beforeEach(() => { addCommentReaction.mockReset() removeCommentReaction.mockReset() + execGhInput.mockReset() }) function event() { @@ -96,6 +102,8 @@ function harness() { add?: boolean success?: boolean error?: string + snapshot?: unknown + target?: PRTarget }> = [] const received = event() const disposed = event() @@ -220,6 +228,76 @@ describe("DiffViewerProvider.openFromCommand", () => { }) describe("DiffViewerProvider remote PR comments", () => { + it("routes PR snapshot loading and new comment creation from the standalone panel", async () => { + const read = spyOn(gh, "execGhRead").mockImplementation(async (args) => { + if (args.some((arg) => arg.includes("/files?"))) + return { + stdout: JSON.stringify([ + { + filename: "src/app.ts", + status: "modified", + additions: 1, + deletions: 1, + patch: "@@ -1 +1 @@\n-old\n+new", + }, + ]), + stderr: "", + } + return { + stdout: JSON.stringify({ + number: 42, + html_url: "https://github.com/example/repo/pull/42", + head: { sha: "a".repeat(40) }, + base: { sha: "b".repeat(40) }, + changed_files: 1, + state: "open", + merged: false, + }), + stderr: "", + } + }) + spyOn(shell, "execWithShellEnv").mockResolvedValue({ stdout: "feature\n", stderr: "" }) + const h = harness() + h.pollers.at(0)!.onStatus("diff", status(), undefined, "feature") + const target = h.posted.findLast((message) => message.type === "diffViewer.prComments")?.target + if (!target) throw new Error("Missing PR target") + + h.received.fire({ ...target, type: "agentManager.loadPRFiles", requestId: "load" }) + await new Promise((resolve) => setTimeout(resolve, 0)) + const loaded = h.messages("agentManager.loadPRFilesResult").at(-1) + expect(loaded).toMatchObject({ success: true, requestId: "load" }) + if (!loaded?.snapshot || typeof loaded.snapshot !== "object") throw new Error("Missing PR snapshot") + + execGhInput.mockResolvedValueOnce({ + stdout: JSON.stringify({ + id: 11, + commit_id: "a".repeat(40), + path: "src/app.ts", + side: "RIGHT", + line: 1, + }), + stderr: "", + }) + h.received.fire({ + ...target, + type: "agentManager.createReviewComment", + requestId: "comment", + snapshotId: (loaded.snapshot as { id: string }).id, + path: "src/app.ts", + side: "RIGHT", + startLine: 1, + endLine: 1, + body: "Please update this.", + }) + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(h.messages("agentManager.createReviewCommentResult").at(-1)).toMatchObject({ + success: true, + requestId: "comment", + }) + expect(h.pollers.at(0)!.refresh).toHaveBeenCalled() + read.mockRestore() + }) + it("adds and removes reactions on comments in the standalone diff", async () => { const h = harness() const item = comment() diff --git a/packages/kilo-vscode/tests/unit/inline-comment-form.test.ts b/packages/kilo-vscode/tests/unit/inline-comment-form.test.ts new file mode 100644 index 00000000000..7c5ec845fd2 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/inline-comment-form.test.ts @@ -0,0 +1,8 @@ +import { it } from "bun:test" +import { fixture } from "../fixtures/run" + +it( + "keeps compact shared comment actions, keyboard behavior, and publication safety", + () => fixture("inline-comment-form"), + 30_000, +) diff --git a/packages/kilo-vscode/tests/unit/pr-diff.test.ts b/packages/kilo-vscode/tests/unit/pr-diff.test.ts new file mode 100644 index 00000000000..1350a1be9f8 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/pr-diff.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "bun:test" +import { canCommentOnPRLine, createPRDiffs } from "../../webview-ui/diff-viewer/pr-diff" +import type { PRDiffSnapshot } from "../../src/shared/pr-comment-actions" + +const patch = ["@@ -1,3 +1,4 @@", " one", "-two", "+updated", "+another", " three", ""].join("\n") + +const snapshot: PRDiffSnapshot = { + id: "snapshot-1", + head: "a".repeat(40), + files: [{ path: "src/file.ts", status: "modified", patch }], +} + +describe("PR diff adapter", () => { + it("projects complete GitHub patches into diff viewer files", () => { + expect(createPRDiffs(snapshot)).toEqual([ + { + file: "src/file.ts", + before: "one\ntwo\nthree\n", + after: "one\nupdated\nanother\nthree\n", + patch: "--- a/src/file.ts\n+++ b/src/file.ts\n" + patch, + additions: 2, + deletions: 1, + status: "modified", + tracked: true, + stamp: "snapshot-1", + }, + ]) + }) + + it("accepts only ranges represented by the PR patch", () => { + expect(canCommentOnPRLine(snapshot, "src/file.ts", "RIGHT", 2, 3)).toBe(true) + expect(canCommentOnPRLine(snapshot, "src/file.ts", "LEFT", 2, 2)).toBe(true) + expect(canCommentOnPRLine(snapshot, "src/file.ts", "RIGHT", 4, 4)).toBe(true) + expect(canCommentOnPRLine(snapshot, "src/file.ts", "RIGHT", 5, 5)).toBe(false) + expect(canCommentOnPRLine(snapshot, "other.ts", "RIGHT", 2, 2)).toBe(false) + }) + + it("does not project unsupported files", () => { + expect(createPRDiffs({ ...snapshot, files: [{ path: "image.png", status: "modified" }] })).toEqual([]) + }) + + it("accepts a line when GitHub rewrites a control-character escape in its patch", () => { + // GitHub reports `\^@` where git reports the literal `\u0000` escape. + const api = "@@ -1,2 +1,2 @@\n context\n-return key(a, b)\n+return `${a}\\^@${b}`" + const value: PRDiffSnapshot = { + id: "rewrite", + head: "a".repeat(40), + files: [{ path: "app.ts", status: "modified", patch: api }], + } + expect(canCommentOnPRLine(value, "app.ts", "RIGHT", 2, 2)).toBe(true) + expect(canCommentOnPRLine(value, "app.ts", "LEFT", 2, 2)).toBe(true) + expect(canCommentOnPRLine(value, "app.ts", "RIGHT", 3, 3)).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/pr-review-actions.test.ts b/packages/kilo-vscode/tests/unit/pr-review-actions.test.ts index 9b44059a813..82e2678ccc3 100644 --- a/packages/kilo-vscode/tests/unit/pr-review-actions.test.ts +++ b/packages/kilo-vscode/tests/unit/pr-review-actions.test.ts @@ -116,7 +116,17 @@ function harness() { }) const review = (snapshot: { id: string }, fields: Record = {}) => send("submitPRReview", { snapshotId: snapshot.id, event: "APPROVE", head, body: "", ...fields }) - return { context, host, actions, sent, refresh, send, load, comment, review } + return { + context, + host, + actions, + sent, + refresh, + send, + load, + comment, + review, + } } function transport( @@ -147,6 +157,32 @@ function transport( } describe("commit-bound PR review actions", () => { + it("rejects a request when the checked-out branch changed", async () => { + const h = harness() + const completion = Promise.withResolvers() + const actions = new PRReviewActions({ + context: () => h.context, + post: completion.resolve, + refresh: () => {}, + dirtyFiles: () => [], + checkBranch: async () => "other", + }) + expect( + actions.handle({ + type: "agentManager.loadPRFiles", + projectId: h.context.projectId, + worktreeId: h.context.worktreeId, + prNumber: h.context.pr.number, + prUrl: h.context.pr.url, + requestId: "branch", + }), + ).toBe(true) + const result = await completion.promise + expect(result.success).toBe(false) + expect(result.error).toContain("Diff branch changed") + expect(execute).not.toHaveBeenCalled() + }) + it("loads actual GitHub patches and posts exact raw body with a multiline range", async () => { let input: Record | undefined transport([file], (value) => { diff --git a/packages/kilo-vscode/tests/unit/review-annotations.test.ts b/packages/kilo-vscode/tests/unit/review-annotations.test.ts new file mode 100644 index 00000000000..c96b646f5b8 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/review-annotations.test.ts @@ -0,0 +1,202 @@ +import { afterEach, describe, expect, it } from "bun:test" +import { Window } from "happy-dom" +import { + buildReviewAnnotation, + type AnnotationLabels, + type AnnotationMeta, + type CommentFormActions, + type CommentFormMount, +} from "../../webview-ui/diff-viewer/review-annotations" + +const labels: AnnotationLabels = { + commentOnLine: (line) => `Comment on line ${line}`, + editCommentOnLine: (line) => `Edit comment on line ${line}`, + placeholder: "Comment", + cancel: "Cancel", + comment: "Comment", + send: "Send", + save: "Save", + sendToChat: "Send to chat", + edit: "Edit", + delete: "Delete", +} + +const original = { + document: globalThis.document, + window: globalThis.window, + raf: globalThis.requestAnimationFrame, + cancel: globalThis.cancelAnimationFrame, +} + +let frames: FrameRequestCallback[] = [] + +afterEach(() => { + globalThis.document = original.document + globalThis.window = original.window + globalThis.requestAnimationFrame = original.raf + globalThis.cancelAnimationFrame = original.cancel + frames = [] +}) + +function setup() { + const view = new Window() + globalThis.document = view.document + globalThis.window = view as unknown as Window & typeof globalThis + globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => { + frames.push(callback) + return frames.length + }) as typeof requestAnimationFrame + globalThis.cancelAnimationFrame = () => {} + return view +} + +function flushFrames() { + for (let i = 0; i < 40 && frames.length; i += 1) frames.shift()?.(0) +} + +function annotation(): AnnotationMeta { + return { type: "draft", comment: null, file: "src/file.ts", side: "additions", line: 2, endLine: 2 } +} + +const diffs = [ + { + file: "src/file.ts", + before: "old\n", + after: "new\n", + additions: 1, + deletions: 1, + patch: "@@ -1 +1 @@\n-old\n+new\n", + }, +] + +function build(opts: { mount?: CommentFormMount; destination?: "local" | "github" } = {}) { + const meta = annotation() + if (opts.destination) meta.destination = opts.destination + const added: string[] = [] + const sent: string[] = [] + const destinations: string[] = [] + const disposals: Array<() => void> = [] + let success = 0 + let cancelled = 0 + const root = buildReviewAnnotation( + { side: "additions", lineNumber: 2, metadata: meta }, + { + diffs, + editing: null, + setEditing: () => {}, + addComment: (_file, _side, _line, text) => added.push(text), + sendComment: (_file, _side, _line, text) => sent.push(text), + updateComment: () => {}, + deleteComment: () => {}, + cancelDraft: () => cancelled++, + completeRemoteDraft: () => success++, + onDestination: (value) => destinations.push(value), + labels, + activeTerminalId: () => undefined, + mount: opts.mount, + track: (_meta, _host, dispose) => disposals.push(dispose), + }, + ) + return { root, meta, added, sent, destinations, disposals, success: () => success, cancelled: () => cancelled } +} + +function mountField() { + const field = document.createElement("textarea") + field.className = "mounted-field" + const actions = document.createElement("div") + actions.className = "am-pr-comment-actions" + const submit = document.createElement("button") + submit.setAttribute("data-action", "submit") + actions.appendChild(submit) + return { field, actions } +} + +describe("review annotation draft", () => { + it("mounts one form and forwards save, send, destination, success, and cancel", () => { + setup() + let actions: CommentFormActions | undefined + let mountedHost: HTMLElement | undefined + const mount: CommentFormMount = (host, _meta, value) => { + actions = value + mountedHost = host + const parts = mountField() + host.appendChild(parts.field) + host.appendChild(parts.actions) + return () => host.replaceChildren() + } + const result = build({ mount }) + if (!result.root) throw new Error("Missing annotation") + expect(result.root.dataset.mounted).toBe("true") + expect(result.root.querySelector(".am-annotation-destination")).toBeNull() + expect(mountedHost).not.toBeUndefined() + if (!actions) throw new Error("Missing actions") + + actions.onBodyChange("Draft text") + expect(result.meta.text).toBe("Draft text") + actions.onSave("Saved body", "selected") + expect(result.added).toEqual(["Saved body"]) + actions.onSend("Sent body", "selected") + expect(result.sent).toEqual(["Sent body"]) + actions.onDestination("github") + expect(result.meta.destination).toBe("github") + expect(result.destinations).toEqual(["github"]) + actions.onGithubSuccess() + expect(result.success()).toBe(1) + actions.onCancel() + expect(result.cancelled()).toBe(1) + }) + + it("focuses the mounted form editor", () => { + setup() + const mount: CommentFormMount = (host) => { + const parts = mountField() + host.appendChild(parts.field) + host.appendChild(parts.actions) + return () => host.replaceChildren() + } + const result = build({ mount }) + if (!result.root) throw new Error("Missing annotation") + document.body.appendChild(result.root) + flushFrames() + expect(document.activeElement).toBe(result.root.querySelector(".am-annotation-form textarea")) + }) + + it("falls back to a native composer without a mount", async () => { + setup() + const result = build() + if (!result.root) throw new Error("Missing annotation") + document.body.appendChild(result.root) + flushFrames() + const textarea = result.root.querySelector("textarea") + if (!textarea) throw new Error("Missing textarea") + expect(document.activeElement).toBe(textarea) + textarea.value = "Native comment" + textarea.dispatchEvent(new window.Event("input", { bubbles: true })) + textarea.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true })) + expect(result.added).toEqual(["Native comment"]) + const send = [...result.root.querySelectorAll("button")].find((button) => button.textContent === "Send") + if (!send) throw new Error("Missing send button") + textarea.value = "To chat" + textarea.dispatchEvent(new window.Event("input", { bubbles: true })) + send.click() + expect(result.sent).toEqual(["To chat"]) + }) + + it("disposes the mounted form when the lifecycle releases it", () => { + setup() + let released = 0 + const mount: CommentFormMount = (host) => { + const parts = mountField() + host.appendChild(parts.field) + host.appendChild(parts.actions) + return () => { + released++ + host.replaceChildren() + } + } + const result = build({ mount }) + if (!result.root) throw new Error("Missing annotation") + result.disposals[0]?.() + expect(released).toBe(1) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/send-all-button.test.ts b/packages/kilo-vscode/tests/unit/send-all-button.test.ts new file mode 100644 index 00000000000..2ce91bd01bb --- /dev/null +++ b/packages/kilo-vscode/tests/unit/send-all-button.test.ts @@ -0,0 +1,4 @@ +import { it } from "bun:test" +import { fixture } from "../fixtures/run" + +it("routes the send-all split button to chat or GitHub", () => fixture("send-all-button"), 30_000) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 40139a719a9..4ee355de189 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -180,6 +180,7 @@ import { useTabScroll } from "./tab-scroll" import { DiffPanelCache } from "./DiffPanelCache" import { createPRNavigation, PRPanelHost } from "./pr/PRPanelHost" import { createPRReview } from "./pr/review" +import { createPRDiffCommentState } from "./pr/diff-comment-state" import { createRevertFile } from "./revert-file" import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView" import { createApplyToLocal } from "./apply-to-local" @@ -1700,6 +1701,16 @@ const AgentManagerContent: Component = () => { panels.open(SidePanel.Diff) }, }) + const prDiffComments = createPRDiffCommentState({ + post: vscode.postMessage, + project: activeProjectId, + statuses: prStatuses, + }) + createEffect(() => { + const ctx = diffCtx() + if (!ctx || (!diffOpen() && !reviewActive())) return + prDiffComments.load(ctx) + }) createEffect(() => { const panel = diffOpen() const active = reviewActive() @@ -2600,6 +2611,10 @@ const AgentManagerContent: Component = () => { } remoteComments={remote.comments} remoteTarget={remote.target} + prTarget={prDiffComments.target} + prSnapshot={prDiffComments.snapshot} + prLoading={prDiffComments.loading} + prError={prDiffComments.error} focusedComment={remote.focus} composer={composers.get} lead={() => diffScopeControls(true)} @@ -2709,6 +2724,10 @@ const AgentManagerContent: Component = () => { sessionKey={`${activeProjectId() ?? "single"}\0${diffScopeId() ?? ""}`} projectId={activeProjectId()} worktreeId={diffCtx()} + prTarget={prDiffComments.target(diffCtx())} + prSnapshot={prDiffComments.snapshot(diffCtx())} + prLoading={prDiffComments.loading(diffCtx())} + prError={prDiffComments.error(diffCtx())} notice={diffNotice()} lead={diffScopeControls(false)} canRevert={scopeCapabilities(review.scope()).revert} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 68d51ecebbd..1cde76b474d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -1,9 +1,9 @@ import { type Component, createMemo, Show, type JSXElement } from "solid-js" import { Accordion } from "@kilocode/kilo-ui/accordion" import { Icon } from "@kilocode/kilo-ui/icon" -import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" -import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../src/context/language" import { DiffStyleSelect } from "../diff-viewer/InlineSelect" import { @@ -23,6 +23,9 @@ import { RemoteCommentsOutside } from "../diff-viewer/remote-comment-renderer" import { ReviewDiffItem } from "../diff-viewer/ReviewDiffItem" import { createReviewView, type ReviewViewProps } from "../diff-viewer/review-controller" import { notice, reviewSendAllKeybind } from "../diff-viewer/review-setup" +import { SendAllButton } from "../diff-viewer/SendAllButton" +import { createDiffCommentForms } from "./pr/diff-comment-forms" +import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" // --- Data model --- @@ -43,6 +46,10 @@ interface DiffPanelProps extends ReviewViewProps { lead?: JSXElement /** Defaults to true. Hides the per-file Revert action when false. */ canRevert?: boolean + prTarget?: PRTarget + prSnapshot?: PRDiffSnapshot + prLoading?: boolean + prError?: string } export const DiffPanel: Component = (props) => { @@ -50,6 +57,12 @@ export const DiffPanel: Component = (props) => { const noticeText = () => notice(t, props.notice) const sendAllKeybind = () => reviewSendAllKeybind(t) let rootRef: HTMLDivElement | undefined + const forms = createDiffCommentForms({ + target: () => props.prTarget, + snapshot: () => props.prSnapshot, + diffs: () => props.diffs, + worktree: () => props.worktreeId ?? props.sessionId ?? "diff", + }) const { open, setOpen, @@ -70,7 +83,15 @@ export const DiffPanel: Component = (props) => { commentsByFile, handleGutterClick, sendAllClick, - } = createReviewView(props, () => rootRef) + sendAllToGithub, + sendAllGithubCount, + sendAllGithubAvailable, + sendAllPending, + sendAllError, + } = createReviewView(props, () => rootRef, { + commentForm: forms.mount, + commentsGithub: forms.github, + }) const handleExpandAll = () => { setOpen(toggleOpenFiles(props.diffs, open())) @@ -95,6 +116,16 @@ export const DiffPanel: Component = (props) => { what you're looking at and is the primary control. Always shown, so an empty scope can still be switched away from. */} {props.lead} + + {(target) => ( + + {t("diffViewer.comment.prContext", { number: target().prNumber })} + + + + + )} + 0}> <> = (props) => { + + +
@@ -222,11 +261,20 @@ export const DiffPanel: Component = (props) => { {comments().length} comment{comments().length !== 1 ? "s" : ""} - - - + + + {sendAllError()} + + +
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanelCache.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanelCache.tsx index 4a14ef4f7b2..c3aa2f67fb0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanelCache.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanelCache.tsx @@ -3,6 +3,7 @@ import type { WorktreeFileDiff } from "../src/types/messages" import type { ReviewComment } from "../diff-viewer/review-comments" import type { ReviewComposer } from "../diff-viewer/review-annotations" import type { PRComment } from "./pr/pr-types" +import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" import { DiffPanel } from "./DiffPanel" import { diffDataKey } from "./worktree-diffs" @@ -29,6 +30,10 @@ interface Props { comments: (ctx: string) => ReviewComment[] remoteComments?: (ctx: string) => PRComment[] remoteTarget?: (ctx: string, comment: PRComment) => import("../../src/shared/pr-comment-actions").PRTarget | undefined + prTarget?: (ctx: string) => PRTarget | undefined + prSnapshot?: (ctx: string) => PRDiffSnapshot | undefined + prLoading?: (ctx: string) => boolean + prError?: (ctx: string) => string | undefined focusedComment?: (key: string) => { id: string; file: string } | undefined setComments: (ctx: string, comments: ReviewComment[]) => void composer: (key: string) => ReviewComposer @@ -121,6 +126,10 @@ export const DiffPanelCache: Component = (props) => { ? props.remoteTarget?.(entry.ctx, comment) : undefined } + prTarget={props.prTarget?.(entry.ctx)} + prSnapshot={props.prSnapshot?.(entry.ctx)} + prLoading={props.prLoading?.(entry.ctx)} + prError={props.prError?.(entry.ctx)} focusedComment={active() ? props.focusedComment?.(entry.key) : undefined} onCommentsChange={(comments) => props.setComments(entry.key, comments)} composer={props.composer(entry.cacheKey)} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index b1531dd183a..d9fd5fb8c65 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -2468,6 +2468,44 @@ body.am-wt-dragging-active * { font-size: inherit; color: var(--text-weak); font-weight: 500; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; +} + +.am-annotation-draft[data-mounted] { + font-family: var(--vscode-font-family, sans-serif); + font-size: var(--font-size-base); + padding: 8px; + gap: 4px; + border: 1px solid var(--border-base); + border-radius: 6px; +} + +.am-annotation-draft[data-mounted]:focus-within { + border-color: var(--border-focus); +} + +.am-annotation-form { + min-width: 0; +} + +.am-diff-pr-context, +.am-review-pr-context { + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--text-weak); + font-size: var(--kilo-font-size-11); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.am-diff-pr-context svg, +.am-review-pr-context svg { + flex: 0 0 auto; } .am-annotation-textarea { @@ -2654,6 +2692,15 @@ body.am-wt-dragging-active * { color: var(--text-weak); } +.am-review-send-error { + margin: 0 8px; + font-size: var(--font-size-small); + color: var(--vscode-testing-iconFailed, #f87171); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* Setup overlay */ .am-setup-overlay { @@ -2808,7 +2855,7 @@ body.am-wt-dragging-active * { .am-split-button[data-variant="primary"] > [data-component="button"] { flex: 1 1 auto; min-width: 0; - min-height: 26px; + min-height: 24px; border: 0; border-radius: 2px 0 0 2px; background: transparent; @@ -2826,7 +2873,7 @@ body.am-wt-dragging-active * { width: 32px; min-width: 32px; height: auto; - min-height: 26px; + min-height: 24px; padding: 0; border: 0; border-radius: 0 2px 2px 0; @@ -2916,6 +2963,19 @@ body.am-wt-dragging-active * { padding: 6px 10px; } +/* Two explicit send-all actions; only the chat action shows the shortcut. */ +.am-send-all-actions { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.am-send-all-actions [data-component="spinner"] { + width: 12px; + height: 12px; + margin-right: 4px; +} + .am-worktree-menu-gap { display: inline-flex; width: 16px; diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 9dffe7fcd8d..cd06d6effea 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -227,6 +227,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "إرسال الكل إلى الدردشة ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "التعليقات المحلية ({{count}})", "agentManager.review.prCount": "تعليقات PR ({{count}})", "agentManager.review.fileCount": "{{count}} ملفًا", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 50696625a1d..851501b94b3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -233,6 +233,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Enviar tudo para o chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Comentários locais ({{count}})", "agentManager.review.prCount": "Comentários de PR ({{count}})", "agentManager.review.fileCount": "{{count}} arquivos", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index 786933dd7f7..00fbd5b4641 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -231,6 +231,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Pošalji sve u chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Lokalni komentari ({{count}})", "agentManager.review.prCount": "PR komentari ({{count}})", "agentManager.review.fileCount": "{{count}} datoteka", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 954b34e272e..6eaa03ca137 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -232,6 +232,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Send alt til chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Lokale kommentarer ({{count}})", "agentManager.review.prCount": "PR-kommentarer ({{count}})", "agentManager.review.fileCount": "{{count}} filer", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 4384386718c..342b2e56e2d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -239,6 +239,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Alles an den Chat senden ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Lokale Kommentare ({{count}})", "agentManager.review.prCount": "PR-Kommentare ({{count}})", "agentManager.review.fileCount": "{{count}} Dateien", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 01e27fcf5a6..a70c83d1731 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -232,6 +232,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Send all to chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Local comments ({{count}})", "agentManager.review.prCount": "PR comments ({{count}})", "agentManager.review.fileCount": "{{count}} files", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index 58b6138743c..e1fdbd6bc08 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -236,6 +236,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Enviar todo al chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Comentarios locales ({{count}})", "agentManager.review.prCount": "Comentarios del PR ({{count}})", "agentManager.review.fileCount": "{{count}} archivos", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts index 8ff591fb0f5..ca520e11b53 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -235,6 +235,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "ارسال همه به چت ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "نظرات محلی ({{count}})", "agentManager.review.prCount": "نظرات PR ({{count}})", "agentManager.review.fileCount": "{{count}} فایل", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index f0bef734c42..5564ddbaeac 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -239,6 +239,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Tout envoyer au chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Commentaires locaux ({{count}})", "agentManager.review.prCount": "Commentaires du PR ({{count}})", "agentManager.review.fileCount": "{{count}} fichiers", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index ba04dafd8ca..cee4a10a6a2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -241,6 +241,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Invia tutto alla chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "Cmd+Invio", "agentManager.review.sendAllShortcut.other": "Ctrl+Invio", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Commenti locali ({{count}})", "agentManager.review.prCount": "Commenti della PR ({{count}})", "agentManager.review.fileCount": "{{count}} file", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index 4d8c6ef2106..4893d46fe9d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -232,6 +232,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "すべてをチャットに送信 ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "ローカルコメント ({{count}})", "agentManager.review.prCount": "PRコメント ({{count}})", "agentManager.review.fileCount": "{{count}} ファイル", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index 959e6ebe365..1a6ed160740 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -230,6 +230,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "모두 채팅으로 보내기 ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "로컬 댓글 ({{count}})", "agentManager.review.prCount": "PR 댓글 ({{count}})", "agentManager.review.fileCount": "{{count}}개 파일", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 4c2e6210684..7f4900b8a25 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -239,6 +239,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Alles naar chat sturen ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Lokale opmerkingen ({{count}})", "agentManager.review.prCount": "PR-opmerkingen ({{count}})", "agentManager.review.fileCount": "{{count}} bestanden", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index c3202c70b7c..eae5c82153a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -230,6 +230,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Send alt til chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Lokale kommentarer ({{count}})", "agentManager.review.prCount": "PR-kommentarer ({{count}})", "agentManager.review.fileCount": "{{count}} filer", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index ca3d0ef15cd..05bbd1c041d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -232,6 +232,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Wyślij wszystko do czatu ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Komentarze lokalne ({{count}})", "agentManager.review.prCount": "Komentarze PR ({{count}})", "agentManager.review.fileCount": "{{count}} plików", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 0e8abb52bc8..5d63d7a94e5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -235,6 +235,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Отправить всё в чат ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Локальные комментарии ({{count}})", "agentManager.review.prCount": "Комментарии PR ({{count}})", "agentManager.review.fileCount": "{{count}} файлов", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index c169772521f..5276639611e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -226,6 +226,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "ส่งทั้งหมดไปยังแชท ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "ความคิดเห็นในเครื่อง ({{count}})", "agentManager.review.prCount": "ความคิดเห็น PR ({{count}})", "agentManager.review.fileCount": "{{count}} ไฟล์", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index 97db13c415b..570e18ad869 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -240,6 +240,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Tümünü sohbete gönder ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Yerel yorumlar ({{count}})", "agentManager.review.prCount": "PR yorumları ({{count}})", "agentManager.review.fileCount": "{{count}} dosya", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 08755e2b1c9..59b52c1ba8d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -243,6 +243,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Надіслати все до чату ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "Локальні коментарі ({{count}})", "agentManager.review.prCount": "Коментарі PR ({{count}})", "agentManager.review.fileCount": "{{count}} файлів", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index cfd7cd449e6..c2c32cfe1f0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -222,6 +222,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "全部发送到聊天 ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "本地评论 ({{count}})", "agentManager.review.prCount": "PR 评论 ({{count}})", "agentManager.review.fileCount": "{{count}} 个文件", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 194cd10acd5..f5af93c1413 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -222,6 +222,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "全部傳送到聊天 ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", "agentManager.review.inlineCount": "本機留言 ({{count}})", "agentManager.review.prCount": "PR 留言 ({{count}})", "agentManager.review.fileCount": "{{count}} 個檔案", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentForm.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentForm.tsx index d41f00c849a..bac5515e906 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentForm.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentForm.tsx @@ -1,5 +1,8 @@ -import { For, Show, createSignal } from "solid-js" +import { For, Show, createMemo, createSignal, untrack } from "solid-js" +import { createStore } from "solid-js/store" import { Button } from "@kilocode/kilo-ui/button" +import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu" +import { Icon } from "@kilocode/kilo-ui/icon" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Spinner } from "@kilocode/kilo-ui/spinner" @@ -16,13 +19,64 @@ interface Draft { pending?: string error?: string preview?: boolean + destination?: "local" | "github" sent?: "reply" | "create" | "edit" | "delete" | "line" | "review" event?: "APPROVE" | "REQUEST_CHANGES" | "COMMENT" } -type Props = { projectId?: string; worktreeId: string } & ( +type Props = { + projectId?: string + worktreeId: string + /** Submit on plain Enter. Diff composers keep their existing Enter-to-send behavior. */ + submitOnEnter?: boolean + /** Called when Escape is pressed in the editor. */ + onEscape?: () => void + /** Replace any stored draft with initialBody. Used when switching comment destination. */ + replaceBody?: boolean + inline?: boolean +} & ( | { action: "reply"; threadId: string } | { action: "create"; prNumber: number; prUrl: string } + | { + projectId?: string + worktreeId: string + action: "local" + file: string + side: "LEFT" | "RIGHT" + startLine: number + endLine: number + selectedText: string + initialBody?: string + onBodyChange?: (body: string) => void + onSubmit: (body: string, selectedText: string) => void + onSend: (body: string, selectedText: string) => void + onCancel: () => void + } + | { + action: "diff" + worktreeId: string + projectId?: string + file: string + side: "LEFT" | "RIGHT" + startLine: number + endLine: number + selectedText: string + destination: "local" | "github" + github?: { + prNumber: number + prUrl: string + snapshotId: string + label: string + closed: boolean + } + initialBody?: string + onBodyChange?: (body: string) => void + onDestinationChange?: (value: "local" | "github") => void + onSave: (body: string, selectedText: string) => void + onSendKilo: (body: string, selectedText: string) => void + onGithubSuccess: () => void + onCancel: () => void + } | (PRTarget & { action: "line" snapshotId: string @@ -30,6 +84,8 @@ type Props = { projectId?: string; worktreeId: string } & ( side: "LEFT" | "RIGHT" startLine: number endLine: number + initialBody?: string + onBodyChange?: (body: string) => void source?: string closed?: boolean onCancel: () => void @@ -58,7 +114,7 @@ type Props = { projectId?: string; worktreeId: string } & ( ) // Keep drafts and in-flight replies across thread collapse and panel remounts. -const [drafts, setDrafts] = createSignal>({}) +const [drafts, setDrafts] = createStore>({}) const blank: Draft = { body: "", open: false } const decisions = [ { event: "COMMENT", action: "review-comment", label: "agentManager.pr.review.comment" }, @@ -66,36 +122,65 @@ const decisions = [ { event: "REQUEST_CHANGES", action: "review-request-changes", label: "agentManager.pr.review.requestChanges" }, ] as const +// The form supports local, inline, reply, edit, and review actions in one shared UI. +// eslint-disable-next-line complexity export function PRCommentForm(props: Props) { const { t } = useLanguage() const vscode = useVSCode() let editor: HTMLInputElement | undefined - const key = () => + // The discriminated union does not narrow inside JSX callbacks, so read the + // diff-only fields through accessors that keep TypeScript happy. + const github = () => (props.action === "diff" ? props.github : undefined) + const key = createMemo(() => JSON.stringify([ props.projectId, props.worktreeId, props.action, - props.action === "reply" ? props.threadId : props.prUrl, + props.action === "reply" + ? props.threadId + : props.action === "local" || props.action === "diff" + ? props.file + : props.prUrl, props.action === "edit" ? props.commentId : undefined, + props.action === "local" || props.action === "diff" + ? [props.file, props.side, props.startLine, props.endLine] + : undefined, props.action === "line" ? [props.snapshotId, props.path, props.side, props.startLine, props.endLine] : undefined, props.action === "review" ? [props.snapshotId, props.head] : undefined, - ]) - const state = () => drafts()[key()] ?? blank + ]), + ) + const destination = () => { + if (props.action !== "diff") return "local" as const + return drafts[key()]?.destination ?? props.destination + } + const state = () => + drafts[key()] ?? + ((props.action === "line" || props.action === "local" || props.action === "diff") && props.initialBody + ? { ...blank, body: props.initialBody } + : blank) const [collapsed, setCollapsed] = createSignal() const compact = () => props.action === "reply" || props.action === "create" - const cancellable = () => props.action === "edit" || compact() + const cancellable = () => props.action === "edit" || props.action === "local" || props.action === "diff" || compact() const expanded = () => !!state().pending || state().open || (collapsed() !== key() && !!(state().body || state().error)) const placeholder = () => t(props.action === "reply" ? "agentManager.pr.comment.replyPlaceholder" : "agentManager.pr.comment.placeholder") - const patch = (value: Partial, id = key()) => - setDrafts((prev) => ({ ...prev, [id]: { ...(prev[id] ?? blank), ...value } })) + const patch = (value: Partial, id = key()) => setDrafts(id, (prev) => ({ ...(prev ?? blank), ...value })) + untrack(() => { + if ( + props.replaceBody && + (props.action === "line" || props.action === "local") && + props.initialBody !== undefined && + drafts[key()]?.body !== props.initialBody + ) + patch({ body: props.initialBody, sent: undefined }) + }) const label = () => props.action === "reply" ? t("agentManager.pr.comment.reply") : props.action === "review" ? t("agentManager.pr.review.summary") - : props.action === "create" || props.action === "line" + : props.action === "create" || props.action === "line" || props.action === "local" || props.action === "diff" ? t("agentManager.pr.comment.add") : t("common.edit") const ready = () => @@ -134,18 +219,72 @@ export function PRCommentForm(props: Props) { open: true, sent: undefined, preview: false, - ...(props.action === "edit" && (!drafts()[key()] || state().sent) ? { body: props.body } : {}), + ...(props.action === "edit" && (!drafts[key()] || state().sent) ? { body: props.body } : {}), }) queueMicrotask(() => editor?.focus()) } function cancel() { if (state().pending) return + if (props.action === "local" || props.action === "diff") { + patch({ body: "", error: undefined, preview: false, sent: undefined }) + props.onCancel() + return + } setCollapsed(key()) patch({ open: false }) } + function sendKilo() { + if (props.action !== "diff" || !ready()) return + props.onSendKilo(state().body, props.selectedText) + patch({ body: "", open: false, preview: false, sent: "line" }) + } + + function saveLocal() { + if (props.action !== "diff" || !ready()) return + props.onSave(state().body, props.selectedText) + patch({ body: "", open: false, preview: false, sent: "line" }) + } + + function sendGithub() { + const gh = github() + if (props.action !== "diff" || !gh || gh.closed) return + if (!ready()) return + const requestId = crypto.randomUUID() + const message: PRCommentRequest = { + type: "agentManager.createReviewComment", + projectId: props.projectId, + worktreeId: props.worktreeId, + prNumber: gh.prNumber, + prUrl: gh.prUrl, + requestId, + snapshotId: gh.snapshotId, + path: props.file, + side: props.side, + startLine: props.startLine, + endLine: props.endLine, + body: state().body, + } + patch({ pending: requestId, error: undefined, sent: undefined }) + reviewRequest(message, vscode.postMessage, (result) => { + patch({ pending: undefined }) + if (result.success) { + patch({ body: "", open: false, preview: false, sent: "line" }) + props.onGithubSuccess() + return + } + patch({ error: result.error || "failed" }) + }) + } + + function sendPrimary() { + if (destination() === "github") sendGithub() + else sendKilo() + } + function submit(deleting = false) { + if (props.action === "diff") return const body = state().body if ( deleting @@ -153,6 +292,11 @@ export function PRCommentForm(props: Props) { : !ready() ) return + if (props.action === "local") { + patch({ body: "", error: undefined, preview: false, sent: undefined }) + props.onSubmit(body, props.selectedText) + return + } const id = key() const requestId = crypto.randomUUID() const route = { projectId: props.projectId, worktreeId: props.worktreeId } @@ -210,7 +354,7 @@ export function PRCommentForm(props: Props) { } return ( -
+
{t("agentManager.pr.review.own")}

-
- - - - - - - - - -
+ +
+ + + + + + + + + +
+
@@ -359,22 +531,11 @@ export function PRCommentForm(props: Props) {
-
- - + +
+ - - + + + {t("diffViewer.comment.sendToKilo")} + + } + > + {(pr) => ( +
+ + + + + + + + { + if (props.action !== "diff") return + patch({ destination: "local" }) + props.onDestinationChange?.("local") + }} + > + + {t("diffViewer.comment.sendToKilo")} + + { + if (props.action !== "diff") return + patch({ destination: "github" }) + props.onDestinationChange?.("github") + }} + > + + + {t("diffViewer.comment.sendToGithub", { number: pr().prNumber })} + + + + + +
+ )} +
+
+ +
+ {t("diffViewer.comment.unavailable")} +
-
+ + +
+ + + + + + + + + + + + + + +
+
+ +
+ {t("diffViewer.comment.unavailable")} +
+
{(error) => ( + + + (threads().includes(comment.threadId) ? target() : undefined)} @@ -312,6 +432,8 @@ const DiffViewerContent: Component = () => { comments={comments()} onCommentsChange={setComments} onSendAll={() => {}} + commentForm={forms.mount} + commentsGithub={forms.github} diffStyle={diffStyle()} onDiffStyleChange={(style) => { setDiffStyle(style) @@ -331,7 +453,7 @@ const DiffViewerContent: Component = () => { post({ type: "diffViewer.revertFile", file }) }} revertingFiles={reverting()} - canRevert={capabilities()?.revert ?? true} + canRevert={!prMode() && (capabilities()?.revert ?? true)} canComment={capabilities()?.comments ?? true} onClose={() => { post({ type: "diffViewer.close" }) diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index 3883b5d35b4..bbca7abb77b 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -13,7 +13,6 @@ import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Spinner } from "@kilocode/kilo-ui/spinner" import { ResizeHandle } from "@kilocode/kilo-ui/resize-handle" -import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../src/context/language" import { FileTree } from "./FileTree" import { @@ -31,6 +30,9 @@ import { RemoteCommentsOutside } from "./remote-comment-renderer" import { ReviewDiffItem } from "./ReviewDiffItem" import { createReviewView, type ReviewViewProps } from "./review-controller" import { notice, reviewSendAllKeybind } from "./review-setup" +import { SendAllButton } from "./SendAllButton" +import { createDiffCommentForms } from "../agent-manager/pr/diff-comment-forms" +import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" type DiffStyle = "unified" | "split" @@ -49,6 +51,10 @@ interface FullScreenDiffViewProps extends ReviewViewProps { canRevert?: boolean /** Optional leading content rendered first in the toolbar's left group. */ lead?: JSXElement + prTarget?: PRTarget + prSnapshot?: PRDiffSnapshot + prLoading?: boolean + prError?: string onClose: () => void } @@ -57,6 +63,12 @@ export const FullScreenDiffView: Component = (props) => const noticeText = () => notice(t, props.notice) const sendAllKeybind = () => reviewSendAllKeybind(t) let rootRef: HTMLDivElement | undefined + const forms = createDiffCommentForms({ + target: () => props.prTarget, + snapshot: () => props.prSnapshot, + diffs: () => props.diffs, + worktree: () => props.worktreeId ?? props.sessionId ?? "diff", + }) const { open, setOpen, @@ -77,7 +89,15 @@ export const FullScreenDiffView: Component = (props) => commentsByFile, handleGutterClick, sendAllClick, - } = createReviewView(props, () => rootRef) + sendAllToGithub, + sendAllGithubCount, + sendAllGithubAvailable, + sendAllPending, + sendAllError, + } = createReviewView(props, () => rootRef, { + commentForm: props.commentForm ?? forms.mount, + commentsGithub: props.commentsGithub ?? forms.github, + }) const [manualActiveFile, setManualActiveFile] = createSignal>({}) const activeFile = createMemo(() => { @@ -191,6 +211,16 @@ export const FullScreenDiffView: Component = (props) =>
{props.lead} + + {(target) => ( + + {t("diffViewer.comment.prContext", { number: target().prNumber })} + + + + + )} + = (props) => {openLabel()} 0 && props.canComment !== false}> - - - + /> + + + + {sendAllError()} +
+ + + {/* Body: file tree + diff viewer */}
diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/SendAllButton.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/SendAllButton.tsx new file mode 100644 index 00000000000..8c2369da9a9 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/SendAllButton.tsx @@ -0,0 +1,70 @@ +import { Show, type Component } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" +import { useLanguage } from "../src/context/language" + +interface Props { + /** Number of local comments. */ + count: number + /** Number of local comments that can be posted to the PR. */ + githubCount: number + /** PR number when a publishable PR is available. */ + githubNumber?: number + pending: boolean + onSendChat: () => void + onSendGithub: () => void + keybind: string + placement?: "top" | "bottom" +} + +/** + * Send-all actions for the review toolbars. + * + * Without a publishable PR it is the plain send-to-chat button. With a PR it + * shows two explicit buttons. Only the chat button advertises the keyboard + * shortcut, so only it can send everything through the keyboard. + */ +export const SendAllButton: Component = (props) => { + const { t } = useLanguage() + const placement = () => props.placement ?? "top" + const github = () => props.githubNumber + const chatLabel = () => t("agentManager.review.sendAllToChatWithCount", { count: props.count }) + const githubLabel = () => + t("agentManager.review.sendAllToGithubWithCount", { count: props.githubCount, number: github() ?? 0 }) + const Chat = () => ( + + + + ) + return ( + }> +
+ + + + +
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/annotation-lifecycle.ts b/packages/kilo-vscode/webview-ui/diff-viewer/annotation-lifecycle.ts new file mode 100644 index 00000000000..cdef06af60e --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/annotation-lifecycle.ts @@ -0,0 +1,32 @@ +import type { AnnotationMeta } from "./review-annotations" + +// Pierre can replace an annotation without invoking its button handlers. +export function createAnnotationLifecycle() { + const mounts = new Map void; connected: boolean }>() + let observer: MutationObserver | undefined + const release = (meta: AnnotationMeta) => { + const entry = mounts.get(meta) + if (!entry) return + mounts.delete(meta) + entry.dispose() + if (mounts.size) return + observer?.disconnect() + observer = undefined + } + const track = (meta: AnnotationMeta, host: HTMLElement, dispose: () => void) => { + release(meta) + mounts.set(meta, { host, dispose, connected: host.isConnected }) + if (observer) return + observer = new MutationObserver(() => { + for (const [meta, entry] of mounts) { + if (entry.host.isConnected) entry.connected = true + else if (entry.connected) release(meta) + } + }) + observer.observe(document.body, { childList: true, subtree: true }) + } + const clear = () => { + for (const meta of mounts.keys()) release(meta) + } + return { track, clear } +} diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/comments-github.ts b/packages/kilo-vscode/webview-ui/diff-viewer/comments-github.ts new file mode 100644 index 00000000000..4408ec5f1ef --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/comments-github.ts @@ -0,0 +1,136 @@ +import type { Accessor } from "solid-js" +import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" +import { parsePatch } from "../../src/shared/pr-patch" +import type { WorktreeFileDiff } from "../src/types/messages" +import type { ReviewComment } from "./review-comments" +import { canCommentOnPRLine } from "./pr-diff" +import { reviewRequest } from "../agent-manager/pr/pr-review-request" + +/** GitHub context for one local comment. `closed` marks a line outside the PR diff. */ +export interface GithubContext { + prNumber: number + prUrl: string + snapshotId: string + label: string + closed: boolean +} + +export interface CommentsGithub { + /** True when a PR with a loaded snapshot is available for publication. */ + available: () => boolean + /** Resolve the GitHub target for a comment. `closed` means the line is not publishable. */ + resolve: (comment: ReviewComment) => GithubContext | undefined + send: (comment: ReviewComment) => Promise<{ success: boolean; error?: string }> +} + +function side(value: ReviewComment["side"]): "LEFT" | "RIGHT" { + return value === "deletions" ? "LEFT" : "RIGHT" +} + +/** + * Resolve the GitHub review target for one line range. + * + * The line must exist in the PR snapshot and in a complete hunk of the patch. + * A missing or incomplete patch returns a closed context so callers can disable + * the action instead of hiding it. + */ +export function resolveGithubContext(opts: { + target?: PRTarget + snapshot?: PRDiffSnapshot + file: string + side: ReviewComment["side"] + start: number + end: number + patch?: string +}): GithubContext | undefined { + if (!opts.target || !opts.snapshot) return + const mapped = side(opts.side) + const allowed = + !!opts.patch && + !!parsePatch(opts.patch, undefined, { side: mapped, start: opts.start, end: opts.end }) && + canCommentOnPRLine(opts.snapshot, opts.file, mapped, opts.start, opts.end) + return { + prNumber: opts.target.prNumber, + prUrl: opts.target.prUrl, + snapshotId: opts.snapshot.id, + label: `GitHub #${opts.target.prNumber}`, + closed: !allowed, + } +} + +interface Options { + target: Accessor + snapshot: Accessor + diffs: Accessor + post: (message: never) => void + /** Gate publication, for example when only local changes are shown. */ + canPublish?: Accessor +} + +export function createCommentsGithub(opts: Options): CommentsGithub { + const resolve = (comment: ReviewComment) => { + if (opts.canPublish?.() === false) return + const diff = opts.diffs().find((item) => item.file === comment.file) + return resolveGithubContext({ + target: opts.target(), + snapshot: opts.snapshot(), + file: comment.file, + side: comment.side, + start: comment.line, + end: comment.line, + patch: diff?.patch, + }) + } + + const available = () => opts.canPublish?.() !== false && !!opts.target() && !!opts.snapshot() + + const send = (comment: ReviewComment) => { + const { promise, resolve: settle } = Promise.withResolvers<{ success: boolean; error?: string }>() + const target = opts.target() + const context = resolve(comment) + if (!target || !context || context.closed) { + settle({ success: false }) + return promise + } + reviewRequest( + { + type: "agentManager.createReviewComment", + projectId: target.projectId, + worktreeId: target.worktreeId, + prNumber: context.prNumber, + prUrl: context.prUrl, + requestId: crypto.randomUUID(), + snapshotId: context.snapshotId, + path: comment.file, + side: side(comment.side), + startLine: comment.line, + endLine: comment.line, + body: comment.comment, + }, + opts.post, + (result) => settle({ success: result.success, error: result.success ? undefined : result.error }), + ) + return promise + } + + return { available, resolve, send } +} + +/** + * Post comments one at a time and stop at the first failure. + * + * A failed request can still have reached GitHub, so callers must keep the + * failed and unposted comments and surface the error instead of retrying. + */ +export async function postAllGithub( + comments: ReviewComment[], + github: CommentsGithub, +): Promise<{ posted: ReviewComment[]; failure?: string }> { + const posted: ReviewComment[] = [] + for (const comment of comments) { + const result = await github.send(comment) + if (!result.success) return { posted, failure: result.error } + posted.push(comment) + } + return { posted } +} diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/pr-diff.ts b/packages/kilo-vscode/webview-ui/diff-viewer/pr-diff.ts new file mode 100644 index 00000000000..9202c37f91c --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/pr-diff.ts @@ -0,0 +1,61 @@ +import { normalizeHunk } from "@kilocode/kilo-ui/session-diff" +import type { PRDiffSnapshot } from "../../src/shared/pr-comment-actions" +import { parsePatch } from "../../src/shared/pr-patch" +import type { WorktreeFileDiff } from "../src/types/messages" + +type Side = "LEFT" | "RIGHT" + +function status(value: string): WorktreeFileDiff["status"] { + if (value === "added") return value + if (value === "deleted" || value === "removed") return "deleted" + return "modified" +} + +function counts(patch: string) { + const total = { additions: 0, deletions: 0 } + let hunk = false + for (const line of patch.split("\n")) { + if (line.startsWith("@@")) { + hunk = true + continue + } + if (!hunk) continue + if (line.startsWith("+")) total.additions += 1 + if (line.startsWith("-")) total.deletions += 1 + } + return total +} + +export function createPRDiffs(snapshot: PRDiffSnapshot): WorktreeFileDiff[] { + return snapshot.files.flatMap((file) => { + if (!file.patch) return [] + const diff = normalizeHunk(file.path, file.patch) + if (!diff) return [] + const total = counts(file.patch) + return [ + { + file: diff.file, + before: diff.before, + after: diff.after, + patch: diff.patch, + additions: total.additions, + deletions: total.deletions, + status: status(file.status), + tracked: true, + stamp: snapshot.id, + }, + ] + }) +} + +export function canCommentOnPRLine( + snapshot: PRDiffSnapshot | undefined, + file: string, + side: Side, + start: number, + end: number, +): boolean { + const patch = snapshot?.files.find((item) => item.path === file)?.patch + if (!patch) return false + return parsePatch(patch, undefined, { side, start, end }) !== undefined +} diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts index 023eb6bf217..fb1bceb4f1e 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts @@ -18,6 +18,22 @@ export interface AnnotationLabels { delete: string } +export interface CommentFormActions { + body: string + onBodyChange: (body: string) => void + onSave: (body: string, selectedText: string) => void + onSend: (body: string, selectedText: string) => void + onGithubSuccess: () => void + onCancel: () => void + onDestination: (value: "local" | "github") => void +} + +export type CommentFormMount = ( + host: HTMLElement, + meta: AnnotationMeta, + actions: CommentFormActions, +) => (() => void) | undefined + export function labels(t: (key: string, params?: UiI18nParams) => string): AnnotationLabels { return { commentOnLine: (line) => t("agentManager.review.commentOnLine", { line }), @@ -44,6 +60,7 @@ export interface AnnotationMeta { endLine?: number editing?: boolean text?: string + destination?: "local" | "github" } export type ReviewDraft = Pick @@ -91,6 +108,7 @@ export function reviewAnnotationSpeechKey(meta: AnnotationMeta): string | undefi } interface AnnotationHandlers { + track?: (meta: AnnotationMeta, host: HTMLElement, dispose: () => void) => void diffs: WorktreeFileDiff[] editing: string | null setEditing: (id: string | null) => void @@ -99,6 +117,10 @@ interface AnnotationHandlers { updateComment: (id: string, text: string) => void deleteComment: (id: string) => void cancelDraft: () => void + completeRemoteDraft?: (meta: AnnotationMeta) => void + /** Remember the destination so the next comment keeps the same choice. */ + onDestination?: (value: "local" | "github") => void + mount?: CommentFormMount labels: AnnotationLabels activeTerminalId: () => string | undefined speech?: { @@ -109,17 +131,23 @@ interface AnnotationHandlers { } } -function focusWhenConnected(el: HTMLTextAreaElement): void { +function focusWhenConnected(el: HTMLElement): () => void { + if (el.isConnected) { + el.focus() + return () => {} + } let attempts = 0 + let frame = 0 const tick = () => { if (el.isConnected) { el.focus() return } attempts += 1 - if (attempts < 20) requestAnimationFrame(tick) + if (attempts < 20) frame = requestAnimationFrame(tick) } - requestAnimationFrame(tick) + frame = requestAnimationFrame(tick) + return () => cancelAnimationFrame(frame) } // Keep composer text off the disposable annotation DOM without making each keystroke reactive. @@ -245,6 +273,87 @@ export function buildReviewAnnotation( if (meta.type === "draft") { wrapper.className = "am-annotation am-annotation-draft" + if (handlers.mount) { + wrapper.dataset.mounted = "true" + const header = document.createElement("div") + header.className = "am-annotation-header" + header.textContent = handlers.labels.commentOnLine(meta.line) + wrapper.appendChild(header) + const host = document.createElement("div") + host.className = "am-annotation-form" + wrapper.appendChild(host) + + let dispose: (() => void) | undefined + let unfocus: (() => void) | undefined + let speechField: HTMLTextAreaElement | undefined + + const submit = () => { + // Speech-to-text confirms with the local action. GitHub publication stays + // on an explicit button click so a voice command cannot post by accident. + const kilo = host.querySelector('[data-action="send-kilo"], [data-action="send"]') + if (kilo && !kilo.disabled) { + kilo.click() + return + } + const primary = host.querySelector('[data-action="send-primary"]') + if (primary && primary.dataset.destination !== "github" && !primary.disabled) { + primary.click() + return + } + const fallback = host.querySelector('[data-action="submit"]') + if (fallback && !fallback.disabled) fallback.click() + } + + // Keep focus and speech-to-text attached to the mounted form's editor. + const afterMount = () => { + const field = host.querySelector("textarea") + if (!field) return + unfocus?.() + unfocus = focusWhenConnected(field) + if (handlers.speech && field !== speechField) { + speechField = field + field.addEventListener("keydown", (event) => { + if (!handlers.speech?.down(meta, event, submit)) return + event.preventDefault() + event.stopPropagation() + }) + field.addEventListener("keyup", (event) => { + if (!handlers.speech?.up(meta, event)) return + event.preventDefault() + event.stopPropagation() + }) + } + if (!handlers.speech) return + const row = host.querySelector('[data-slot="comment-actions"]') + const speechHost = handlers.speech.render(meta, field) + if (speechHost && row) row.prepend(speechHost) + } + + dispose = handlers.mount(host, meta, { + body: meta.text ?? "", + onBodyChange: (body) => { + meta.text = body + }, + onSave: (body, selected) => handlers.addComment(meta.file, meta.side, meta.line, body, selected), + onSend: (body, selected) => handlers.sendComment(meta.file, meta.side, meta.line, body, selected), + onGithubSuccess: () => handlers.completeRemoteDraft?.(meta), + onCancel: handlers.cancelDraft, + onDestination: (value) => { + meta.destination = value + handlers.onDestination?.(value) + }, + }) + afterMount() + + handlers.track?.(meta, wrapper, () => { + unfocus?.() + dispose?.() + dispose = undefined + }) + return wrapper + } + + // Fallback native composer for surfaces without a mounted form (for example the document panel). const header = document.createElement("div") header.className = "am-annotation-header" header.textContent = handlers.labels.commentOnLine(meta.line) @@ -351,6 +460,11 @@ export function buildReviewAnnotation( return wrapper } + return buildSavedAnnotation(meta, handlers) +} + +function buildSavedAnnotation(meta: AnnotationMeta, handlers: AnnotationHandlers): HTMLElement { + const wrapper = document.createElement("div") const comment = meta.comment! if (meta.editing) { wrapper.className = "am-annotation am-annotation-draft" diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/review-controller.ts b/packages/kilo-vscode/webview-ui/diff-viewer/review-controller.ts index 7ea25657ba2..616875b2bd9 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/review-controller.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/review-controller.ts @@ -1,4 +1,14 @@ -import { createEffect, createMemo, createRenderEffect, createSignal, on, untrack, type Accessor } from "solid-js" +import { + createEffect, + createMemo, + createRenderEffect, + createSignal, + on, + onCleanup, + untrack, + type Accessor, +} from "solid-js" +import { createAnnotationLifecycle } from "./annotation-lifecycle" import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pierre/diffs" import type { UiI18nParams } from "@kilocode/kilo-ui/context" import type { DiffHandle } from "@kilocode/kilo-ui/pierre" @@ -20,6 +30,7 @@ import { sendReviewComments, labels, type AnnotationMeta, + type CommentFormMount, type ReviewComposer, } from "./review-annotations" import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech" @@ -29,6 +40,7 @@ import { createReviewOpenState } from "./review-state" import { createReviewScrollPreserver } from "./review-scroll" import { createDiffRows } from "./diff-state" import { createDiffRequests } from "./diff-requests" +import { postAllGithub, type CommentsGithub } from "./comments-github" import { treeOrder } from "./file-tree-utils" import { isDiffExpandable, shouldVirtualizeDiff } from "./diff-open-policy" import { isMarkdownFile } from "./MarkdownDiffView" @@ -49,9 +61,14 @@ type Props = { canComment?: Accessor onSendClick?: () => void onSendAll?: () => void + commentForm?: Accessor + commentsGithub?: CommentsGithub } export function createReviewController(props: Props) { + const lifecycle = createAnnotationLifecycle() + onCleanup(lifecycle.clear) + const [preferredDestination, setPreferredDestination] = createSignal<"local" | "github">("local") const active = props.active ?? (() => true) const canComment = props.canComment ?? (() => true) const [draft, setDraft] = createSignal(reviewComposerDraft(props.composer())) @@ -98,6 +115,7 @@ export function createReviewController(props: Props) { props.key, () => { if (!active()) return + lifecycle.clear() setDraft(null) draftMeta = null setEditing(null) @@ -226,6 +244,11 @@ export function createReviewController(props: Props) { if (id === null) props.focus() } + const completeRemoteDraft = (meta: AnnotationMeta) => { + if (draftMeta !== meta) return + cancelDraft() + } + const annotationsForFile = (file: string): DiffLineAnnotation[] => { const result = buildFileAnnotations(file, commentsByFile().get(file) ?? [], editing(), draft(), draftMeta, editMeta) draftMeta = result.draftMeta @@ -239,6 +262,7 @@ export function createReviewController(props: Props) { const buildAnnotation = (annotation: DiffLineAnnotation): HTMLElement | undefined => buildReviewAnnotation(annotation, { + track: lifecycle.track, diffs: props.diffs(), editing: editing(), setEditing: setEditState, @@ -247,6 +271,9 @@ export function createReviewController(props: Props) { updateComment, deleteComment, cancelDraft, + completeRemoteDraft, + onDestination: setPreferredDestination, + mount: props.commentForm?.(), labels: labels(props.label), activeTerminalId: props.activeTerminalId, speech, @@ -255,9 +282,10 @@ export function createReviewController(props: Props) { const handleGutterClick = (file: string, range: SelectedLineRange) => { if (!canComment() || draft()) return const side: AnnotationSide = range.side === "deletions" ? "deletions" : "additions" + const destination = preferredDestination() props.preserveScroll(() => { const next = { file, side, line: range.start, endLine: range.end } - draftMeta = { type: "draft", comment: null, ...next } + draftMeta = { type: "draft", comment: null, ...next, destination } props.composer().draft = draftMeta setDraft(next) }) @@ -271,6 +299,44 @@ export function createReviewController(props: Props) { props.onSendAll?.() } + const [sendAllPending, setSendAllPending] = createSignal(false) + const [sendAllError, setSendAllError] = createSignal() + + const githubComments = () => { + const github = props.commentsGithub + if (!github) return [] + return props.comments().filter((comment) => { + const context = github.resolve(comment) + return !!context && !context.closed + }) + } + + const sendAllGithubCount = () => githubComments().length + const sendAllGithubAvailable = () => sendAllGithubCount() > 0 + + const sendAllToGithub = async () => { + const github = props.commentsGithub + if (!github || sendAllPending()) return + const pending = githubComments() + if (pending.length === 0) return + props.onSendClick?.() + setSendAllPending(true) + setSendAllError(undefined) + const { posted, failure } = await postAllGithub(pending, github) + if (posted.length > 0) { + const ids = new Set(posted.map((comment) => comment.id)) + props.preserveScroll(() => props.setComments(props.comments().filter((comment) => !ids.has(comment.id)))) + } + setSendAllPending(false) + if (failure !== undefined) { + setSendAllError( + props.label("agentManager.review.sendAllToGithubFailed", { + error: failure || props.label("common.requestFailed"), + }), + ) + } + } + const sendAllClick = () => { props.onSendClick?.() sendAllToChat() @@ -289,7 +355,12 @@ export function createReviewController(props: Props) { setEditState, handleGutterClick, sendAllToChat, + sendAllToGithub, sendAllClick, + sendAllGithubCount, + sendAllGithubAvailable, + sendAllPending, + sendAllError, } } @@ -314,9 +385,20 @@ export interface ReviewViewProps { onRequestDiff?: (file: string) => void onOpenFile?: (file: string, line?: number) => void canComment?: boolean + commentForm?: CommentFormMount + commentsGithub?: CommentsGithub } -export function createReviewView(props: ReviewViewProps, root: Accessor) { +interface ReviewViewOverrides { + commentForm?: CommentFormMount + commentsGithub?: CommentsGithub +} + +export function createReviewView( + props: ReviewViewProps, + root: Accessor, + overrides?: ReviewViewOverrides, +) { const { t } = useLanguage() const vscode = useVSCode() const local = createReviewComposer() @@ -384,6 +466,8 @@ export function createReviewView(props: ReviewViewProps, root: Accessor props.canComment !== false, onSendClick: props.onSendClick, onSendAll: props.onSendAll, + commentForm: () => overrides?.commentForm ?? props.commentForm, + commentsGithub: overrides?.commentsGithub ?? props.commentsGithub, }) const pinned = createMemo(() => { const keep = new Set(review.pinned()) @@ -455,5 +539,10 @@ export function createReviewView(props: ReviewViewProps, root: Accessor Date: Thu, 10 Sep 2026 19:32:43 +0200 Subject: [PATCH 2/8] refactor(vscode): share diff review notice and setup --- .../webview-ui/agent-manager/DiffPanel.tsx | 43 ++++--------------- .../diff-viewer/DiffViewerNotice.tsx | 19 ++++++++ .../diff-viewer/FullScreenDiffView.tsx | 42 ++++-------------- .../webview-ui/diff-viewer/review-surface.ts | 33 ++++++++++++++ 4 files changed, 70 insertions(+), 67 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerNotice.tsx create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/review-surface.ts diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 1cde76b474d..783785b4101 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -1,10 +1,8 @@ import { type Component, createMemo, Show, type JSXElement } from "solid-js" import { Accordion } from "@kilocode/kilo-ui/accordion" -import { Icon } from "@kilocode/kilo-ui/icon" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Spinner } from "@kilocode/kilo-ui/spinner" import { Tooltip } from "@kilocode/kilo-ui/tooltip" -import { useLanguage } from "../src/context/language" import { DiffStyleSelect } from "../diff-viewer/InlineSelect" import { LONG_DIFF_MARKER_FILE_COUNT, @@ -15,16 +13,16 @@ import { toggleOpenFiles, } from "../diff-viewer/diff-open-policy" import { DiffEndMarker } from "../diff-viewer/DiffEndMarker" +import { DiffViewerNotice } from "../diff-viewer/DiffViewerNotice" import { VirtualDiffList } from "../diff-viewer/VirtualDiffList" import { createDiffViewport } from "../diff-viewer/diff-requests" import "./pr/pr-panel.css" import "../diff-viewer/remote-comments.css" import { RemoteCommentsOutside } from "../diff-viewer/remote-comment-renderer" import { ReviewDiffItem } from "../diff-viewer/ReviewDiffItem" -import { createReviewView, type ReviewViewProps } from "../diff-viewer/review-controller" -import { notice, reviewSendAllKeybind } from "../diff-viewer/review-setup" +import { type ReviewViewProps } from "../diff-viewer/review-controller" import { SendAllButton } from "../diff-viewer/SendAllButton" -import { createDiffCommentForms } from "./pr/diff-comment-forms" +import { createReviewSurface } from "../diff-viewer/review-surface" import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" // --- Data model --- @@ -53,17 +51,11 @@ interface DiffPanelProps extends ReviewViewProps { } export const DiffPanel: Component = (props) => { - const { t } = useLanguage() - const noticeText = () => notice(t, props.notice) - const sendAllKeybind = () => reviewSendAllKeybind(t) let rootRef: HTMLDivElement | undefined - const forms = createDiffCommentForms({ - target: () => props.prTarget, - snapshot: () => props.prSnapshot, - diffs: () => props.diffs, - worktree: () => props.worktreeId ?? props.sessionId ?? "diff", - }) const { + t, + noticeText, + sendAllKeybind, open, setOpen, rows, @@ -88,10 +80,7 @@ export const DiffPanel: Component = (props) => { sendAllGithubAvailable, sendAllPending, sendAllError, - } = createReviewView(props, () => rootRef, { - commentForm: forms.mount, - commentsGithub: forms.github, - }) + } = createReviewSurface(props, () => rootRef) const handleExpandAll = () => { setOpen(toggleOpenFiles(props.diffs, open())) @@ -179,23 +168,9 @@ export const DiffPanel: Component = (props) => {
- - - + - -
- - - - {noticeText()} -
-
+
diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerNotice.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerNotice.tsx new file mode 100644 index 00000000000..e92f2726803 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerNotice.tsx @@ -0,0 +1,19 @@ +import { Show, type Component } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" + +interface Props { + text?: string + role: "alert" | "status" +} + +/** Shared warning banner used by the inline and full-screen diff views. */ +export const DiffViewerNotice: Component = (props) => ( + +
+ + + + {props.text} +
+
+) diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index bbca7abb77b..a98bbbf7dfd 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -13,7 +13,6 @@ import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Spinner } from "@kilocode/kilo-ui/spinner" import { ResizeHandle } from "@kilocode/kilo-ui/resize-handle" -import { useLanguage } from "../src/context/language" import { FileTree } from "./FileTree" import { LONG_DIFF_MARKER_FILE_COUNT, @@ -24,14 +23,14 @@ import { toggleOpenFiles, } from "./diff-open-policy" import { DiffEndMarker } from "./DiffEndMarker" +import { DiffViewerNotice } from "./DiffViewerNotice" import { VirtualDiffList } from "./VirtualDiffList" import { createDiffViewport } from "./diff-requests" import { RemoteCommentsOutside } from "./remote-comment-renderer" import { ReviewDiffItem } from "./ReviewDiffItem" -import { createReviewView, type ReviewViewProps } from "./review-controller" -import { notice, reviewSendAllKeybind } from "./review-setup" +import { type ReviewViewProps } from "./review-controller" +import { createReviewSurface } from "./review-surface" import { SendAllButton } from "./SendAllButton" -import { createDiffCommentForms } from "../agent-manager/pr/diff-comment-forms" import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" type DiffStyle = "unified" | "split" @@ -59,17 +58,11 @@ interface FullScreenDiffViewProps extends ReviewViewProps { } export const FullScreenDiffView: Component = (props) => { - const { t } = useLanguage() - const noticeText = () => notice(t, props.notice) - const sendAllKeybind = () => reviewSendAllKeybind(t) let rootRef: HTMLDivElement | undefined - const forms = createDiffCommentForms({ - target: () => props.prTarget, - snapshot: () => props.prSnapshot, - diffs: () => props.diffs, - worktree: () => props.worktreeId ?? props.sessionId ?? "diff", - }) const { + t, + noticeText, + sendAllKeybind, open, setOpen, rows, @@ -94,10 +87,7 @@ export const FullScreenDiffView: Component = (props) => sendAllGithubAvailable, sendAllPending, sendAllError, - } = createReviewView(props, () => rootRef, { - commentForm: props.commentForm ?? forms.mount, - commentsGithub: props.commentsGithub ?? forms.github, - }) + } = createReviewSurface(props, () => rootRef) const [manualActiveFile, setManualActiveFile] = createSignal>({}) const activeFile = createMemo(() => { @@ -274,14 +264,7 @@ export const FullScreenDiffView: Component = (props) =>
- - - + {/* Body: file tree + diff viewer */}
@@ -306,14 +289,7 @@ export const FullScreenDiffView: Component = (props) => />
- -
- - - - {noticeText()} -
-
+
diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/review-surface.ts b/packages/kilo-vscode/webview-ui/diff-viewer/review-surface.ts new file mode 100644 index 00000000000..35227112d75 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/review-surface.ts @@ -0,0 +1,33 @@ +import { useLanguage } from "../src/context/language" +import { createDiffCommentForms } from "../agent-manager/pr/diff-comment-forms" +import { createReviewView, type ReviewViewProps } from "./review-controller" +import { notice, reviewSendAllKeybind } from "./review-setup" +import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" + +interface SurfaceProps extends ReviewViewProps { + notice?: string + sessionId?: string + prTarget?: PRTarget + prSnapshot?: PRDiffSnapshot +} + +/** Shared wiring for the inline and full-screen diff review surfaces. */ +export function createReviewSurface(props: SurfaceProps, root: () => HTMLDivElement | undefined) { + const { t } = useLanguage() + const forms = createDiffCommentForms({ + target: () => props.prTarget, + snapshot: () => props.prSnapshot, + diffs: () => props.diffs, + worktree: () => props.worktreeId ?? props.sessionId ?? "diff", + }) + const view = createReviewView(props, root, { + commentForm: props.commentForm ?? forms.mount, + commentsGithub: props.commentsGithub ?? forms.github, + }) + return { + t, + noticeText: () => notice(t, props.notice), + sendAllKeybind: () => reviewSendAllKeybind(t), + ...view, + } +} From cdffd39940bd042022574598270584ef9d5de46d Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 10 Sep 2026 19:42:19 +0200 Subject: [PATCH 3/8] fix(vscode): address PR review feedback on comment flows --- .../tests/fixtures/diff-comment-form.tsx | 27 +++++++++++++++++++ .../tests/unit/annotation-lifecycle.test.ts | 14 ++++++++++ .../agent-manager/pr/PRCommentForm.tsx | 14 ++-------- .../agent-manager/pr/diff-comment-state.ts | 6 +++-- .../webview-ui/diff-viewer/DiffViewerApp.tsx | 9 ++++++- .../diff-viewer/annotation-lifecycle.ts | 10 ++++--- .../diff-viewer/review-controller.ts | 2 +- 7 files changed, 62 insertions(+), 20 deletions(-) diff --git a/packages/kilo-vscode/tests/fixtures/diff-comment-form.tsx b/packages/kilo-vscode/tests/fixtures/diff-comment-form.tsx index 08dde1970a0..a74c56b4e8e 100644 --- a/packages/kilo-vscode/tests/fixtures/diff-comment-form.tsx +++ b/packages/kilo-vscode/tests/fixtures/diff-comment-form.tsx @@ -54,11 +54,37 @@ const release = mount(() => ( onDestinationChange={() => {}} />
+
+ {}} + onSendKilo={() => {}} + onGithubSuccess={() => completed++} + onCancel={() => cancelled++} + onDestinationChange={() => {}} + /> +
)) await wait() const local = node("#local") const remote = node("#remote") +const remote2 = node("#remote2") // Local-only destination exposes Kilo actions, never the GitHub split button. assert.equal(button("send-kilo", local).textContent, "Send to Kilo") @@ -97,6 +123,7 @@ node('[aria-label="Choose destination"]', remote) type(remote, "Do not post") input(remote).dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true })) assert.equal(messages.length, 0, "Enter never posts to GitHub") +assert.equal(input(remote2).value, "", "a draft is scoped to its own PR identity") type(remote, "Post me") button("send-primary", remote).click() diff --git a/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts b/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts index aacc0f40a8c..8a7368fcc85 100644 --- a/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts +++ b/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts @@ -6,6 +6,20 @@ import type { AnnotationMeta } from "../../webview-ui/diff-viewer/review-annotat const previous = { document: globalThis.document, MutationObserver: globalThis.MutationObserver } afterEach(() => Object.assign(globalThis, previous)) +it("releases a wrapper that is never inserted", async () => { + const window = new Window() + Object.assign(globalThis, { document: window.document, MutationObserver: window.MutationObserver }) + const lifecycle = createAnnotationLifecycle() + const meta: AnnotationMeta = { type: "draft", comment: null, file: "never.ts", side: "additions", line: 1 } + let released = 0 + lifecycle.track(meta, document.createElement("div"), () => released++) + document.body.append(document.createElement("span")) + await window.happyDOM.waitUntilComplete() + expect(released).toBe(1) + lifecycle.clear() + await window.happyDOM.close() +}) + it("disposes detached and replaced annotation roots exactly once", async () => { const window = new Window() Object.assign(globalThis, { document: window.document, MutationObserver: window.MutationObserver }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentForm.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentForm.tsx index bac5515e906..ae14746c5a2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentForm.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentForm.tsx @@ -1,4 +1,4 @@ -import { For, Show, createMemo, createSignal, untrack } from "solid-js" +import { For, Show, createMemo, createSignal } from "solid-js" import { createStore } from "solid-js/store" import { Button } from "@kilocode/kilo-ui/button" import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu" @@ -31,8 +31,6 @@ type Props = { submitOnEnter?: boolean /** Called when Escape is pressed in the editor. */ onEscape?: () => void - /** Replace any stored draft with initialBody. Used when switching comment destination. */ - replaceBody?: boolean inline?: boolean } & ( | { action: "reply"; threadId: string } @@ -145,6 +143,7 @@ export function PRCommentForm(props: Props) { props.action === "local" || props.action === "diff" ? [props.file, props.side, props.startLine, props.endLine] : undefined, + props.action === "diff" ? [props.github?.prNumber, props.github?.snapshotId] : undefined, props.action === "line" ? [props.snapshotId, props.path, props.side, props.startLine, props.endLine] : undefined, props.action === "review" ? [props.snapshotId, props.head] : undefined, ]), @@ -166,15 +165,6 @@ export function PRCommentForm(props: Props) { const placeholder = () => t(props.action === "reply" ? "agentManager.pr.comment.replyPlaceholder" : "agentManager.pr.comment.placeholder") const patch = (value: Partial, id = key()) => setDrafts(id, (prev) => ({ ...(prev ?? blank), ...value })) - untrack(() => { - if ( - props.replaceBody && - (props.action === "line" || props.action === "local") && - props.initialBody !== undefined && - drafts[key()]?.body !== props.initialBody - ) - patch({ body: props.initialBody, sent: undefined }) - }) const label = () => props.action === "reply" ? t("agentManager.pr.comment.reply") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/diff-comment-state.ts b/packages/kilo-vscode/webview-ui/agent-manager/pr/diff-comment-state.ts index 4f2339e061c..a8b1de84db7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/diff-comment-state.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/diff-comment-state.ts @@ -1,4 +1,4 @@ -import { createSignal, type Accessor } from "solid-js" +import { createSignal, untrack, type Accessor } from "solid-js" import type { PRDiffSnapshot, PRTarget } from "../../../src/shared/pr-comment-actions" import type { PRStatus } from "../../src/types/messages" import { reviewRequest } from "./pr-review-request" @@ -45,7 +45,9 @@ export function createPRDiffCommentState(opts: Options) { const route = target(ctx) if (!route) return const id = key(ctx) - if (snapshots()[id] || pending().has(id)) return + // Read the dedupe state untracked so a failed load does not re-trigger the + // effect that called this, which would immediately retry forever. + if (untrack(() => Boolean(snapshots()[id] || pending().has(id)))) return setPending((prev) => new Set(prev).add(id)) setErrors((prev) => { const next = { ...prev } diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx index 53834281085..c89af70e2e3 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx @@ -28,6 +28,11 @@ import type { PRComment } from "../agent-manager/pr/pr-types" import { reviewRequest } from "../agent-manager/pr/pr-review-request" import type { PRDiffSnapshot, PRTarget } from "../../src/shared/pr-comment-actions" import { createPRDiffs } from "./pr-diff" + +// Compare only the PR identity. Ref-only refreshes must not clear local comments. +function samePR(a: PRTarget | undefined, b: PRTarget | undefined) { + return a?.projectId === b?.projectId && a?.prNumber === b?.prNumber && a?.prUrl === b?.prUrl +} import { createDiffCommentForms } from "../agent-manager/pr/diff-comment-forms" import { DiffPickerHeader } from "./DiffPickerHeader" import { BaseBranchPicker } from "./BaseBranchPicker" @@ -209,7 +214,9 @@ const DiffViewerContent: Component = () => { return } if (msg.type === "diffViewer.prComments") { - const changed = JSON.stringify(target()) !== JSON.stringify(msg.target) + // Only clear local comments when the PR identity changes. Ref-only + // refreshes (a push or rebase) must keep unsent comments. + const changed = !samePR(target(), msg.target) batch(() => { setRemote(msg.comments) setTarget(msg.target) diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/annotation-lifecycle.ts b/packages/kilo-vscode/webview-ui/diff-viewer/annotation-lifecycle.ts index cdef06af60e..adf3c5862b1 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/annotation-lifecycle.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/annotation-lifecycle.ts @@ -2,7 +2,7 @@ import type { AnnotationMeta } from "./review-annotations" // Pierre can replace an annotation without invoking its button handlers. export function createAnnotationLifecycle() { - const mounts = new Map void; connected: boolean }>() + const mounts = new Map void }>() let observer: MutationObserver | undefined const release = (meta: AnnotationMeta) => { const entry = mounts.get(meta) @@ -15,12 +15,14 @@ export function createAnnotationLifecycle() { } const track = (meta: AnnotationMeta, host: HTMLElement, dispose: () => void) => { release(meta) - mounts.set(meta, { host, dispose, connected: host.isConnected }) + mounts.set(meta, { host, dispose }) if (observer) return observer = new MutationObserver(() => { + // The wrapper is inserted synchronously after track returns, so any host + // still detached on an observer flush will never be shown. Releasing it + // keeps a dropped annotation from retaining its form for the session. for (const [meta, entry] of mounts) { - if (entry.host.isConnected) entry.connected = true - else if (entry.connected) release(meta) + if (!entry.host.isConnected) release(meta) } }) observer.observe(document.body, { childList: true, subtree: true }) diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/review-controller.ts b/packages/kilo-vscode/webview-ui/diff-viewer/review-controller.ts index 616875b2bd9..ad6c7ab1e1b 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/review-controller.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/review-controller.ts @@ -328,7 +328,7 @@ export function createReviewController(props: Props) { props.preserveScroll(() => props.setComments(props.comments().filter((comment) => !ids.has(comment.id)))) } setSendAllPending(false) - if (failure !== undefined) { + if (failure !== undefined || posted.length < pending.length) { setSendAllError( props.label("agentManager.review.sendAllToGithubFailed", { error: failure || props.label("common.requestFailed"), From f5f75ea67e55903e8d7216cbb309130f5b558249 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 10 Sep 2026 20:02:29 +0200 Subject: [PATCH 4/8] test(vscode): stop gh input mock leaking across test files --- .../tests/unit/diff-viewer-provider.test.ts | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/diff-viewer-provider.test.ts b/packages/kilo-vscode/tests/unit/diff-viewer-provider.test.ts index f93ec45271c..b5111f9b149 100644 --- a/packages/kilo-vscode/tests/unit/diff-viewer-provider.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-viewer-provider.test.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode" import { DiffViewerProvider } from "../../src/diff/DiffViewerProvider" import * as gh from "../../src/agent-manager/gh" import * as shell from "../../src/agent-manager/shell-env" +import { execGhInput as ghInput } from "../../src/agent-manager/pr/PRActions" import type { DiffPRPoller, DiffPRPollerOptions } from "../../src/diff/pr-poller" import type { PRComment, PRStatus } from "../../src/agent-manager/types" import type { PRReviewCommentData } from "../../src/shared/review-comments" @@ -11,14 +12,15 @@ import type { PRTarget } from "../../src/shared/pr-comment-actions" const addCommentReaction = mock(async (_commentId: string, _reaction: string, _cwd: string) => {}) const removeCommentReaction = mock(async (_commentId: string, _reaction: string, _cwd: string) => {}) -const execGhInput = mock(async () => ({ stdout: "{}", stderr: "" })) const isPRReactionContent = (value: unknown): value is string => typeof value === "string" && ["THUMBS_UP", "THUMBS_DOWN", "LAUGH", "HOORAY", "CONFUSED", "HEART", "ROCKET", "EYES"].includes(value) +// Keep the real `execGhInput` so this process-wide module mock does not leak a +// reset mock into other test files that post comments through `gh`. mock.module("../../src/agent-manager/pr/PRActions", () => ({ addCommentReaction, - execGhInput, + execGhInput: ghInput, isPRReactionContent, removeCommentReaction, })) @@ -44,7 +46,6 @@ afterEach(() => { beforeEach(() => { addCommentReaction.mockReset() removeCommentReaction.mockReset() - execGhInput.mockReset() }) function event() { @@ -230,6 +231,17 @@ describe("DiffViewerProvider.openFromCommand", () => { describe("DiffViewerProvider remote PR comments", () => { it("routes PR snapshot loading and new comment creation from the standalone panel", async () => { const read = spyOn(gh, "execGhRead").mockImplementation(async (args) => { + if (args.includes("--input")) + return { + stdout: JSON.stringify({ + id: 11, + commit_id: "a".repeat(40), + path: "src/app.ts", + side: "RIGHT", + line: 1, + }), + stderr: "", + } if (args.some((arg) => arg.includes("/files?"))) return { stdout: JSON.stringify([ @@ -268,16 +280,6 @@ describe("DiffViewerProvider remote PR comments", () => { expect(loaded).toMatchObject({ success: true, requestId: "load" }) if (!loaded?.snapshot || typeof loaded.snapshot !== "object") throw new Error("Missing PR snapshot") - execGhInput.mockResolvedValueOnce({ - stdout: JSON.stringify({ - id: 11, - commit_id: "a".repeat(40), - path: "src/app.ts", - side: "RIGHT", - line: 1, - }), - stderr: "", - }) h.received.fire({ ...target, type: "agentManager.createReviewComment", @@ -290,6 +292,10 @@ describe("DiffViewerProvider remote PR comments", () => { body: "Please update this.", }) await new Promise((resolve) => setTimeout(resolve, 0)) + // The real execGhInput writes its input to a temp file, so give the round + // trip a bounded amount of time instead of a single macrotask. + for (let i = 0; i < 100 && !h.messages("agentManager.createReviewCommentResult").length; i++) + await new Promise((resolve) => setTimeout(resolve, 2)) expect(h.messages("agentManager.createReviewCommentResult").at(-1)).toMatchObject({ success: true, requestId: "comment", From d7183694a8f40ad054c74b5b21149a60718c1c24 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 11 Sep 2026 09:30:36 +0200 Subject: [PATCH 5/8] fix(vscode): reload PR snapshot on ref-only refresh --- .../tests/unit/diff-preview-request.test.ts | 195 +++++++++++------- .../webview-ui/diff-viewer/DiffViewerApp.tsx | 2 +- 2 files changed, 116 insertions(+), 81 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/diff-preview-request.test.ts b/packages/kilo-vscode/tests/unit/diff-preview-request.test.ts index 2c99b863e4a..ca30f660383 100644 --- a/packages/kilo-vscode/tests/unit/diff-preview-request.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-preview-request.test.ts @@ -201,10 +201,7 @@ describe("diff preview detail requests", () => { }) it("discards cancelled standalone details and recovers real failures through the message handler", async () => { - const solid = path.dirname(Bun.resolveSync("solid-js/package.json", WEBVIEW)) - const result = await build({ - stdin: { - contents: ` + const child = await renderSurface(` import assert from "node:assert/strict" import { createRoot } from "solid-js" import { SourceController } from "../src/diff/SourceController" @@ -252,81 +249,119 @@ describe("diff preview detail requests", () => { controller.dispose() dispose() })().catch((err) => { console.error(err); process.exitCode = 1 }) - `, - resolveDir: WEBVIEW, - sourcefile: "detail-recovery.ts", - loader: "ts", - }, - bundle: true, - platform: "node", - format: "cjs", - write: false, - logLevel: "silent", - plugins: [ - { - name: "review-surface", - setup(ctx) { - ctx.onResolve({ filter: /^solid-js$/ }, () => ({ path: path.join(solid, "dist/solid.js") })) - ctx.onResolve({ filter: /^solid-js\/web$/ }, () => ({ path: path.join(solid, "web/dist/server.js") })) - ctx.onResolve({ filter: /.*/ }, (args) => { - if ( - args.path !== "probe:surface" && - (!args.importer.endsWith("/DiffViewerApp.tsx") || ["solid-js", "./diff-state"].includes(args.path)) - ) - return - return { path: "surface", namespace: "probe" } - }) - ctx.onLoad({ filter: /.*/, namespace: "probe" }, () => ({ - contents: ` - export const state = { posted: [] } - export const useVSCode = () => ({ onMessage(receive) { state.receive = receive; return () => {} } }) - export const getVSCodeAPI = () => ({ postMessage: (message) => state.posted.push(message) }) - export const useLanguage = () => ({ t: (key) => key }) - export const useServer = () => ({}) - export const FullScreenDiffView = (props) => { state.view = props; return "" } - export const Toast = { Region: () => "" } - export const reviewRequest = () => {} - export const createPRDiffs = () => [] - export const createDiffCommentForms = () => ({ mount: () => () => {} }) - ${[ - "DialogProvider", - "CodeComponentProvider", - "DiffComponentProvider", - "FileComponentProvider", - "MarkedProvider", - "ThemeProvider", - "LanguageProvider", - "ServerProvider", - "ConfigProvider", - "ProviderProvider", - "VSCodeProvider", - "SpeechToTextModelsProvider", - "SpeechToTextPrewarm", - "Code", - "Diff", - "File", - "Icon", - "IconButton", - "Button", - "Spinner", - "DiffPickerHeader", - "BaseBranchPicker", - ] - .map((name) => `export const ${name} = (props) => props.children`) - .join("\n")} - `, - loader: "js", - })) - }, - }, - solidPlugin({ solid: { generate: "ssr" } }), - ], - }) - const child = Bun.spawnSync(["bun", "-e", result.outputFiles.at(0)!.text], { - cwd: WEBVIEW, - stdout: "pipe", - stderr: "pipe", - }) - expect(child.exitCode, child.stdout.toString() + child.stderr.toString()).toBe(0) + `) + expectPass(child) + }) + + it("reloads the PR snapshot on a ref-only refresh", async () => { + const child = await renderSurface(` + import assert from "node:assert/strict" + import { createRoot } from "solid-js" + import { DiffViewerApp } from "./diff-viewer/DiffViewerApp" + import { state } from "probe:surface" + globalThis.window = new EventTarget() + const dispose = createRoot((dispose) => { DiffViewerApp({}); return dispose }) + const target = (head) => ({ + projectId: "p", + worktreeId: "diff", + prNumber: 7, + prUrl: "https://github.com/o/r/pull/7", + baseRefOid: "base", + headRefOid: head, + }) + state.receive({ type: "diffViewer.prComments", comments: [], target: target("a"), threads: [] }) + assert.equal(state.requests.length, 1, "initial target loads the snapshot") + assert.equal(state.requests[0].headRefOid, "a") + state.receive({ type: "diffViewer.prComments", comments: [], target: target("b"), threads: [] }) + assert.equal(state.requests.length, 2, "ref-only refresh reloads the snapshot") + assert.equal(state.requests[1].headRefOid, "b") + dispose() + `) + expectPass(child) }) }) + +async function renderSurface(script: string) { + const solid = path.dirname(Bun.resolveSync("solid-js/package.json", WEBVIEW)) + const result = await build({ + stdin: { + contents: script, + resolveDir: WEBVIEW, + sourcefile: "review-surface.ts", + loader: "ts", + }, + bundle: true, + platform: "node", + format: "cjs", + write: false, + logLevel: "silent", + plugins: [ + { + name: "review-surface", + setup(ctx) { + ctx.onResolve({ filter: /^solid-js$/ }, () => ({ path: path.join(solid, "dist/solid.js") })) + ctx.onResolve({ filter: /^solid-js\/web$/ }, () => ({ path: path.join(solid, "web/dist/server.js") })) + ctx.onResolve({ filter: /.*/ }, (args) => { + if ( + args.path !== "probe:surface" && + (!args.importer.endsWith("/DiffViewerApp.tsx") || ["solid-js", "./diff-state"].includes(args.path)) + ) + return + return { path: "surface", namespace: "probe" } + }) + ctx.onLoad({ filter: /.*/, namespace: "probe" }, () => ({ + contents: ` + export const state = { posted: [], requests: [] } + export const useVSCode = () => ({ onMessage(receive) { state.receive = receive; return () => {} } }) + export const getVSCodeAPI = () => ({ postMessage: (message) => state.posted.push(message) }) + export const useLanguage = () => ({ t: (key) => key }) + export const useServer = () => ({}) + export const FullScreenDiffView = (props) => { state.view = props; return "" } + export const Toast = { Region: () => "" } + export const reviewRequest = (request) => { state.requests.push(request) } + export const createPRDiffs = () => [] + export const createDiffCommentForms = () => ({ mount: () => () => {} }) + ${[ + "DialogProvider", + "CodeComponentProvider", + "DiffComponentProvider", + "FileComponentProvider", + "MarkedProvider", + "ThemeProvider", + "LanguageProvider", + "ServerProvider", + "ConfigProvider", + "ProviderProvider", + "VSCodeProvider", + "SpeechToTextModelsProvider", + "SpeechToTextPrewarm", + "Code", + "Diff", + "File", + "Icon", + "IconButton", + "Button", + "Spinner", + "DiffPickerHeader", + "BaseBranchPicker", + ] + .map((name) => `export const ${name} = (props) => props.children`) + .join("\n")} + `, + loader: "js", + })) + }, + }, + solidPlugin({ solid: { generate: "ssr" } }), + ], + }) + return Bun.spawnSync(["bun", "-e", result.outputFiles.at(0)!.text], { + cwd: WEBVIEW, + stdout: "pipe", + stderr: "pipe", + }) +} + +function expectPass(child: ReturnType) { + expect(child.exitCode, child.stdout.toString() + child.stderr.toString()).toBe(0) +} diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx index c89af70e2e3..c6e1fd8cdad 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx @@ -227,7 +227,7 @@ const DiffViewerContent: Component = () => { setPRMode(false) } }) - if (changed) requestPRFiles(msg.target) + requestPRFiles(msg.target) return } if (msg.type === "diffViewer.focusComment") { From f6d18b292f91d603758e801d509b617264bfb096 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 14 Sep 2026 09:01:05 +0200 Subject: [PATCH 6/8] fix(vscode): align diff review comment tests with mounted composer --- .../tests/diff-scroll-preservation.spec.ts | 14 +++++++------- .../webview-ui/diff-viewer/review-annotations.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/kilo-vscode/tests/diff-scroll-preservation.spec.ts b/packages/kilo-vscode/tests/diff-scroll-preservation.spec.ts index 35889bcb267..4d6bf0b5821 100644 --- a/packages/kilo-vscode/tests/diff-scroll-preservation.spec.ts +++ b/packages/kilo-vscode/tests/diff-scroll-preservation.spec.ts @@ -198,18 +198,18 @@ test("preserves scroll while adding and editing a review comment", async ({ page const line = target.locator('[data-line="1"]').last() await line.hover() await target.locator("[data-utility-button]").last().click() - await expect(target.locator(".am-annotation-textarea")).toBeVisible() - await target.locator(".am-annotation-textarea").fill("Keep this stable") + await expect(target.locator(".am-annotation-draft textarea")).toBeVisible() + await target.locator(".am-annotation-draft textarea").fill("Keep this stable") const top = await target.evaluate((el) => el.getBoundingClientRect().top) const before = await scroller.evaluate((el) => el.scrollTop) await page.getByRole("button", { name: "Apply agent edit" }).click() await expect(page.getByTestId("agent-edit-version")).toHaveText("after") - await expect(target.locator(".am-annotation-textarea")).toHaveValue("Keep this stable") + await expect(target.locator(".am-annotation-draft textarea")).toHaveValue("Keep this stable") await expect.poll(async () => scroller.evaluate((el) => el.scrollTop)).toBeCloseTo(before, 0) await expect.poll(async () => target.evaluate((el) => el.getBoundingClientRect().top)).toBeCloseTo(top, 0) - await target.getByRole("button", { name: "Comment" }).click() + await target.locator('[data-action="save"]').click() await expect(target.getByText("Keep this stable")).toBeVisible() const saved = await scroller.evaluate((el) => el.scrollTop) @@ -229,15 +229,15 @@ for (const modifier of ["Meta", "Control"] as const) { for (const text of ["First comment", "Second comment"]) { await target.locator('[data-line="1"]').last().hover() await target.locator("[data-utility-button]").last().click() - await target.locator(".am-annotation-textarea").fill(text) + await target.locator(".am-annotation-draft textarea").fill(text) if (text === "First comment") { - await target.getByRole("button", { name: "Comment", exact: true }).click() + await target.locator('[data-action="save"]').click() await expect(target.getByText(text, { exact: true })).toBeVisible() } } await page.keyboard.press("Shift+Enter") - await expect(target.locator(".am-annotation-textarea")).toHaveValue("Second comment\n") + await expect(target.locator(".am-annotation-draft textarea")).toHaveValue("Second comment\n") const result = await page.evaluate((modifier) => { const sent: Array<{ comments: Array<{ comment: string }>; autoSend: boolean }> = [] diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts index fb1bceb4f1e..8f3b9ce62d5 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts @@ -334,8 +334,8 @@ export function buildReviewAnnotation( onBodyChange: (body) => { meta.text = body }, - onSave: (body, selected) => handlers.addComment(meta.file, meta.side, meta.line, body, selected), - onSend: (body, selected) => handlers.sendComment(meta.file, meta.side, meta.line, body, selected), + onSave: (body, selected) => handlers.addComment(meta.file, meta.side, meta.line, body.trim(), selected), + onSend: (body, selected) => handlers.sendComment(meta.file, meta.side, meta.line, body.trim(), selected), onGithubSuccess: () => handlers.completeRemoteDraft?.(meta), onCancel: handlers.cancelDraft, onDestination: (value) => { From 5c981576db468c4e03bb871f0b05b3bff2950a90 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 14 Sep 2026 09:15:51 +0200 Subject: [PATCH 7/8] test(vscode): wait for annotation disposal callbacks --- .../tests/unit/annotation-lifecycle.test.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts b/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts index 8a7368fcc85..0f833e542c1 100644 --- a/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts +++ b/packages/kilo-vscode/tests/unit/annotation-lifecycle.test.ts @@ -12,9 +12,13 @@ it("releases a wrapper that is never inserted", async () => { const lifecycle = createAnnotationLifecycle() const meta: AnnotationMeta = { type: "draft", comment: null, file: "never.ts", side: "additions", line: 1 } let released = 0 - lifecycle.track(meta, document.createElement("div"), () => released++) + const disposed = Promise.withResolvers() + lifecycle.track(meta, document.createElement("div"), () => { + released++ + disposed.resolve() + }) document.body.append(document.createElement("span")) - await window.happyDOM.waitUntilComplete() + await disposed.promise expect(released).toBe(1) lifecycle.clear() await window.happyDOM.close() @@ -27,12 +31,17 @@ it("disposes detached and replaced annotation roots exactly once", async () => { const meta: AnnotationMeta = { type: "draft", comment: null, file: "test.ts", side: "additions", line: 1 } const host = document.createElement("div") let released = 0 - lifecycle.track(meta, host, () => released++) + const disposed = Promise.withResolvers() + lifecycle.track(meta, host, () => { + released++ + disposed.resolve() + }) document.body.append(host) - await window.happyDOM.waitUntilComplete() + // Flush the insertion observer without HappyDOM's timer-based completion wait. + await Promise.resolve() expect(released).toBe(0) host.remove() - await window.happyDOM.waitUntilComplete() + await disposed.promise expect(released).toBe(1) lifecycle.track(meta, host, () => released++) lifecycle.track(meta, document.createElement("div"), () => released++) From 9133a745ce041359b35421c6880b6e03fda9fb22 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 14 Sep 2026 09:38:37 +0200 Subject: [PATCH 8/8] fix(vscode): translate PR comment actions across all locales --- .../plans/diff-comment-simplification-plan.md | 133 ------------ .../diff-viewer-github-comment-creation.md | 205 ------------------ .../webview-ui/agent-manager/i18n/ar.ts | 4 +- .../webview-ui/agent-manager/i18n/br.ts | 4 +- .../webview-ui/agent-manager/i18n/bs.ts | 4 +- .../webview-ui/agent-manager/i18n/da.ts | 4 +- .../webview-ui/agent-manager/i18n/de.ts | 4 +- .../webview-ui/agent-manager/i18n/es.ts | 4 +- .../webview-ui/agent-manager/i18n/fa.ts | 4 +- .../webview-ui/agent-manager/i18n/fr.ts | 4 +- .../webview-ui/agent-manager/i18n/it.ts | 4 +- .../webview-ui/agent-manager/i18n/ja.ts | 4 +- .../webview-ui/agent-manager/i18n/ko.ts | 4 +- .../webview-ui/agent-manager/i18n/nl.ts | 4 +- .../webview-ui/agent-manager/i18n/no.ts | 4 +- .../webview-ui/agent-manager/i18n/pl.ts | 4 +- .../webview-ui/agent-manager/i18n/ru.ts | 4 +- .../webview-ui/agent-manager/i18n/th.ts | 4 +- .../webview-ui/agent-manager/i18n/tr.ts | 4 +- .../webview-ui/agent-manager/i18n/uk.ts | 4 +- .../webview-ui/agent-manager/i18n/zh.ts | 4 +- .../webview-ui/agent-manager/i18n/zht.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/br.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/bs.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/da.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/de.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/es.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/fa.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/fr.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/it.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/ja.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/ko.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/nl.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/no.ts | 23 +- .../kilo-vscode/webview-ui/src/i18n/pl.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/ru.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/th.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/tr.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/uk.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/zh.ts | 22 +- .../kilo-vscode/webview-ui/src/i18n/zht.ts | 22 +- 42 files changed, 261 insertions(+), 598 deletions(-) delete mode 100644 .kilo/plans/diff-comment-simplification-plan.md delete mode 100644 .kilo/plans/diff-viewer-github-comment-creation.md diff --git a/.kilo/plans/diff-comment-simplification-plan.md b/.kilo/plans/diff-comment-simplification-plan.md deleted file mode 100644 index 74985024780..00000000000 --- a/.kilo/plans/diff-comment-simplification-plan.md +++ /dev/null @@ -1,133 +0,0 @@ -# Simplify the Diff Comment Implementation - -## Goal - -Reduce the diff size and structural complexity of the PR comment work without changing behavior. Optimize for reviewability and for keeping shared existing files close to their original shape. - -## Current Size - -| Area | Added | -|---|---| -| Existing tracked files | ~1124 insertions, 101 deletions across 18 files | -| New source modules | `pr-diff.ts`, `annotation-lifecycle.ts`, `diff-comment-forms.tsx`, `diff-comment-state.ts` (~285 lines) | -| New tests | 5 unit/fixture files | -| Untracked scratch | `.kilo/plans/diff-viewer-github-comment-creation.md` (remove from the shipped diff) | - -The largest existing-file growth is `review-annotations.ts` (+310), `PRCommentForm.tsx` (+245), and `DiffViewerApp.tsx` (+165). - -## Principles - -- One owner for PR snapshot state and comment form creation. No parallel implementations. -- Production always mounts `PRCommentForm`. Do not keep imperative fallbacks that only tests use. -- Prefer collapsing props into one object over adding a prop per field. -- Keep keyboard, focus, and safety behavior exactly as tested. -- Do not edit shared upstream opencode files. This package is Kilo-owned. - -## Proposals - -### 1. Create comment forms inside `createReviewView` (high impact, medium risk) - -`createDiffCommentForms` is instantiated in three places: `DiffPanel.tsx`, `FullScreenDiffView.tsx`, and `DiffViewerApp.tsx`. Two of them then pass it back into `createReviewView` through `localForm`/`remote`/`remoteAccessor` plus a `ReviewViewOverrides` layer. - -Change: - -- Create the forms once in `createReviewView` from `diffs`, `worktreeId`, and a single PR context. -- Delete the `createDiffCommentForms` blocks in `DiffPanel` and `FullScreenDiffView`. -- Remove `ReviewViewOverrides`, `ReviewViewProps.remote`, `ReviewViewProps.localForm`, and `ReviewViewProps.remoteAccessor`. -- `DiffViewerApp` keeps computing PR diffs and the snapshot for PR mode, but stops creating forms. - -Estimated: −60 to −80 lines and one less indirection layer. - -### 2. Collapse the four PR props into one `pr` context (high impact, low risk) - -`prTarget`, `prSnapshot`, `prLoading`, and `prError` are threaded `AgentManagerApp` → `DiffPanelCache` → `DiffPanel`, plus three of them through `FullScreenDiffView`. - -Change: - -- Introduce `pr?: { target: PRTarget; snapshot?: PRDiffSnapshot; loading?: boolean; error?: string }`. -- Pass one prop per layer. `DiffPanelCache` drops four prop definitions and four call-sites to one. - -Estimated: −15 to −20 lines and a smaller public surface. - -### 3. Delete the unused non-mounted remote fallback (high impact, low risk) - -Production always supplies `remote.mount`. `remote.submit`, `publish()`, `githubSubmitButton`, the non-mounted `hint`, and the `remoteMounted` branches in `update()` exist only for tests. - -Change: - -- Make `mount` required in `RemoteCommentConfig`. -- Delete `submit`, `publish()`, `githubSubmitButton`, the non-mounted hint, and the `handlers.remote && !handlers.localMount` branch. -- Update `tests/unit/review-annotations.test.ts` to always pass a mount. - -Estimated: −60 to −80 lines in `review-annotations.ts`. - -### 4. Evaluate removing `annotation-lifecycle.ts` (medium impact, needs a check first) - -This module (32 lines + tests + `track` plumbing) exists because Pierre may replace annotation DOM without invoking button handlers. - -Change: - -- Add a focused test that rebuilds the same open draft twice and checks whether the mounted root is disposed. -- If Pierre reuses the wrapper for an open draft, dispose only on draft change, cancel, and complete (already partly handled in `review-controller.ts`), and delete the module plus the `track` handler. -- If Pierre does detach, keep it but reuse the existing mounted-registry pattern from `remote-comment-renderer.tsx` instead of adding a second `MutationObserver` implementation. - -Estimated: −40 to −60 lines if removable. - -### 5. Collapse the new `PRCommentForm` props into one variant (medium impact, low risk) - -`submitOnEnter`, `onEscape`, `replaceBody`, and `inline` are four new props that are always set together by the diff composers. - -Change: - -- Replace with `variant?: "inline"`. -- `inline` implies submit-on-Enter, Escape-to-cancel, and body replacement on destination switch. -- Move the render-time `untrack` patch to a mount effect so it does not write during component creation. - -Estimated: −15 to −25 lines and a clearer API. - -### 6. Deduplicate PR context label CSS and strings (small, low risk) - -Three near-identical rules for the same label: `.diff-pr-context` in `banners.css`, `.am-diff-pr-context` and `.am-review-pr-context` in `agent-manager.css`, including duplicated `svg` rules. - -Change: - -- Use `.diff-pr-context` in all three components and delete the other two rule sets. -- Reuse existing i18n keys where semantics match and drop duplicates such as `diffViewer.comment.postToGithub` if an equivalent `agentManager.pr.*` key exists. - -Estimated: −20 CSS lines, −3 to −4 i18n keys. - -### 7. Small host-side cleanup (small, low risk) - -- `PRReviewActions.handle()` calls `checkBranch` and then `load()` calls it again. Keep one check per action. -- Reuse an existing git helper in `DiffViewerProvider` instead of the inline `execWithShellEnv` closure if one fits. - -Estimated: −5 lines and one fewer `git rev-parse` per load. - -### 8. Optional: use GitHub `additions`/`deletions` (neutral) - -`review-actions.parse()` already reads `additions`/`deletions` but drops them, so `pr-diff.ts` re-counts patch lines. Adding the two fields to `PRFile` and using them removes the `counts()` helper. - -Estimated: roughly neutral diff, one fewer parser. - -## Recommended Order - -| Order | Item | Why | -|---|---|---| -| 1 | 3, delete fallback | Largest reduction, no behavior risk | -| 2 | 5, one form variant | Unblocks reading the rest of `PRCommentForm` | -| 3 | 1 + 2, single form owner and `pr` object | Biggest structural win | -| 4 | 6 + 7, small dedupe | Cheap cleanup | -| 5 | 4, lifecycle decision | Needs a test first, keep if unproven | -| 6 | 8, optional | Neutral, do only if touching `parse()` anyway | - -## Verification - -- Keep the focused suites green: `inline-comment-form`, `review-annotations`, `annotation-lifecycle` (or its replacement), `pr-diff`, `pr-review-actions`, `pr-review-render`, `diff-comment-*`, `remote-comments`, `agent-manager-arch`. -- Re-run `bun run compile`, `bun run lint`, `bun run knip`, and `bun run check-kilocode-change`. -- Re-run the isolated VS Code checks for focus, Enter, Shift+Enter, Escape, preview, destination switch, local save, and GitHub post on the disposable test repo only. - -## Non-Goals - -- No behavior changes to keyboard, focus, or publication safety. -- No new features or UX changes beyond what is already merged in this branch. -- No changes to `packages/opencode/` or other shared upstream files. diff --git a/.kilo/plans/diff-viewer-github-comment-creation.md b/.kilo/plans/diff-viewer-github-comment-creation.md deleted file mode 100644 index 17c2cb5ab32..00000000000 --- a/.kilo/plans/diff-viewer-github-comment-creation.md +++ /dev/null @@ -1,205 +0,0 @@ -# Create GitHub Comments From the Diff Viewer - -## Goal - -After checking out another person's PR in the current worktree, let the user select a line in the diff viewer and post a new inline comment to that PR. Keep local comments for agent feedback clearly separate from comments published to GitHub. - -This is an implementation plan only. It does not authorize checkout operations or posting comments during planning. - -## Existing Foundation - -Source inspection shows that Agent Manager's PR Files view already creates GitHub inline comments, including multiline comments. The standalone Changes viewer loads GitHub threads and supports replies and other existing thread actions, but does not route new-comment creation. This work is primarily an integration and UX change, not a new GitHub API feature. - -All paths below are relative to `packages/kilo-vscode/`. - -| Area | Existing Implementation | -|---|---| -| Changes viewer and local comments | `webview-ui/diff-viewer/DiffViewerApp.tsx`, `review-controller.ts`, `review-annotations.ts` | -| Changes host and thread actions | `src/diff/DiffViewerProvider.ts`, `src/diff/comment-actions.ts` | -| GitHub thread rendering and safe display mapping | `webview-ui/diff-viewer/remote-comments.ts`, `remote-comment-renderer.tsx` | -| Snapshot-backed PR diff and composer | `webview-ui/agent-manager/pr/PRFiles.tsx`, `PRCommentForm.tsx` | -| Shared creation and snapshot contracts | `src/shared/pr-comment-actions.ts`, `src/shared/pr-patch.ts` | -| Validated GitHub writes | `src/agent-manager/pr/review-actions.ts` (`PRReviewActions`) | -| Existing host integration example | `src/agent-manager/pr-status-bridge.ts` | -| Worktree-scoped PR discovery | `src/diff/pr-poller.ts`, `src/agent-manager/PRStatusPoller.ts` | - -Reuse `loadPRFiles`, `createReviewComment`, `PRDiffSnapshot`, and `PRCommentForm` with `action="line"`. The existing write handler validates the patch and fresh base/head revisions before posting through `gh`. No CLI endpoint, SDK regeneration, or new authentication system should be needed. - -## Recommended UX - -Use one inline composer with an explicit destination, not a global setting that silently changes what the existing comment action does. - -| Element | Local Comment | GitHub Comment | -|---|---|---| -| Destination label | `Local` | `GitHub` | -| Helper text | `Saved locally. Not posted to GitHub. Send to the agent when ready.` | `Posts immediately to owner/repo#123. Visible to people with access to this PR.` | -| Submit action | `Save local comment` | `Post to GitHub` | -| Saved appearance | Local badge and existing edit/remove actions | GitHub badge, author, timestamp, and link | -| Agent submission | Included in the existing local-comment flow | Not included automatically | -| Publication | Never automatic | Only after the user selects the GitHub destination and posts | - -- Keep the existing gutter action. Open the composer with `Local` selected by default, including on PR branches. -- Show the `Local | GitHub` destination control inside the composer. Do not persist a GitHub default across composers, sessions, or worktrees in the first version. -- Show a compact PR context label in the viewer header, for example `GitHub: owner/repo#123`, with an open-in-browser action. -- Show the target PR and authenticated GitHub account before publication. Use the same account and authentication path as existing replies. -- When GitHub commenting is unavailable, explain why beside the disabled destination. Keep local commenting available. -- In a local diff, show `Open PR changes to comment on GitHub` rather than pretending local coordinates are publishable. Switch to the snapshot-backed PR source and require a new line selection. Direct posting from arbitrary local diffs is deferred. -- Switching the destination preserves the typed text but does not save or publish it. Once saved or posted, a comment's destination does not change. -- Label existing GitHub reply buttons `Reply on GitHub` so replies and new comments share the same publication model. -- Keep local counts and the action to send comments to the agent separate from GitHub thread counts. Do not let a generic `Send comments` action publish GitHub drafts. -- Use text labels and existing icons, not color alone. Preserve the current visual style, keyboard flow, and narrow-panel layout. - -### Example Composer - -```text -src/example.ts:42 -[ Local ] [ GitHub ] - -GitHub: owner/repo#123 | Posting as @reviewer -Posts immediately. Visible to people with access to this PR. - -[ Comment text ] - -[ Cancel ] [ Post to GitHub ] -``` - -## First-Version Scope - -- Create one published inline PR comment at a time, using the existing GitHub thread display and reply flow afterward. -- Support additions, deletions, valid context lines, and same-side multiline ranges through the existing PR patch validator. Reject cross-side or invalid hunk selections. -- Support PRs from forks, not only branches in the base repository. -- Work in the current worktree. The user checks out the PR through existing tools; a new checkout UI is not required. -- Keep local comments and GitHub composer drafts in separate state. Unposted GitHub text must never enter the local agent-feedback payload. -- Retain failed drafts while the viewer stays open, including across diff refreshes. Warn before an explicit action discards text. Cross-restart draft persistence is not required for this version. - -Out of scope: pending GitHub reviews, batch submission, approve/request-changes actions, editing/deleting published comments, thread resolution, automatic conversion of local comments, file-level comments, and comments on arbitrary uncommitted lines. - -Existing edit/delete/resolve actions remain unchanged; they are not new work in this plan. Preserve the current GitHub.com-only write support. GitHub Enterprise support and detached-HEAD PR discovery are separate follow-ups. - -## Correct PR and Line Targeting - -The main correctness requirement is that the displayed source and line match the PR snapshot sent to GitHub. A local branch diff is not automatically the GitHub PR diff. - -1. Resolve the checked-out PR using the existing worktree-scoped GitHub integration. Capture the host, base repository, PR number and URL, base/head SHAs, and state. Use the base repository for API writes, including fork PRs. -2. Reuse the current discovery order: bare `gh pr view`, branch lookup, then an exact local-HEAD SHA match. Preserve host-side branch and panel-generation checks. If discovery is ambiguous or unsupported, disable publication with a clear reason rather than introducing a PR picker in this first version. -3. Provide a clearly labeled `PR changes` source in the existing viewer using the existing `PRFiles` snapshot loader and GitHub patches. Do not use a worktree creation base or working-copy contents. Prefer a small extraction of shared rendering where needed over copying PR Files into a second implementation. -4. Bind the displayed patch and composer to a host-owned snapshot identity. Keep PR review content separate from uncommitted changes, so a dirty worktree does not invalidate a correctly loaded PR snapshot. Never reset, stash, or overwrite local files to enable commenting. -5. Validate the path, side, and line against a complete PR patch. Use `LEFT` for deleted lines and `RIGHT` for added lines. Map context lines to verified coordinates. Do not guess when a patch is truncated, a file is binary, or a rename cannot be mapped reliably. -6. Before publication, verify that the PR context and base/head revisions still match the snapshot. If they changed, preserve the text, refresh the diff, and require the user to select a valid line again. Do not silently retarget a draft. -7. GitHub can still change between validation and publication. Send the captured commit SHA, handle API rejection, and keep any successfully published comment attached to its actual commit, even if it becomes outdated immediately afterward. - -Keep draft identity scoped to the worktree, PR, snapshot, path, side, and line. Ignore responses for a different viewer context; do not attach old drafts or responses to a newly checked-out PR. - -## GitHub Write Contract - -Reuse the extension's existing `gh` execution and authentication path. Do not introduce a token store or send credentials to the webview. - -Use the review-comment endpoint, not a PR timeline comment: - -```text -POST /repos/{owner}/{repo}/pulls/{pull_number}/comments -``` - -Single-line request body: - -```json -{ - "body": "The user's comment", - "commit_id": "", - "path": "src/example.ts", - "line": 42, - "side": "RIGHT" -} -``` - -- Use `line` and `side`, not the deprecated `position` field. Reuse the existing multiline support, which also sends `start_line` and `start_side`. -- Resolve the repository and commit from host-owned context. Validate all webview input, including nonempty text, integer line numbers, allowed sides, and membership in the loaded patch. -- Pass JSON safely through the existing process helper, with no shell interpolation of comment text. -- Correlate requests and results with a request ID. Disable repeat submission while the request is pending. -- On success, clear only the submitted draft and refresh the existing thread list. If thread refresh fails, report that publication succeeded and offer refresh, not another post. -- On authentication, permission, rate-limit, or invalid-line errors, retain the draft and show a specific next action. A network timeout can mean the write succeeded: show `Publication status unknown`, reload comments to check, and do not automatically retry the POST. -- Reuse existing logging conventions without recording comment bodies or credentials. - -## Implementation Steps - -### 1. Connect the Existing Host Actions - -- Extend the standalone diff integration to route `loadPRFiles` and `createReviewComment` to `PRReviewActions`, following `pr-status-bridge.ts`. -- Supply the host-owned directory, validated PR context, result callback, and poll-refresh callback. Preserve the panel instance/open-generation/branch/PR target checks in `comment-actions.ts`. -- New-thread creation must work when a PR has zero existing threads. Do not apply the existing-thread membership requirement to creation. -- Enable only the required operations. Do not expose review submission or suggestion application as a side effect. - -### 2. Add the PR Diff Source - -- Add `PR changes` to the Changes viewer's source controls when a supported PR is detected. -- Reuse `PRFiles.tsx` and its snapshot-backed selection behavior. Adapt or extract only the shared pieces needed to fit the existing viewer layout and local-comment annotations. -- Keep the current local source and its comparison-base controls unchanged. Label the immutable PR source so it is not confused with working-copy changes. -- Preserve existing snapshot limits: complete patches only, at most 3,000 files, 4 MiB of snapshot data, and bounded retained snapshots. Surface unsupported files and expired snapshots as unavailable targets. -- Do not use `remote-comments.ts` as an inverse line mapper. Its safe display checks do not prove that a local selection is publishable. - -### 3. Make the Destination Explicit - -- Add the destination control and exact-action button labels to the inline composer. Reuse `PRCommentForm action="line"` for the GitHub path rather than duplicating its pending, error, and correlation behavior. -- Keep the existing local `ReviewComment[]` format and agent submission route unchanged. Local records do not retain range endpoints, so never reconstruct a GitHub range from a saved local comment's selected text. -- Preserve range endpoints in the remote draft and bind it to the snapshot. Keep local and GitHub annotations distinct when both are displayed at the same line. -- Review current source/context-switch clearing behavior. Retain remote drafts during refresh, and warn before explicit navigation discards them. Never transfer a draft silently to another snapshot. -- Add localized copy, accessible destination labels, and existing-theme styling. Update any shared composer callers so Agent Manager PR Files keeps working. - -### 4. Verify and Release - -- Extend the focused tests below rather than duplicating existing patch-validation coverage. -- Run the extension checks and isolated VS Code flow before considering the implementation complete. -- Add one patch changeset for the extension, for example: `Post GitHub PR comments from the Changes viewer with explicit local and GitHub destinations.` No changeset is needed for this plan-only change. - -## Verification Plan - -### Focused Automated Coverage - -| Existing Test | Add or Verify | -|---|---| -| `tests/unit/diff-comment-actions.test.ts` | New-operation routing, zero-thread creation, branch and panel checks, result correlation, refresh | -| `tests/unit/diff-comment-target.test.ts` | Stale generations, wrong worktrees, historical contexts without live write targets | -| `tests/unit/pr-review-actions.test.ts` | Reuse payload/range/revision coverage; add only missing adapter-specific and uncertain-write cases | -| `tests/unit/pr-review-render.test.ts` | Reused PR Files/form behavior remains intact | -| `tests/unit/diff-comment-render.test.ts` | Explicit destinations, correct action labels, pending/error state, drafts, no accidental agent submission | -| `tests/unit/remote-comments.test.ts` | New threads integrate with existing rendering without weakening anchor validation | -| `tests/unit/pr-comment-context.test.ts` | Dirty worktree, different HEAD/index, renames, missing objects, snapshot-pinned content | - -Use real temporary Git repositories and existing implementation tests where possible. Use controlled GitHub boundary fixtures only where network writes or failure injection require them. - -From `packages/kilo-vscode/`, run the affected tests with `bun test tests/unit/.test.ts`, then `bun run typecheck`, `bun run lint`, `bun run compile`, `bun run knip`, and `bun run check-kilocode-change`. Run the broader `bun run test:unit` suite before release. If implementation adds or changes source URLs, run the repository's source-link extraction guard. - -### Isolated UI Verification - -Load `self-testing` and `vscode-self-test` and use the isolated VS Code harness with a disposable fixture workspace. Do not use Storybook as a substitute or real credentials in the automated harness. - -1. Open Changes for a fixture PR with zero threads, select `PR changes`, create a GitHub comment, and verify the correct request, success state, thread display, and reply action through a controlled GitHub boundary. -2. Create a local comment on the same file. Verify the badge, local-only count, and agent submission route. Confirm that no GitHub write occurs. -3. Check additions, deletions, multiline ranges, renames, unsupported patches, a fork PR, and dirty/unpushed local changes. -4. Change the PR revisions or worktree while a form or request is active. Verify safe blocking, retained text, and no result appearing in the wrong context. -5. Exercise permission failure, authentication failure, timeout, double-click submission, and successful publication followed by failed refresh. -6. Inspect screenshots for keyboard access, narrow width, readable destination labels, and mixed local/GitHub threads. Check that Agent Manager's existing PR Files flow has no regression. - -Separately, an authorized human smoke test on a disposable GitHub PR should confirm that a new comment appears on the correct line under the expected account, including a fork PR. Do not post to production discussions as an automated test. Report fixture-only verification as such if this live check is unavailable. - -## Delivery Size - -This is a medium-sized integration with an existing API foundation. Most work is in composing the two diff/comment UIs, preserving draft state, and keeping write targets safe. A new API client is unnecessary. - -Prefer two focused implementation increments if needed: first connect the existing snapshot-backed PR view and creation handler to Changes; then add the explicit destination UX and regression coverage. Both are required for the user-facing feature to be complete. Avoid expanding the work into arbitrary local-to-PR line translation or a full review workflow. - -## Acceptance Criteria - -1. A user can check out another person's PR, open its diff, select an added or deleted line, choose `GitHub`, and post a new thread visible at the same location on GitHub. -2. The target repository, PR number, account, and immediate publication behavior are clear before posting. -3. Saving local comments and sending them to the agent work as before and never write to GitHub. -4. New GitHub comments use the existing thread display and can be replied to through the existing flow. -5. A fork PR posts to the base repository, not the fork's repository or another open worktree's PR. -6. Local-only lines cannot be published as if they were PR lines. A changed snapshot blocks submission and preserves the draft. -7. Failed or uncertain writes do not lose text, automatically retry, or claim success without evidence. -8. Switching branches, PRs, worktrees, or diff sources does not mix comment destinations, line anchors, or pending results. - -## References - -- [GitHub REST: Create a review comment](https://docs.github.com/en/rest/pulls/comments#create-a-review-comment-for-a-pull-request) -- [GitHub CLI: PR metadata](https://cli.github.com/manual/gh_pr_view) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index cd06d6effea..5f047b83245 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -227,8 +227,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "إرسال الكل إلى الدردشة ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "إرسال {{count}} إلى GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "توقف الإرسال بسبب خطأ في GitHub: {{error}}", "agentManager.review.inlineCount": "التعليقات المحلية ({{count}})", "agentManager.review.prCount": "تعليقات PR ({{count}})", "agentManager.review.fileCount": "{{count}} ملفًا", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 851501b94b3..9cbf250343c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -233,8 +233,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Enviar tudo para o chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Enviar {{count}} para o GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Envio interrompido por um erro do GitHub: {{error}}", "agentManager.review.inlineCount": "Comentários locais ({{count}})", "agentManager.review.prCount": "Comentários de PR ({{count}})", "agentManager.review.fileCount": "{{count}} arquivos", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index 00fbd5b4641..5e39ce637cc 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -231,8 +231,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Pošalji sve u chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Pošalji {{count}} na GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Slanje je zaustavljeno zbog greške na GitHubu: {{error}}", "agentManager.review.inlineCount": "Lokalni komentari ({{count}})", "agentManager.review.prCount": "PR komentari ({{count}})", "agentManager.review.fileCount": "{{count}} datoteka", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 6eaa03ca137..7ed33f8c0da 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -232,8 +232,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Send alt til chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} til GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Afsendelsen blev stoppet på grund af en GitHub-fejl: {{error}}", "agentManager.review.inlineCount": "Lokale kommentarer ({{count}})", "agentManager.review.prCount": "PR-kommentarer ({{count}})", "agentManager.review.fileCount": "{{count}} filer", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 342b2e56e2d..6a213fe9666 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -239,8 +239,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Alles an den Chat senden ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "{{count}} an GitHub #{{number}} senden", + "agentManager.review.sendAllToGithubFailed": "Senden wegen eines GitHub-Fehlers gestoppt: {{error}}", "agentManager.review.inlineCount": "Lokale Kommentare ({{count}})", "agentManager.review.prCount": "PR-Kommentare ({{count}})", "agentManager.review.fileCount": "{{count}} Dateien", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index e1fdbd6bc08..6d61a25ed66 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -236,8 +236,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Enviar todo al chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Enviar {{count}} a GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Envío detenido por un error de GitHub: {{error}}", "agentManager.review.inlineCount": "Comentarios locales ({{count}})", "agentManager.review.prCount": "Comentarios del PR ({{count}})", "agentManager.review.fileCount": "{{count}} archivos", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts index ca520e11b53..7c87d7198f5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -235,8 +235,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "ارسال همه به چت ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "ارسال {{count}} مورد به GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "ارسال به دلیل خطای GitHub متوقف شد: {{error}}", "agentManager.review.inlineCount": "نظرات محلی ({{count}})", "agentManager.review.prCount": "نظرات PR ({{count}})", "agentManager.review.fileCount": "{{count}} فایل", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 5564ddbaeac..8df4cb61c96 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -239,8 +239,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Tout envoyer au chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Envoyer {{count}} à GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Envoi interrompu en raison d’une erreur GitHub : {{error}}", "agentManager.review.inlineCount": "Commentaires locaux ({{count}})", "agentManager.review.prCount": "Commentaires du PR ({{count}})", "agentManager.review.fileCount": "{{count}} fichiers", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index cee4a10a6a2..d3976fb9ab6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -241,8 +241,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Invia tutto alla chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "Cmd+Invio", "agentManager.review.sendAllShortcut.other": "Ctrl+Invio", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Invia {{count}} a GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Invio interrotto a causa di un errore di GitHub: {{error}}", "agentManager.review.inlineCount": "Commenti locali ({{count}})", "agentManager.review.prCount": "Commenti della PR ({{count}})", "agentManager.review.fileCount": "{{count}} file", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index 4893d46fe9d..7fc490da636 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -232,8 +232,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "すべてをチャットに送信 ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "{{count}}件をGitHub #{{number}}に送信", + "agentManager.review.sendAllToGithubFailed": "GitHubのエラーにより送信を停止しました: {{error}}", "agentManager.review.inlineCount": "ローカルコメント ({{count}})", "agentManager.review.prCount": "PRコメント ({{count}})", "agentManager.review.fileCount": "{{count}} ファイル", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index 1a6ed160740..9d05e13b901 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -230,8 +230,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "모두 채팅으로 보내기 ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "{{count}}개를 GitHub #{{number}}로 보내기", + "agentManager.review.sendAllToGithubFailed": "GitHub 오류로 전송이 중단되었습니다: {{error}}", "agentManager.review.inlineCount": "로컬 댓글 ({{count}})", "agentManager.review.prCount": "PR 댓글 ({{count}})", "agentManager.review.fileCount": "{{count}}개 파일", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 7f4900b8a25..e773024c2ef 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -239,8 +239,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Alles naar chat sturen ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "{{count}} naar GitHub #{{number}} sturen", + "agentManager.review.sendAllToGithubFailed": "Verzenden gestopt door een GitHub-fout: {{error}}", "agentManager.review.inlineCount": "Lokale opmerkingen ({{count}})", "agentManager.review.prCount": "PR-opmerkingen ({{count}})", "agentManager.review.fileCount": "{{count}} bestanden", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index eae5c82153a..00560cf75fb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -230,8 +230,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Send alt til chat ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Send {{count}} til GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Sendingen ble stoppet på grunn av en GitHub-feil: {{error}}", "agentManager.review.inlineCount": "Lokale kommentarer ({{count}})", "agentManager.review.prCount": "PR-kommentarer ({{count}})", "agentManager.review.fileCount": "{{count}} filer", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 05bbd1c041d..24620951e53 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -232,8 +232,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Wyślij wszystko do czatu ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Wyślij {{count}} do GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Wysyłanie zatrzymane z powodu błędu GitHuba: {{error}}", "agentManager.review.inlineCount": "Komentarze lokalne ({{count}})", "agentManager.review.prCount": "Komentarze PR ({{count}})", "agentManager.review.fileCount": "{{count}} plików", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 5d63d7a94e5..cec6fa80f88 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -235,8 +235,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Отправить всё в чат ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Отправить {{count}} в GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Отправка остановлена из-за ошибки GitHub: {{error}}", "agentManager.review.inlineCount": "Локальные комментарии ({{count}})", "agentManager.review.prCount": "Комментарии PR ({{count}})", "agentManager.review.fileCount": "{{count}} файлов", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 5276639611e..a8740363f86 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -226,8 +226,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "ส่งทั้งหมดไปยังแชท ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "ส่ง {{count}} รายการไปยัง GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "หยุดการส่งเนื่องจากข้อผิดพลาดของ GitHub: {{error}}", "agentManager.review.inlineCount": "ความคิดเห็นในเครื่อง ({{count}})", "agentManager.review.prCount": "ความคิดเห็น PR ({{count}})", "agentManager.review.fileCount": "{{count}} ไฟล์", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index 570e18ad869..42e53b1cc92 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -240,8 +240,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Tümünü sohbete gönder ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "{{count}} yorumu GitHub #{{number}} hedefine gönder", + "agentManager.review.sendAllToGithubFailed": "Gönderim bir GitHub hatası nedeniyle durduruldu: {{error}}", "agentManager.review.inlineCount": "Yerel yorumlar ({{count}})", "agentManager.review.prCount": "PR yorumları ({{count}})", "agentManager.review.fileCount": "{{count}} dosya", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 59b52c1ba8d..5fe85216a25 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -243,8 +243,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "Надіслати все до чату ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "Надіслати {{count}} до GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "Надсилання зупинено через помилку GitHub: {{error}}", "agentManager.review.inlineCount": "Локальні коментарі ({{count}})", "agentManager.review.prCount": "Коментарі PR ({{count}})", "agentManager.review.fileCount": "{{count}} файлів", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index c2c32cfe1f0..ba6cf78440a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -222,8 +222,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "全部发送到聊天 ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "发送 {{count}} 条评论到 GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "因 GitHub 错误而停止发送:{{error}}", "agentManager.review.inlineCount": "本地评论 ({{count}})", "agentManager.review.prCount": "PR 评论 ({{count}})", "agentManager.review.fileCount": "{{count}} 个文件", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index f5af93c1413..d0dc8cb7464 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -222,8 +222,8 @@ export const dict = { "agentManager.review.sendAllToChatWithCount": "全部傳送到聊天 ({{count}})", "agentManager.review.sendAllShortcut.mac": "⌘Enter", "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", - "agentManager.review.sendAllToGithubWithCount": "Send {{count}} to GitHub #{{number}}", - "agentManager.review.sendAllToGithubFailed": "Stopped at a GitHub error: {{error}}", + "agentManager.review.sendAllToGithubWithCount": "傳送 {{count}} 則留言到 GitHub #{{number}}", + "agentManager.review.sendAllToGithubFailed": "因 GitHub 錯誤而停止傳送:{{error}}", "agentManager.review.inlineCount": "本機留言 ({{count}})", "agentManager.review.prCount": "PR 留言 ({{count}})", "agentManager.review.fileCount": "{{count}} 個檔案", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 43556bae631..37432d352ca 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1271,18 +1271,18 @@ export const dict = { "الملفات التي غيّرها Kilo خلال الجلسة الحالية، بناءً على لقطات لكل دور. يُعاد ضبطها عند بدء جلسة جديدة.", "diffViewer.group.session": "الجلسة", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "حفظ محليًا", + "diffViewer.comment.sendToAgent": "إرسال إلى الوكيل", + "diffViewer.comment.postToGithub": "نشر على GitHub", + "diffViewer.comment.loadFailed": "تعذر تحميل تغييرات طلب السحب.", + "diffViewer.comment.unavailable": "هذا السطر غير متاح في اللقطة الحالية لطلب السحب.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "فتح طلب السحب", + "diffViewer.comment.localChanges": "التغييرات المحلية", + "diffViewer.comment.prChanges": "تغييرات PR", + "diffViewer.comment.sendToKilo": "إرسال إلى Kilo", + "diffViewer.comment.sendToGithub": "إرسال إلى GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "اختيار الوجهة", "diffViewer.notice.snapshotsDisabled": "اللقطات معطّلة لهذا المستودع. يُرجى تعديل ملفات الإعدادات لعرض تغييرات الجلسة.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index c2948de5ebb..3da69bc6c06 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1315,18 +1315,18 @@ export const dict = { "Arquivos modificados pelo Kilo durante a sessão atual, com base em snapshots por turno. Reinicia ao começar uma nova sessão.", "diffViewer.group.session": "Sessão", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Salvar localmente", + "diffViewer.comment.sendToAgent": "Enviar para o agente", + "diffViewer.comment.postToGithub": "Publicar no GitHub", + "diffViewer.comment.loadFailed": "Não foi possível carregar as alterações da solicitação de extração.", + "diffViewer.comment.unavailable": "Esta linha não está disponível no snapshot atual da solicitação de extração.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Abrir PR", + "diffViewer.comment.localChanges": "Alterações locais", + "diffViewer.comment.prChanges": "Alterações do PR", + "diffViewer.comment.sendToKilo": "Enviar para o Kilo", + "diffViewer.comment.sendToGithub": "Enviar para o GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Escolher destino", "diffViewer.notice.snapshotsDisabled": "Os snapshots estão desativados para este repositório. Edite seus arquivos de configuração para exibir as alterações da sessão.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 46cf414deae..63528e4905a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1306,18 +1306,18 @@ export const dict = { "Datoteke koje je Kilo promijenio tokom trenutne sesije, na osnovu snapshota po koraku. Resetuje se kada pokrenete novu sesiju.", "diffViewer.group.session": "Sesija", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Sačuvaj lokalno", + "diffViewer.comment.sendToAgent": "Pošalji agentu", + "diffViewer.comment.postToGithub": "Objavi na GitHubu", + "diffViewer.comment.loadFailed": "Nije moguće učitati izmjene zahtjeva za povlačenje.", + "diffViewer.comment.unavailable": "Ovaj red nije dostupan u trenutnom snimku zahtjeva za povlačenje.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Otvori PR", + "diffViewer.comment.localChanges": "Lokalne izmjene", + "diffViewer.comment.prChanges": "PR izmjene", + "diffViewer.comment.sendToKilo": "Pošalji Kilu", + "diffViewer.comment.sendToGithub": "Pošalji na GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Odaberi odredište", "diffViewer.notice.snapshotsDisabled": "Snapshotovi su onemogućeni za ovaj repozitorij. Uredite konfiguracijske datoteke da biste prikazali promjene sesije.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index cb42b127220..19535187afd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1300,18 +1300,18 @@ export const dict = { "Filer ændret af Kilo i den aktuelle session, baseret på snapshots pr. tur. Nulstilles, når du starter en ny session.", "diffViewer.group.session": "Session", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Gem lokalt", + "diffViewer.comment.sendToAgent": "Send til agent", + "diffViewer.comment.postToGithub": "Udgiv på GitHub", + "diffViewer.comment.loadFailed": "Kunne ikke indlæse ændringerne i pull requesten.", + "diffViewer.comment.unavailable": "Denne linje er ikke tilgængelig i det aktuelle snapshot af pull requesten.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Åbn PR", + "diffViewer.comment.localChanges": "Lokale ændringer", + "diffViewer.comment.prChanges": "PR-ændringer", + "diffViewer.comment.sendToKilo": "Send til Kilo", + "diffViewer.comment.sendToGithub": "Send til GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Vælg destination", "diffViewer.notice.snapshotsDisabled": "Snapshots er deaktiveret for dette repository. Rediger dine konfigurationsfiler for at vise sessionens ændringer.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 39674939406..05e9a335459 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1328,18 +1328,18 @@ export const dict = { "Von Kilo während der aktuellen Sitzung geänderte Dateien, basierend auf Snapshots pro Runde. Wird beim Start einer neuen Sitzung zurückgesetzt.", "diffViewer.group.session": "Sitzung", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Lokal speichern", + "diffViewer.comment.sendToAgent": "An Agent senden", + "diffViewer.comment.postToGithub": "Auf GitHub veröffentlichen", + "diffViewer.comment.loadFailed": "Die Änderungen des Pull Requests konnten nicht geladen werden.", + "diffViewer.comment.unavailable": "Diese Zeile ist im aktuellen Snapshot des Pull Requests nicht verfügbar.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Pull Request öffnen", + "diffViewer.comment.localChanges": "Lokale Änderungen", + "diffViewer.comment.prChanges": "PR-Änderungen", + "diffViewer.comment.sendToKilo": "An Kilo senden", + "diffViewer.comment.sendToGithub": "An GitHub #{{number}} senden", + "diffViewer.comment.chooseDestination": "Ziel auswählen", "diffViewer.notice.snapshotsDisabled": "Snapshots sind für dieses Repository deaktiviert. Bitte bearbeite deine Konfigurationsdateien, um die Sitzungsänderungen anzuzeigen.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 8b5b0d7586f..ac15acdbf1c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1316,18 +1316,18 @@ export const dict = { "Archivos modificados por Kilo durante la sesión actual, basado en snapshots por turno. Se reinicia al empezar una nueva sesión.", "diffViewer.group.session": "Sesión", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Guardar localmente", + "diffViewer.comment.sendToAgent": "Enviar al agente", + "diffViewer.comment.postToGithub": "Publicar en GitHub", + "diffViewer.comment.loadFailed": "No se pudieron cargar los cambios del pull request.", + "diffViewer.comment.unavailable": "Esta línea no está disponible en la instantánea actual del pull request.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Abrir pull request", + "diffViewer.comment.localChanges": "Cambios locales", + "diffViewer.comment.prChanges": "Cambios del PR", + "diffViewer.comment.sendToKilo": "Enviar a Kilo", + "diffViewer.comment.sendToGithub": "Enviar a GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Elegir destino", "diffViewer.notice.snapshotsDisabled": "Las instantáneas están deshabilitadas para este repositorio. Edita tus archivos de configuración para mostrar los cambios de la sesión.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts index 043e72f53f2..fe87b590edd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -1300,18 +1300,18 @@ export const dict = { "فایل‌هایی که توسط Kilo در جلسه جاری تغییر کرده‌اند، بر اساس عکس‌های فوری هر نوبت. با شروع جلسه جدید بازنشانی می‌شود.", "diffViewer.group.session": "جلسه", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "ذخیرهٔ محلی", + "diffViewer.comment.sendToAgent": "ارسال به عامل", + "diffViewer.comment.postToGithub": "انتشار در GitHub", + "diffViewer.comment.loadFailed": "بارگذاری تغییرات درخواست ادغام ممکن نشد.", + "diffViewer.comment.unavailable": "این خط در تصویر لحظه‌ای فعلی درخواست ادغام موجود نیست.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "باز کردن درخواست ادغام", + "diffViewer.comment.localChanges": "تغییرات محلی", + "diffViewer.comment.prChanges": "تغییرات PR", + "diffViewer.comment.sendToKilo": "ارسال به Kilo", + "diffViewer.comment.sendToGithub": "ارسال به GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "انتخاب مقصد", "diffViewer.notice.snapshotsDisabled": "عکس‌های فوری برای این مخزن غیرفعال هستند. لطفاً فایل‌های پیکربندی خود را ویرایش کنید تا تغییرات جلسه نمایش داده شوند.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index c0d3f4e2f0f..343df6ca5e4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1337,18 +1337,18 @@ export const dict = { "Fichiers modifiés par Kilo pendant la session actuelle, basé sur des snapshots par tour. Réinitialisé lors du démarrage d'une nouvelle session.", "diffViewer.group.session": "Session", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Enregistrer localement", + "diffViewer.comment.sendToAgent": "Envoyer à l’agent", + "diffViewer.comment.postToGithub": "Publier sur GitHub", + "diffViewer.comment.loadFailed": "Impossible de charger les modifications de la pull request.", + "diffViewer.comment.unavailable": "Cette ligne n’est pas disponible dans l’instantané actuel de la pull request.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Ouvrir la pull request", + "diffViewer.comment.localChanges": "Modifications locales", + "diffViewer.comment.prChanges": "Modifications du PR", + "diffViewer.comment.sendToKilo": "Envoyer à Kilo", + "diffViewer.comment.sendToGithub": "Envoyer à GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Choisir la destination", "diffViewer.notice.snapshotsDisabled": "Les instantanés sont désactivés pour ce dépôt. Veuillez modifier vos fichiers de configuration pour afficher les changements de la session.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index cd42a7ee367..93e7ad59cec 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1181,18 +1181,18 @@ export const dict = { "File modificati da Kilo durante la sessione corrente, basati su snapshot per turno. Si resetta quando inizi una nuova sessione.", "diffViewer.group.session": "Sessione", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Salva in locale", + "diffViewer.comment.sendToAgent": "Invia all'agente", + "diffViewer.comment.postToGithub": "Pubblica su GitHub", + "diffViewer.comment.loadFailed": "Impossibile caricare le modifiche della pull request.", + "diffViewer.comment.unavailable": "Questa riga non è disponibile nell'istantanea attuale della pull request.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Apri pull request", + "diffViewer.comment.localChanges": "Modifiche locali", + "diffViewer.comment.prChanges": "Modifiche della PR", + "diffViewer.comment.sendToKilo": "Invia a Kilo", + "diffViewer.comment.sendToGithub": "Invia a GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Scegli destinazione", "diffViewer.notice.snapshotsDisabled": "Gli snapshot sono disabilitati per questa repository. Modifica i file di configurazione per visualizzare le modifiche della sessione.", "diffViewer.baseBranch.auto": "Predefinito", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 47d382f8d61..9d2bf628a61 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1293,18 +1293,18 @@ export const dict = { "現在のセッション中に Kilo が変更したファイル。ターンごとのスナップショットに基づきます。新しいセッションを開始するとリセットされます。", "diffViewer.group.session": "セッション", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "ローカルに保存", + "diffViewer.comment.sendToAgent": "エージェントに送信", + "diffViewer.comment.postToGithub": "GitHubに投稿", + "diffViewer.comment.loadFailed": "プルリクエストの変更を読み込めませんでした。", + "diffViewer.comment.unavailable": "この行は現在のプルリクエストのスナップショットでは利用できません。", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "プルリクエストを開く", + "diffViewer.comment.localChanges": "ローカルの変更", + "diffViewer.comment.prChanges": "PRの変更", + "diffViewer.comment.sendToKilo": "Kiloに送信", + "diffViewer.comment.sendToGithub": "GitHub #{{number}}に送信", + "diffViewer.comment.chooseDestination": "送信先を選択", "diffViewer.notice.snapshotsDisabled": "このリポジトリではスナップショットが無効になっています。セッションの変更を表示するには、構成ファイルを編集してください。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 41db577cce3..1338458d003 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1280,18 +1280,18 @@ export const dict = { "현재 세션 동안 Kilo가 변경한 파일로, 턴별 스냅샷을 기반으로 합니다. 새 세션을 시작하면 초기화됩니다.", "diffViewer.group.session": "세션", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "로컬에 저장", + "diffViewer.comment.sendToAgent": "에이전트로 보내기", + "diffViewer.comment.postToGithub": "GitHub에 게시", + "diffViewer.comment.loadFailed": "풀 리퀘스트 변경 사항을 불러올 수 없습니다.", + "diffViewer.comment.unavailable": "이 줄은 현재 풀 리퀘스트 스냅샷에서 사용할 수 없습니다.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "풀 리퀘스트 열기", + "diffViewer.comment.localChanges": "로컬 변경 사항", + "diffViewer.comment.prChanges": "PR 변경 사항", + "diffViewer.comment.sendToKilo": "Kilo로 보내기", + "diffViewer.comment.sendToGithub": "GitHub #{{number}}로 보내기", + "diffViewer.comment.chooseDestination": "대상 선택", "diffViewer.notice.snapshotsDisabled": "이 리포지토리에서 스냅샷이 비활성화되어 있습니다. 세션 변경 사항을 표시하려면 구성 파일을 편집하세요.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 77deceff87e..971f15033f4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1328,18 +1328,18 @@ export const dict = { "Bestanden die door Kilo tijdens de huidige sessie zijn gewijzigd, gebaseerd op snapshots per beurt. Wordt gereset bij het starten van een nieuwe sessie.", "diffViewer.group.session": "Sessie", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Lokaal opslaan", + "diffViewer.comment.sendToAgent": "Naar agent sturen", + "diffViewer.comment.postToGithub": "Op GitHub plaatsen", + "diffViewer.comment.loadFailed": "De wijzigingen van de pull request konden niet worden geladen.", + "diffViewer.comment.unavailable": "Deze regel is niet beschikbaar in de huidige snapshot van de pull request.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Pull request openen", + "diffViewer.comment.localChanges": "Lokale wijzigingen", + "diffViewer.comment.prChanges": "PR-wijzigingen", + "diffViewer.comment.sendToKilo": "Naar Kilo sturen", + "diffViewer.comment.sendToGithub": "Naar GitHub #{{number}} sturen", + "diffViewer.comment.chooseDestination": "Bestemming kiezen", "diffViewer.notice.snapshotsDisabled": "Snapshots zijn uitgeschakeld voor deze repository. Bewerk je configuratiebestanden om de sessiewijzigingen weer te geven.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 0d42f7fd095..d558fba68a1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1296,18 +1296,19 @@ export const dict = { "Filer endret av Kilo i løpet av gjeldende økt, basert på øyeblikksbilder per tur. Tilbakestilles når du starter en ny økt.", "diffViewer.group.session": "Økt", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Lagre lokalt", + "diffViewer.comment.sendToAgent": "Send til agent", + "diffViewer.comment.postToGithub": "Publiser på GitHub", + "diffViewer.comment.loadFailed": "Kunne ikke laste inn endringene i pull requesten.", + "diffViewer.comment.unavailable": + "Denne linjen er ikke tilgjengelig i det gjeldende øyeblikksbildet av pull requesten.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Åpne pull request", + "diffViewer.comment.localChanges": "Lokale endringer", + "diffViewer.comment.prChanges": "PR-endringer", + "diffViewer.comment.sendToKilo": "Send til Kilo", + "diffViewer.comment.sendToGithub": "Send til GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Velg mål", "diffViewer.notice.snapshotsDisabled": "Snapshots er deaktivert for dette repositoriet. Rediger konfigurasjonsfilene for å vise øktens endringer.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index c8a31cdcfa8..9e9e45b0459 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1306,18 +1306,18 @@ export const dict = { "Pliki zmienione przez Kilo w trakcie bieżącej sesji, na podstawie snapshotów na turę. Resetowane przy rozpoczęciu nowej sesji.", "diffViewer.group.session": "Sesja", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Zapisz lokalnie", + "diffViewer.comment.sendToAgent": "Wyślij do agenta", + "diffViewer.comment.postToGithub": "Opublikuj na GitHubie", + "diffViewer.comment.loadFailed": "Nie udało się wczytać zmian pull requesta.", + "diffViewer.comment.unavailable": "Ten wiersz nie jest dostępny w bieżącej migawce pull requesta.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Otwórz pull request", + "diffViewer.comment.localChanges": "Zmiany lokalne", + "diffViewer.comment.prChanges": "Zmiany PR", + "diffViewer.comment.sendToKilo": "Wyślij do Kilo", + "diffViewer.comment.sendToGithub": "Wyślij do GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Wybierz miejsce docelowe", "diffViewer.notice.snapshotsDisabled": "Migawki są wyłączone dla tego repozytorium. Edytuj pliki konfiguracyjne, aby wyświetlać zmiany sesji.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 6ae0327628c..d8c7add0a16 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1300,18 +1300,18 @@ export const dict = { "Файлы, изменённые Kilo в текущей сессии, на основе снимков по ходу. Сбрасывается при начале новой сессии.", "diffViewer.group.session": "Сессия", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Сохранить локально", + "diffViewer.comment.sendToAgent": "Отправить агенту", + "diffViewer.comment.postToGithub": "Опубликовать на GitHub", + "diffViewer.comment.loadFailed": "Не удалось загрузить изменения запроса на слияние.", + "diffViewer.comment.unavailable": "Эта строка недоступна в текущем снимке запроса на слияние.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Открыть PR", + "diffViewer.comment.localChanges": "Локальные изменения", + "diffViewer.comment.prChanges": "Изменения PR", + "diffViewer.comment.sendToKilo": "Отправить в Kilo", + "diffViewer.comment.sendToGithub": "Отправить в GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Выбрать назначение", "diffViewer.notice.snapshotsDisabled": "Снимки отключены для этого репозитория. Пожалуйста, отредактируйте файлы конфигурации, чтобы отображать изменения сессии.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 9c1eef8a775..9eabc2e215f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1277,18 +1277,18 @@ export const dict = { "ไฟล์ที่ Kilo แก้ไขในช่วงเซสชันปัจจุบัน โดยอิงจากสแน็ปช็อตต่อเทิร์น จะรีเซ็ตเมื่อเริ่มเซสชันใหม่", "diffViewer.group.session": "เซสชัน", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "บันทึกในเครื่อง", + "diffViewer.comment.sendToAgent": "ส่งไปยังเอเจนต์", + "diffViewer.comment.postToGithub": "โพสต์ไปยัง GitHub", + "diffViewer.comment.loadFailed": "ไม่สามารถโหลดการเปลี่ยนแปลงของคำขอรวมโค้ดได้", + "diffViewer.comment.unavailable": "บรรทัดนี้ไม่มีอยู่ในสแนปช็อตปัจจุบันของคำขอรวมโค้ด", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "เปิด Pull Request", + "diffViewer.comment.localChanges": "การเปลี่ยนแปลงในเครื่อง", + "diffViewer.comment.prChanges": "การเปลี่ยนแปลงของ PR", + "diffViewer.comment.sendToKilo": "ส่งไปยัง Kilo", + "diffViewer.comment.sendToGithub": "ส่งไปยัง GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "เลือกปลายทาง", "diffViewer.notice.snapshotsDisabled": "ปิดใช้งานสแนปช็อตสำหรับที่เก็บนี้ กรุณาแก้ไขไฟล์การกำหนดค่าเพื่อแสดงการเปลี่ยนแปลงของเซสชัน", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index f5d8b5c6f3c..5598e3c2de1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1316,18 +1316,18 @@ export const dict = { "Geçerli oturum sırasında Kilo tarafından değiştirilen dosyalar, tur başı anlık görüntülere dayanır. Yeni bir oturum başlatıldığında sıfırlanır.", "diffViewer.group.session": "Oturum", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Yerel olarak kaydet", + "diffViewer.comment.sendToAgent": "Ajana gönder", + "diffViewer.comment.postToGithub": "GitHub'da paylaş", + "diffViewer.comment.loadFailed": "Çekme isteğindeki değişiklikler yüklenemedi.", + "diffViewer.comment.unavailable": "Bu satır, çekme isteğinin mevcut anlık görüntüsünde bulunmuyor.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Pull request'i aç", + "diffViewer.comment.localChanges": "Yerel değişiklikler", + "diffViewer.comment.prChanges": "PR değişiklikleri", + "diffViewer.comment.sendToKilo": "Kilo'ya gönder", + "diffViewer.comment.sendToGithub": "GitHub #{{number}} hedefine gönder", + "diffViewer.comment.chooseDestination": "Hedef seç", "diffViewer.notice.snapshotsDisabled": "Bu depoda anlık görüntüler devre dışı bırakılmıştır. Oturum değişikliklerini görüntülemek için yapılandırma dosyalarınızı düzenleyin.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 0e81c3c843a..5d762ca80f4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1316,18 +1316,18 @@ export const dict = { "Файли, змінені Kilo під час поточної сесії, на основі знімків по ходу. Скидається при старті нової сесії.", "diffViewer.group.session": "Сесія", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "Зберегти локально", + "diffViewer.comment.sendToAgent": "Надіслати агенту", + "diffViewer.comment.postToGithub": "Опублікувати на GitHub", + "diffViewer.comment.loadFailed": "Не вдалося завантажити зміни пул-реквесту.", + "diffViewer.comment.unavailable": "Цей рядок недоступний у поточному знімку пул-реквесту.", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "Відкрити PR", + "diffViewer.comment.localChanges": "Локальні зміни", + "diffViewer.comment.prChanges": "Зміни PR", + "diffViewer.comment.sendToKilo": "Надіслати до Kilo", + "diffViewer.comment.sendToGithub": "Надіслати до GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "Вибрати призначення", "diffViewer.notice.snapshotsDisabled": "Знімки вимкнено для цього репозиторію. Будь ласка, відредагуйте файли конфігурації, щоб відображати зміни сесії.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index f992c6cfb31..3ed772a5206 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1231,18 +1231,18 @@ export const dict = { "diffViewer.source.session.tooltip": "Kilo 在当前会话中更改的文件,基于每轮快照。开始新会话时重置。", "diffViewer.group.session": "会话", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "保存到本地", + "diffViewer.comment.sendToAgent": "发送给智能体", + "diffViewer.comment.postToGithub": "发布到 GitHub", + "diffViewer.comment.loadFailed": "无法加载拉取请求的更改。", + "diffViewer.comment.unavailable": "此行在当前拉取请求快照中不可用。", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "打开拉取请求", + "diffViewer.comment.localChanges": "本地更改", + "diffViewer.comment.prChanges": "PR 更改", + "diffViewer.comment.sendToKilo": "发送到 Kilo", + "diffViewer.comment.sendToGithub": "发送到 GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "选择目标", "diffViewer.notice.snapshotsDisabled": "此仓库的快照已禁用。请编辑配置文件以显示会话变更。", "diffViewer.baseBranch.auto": "默认", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index c5116ff68da..c90388f4030 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1235,18 +1235,18 @@ export const dict = { "diffViewer.source.session.tooltip": "Kilo 在目前工作階段中變更的檔案,依據每輪快照。開始新工作階段時重置。", "diffViewer.group.session": "工作階段", "diffViewer.group.git": "Git", - "diffViewer.comment.saveLocal": "Save local", - "diffViewer.comment.sendToAgent": "Send to agent", - "diffViewer.comment.postToGithub": "Post to GitHub", - "diffViewer.comment.loadFailed": "Could not load the pull request changes.", - "diffViewer.comment.unavailable": "This line is not available in the current pull request snapshot.", + "diffViewer.comment.saveLocal": "儲存至本機", + "diffViewer.comment.sendToAgent": "傳送給代理程式", + "diffViewer.comment.postToGithub": "發佈到 GitHub", + "diffViewer.comment.loadFailed": "無法載入提取請求的變更。", + "diffViewer.comment.unavailable": "此行在目前的提取請求快照中無法使用。", "diffViewer.comment.prContext": "PR #{{number}}", - "diffViewer.comment.openPR": "Open pull request", - "diffViewer.comment.localChanges": "Local changes", - "diffViewer.comment.prChanges": "PR changes", - "diffViewer.comment.sendToKilo": "Send to Kilo", - "diffViewer.comment.sendToGithub": "Send to GitHub #{{number}}", - "diffViewer.comment.chooseDestination": "Choose destination", + "diffViewer.comment.openPR": "開啟提取請求", + "diffViewer.comment.localChanges": "本機變更", + "diffViewer.comment.prChanges": "PR 變更", + "diffViewer.comment.sendToKilo": "傳送到 Kilo", + "diffViewer.comment.sendToGithub": "傳送到 GitHub #{{number}}", + "diffViewer.comment.chooseDestination": "選擇目標", "diffViewer.notice.snapshotsDisabled": "此存放庫的快照已停用。請編輯設定檔以顯示工作階段的變更。", "diffViewer.baseBranch.auto": "預設",