diff --git a/.changeset/pr-comments-github-style.md b/.changeset/pr-comments-github-style.md new file mode 100644 index 0000000000..5fe95f6a13 --- /dev/null +++ b/.changeset/pr-comments-github-style.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Rework PR review comments in the Agent Manager PR panel: resolved threads now collapse into one-line rows in a Resolved group instead of being dimmed, each thread shows its replies, and every card has prominent Send to agent, Resolve, Copy, Open file, and Open on GitHub actions. A single button sends all unresolved comments to the agent, and comments arrive as structured review comments instead of pasted text. diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 171baccec0..528b8d3332 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -5,7 +5,14 @@ import { execWithShellEnv } from "./shell-env" import { execGhRead } from "./gh" import { classifyPRError } from "./git-import" import type { Semaphore } from "./semaphore" -import { parsePRResult, checkStatus, formatCheckDuration, parseComments, parseReviewers } from "./pr/am-pr-utils" +import { + parsePRResult, + checkStatus, + commentsSig, + formatCheckDuration, + parseComments, + parseReviewers, +} from "./pr/am-pr-utils" import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./pr/am-pr-types" interface PRStatusPollerOptions { @@ -273,7 +280,7 @@ export class PRStatusPoller { } const reviewersSig = reviewers.map((r) => `${r.login}:${r.state}`).join(",") - const hash = `${worktreeId}:${pr.number}:${pr.title}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewersSig}:${pr.body ?? ""}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}` + const hash = `${worktreeId}:${pr.number}:${pr.title}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewersSig}:${pr.body ?? ""}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}:${commentsSig(comments?.comments)}` if (this.lastHash.get(worktreeId) === hash) return this.lastHash.set(worktreeId, hash) @@ -489,13 +496,15 @@ export class PRStatusPoller { nodes { id isResolved - comments(first: 1) { + isOutdated + comments(first: 10) { nodes { id author { login avatarUrl } body path line + originalLine url createdAt diffHunk diff --git a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts index 40328a63c2..f4cd269eb3 100644 --- a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts +++ b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts @@ -10,6 +10,7 @@ import type { Disposable } from "./host" import type { Semaphore } from "./semaphore" import { PRStatusPoller } from "./PRStatusPoller" import { resolveComment, unresolveComment } from "./pr/PRActions" +import { ghErrorReason } from "./pr/am-pr-utils" interface PRBridgeHost { getWorktrees(): Worktree[] @@ -124,6 +125,7 @@ export class PRStatusBridge { worktreeId: id, threadId, success: false, + error: ghErrorReason(msg), }) }, ) diff --git a/packages/kilo-vscode/src/agent-manager/pr/am-pr-types.ts b/packages/kilo-vscode/src/agent-manager/pr/am-pr-types.ts index 506e0c727a..f733f57f7d 100644 --- a/packages/kilo-vscode/src/agent-manager/pr/am-pr-types.ts +++ b/packages/kilo-vscode/src/agent-manager/pr/am-pr-types.ts @@ -12,6 +12,7 @@ export interface GhComment { body?: string path?: string line?: number + originalLine?: number url?: string createdAt?: string diffHunk?: string @@ -19,6 +20,7 @@ export interface GhComment { export interface GhThread { id?: string isResolved?: boolean + isOutdated?: boolean comments?: { nodes?: GhComment[] } } export interface GhReviewRequest { diff --git a/packages/kilo-vscode/src/agent-manager/pr/am-pr-utils.ts b/packages/kilo-vscode/src/agent-manager/pr/am-pr-utils.ts index f0521a1a61..37a9f1e97a 100644 --- a/packages/kilo-vscode/src/agent-manager/pr/am-pr-utils.ts +++ b/packages/kilo-vscode/src/agent-manager/pr/am-pr-utils.ts @@ -69,8 +69,10 @@ const REVIEWER_STATE: Record = { export function parseComments(threads: GhThread[]): PRComment[] { const items: PRComment[] = [] for (const thread of threads) { - const first = thread.comments?.nodes?.[0] + const nodes = thread.comments?.nodes ?? [] + const first = nodes[0] if (!first) continue + const replies = nodes.slice(1).map((node) => ({ author: node.author?.login ?? "unknown", body: node.body ?? "" })) items.push({ id: first.id, threadId: thread.id ?? first.id, @@ -78,11 +80,14 @@ export function parseComments(threads: GhThread[]): PRComment[] { avatar: first.author?.avatarUrl, body: first.body ?? "", file: first.path, - line: first.line, + // An outdated thread has no current line, so fall back to the line it was written against. + line: first.line ?? first.originalLine, url: first.url, resolved: thread.isResolved ?? false, + outdated: thread.isOutdated ?? false, createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined, diffHunk: first.diffHunk, + replies: replies.length > 0 ? replies : undefined, }) } return items @@ -105,3 +110,30 @@ export function parseReviewers(requests: GhReviewRequest[], reviews: GhReview[]) } return [...map.values()] } + +/** + * Short, user-facing reason from a failed `gh` invocation. The raw message + * repeats the whole command line, which is useless inside a comment card. + */ +export function ghErrorReason(message: string): string { + const lines = message + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith("Command failed")) + const last = [...lines].reverse().find((line) => !line.startsWith("query") && !line.startsWith("mutation")) + return (last ?? message.trim()).replace(/^gh:\s*/, "").slice(0, 200) +} + +/** + * Signature of the comment threads, for poll deduplication. Thread and + * unresolved counts alone hide edits and new replies, which the panel renders. + */ +export function commentsSig(comments?: PRComment[]): string { + if (!comments) return "" + return comments + .map( + (item) => + `${item.threadId}:${item.resolved ? 1 : 0}${item.outdated ? "o" : ""}:${item.line ?? ""}:${item.body.length}:${(item.replies ?? []).map((reply) => reply.body.length).join("/")}`, + ) + .join(",") +} diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index edfa7666f6..56d92ed0ce 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -59,6 +59,11 @@ export interface PRCheck { duration?: string } +export interface PRCommentReply { + author: string + body: string +} + export interface PRComment { id: string threadId: string @@ -69,8 +74,10 @@ export interface PRComment { line?: number url?: string resolved: boolean + outdated: boolean createdAt?: number diffHunk?: string + replies?: PRCommentReply[] } export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented" diff --git a/packages/kilo-vscode/src/shared/review-comments.ts b/packages/kilo-vscode/src/shared/review-comments.ts index 1e5ae19d65..beebe503ac 100644 --- a/packages/kilo-vscode/src/shared/review-comments.ts +++ b/packages/kilo-vscode/src/shared/review-comments.ts @@ -7,9 +7,33 @@ export interface ReviewCommentData { selectedText: string } +export interface PRReviewReply { + author: string + body: string +} + +/** A GitHub PR review thread handed to the agent from the Agent Manager PR panel. */ +export interface PRReviewCommentData { + id: string + origin: "pr" + author: string + body: string + file?: string + line?: number + diffHunk?: string + outdated?: boolean + replies?: PRReviewReply[] +} + +export type ReviewCommentEntry = ReviewCommentData | PRReviewCommentData + +export function isPRReviewComment(item: ReviewCommentEntry): item is PRReviewCommentData { + return "origin" in item && item.origin === "pr" +} + export interface ReviewMessageData { version: 1 - comments: ReviewCommentData[] + comments: ReviewCommentEntry[] } interface ReviewMessageView { @@ -21,24 +45,48 @@ const LIMIT = 100 const TOTAL_LIMIT = 1_000_000 const TEXT_LIMIT = 100_000 const SELECTION_LIMIT = 200_000 +const AUTHOR_LIMIT = 256 +const REPLY_LIMIT = 20 function escapeInline(value: string): string { return value.replace(/([\\`*_\[\]{}()#+\-!|])/g, "\\$1") } -export function formatReviewCommentMarkdown(comment: ReviewCommentData): string { +/** Wrap a snippet in a fence long enough to survive backticks inside it. */ +function fenced(value: string): string[] { + const matches = value.match(/`+/g) ?? [] + const longest = matches.reduce((max, item) => Math.max(max, item.length), 0) + const fence = "`".repeat(Math.max(3, longest + 1)) + return [fence, value, fence] +} + +function quote(value: string): string { + return value + .split("\n") + .map((line) => (line ? `> ${line}` : ">")) + .join("\n") +} + +function formatPR(comment: PRReviewCommentData): string { + const at = comment.file + ? `**${escapeInline(comment.file)}**${comment.line ? ` (line ${comment.line})` : ""}, PR comment` + : "PR comment" + const lines = [`${at} by @${comment.author}${comment.outdated ? " (outdated)" : ""}:`] + if (comment.diffHunk) lines.push(...fenced(comment.diffHunk)) + lines.push(comment.body) + for (const reply of comment.replies ?? []) lines.push("", quote(`@${reply.author}: ${reply.body}`)) + return lines.join("\n") +} + +export function formatReviewCommentMarkdown(comment: ReviewCommentEntry): string { + if (isPRReviewComment(comment)) return formatPR(comment) const lines = [`**${escapeInline(comment.file)}** (line ${comment.line}):`] - if (comment.selectedText) { - const matches = comment.selectedText.match(/`+/g) ?? [] - const longest = matches.reduce((max, item) => Math.max(max, item.length), 0) - const fence = "`".repeat(Math.max(3, longest + 1)) - lines.push(fence, comment.selectedText, fence) - } + if (comment.selectedText) lines.push(...fenced(comment.selectedText)) lines.push(comment.comment) return lines.join("\n") } -export function formatReviewCommentsMarkdown(comments: ReviewCommentData[]): string { +export function formatReviewCommentsMarkdown(comments: ReviewCommentEntry[]): string { const lines = ["## Review Comments", ""] for (const item of comments) { lines.push(formatReviewCommentMarkdown(item), "") @@ -56,9 +104,80 @@ function text(value: unknown, limit: number): string | undefined { return value } -function parseComment(value: unknown): ReviewCommentData | undefined { +function safe(file: string): boolean { + const absolute = file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(file) + const traversal = file.split(/[\\/]/).includes("..") + return !absolute && !traversal && !file.includes("\0") +} + +function parseReply(value: unknown): PRReviewReply | undefined { const item = record(value) if (!item) return undefined + const author = text(item.author, AUTHOR_LIMIT) + const body = text(item.body, TEXT_LIMIT) + if (!author || body === undefined) return undefined + return { author, body } +} + +function parseReplies(value: unknown): PRReviewReply[] | undefined { + if (!Array.isArray(value) || value.length > REPLY_LIMIT) return undefined + const list: PRReviewReply[] = [] + for (const item of value) { + const reply = parseReply(item) + if (!reply) return undefined + list.push(reply) + } + return list +} + +/** Optional PR field: `undefined` when absent, `false` when present but invalid. */ +function optional(value: unknown, limit: number, valid?: (item: string) => boolean): string | false | undefined { + if (value === undefined) return undefined + const item = text(value, limit) + if (item === undefined) return false + if (valid && !valid(item)) return false + return item +} + +function optionalLine(value: unknown): number | false | undefined { + if (value === undefined) return undefined + if (typeof value !== "number" || !Number.isInteger(value) || value < 1) return false + return value +} + +function parsePR(item: Record): PRReviewCommentData | undefined { + const id = text(item.id, 512) + const author = text(item.author, AUTHOR_LIMIT) + const body = text(item.body, TEXT_LIMIT) + if (!id || !author || body === undefined) return undefined + + const file = optional(item.file, 4_096, safe) + const hunk = optional(item.diffHunk, SELECTION_LIMIT) + const line = optionalLine(item.line) + if (file === false || hunk === false || line === false) return undefined + if (item.outdated !== undefined && typeof item.outdated !== "boolean") return undefined + + const replies = item.replies === undefined ? undefined : parseReplies(item.replies) + if (item.replies !== undefined && !replies) return undefined + + return { + id, + origin: "pr", + author, + body, + file, + line, + diffHunk: hunk, + outdated: item.outdated, + replies, + } +} + +function parseComment(value: unknown): ReviewCommentEntry | undefined { + const item = record(value) + if (!item) return undefined + if (item.origin === "pr") return parsePR(item) + if (item.origin !== undefined) return undefined const id = text(item.id, 512) const file = text(item.file, 4_096) @@ -67,30 +186,39 @@ function parseComment(value: unknown): ReviewCommentData | undefined { const side = item.side const line = item.line if (!id || !file || comment === undefined || selectedText === undefined) return undefined - const absolute = file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(file) - const traversal = file.split(/[\\/]/).includes("..") - if (absolute || traversal || file.includes("\0")) return undefined + if (!safe(file)) return undefined if (side !== "additions" && side !== "deletions") return undefined if (typeof line !== "number" || !Number.isInteger(line) || line < 1) return undefined return { id, file, side, line, comment, selectedText } } +function weight(item: ReviewCommentEntry): number { + if (!isPRReviewComment(item)) + return item.id.length + item.file.length + item.comment.length + item.selectedText.length + const replies = (item.replies ?? []).reduce((total, reply) => total + reply.author.length + reply.body.length, 0) + return ( + item.id.length + + item.author.length + + item.body.length + + (item.file?.length ?? 0) + + (item.diffHunk?.length ?? 0) + + replies + ) +} + function view(value: unknown, content: string): ReviewMessageView | undefined { const data = record(value) if (!data || data.version !== 1 || !Array.isArray(data.comments)) return undefined if (data.comments.length === 0 || data.comments.length > LIMIT) return undefined - const comments: ReviewCommentData[] = [] + const comments: ReviewCommentEntry[] = [] for (const value of data.comments) { const item = parseComment(value) if (!item) return undefined comments.push(item) } - const size = comments.reduce( - (total, item) => total + item.id.length + item.file.length + item.comment.length + item.selectedText.length, - 0, - ) + const size = comments.reduce((total, item) => total + weight(item), 0) if (size > TOTAL_LIMIT) return undefined const prefix = formatReviewCommentsMarkdown(comments) diff --git a/packages/kilo-vscode/tests/fixtures/pr-comments-render.tsx b/packages/kilo-vscode/tests/fixtures/pr-comments-render.tsx index 50fa586858..244c40b073 100644 --- a/packages/kilo-vscode/tests/fixtures/pr-comments-render.tsx +++ b/packages/kilo-vscode/tests/fixtures/pr-comments-render.tsx @@ -29,14 +29,15 @@ Object.assign(globalThis, { CustomEvent: window.CustomEvent, Event: window.Event, MouseEvent: window.MouseEvent, + MessageEvent: window.MessageEvent, requestAnimationFrame: window.requestAnimationFrame.bind(window), cancelAnimationFrame: window.cancelAnimationFrame.bind(window), }) const { render } = await import("solid-js/web") -const { I18nProvider } = await import("@kilocode/kilo-ui/context") const { MarkedProvider } = await import("@kilocode/kilo-ui/context/marked") const { VSCodeProvider } = await import("../../webview-ui/src/context/vscode") +const { LanguageProvider } = await import("../../webview-ui/src/context/language") const { PRComments } = await import("../../webview-ui/agent-manager/pr/PRComments") const root = document.createElement("div") @@ -45,56 +46,113 @@ colors.textContent = ":root { --syntax-keyword: rgb(72, 160, 199); --syntax-stri document.head.append(colors) document.body.append(root) +const HUNK = + '@@ -1 +1,14 @@\n+import { File as BaseFile, type FileProps } from "@opencode-ai/ui/file"\n+import type { JSX } from "solid-js"\n+import { createDefaultOptions } from "../pierre"\n+\n export * from "@opencode-ai/ui/file"\n+\n+export function File(props: FileProps) {\n+ const View = BaseFile as unknown as (props: FileProps) => JSX.Element\n+ if (props.mode === "text") return \n+\n+ // Keep inline file diffs on the same Pierre defaults as the dedicated viewer.\n+ const options = { ...createDefaultOptions(props.diffStyle), ...props } as FileProps\n' + +const sent: unknown[] = [] +window.addEventListener("message", (ev: MessageEvent) => { + if (ev.data?.type === "appendReviewComments") sent.push(ev.data) +}) + const dispose = render( () => ( - "en", - t: (key: string) => key, - plural: (key: string) => key, - } as never - } - > + (props: FileProps) {\n+ const View = BaseFile as unknown as (props: FileProps) => JSX.Element\n+ if (props.mode === "text") return \n+\n+ // Keep inline file diffs on the same Pierre defaults as the dedicated viewer.\n+ const options = { ...createDefaultOptions(props.diffStyle), ...props } as FileProps\n', + outdated: false, + diffHunk: HUNK, + replies: [{ author: "marius", body: "reply body is visible" }], + }, + { + id: "PRRC_done", + threadId: "PRRT_done", + author: "reviewer", + body: "settled discussion\n\nsecond paragraph only shows when expanded", + file: "packages/kilo-ui/src/components/other.tsx", + line: 3, + resolved: true, + outdated: false, }, ], }} /> - + ), root, ) await window.happyDOM.waitUntilComplete() + +// The unresolved thread renders expanded, with its hunk and its replies. const host = root.querySelector("diffs-container") const shadow = host?.shadowRoot const keyword = shadow?.querySelector('[data-content] span[style*="--syntax-keyword"]') const string = shadow?.querySelector('[data-content] span[style*="--syntax-string"]') assert.match(root.textContent ?? "", /comment body survives Pierre rendering/) +assert.match(root.textContent ?? "", /reply body is visible/) assert.equal(root.querySelectorAll('[data-component="diff"]').length, 1) assert.ok(keyword) assert.ok(string) assert.match(keyword!.getAttribute("style") ?? "", /--syntax-keyword/) assert.match(string!.getAttribute("style") ?? "", /--syntax-string/) assert.notEqual(keyword!.getAttribute("style"), string!.getAttribute("style")) + +// The resolved thread is hidden behind a collapsed group. +assert.doesNotMatch(root.textContent ?? "", /settled discussion/) +const groups = [...root.querySelectorAll(".am-pr-panel-section-toggle")] +const resolvedGroup = groups.find((node) => /Resolved \(1\)/.test(node.textContent ?? "")) +assert.ok(resolvedGroup, "resolved group heading is present") +;(resolvedGroup as HTMLButtonElement).click() +await window.happyDOM.waitUntilComplete() + +// Opening the group reveals a one-line row, not the whole card. +const rows = [...root.querySelectorAll(".am-pr-comment-head")] +const resolvedRow = rows.find((node) => /reviewer/.test(node.textContent ?? "")) +assert.ok(resolvedRow, "resolved row is present") +assert.equal(resolvedRow!.getAttribute("aria-expanded"), "false") +assert.ok(resolvedRow!.querySelector(".am-pr-comment-preview"), "collapsed row shows a preview") +assert.doesNotMatch(root.textContent ?? "", /second paragraph only shows when expanded/) + +// The row expands into a full card whose unresolve action is enabled. +;(resolvedRow as HTMLButtonElement).click() +await window.happyDOM.waitUntilComplete() +assert.equal(resolvedRow!.getAttribute("aria-expanded"), "true") +assert.match(root.textContent ?? "", /second paragraph only shows when expanded/) +const card = resolvedRow!.parentElement! +const actions = [...card.querySelectorAll('[data-component="button"]')] +const unresolve = actions.find((node) => /Unresolve/.test(node.textContent ?? "")) +assert.ok(unresolve, "unresolve button is rendered") +assert.equal((unresolve as HTMLButtonElement).disabled, false) +assert.equal(unresolve!.getAttribute("data-disabled"), null) + +// Send to agent hands the thread over as a structured review comment. +const send = [...root.querySelectorAll('[data-component="button"]')].find((node) => + /Send to agent/.test(node.textContent ?? ""), +) +assert.ok(send, "send button is rendered") +;(send as HTMLButtonElement).click() +await window.happyDOM.waitUntilComplete() +assert.equal(sent.length, 1) +const payload = sent[0] as { comments: { id: string; origin: string; author: string; replies?: unknown[] }[] } +assert.equal(payload.comments.length, 1) +assert.equal(payload.comments[0]!.origin, "pr") +assert.equal(payload.comments[0]!.id, "PRRT_open") +assert.equal(payload.comments[0]!.replies?.length, 1) + dispose() diff --git a/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts b/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts index 21bf23b56c..e127450b37 100644 --- a/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts @@ -2,11 +2,14 @@ import { describe, expect, it } from "bun:test" import { parsePRResult, checkStatus, + commentsSig, formatCheckDuration, + ghErrorReason, parseComments, parseReviewers, } from "../../src/agent-manager/pr/am-pr-utils" import type { GhThread, GhReviewRequest, GhReview } from "../../src/agent-manager/pr/am-pr-types" +import type { PRComment } from "../../src/agent-manager/types" // --- parsePRResult --- @@ -248,8 +251,10 @@ describe("parseComments", () => { line: 10, url: "https://url", resolved: true, + outdated: false, createdAt: new Date("2024-01-01T00:00:00Z").getTime(), diffHunk: undefined, + replies: undefined, }, ]) }) @@ -278,15 +283,15 @@ describe("parseComments", () => { expect(parseComments(threads)[0]?.author).toBe("unknown") }) - it("only uses the first comment of each thread", () => { + it("keeps later thread comments as replies of the first one", () => { const threads: GhThread[] = [ { id: "PRT_t2", isResolved: false, comments: { nodes: [ - { id: "first", body: "first comment" }, - { id: "second", body: "second comment" }, + { id: "first", body: "first comment", author: { login: "alice" } }, + { id: "second", body: "second comment", author: { login: "bob" } }, ], }, }, @@ -294,6 +299,76 @@ describe("parseComments", () => { const result = parseComments(threads) expect(result).toHaveLength(1) expect(result[0]?.id).toBe("first") + expect(result[0]?.replies).toEqual([{ author: "bob", body: "second comment" }]) + }) + + it("marks an outdated thread", () => { + const threads: GhThread[] = [ + { id: "PRT_t3", isResolved: false, isOutdated: true, comments: { nodes: [{ id: "c4", body: "stale" }] } }, + ] + expect(parseComments(threads)[0]?.outdated).toBe(true) + }) + + it("falls back to the original line when the thread has no current line", () => { + const threads: GhThread[] = [ + { + id: "PRT_t4", + isResolved: false, + isOutdated: true, + comments: { nodes: [{ id: "c5", body: "moved", path: "src/foo.ts", originalLine: 42 }] }, + }, + ] + expect(parseComments(threads)[0]?.line).toBe(42) + }) +}) + +// --- commentsSig --- + +describe("commentsSig", () => { + const thread = (overrides: Partial = {}): PRComment => ({ + id: "c1", + threadId: "PRRT_1", + author: "alice", + body: "looks good", + resolved: false, + outdated: false, + ...overrides, + }) + + it("returns an empty signature when there are no comments", () => { + expect(commentsSig()).toBe("") + }) + + it("changes when a reply is added, which thread counts alone cannot detect", () => { + const before = commentsSig([thread()]) + const after = commentsSig([thread({ replies: [{ author: "bob", body: "guard it" }] })]) + expect(after).not.toBe(before) + }) + + it("changes when a body is edited or a thread moves line", () => { + expect(commentsSig([thread({ body: "looks good!" })])).not.toBe(commentsSig([thread()])) + expect(commentsSig([thread({ line: 5 })])).not.toBe(commentsSig([thread()])) + }) + + it("stays stable for unchanged comments", () => { + expect(commentsSig([thread()])).toBe(commentsSig([thread()])) + }) +}) + +// --- ghErrorReason --- + +describe("ghErrorReason", () => { + it("keeps the last meaningful line and strips the gh prefix", () => { + const message = "Command failed: gh api graphql -f query=mutation...\ngh: Resource not accessible by integration" + expect(ghErrorReason(message)).toBe("Resource not accessible by integration") + }) + + it("falls back to the raw message when there is nothing else", () => { + expect(ghErrorReason(" boom ")).toBe("boom") + }) + + it("truncates very long output", () => { + expect(ghErrorReason("x".repeat(500)).length).toBe(200) }) }) diff --git a/packages/kilo-vscode/tests/unit/pr-comments-render.test.ts b/packages/kilo-vscode/tests/unit/pr-comments-render.test.ts index 7a39a961ed..b98fcf5854 100644 --- a/packages/kilo-vscode/tests/unit/pr-comments-render.test.ts +++ b/packages/kilo-vscode/tests/unit/pr-comments-render.test.ts @@ -9,7 +9,7 @@ const WEBVIEW = path.join(ROOT, "webview-ui") const FIXTURE = path.join(ROOT, "tests/fixtures/pr-comments-render.tsx") describe("PR comments", () => { - it("keeps a comment mounted while Pierre renders its GitHub hunk", async () => { + it("renders hunks, collapses resolved threads, and sends a thread to the agent", async () => { const solid = path.dirname(Bun.resolveSync("solid-js/package.json", WEBVIEW)) const aliases: Record = { "solid-js": path.join(solid, "dist/solid.js"), diff --git a/packages/kilo-vscode/tests/unit/review-comments-pr.test.ts b/packages/kilo-vscode/tests/unit/review-comments-pr.test.ts new file mode 100644 index 0000000000..1d8547c7fe --- /dev/null +++ b/packages/kilo-vscode/tests/unit/review-comments-pr.test.ts @@ -0,0 +1,179 @@ +/** + * PR review comment payload + * + * GitHub PR threads travel to the agent through the same review-comment payload + * as local diff comments. The message text must be reproducible from the part + * metadata, otherwise a historical message loses its comment chips. + */ +import { describe, it, expect } from "bun:test" +import { + formatReviewCommentsMarkdown, + isPRReviewComment, + partReview, + parseReview, + reviewMetadata, + type PRReviewCommentData, + type ReviewCommentData, +} from "../../src/shared/review-comments" +import { githubUrl, prMarkdown, prPayload, preview } from "../../webview-ui/agent-manager/pr/pr-comment-payload" +import type { PRComment } from "../../webview-ui/agent-manager/pr/pr-types" + +function pr(overrides: Partial = {}): PRReviewCommentData { + return { + id: "PRRT_1", + origin: "pr", + author: "alice", + body: "This throws when gh is missing.", + file: "src/gh.ts", + line: 42, + ...overrides, + } +} + +function local(): ReviewCommentData { + return { id: "c1", file: "src/a.ts", side: "additions", line: 3, comment: "rename", selectedText: "const x = 1" } +} + +function thread(overrides: Partial = {}): PRComment { + return { + id: "PRRC_1", + threadId: "PRRT_1", + author: "alice", + body: "This throws when gh is missing.", + file: "src/gh.ts", + line: 42, + resolved: false, + outdated: false, + ...overrides, + } +} + +describe("PR review comment markdown", () => { + it("names the file, line, and author", () => { + expect(formatReviewCommentsMarkdown([pr()])).toBe( + "## Review Comments\n\n**src/gh.ts** (line 42), PR comment by @alice:\nThis throws when gh is missing.", + ) + }) + + it("drops the location when the thread has none", () => { + const text = formatReviewCommentsMarkdown([pr({ file: undefined, line: undefined })]) + expect(text).toContain("PR comment by @alice:") + expect(text).not.toContain("(line") + }) + + it("marks outdated threads", () => { + expect(formatReviewCommentsMarkdown([pr({ outdated: true })])).toContain("by @alice (outdated):") + }) + + it("fences the diff hunk and quotes replies", () => { + const text = formatReviewCommentsMarkdown([ + pr({ + diffHunk: "@@ -1 +1 @@\n-const x = 1\n+const x = 2", + replies: [{ author: "bob", body: "agreed\nguard it" }], + }), + ]) + expect(text).toContain("```\n@@ -1 +1 @@\n-const x = 1\n+const x = 2\n```") + expect(text).toContain("> @bob: agreed\n> guard it") + }) +}) + +describe("PR review comment metadata", () => { + it("round-trips through the message body", () => { + const data = { + version: 1 as const, + comments: [pr({ diffHunk: "@@ -1 +1 @@", replies: [{ author: "bob", body: "ok" }] })], + } + const text = `${formatReviewCommentsMarkdown(data.comments)}\n\nplease fix these` + const view = partReview(reviewMetadata(data), text) + expect(view?.body).toBe("please fix these") + expect(view?.data.comments[0]).toEqual(data.comments[0]) + }) + + it("round-trips a mixed local and PR payload", () => { + const comments = [local(), pr()] + const text = formatReviewCommentsMarkdown(comments) + const parsed = parseReview({ version: 1, comments }, text) + expect(parsed?.comments).toEqual(comments) + expect(parsed?.comments.filter(isPRReviewComment)).toHaveLength(1) + }) + + it("keeps parsing a legacy local-only payload", () => { + const comments = [local()] + expect(parseReview({ version: 1, comments }, formatReviewCommentsMarkdown(comments))?.comments).toEqual(comments) + }) + + it("rejects an unknown origin", () => { + const text = formatReviewCommentsMarkdown([local()]) + expect(parseReview({ version: 1, comments: [{ ...local(), origin: "gitlab" }] }, text)).toBeUndefined() + expect(parseReview({ version: 1, comments: [local()] }, text)?.comments).toHaveLength(1) + }) + + it("rejects a PR entry with a traversal path", () => { + const comments = [pr({ file: "../../etc/passwd" })] + expect(parseReview({ version: 1, comments }, formatReviewCommentsMarkdown(comments))).toBeUndefined() + }) + + it("rejects a PR entry with a bogus line", () => { + const comments = [pr({ line: 0 })] + expect(parseReview({ version: 1, comments }, formatReviewCommentsMarkdown(comments))).toBeUndefined() + }) +}) + +describe("prPayload", () => { + it("keys the payload by thread so a repeat send replaces the chip", () => { + expect(prPayload(thread()).id).toBe("PRRT_1") + }) + + it("caps the body", () => { + const payload = prPayload(thread({ body: "x".repeat(5_000) })) + expect(payload.body.length).toBeLessThan(5_000) + expect(payload.body.endsWith("...")).toBe(true) + }) + + it("keeps the hunk header and the tail of a long hunk", () => { + const hunk = Array.from({ length: 80 }, (_, i) => `line ${i}`).join("\n") + const payload = prPayload(thread({ diffHunk: hunk })) + const lines = payload.diffHunk?.split("\n") ?? [] + expect(lines).toHaveLength(42) + expect(lines[0]).toBe("line 0") + expect(lines[1]).toBe("...") + expect(payload.diffHunk?.endsWith("line 79")).toBe(true) + }) + + it("caps a single-line hunk by characters", () => { + const payload = prPayload(thread({ diffHunk: `@@ -1 +1 @@ ${"x".repeat(20_000)}` })) + expect(payload.diffHunk!.length).toBeLessThan(9_000) + expect(payload.diffHunk?.endsWith("...")).toBe(true) + }) + + it("caps replies and drops empty reply lists", () => { + const replies = Array.from({ length: 9 }, (_, i) => ({ id: `r${i}`, author: "bob", body: `reply ${i}` })) + expect(prPayload(thread({ replies })).replies).toHaveLength(5) + expect(prPayload(thread({ replies: [] })).replies).toBeUndefined() + }) + + it("only treats https comment urls as openable", () => { + expect(githubUrl("http://example.com")).toBeUndefined() + expect(githubUrl("javascript:alert(1)")).toBeUndefined() + expect(githubUrl("https://github.com/org/repo/pull/1#discussion_r1")).toBe( + "https://github.com/org/repo/pull/1#discussion_r1", + ) + }) + + it("survives the payload parser", () => { + const comments = [prPayload(thread({ diffHunk: "@@ -1 +1 @@", outdated: true }))] + expect(parseReview({ version: 1, comments }, formatReviewCommentsMarkdown(comments))?.comments).toEqual(comments) + }) + + it("formats the whole thread for the copy action", () => { + expect(prMarkdown(thread())).toBe("**src/gh.ts** (line 42), PR comment by @alice:\nThis throws when gh is missing.") + }) +}) + +describe("preview", () => { + it("uses the first meaningful line without markdown noise", () => { + expect(preview("\n\n## Heading\nrest")).toBe("Heading") + expect(preview("- nit: rename this")).toBe("nit: rename this") + expect(preview("")).toBe("") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 10e72057ad..33ed0b63a7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -2672,6 +2672,7 @@ const AgentManagerContent: Component = () => { pr={activePR()!.pr} worktree={activePR()!.wt} worktreeId={activePR()!.selected} + activeTerminalId={terms.activeId()} onClose={() => setSidePanel(null)} onOpenExternal={() => vscode.postMessage({ @@ -2680,6 +2681,18 @@ const AgentManagerContent: Component = () => { url: activePR()!.pr.url, }) } + onOpenFile={(file, line) => { + const id = diffCtx() + if (id) + vscode.postMessage({ type: "agentManager.openFile", sessionId: id, filePath: file, line }) + }} + onOpenUrl={(url) => + vscode.postMessage({ + type: "agentManager.openPR", + worktreeId: activePR()!.selected, + url, + }) + } /> 0}> 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 a4d8180b12..637166e6bb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -187,6 +187,21 @@ export const dict = { "agentManager.review.metaFile": "الملف", "agentManager.review.metaLine": "السطر", "agentManager.review.metaComment": "تعليق المستخدم", + "agentManager.review.metaAuthor": "المؤلف", + "agentManager.pr.comment.title": "التعليقات", + "agentManager.pr.comment.unresolvedCount": "{{count}} غير محلولة", + "agentManager.pr.comment.resolvedGroup": "تم الحل ({{count}})", + "agentManager.pr.comment.sendAll": "إرسال {{count}} غير محلولة إلى الوكيل", + "agentManager.pr.comment.sendAllToTerminal": "إرسال {{count}} غير محلولة إلى الطرفية", + "agentManager.pr.comment.send": "إرسال إلى الوكيل", + "agentManager.pr.comment.resolve": "حل", + "agentManager.pr.comment.unresolve": "إلغاء الحل", + "agentManager.pr.comment.outdated": "قديم", + "agentManager.pr.comment.sent": "تم الإرسال", + "agentManager.pr.comment.copy": "نسخ التعليق", + "agentManager.pr.comment.openOnGitHub": "فتح على GitHub", + "agentManager.pr.comment.resolveFailed": "تعذر الحل. {{error}}", + "agentManager.pr.comment.unresolveFailed": "تعذر إلغاء الحل. {{error}}", "agentManager.review.collapsedOnly": "{{count}} مطوي", "agentManager.review.collapsedWithLarge": "{{collapsed}} مطوي، {{large}} كبير", "agentManager.review.largeFileCollapsed": "ملف كبير (مطوي)", 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 a3c8066462..48bd840bcb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -190,6 +190,21 @@ export const dict = { "agentManager.review.metaFile": "Arquivo", "agentManager.review.metaLine": "Linha", "agentManager.review.metaComment": "Comentário do usuário", + "agentManager.review.metaAuthor": "Autor", + "agentManager.pr.comment.title": "Comentários", + "agentManager.pr.comment.unresolvedCount": "{{count}} não resolvidos", + "agentManager.pr.comment.resolvedGroup": "Resolvidos ({{count}})", + "agentManager.pr.comment.sendAll": "Enviar {{count}} não resolvidos para o agente", + "agentManager.pr.comment.sendAllToTerminal": "Enviar {{count}} não resolvidos para o terminal", + "agentManager.pr.comment.send": "Enviar para o agente", + "agentManager.pr.comment.resolve": "Resolver", + "agentManager.pr.comment.unresolve": "Desfazer resolução", + "agentManager.pr.comment.outdated": "Desatualizado", + "agentManager.pr.comment.sent": "Enviado", + "agentManager.pr.comment.copy": "Copiar comentário", + "agentManager.pr.comment.openOnGitHub": "Abrir no GitHub", + "agentManager.pr.comment.resolveFailed": "Não foi possível resolver. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Não foi possível desfazer a resolução. {{error}}", "agentManager.review.collapsedOnly": "{{count}} recolhidos", "agentManager.review.collapsedWithLarge": "{{collapsed}} recolhidos, {{large}} grandes", "agentManager.review.largeFileCollapsed": "Arquivo grande (recolhido)", 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 e42a222d5c..08e924fb2c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -190,6 +190,21 @@ export const dict = { "agentManager.review.metaFile": "Datoteka", "agentManager.review.metaLine": "Linija", "agentManager.review.metaComment": "Komentar korisnika", + "agentManager.review.metaAuthor": "Autor", + "agentManager.pr.comment.title": "Komentari", + "agentManager.pr.comment.unresolvedCount": "{{count}} neriješenih", + "agentManager.pr.comment.resolvedGroup": "Riješeno ({{count}})", + "agentManager.pr.comment.sendAll": "Pošalji {{count}} neriješenih agentu", + "agentManager.pr.comment.sendAllToTerminal": "Pošalji {{count}} neriješenih terminalu", + "agentManager.pr.comment.send": "Pošalji agentu", + "agentManager.pr.comment.resolve": "Riješi", + "agentManager.pr.comment.unresolve": "Poništi rješenje", + "agentManager.pr.comment.outdated": "Zastarjelo", + "agentManager.pr.comment.sent": "Poslano", + "agentManager.pr.comment.copy": "Kopiraj komentar", + "agentManager.pr.comment.openOnGitHub": "Otvori na GitHub", + "agentManager.pr.comment.resolveFailed": "Nije moguće riješiti. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Nije moguće poništiti rješenje. {{error}}", "agentManager.review.collapsedOnly": "{{count}} sažeto", "agentManager.review.collapsedWithLarge": "{{collapsed}} sažeto, {{large}} velikih", "agentManager.review.largeFileCollapsed": "Velika datoteka (sažeto)", 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 22f6811774..e1907e8c1a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -191,6 +191,21 @@ export const dict = { "agentManager.review.metaFile": "Fil", "agentManager.review.metaLine": "Linje", "agentManager.review.metaComment": "Brugerkommentar", + "agentManager.review.metaAuthor": "Forfatter", + "agentManager.pr.comment.title": "Kommentarer", + "agentManager.pr.comment.unresolvedCount": "{{count}} uløste", + "agentManager.pr.comment.resolvedGroup": "Løste ({{count}})", + "agentManager.pr.comment.sendAll": "Send {{count}} uløste til agenten", + "agentManager.pr.comment.sendAllToTerminal": "Send {{count}} uløste til terminalen", + "agentManager.pr.comment.send": "Send til agenten", + "agentManager.pr.comment.resolve": "Løs", + "agentManager.pr.comment.unresolve": "Ophæv løsning", + "agentManager.pr.comment.outdated": "Forældet", + "agentManager.pr.comment.sent": "Sendt", + "agentManager.pr.comment.copy": "Kopiér kommentar", + "agentManager.pr.comment.openOnGitHub": "Åbn på GitHub", + "agentManager.pr.comment.resolveFailed": "Kunne ikke løse. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Kunne ikke ophæve løsning. {{error}}", "agentManager.review.collapsedOnly": "{{count}} foldet sammen", "agentManager.review.collapsedWithLarge": "{{collapsed}} foldet sammen, {{large}} store", "agentManager.review.largeFileCollapsed": "Stor fil (sammenklappet)", 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 7f70f0feac..94afb562bf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -192,6 +192,21 @@ export const dict = { "agentManager.review.metaFile": "Datei", "agentManager.review.metaLine": "Zeile", "agentManager.review.metaComment": "Benutzerkommentar", + "agentManager.review.metaAuthor": "Autor", + "agentManager.pr.comment.title": "Kommentare", + "agentManager.pr.comment.unresolvedCount": "{{count}} ungelöst", + "agentManager.pr.comment.resolvedGroup": "Gelöst ({{count}})", + "agentManager.pr.comment.sendAll": "{{count}} ungelöste an Agent senden", + "agentManager.pr.comment.sendAllToTerminal": "{{count}} ungelöste an das Terminal senden", + "agentManager.pr.comment.send": "An Agent senden", + "agentManager.pr.comment.resolve": "Auflösen", + "agentManager.pr.comment.unresolve": "Auflösung aufheben", + "agentManager.pr.comment.outdated": "Veraltet", + "agentManager.pr.comment.sent": "Gesendet", + "agentManager.pr.comment.copy": "Kommentar kopieren", + "agentManager.pr.comment.openOnGitHub": "Auf GitHub öffnen", + "agentManager.pr.comment.resolveFailed": "Konnte nicht aufgelöst werden. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Auflösung konnte nicht aufgehoben werden. {{error}}", "agentManager.review.collapsedOnly": "{{count}} eingeklappt", "agentManager.review.collapsedWithLarge": "{{collapsed}} eingeklappt, {{large}} groß", "agentManager.review.largeFileCollapsed": "Große Datei (eingeklappt)", 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 cd455fab94..b063ccd30c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -193,6 +193,21 @@ export const dict = { "agentManager.review.metaFile": "File", "agentManager.review.metaLine": "Line", "agentManager.review.metaComment": "User comment", + "agentManager.review.metaAuthor": "Author", + "agentManager.pr.comment.title": "Comments", + "agentManager.pr.comment.unresolvedCount": "{{count}} unresolved", + "agentManager.pr.comment.resolvedGroup": "Resolved ({{count}})", + "agentManager.pr.comment.sendAll": "Send {{count}} unresolved to agent", + "agentManager.pr.comment.sendAllToTerminal": "Send {{count}} unresolved to terminal", + "agentManager.pr.comment.send": "Send to agent", + "agentManager.pr.comment.resolve": "Resolve", + "agentManager.pr.comment.unresolve": "Unresolve", + "agentManager.pr.comment.outdated": "Outdated", + "agentManager.pr.comment.sent": "Sent", + "agentManager.pr.comment.copy": "Copy comment", + "agentManager.pr.comment.openOnGitHub": "Open on GitHub", + "agentManager.pr.comment.resolveFailed": "Could not resolve. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Could not unresolve. {{error}}", "agentManager.review.collapsedOnly": "{{count}} collapsed", "agentManager.review.collapsedWithLarge": "{{collapsed}} collapsed, {{large}} large", "agentManager.review.largeFileCollapsed": "Large file (collapsed)", 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 b0ab933273..29fee4e022 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -191,6 +191,21 @@ export const dict = { "agentManager.review.metaFile": "Archivo", "agentManager.review.metaLine": "Línea", "agentManager.review.metaComment": "Comentario del usuario", + "agentManager.review.metaAuthor": "Autor", + "agentManager.pr.comment.title": "Comentarios", + "agentManager.pr.comment.unresolvedCount": "{{count}} sin resolver", + "agentManager.pr.comment.resolvedGroup": "Resueltos ({{count}})", + "agentManager.pr.comment.sendAll": "Enviar {{count}} sin resolver al agente", + "agentManager.pr.comment.sendAllToTerminal": "Enviar {{count}} sin resolver al terminal", + "agentManager.pr.comment.send": "Enviar al agente", + "agentManager.pr.comment.resolve": "Resolver", + "agentManager.pr.comment.unresolve": "Deshacer resolución", + "agentManager.pr.comment.outdated": "Obsoleto", + "agentManager.pr.comment.sent": "Enviado", + "agentManager.pr.comment.copy": "Copiar comentario", + "agentManager.pr.comment.openOnGitHub": "Abrir en GitHub", + "agentManager.pr.comment.resolveFailed": "No se pudo resolver. {{error}}", + "agentManager.pr.comment.unresolveFailed": "No se pudo deshacer la resolución. {{error}}", "agentManager.review.collapsedOnly": "{{count}} contraídos", "agentManager.review.collapsedWithLarge": "{{collapsed}} contraídos, {{large}} grandes", "agentManager.review.largeFileCollapsed": "Archivo grande (contraído)", 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 b2c47eaea6..2f54d331e6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -194,6 +194,21 @@ export const dict = { "agentManager.review.metaFile": "فایل", "agentManager.review.metaLine": "خط", "agentManager.review.metaComment": "نظر کاربر", + "agentManager.review.metaAuthor": "نویسنده", + "agentManager.pr.comment.title": "نظرات", + "agentManager.pr.comment.unresolvedCount": "{{count}} حل‌نشده", + "agentManager.pr.comment.resolvedGroup": "حل‌شده ({{count}})", + "agentManager.pr.comment.sendAll": "ارسال {{count}} مورد حل‌نشده به عامل", + "agentManager.pr.comment.sendAllToTerminal": "ارسال {{count}} مورد حل‌نشده به ترمینال", + "agentManager.pr.comment.send": "ارسال به عامل", + "agentManager.pr.comment.resolve": "حل کردن", + "agentManager.pr.comment.unresolve": "لغو حل", + "agentManager.pr.comment.outdated": "منسوخ", + "agentManager.pr.comment.sent": "ارسال شد", + "agentManager.pr.comment.copy": "کپی نظر", + "agentManager.pr.comment.openOnGitHub": "باز کردن در GitHub", + "agentManager.pr.comment.resolveFailed": "حل کردن ممکن نبود. {{error}}", + "agentManager.pr.comment.unresolveFailed": "لغو حل ممکن نبود. {{error}}", "agentManager.review.collapsedOnly": "{{count}} جمع‌شده", "agentManager.review.collapsedWithLarge": "{{collapsed}} جمع‌شده، {{large}} بزرگ", "agentManager.review.largeFileCollapsed": "فایل بزرگ (جمع‌شده)", 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 57794271f5..9825dae381 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -192,6 +192,21 @@ export const dict = { "agentManager.review.metaFile": "Fichier", "agentManager.review.metaLine": "Ligne", "agentManager.review.metaComment": "Commentaire de l'utilisateur", + "agentManager.review.metaAuthor": "Auteur", + "agentManager.pr.comment.title": "Commentaires", + "agentManager.pr.comment.unresolvedCount": "{{count}} non résolus", + "agentManager.pr.comment.resolvedGroup": "Résolus ({{count}})", + "agentManager.pr.comment.sendAll": "Envoyer {{count}} non résolus à l’agent", + "agentManager.pr.comment.sendAllToTerminal": "Envoyer {{count}} non résolus au terminal", + "agentManager.pr.comment.send": "Envoyer à l’agent", + "agentManager.pr.comment.resolve": "Résoudre", + "agentManager.pr.comment.unresolve": "Annuler la résolution", + "agentManager.pr.comment.outdated": "Obsolète", + "agentManager.pr.comment.sent": "Envoyé", + "agentManager.pr.comment.copy": "Copier le commentaire", + "agentManager.pr.comment.openOnGitHub": "Ouvrir sur GitHub", + "agentManager.pr.comment.resolveFailed": "Impossible de résoudre. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Impossible d’annuler la résolution. {{error}}", "agentManager.review.collapsedOnly": "{{count}} repliés", "agentManager.review.collapsedWithLarge": "{{collapsed}} repliés, {{large}} volumineux", "agentManager.review.largeFileCollapsed": "Fichier volumineux (replié)", 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 12543b3b2d..6ab61d8d43 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -197,6 +197,21 @@ export const dict = { "agentManager.review.metaFile": "File", "agentManager.review.metaLine": "Riga", "agentManager.review.metaComment": "Commento utente", + "agentManager.review.metaAuthor": "Autore", + "agentManager.pr.comment.title": "Commenti", + "agentManager.pr.comment.unresolvedCount": "{{count}} non risolti", + "agentManager.pr.comment.resolvedGroup": "Risolti ({{count}})", + "agentManager.pr.comment.sendAll": "Invia {{count}} non risolti all'agente", + "agentManager.pr.comment.sendAllToTerminal": "Invia {{count}} non risolti al terminale", + "agentManager.pr.comment.send": "Invia all'agente", + "agentManager.pr.comment.resolve": "Risolvi", + "agentManager.pr.comment.unresolve": "Annulla risoluzione", + "agentManager.pr.comment.outdated": "Obsoleto", + "agentManager.pr.comment.sent": "Inviato", + "agentManager.pr.comment.copy": "Copia commento", + "agentManager.pr.comment.openOnGitHub": "Apri su GitHub", + "agentManager.pr.comment.resolveFailed": "Impossibile risolvere. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Impossibile annullare la risoluzione. {{error}}", "agentManager.review.collapsedOnly": "{{count}} compressi", "agentManager.review.collapsedWithLarge": "{{collapsed}} compressi, {{large}} grandi", "agentManager.review.largeFileCollapsed": "File grande (compresso)", 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 91b66c4aa7..cedcf99b92 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -191,6 +191,21 @@ export const dict = { "agentManager.review.metaFile": "ファイル", "agentManager.review.metaLine": "行", "agentManager.review.metaComment": "ユーザーコメント", + "agentManager.review.metaAuthor": "作成者", + "agentManager.pr.comment.title": "コメント", + "agentManager.pr.comment.unresolvedCount": "{{count}} 件未解決", + "agentManager.pr.comment.resolvedGroup": "解決済み ({{count}})", + "agentManager.pr.comment.sendAll": "{{count}} 件の未解決コメントをエージェントに送信", + "agentManager.pr.comment.sendAllToTerminal": "{{count}} 件の未解決コメントをターミナルに送信", + "agentManager.pr.comment.send": "エージェントに送信", + "agentManager.pr.comment.resolve": "解決", + "agentManager.pr.comment.unresolve": "解決を取り消す", + "agentManager.pr.comment.outdated": "古い", + "agentManager.pr.comment.sent": "送信済み", + "agentManager.pr.comment.copy": "コメントをコピー", + "agentManager.pr.comment.openOnGitHub": "GitHubで開く", + "agentManager.pr.comment.resolveFailed": "解決できませんでした。{{error}}", + "agentManager.pr.comment.unresolveFailed": "解決を取り消せませんでした。{{error}}", "agentManager.review.collapsedOnly": "{{count}} 件折りたたみ", "agentManager.review.collapsedWithLarge": "{{collapsed}} 件折りたたみ、{{large}} 件がサイズ大", "agentManager.review.largeFileCollapsed": "大きなファイル(折りたたみ)", 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 be6304117a..9f740401a7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -189,6 +189,21 @@ export const dict = { "agentManager.review.metaFile": "파일", "agentManager.review.metaLine": "라인", "agentManager.review.metaComment": "사용자 코멘트", + "agentManager.review.metaAuthor": "작성자", + "agentManager.pr.comment.title": "댓글", + "agentManager.pr.comment.unresolvedCount": "{{count}}개 미해결", + "agentManager.pr.comment.resolvedGroup": "해결됨 ({{count}})", + "agentManager.pr.comment.sendAll": "{{count}}개 미해결 댓글을 에이전트로 보내기", + "agentManager.pr.comment.sendAllToTerminal": "{{count}}개 미해결 댓글을 터미널로 보내기", + "agentManager.pr.comment.send": "에이전트로 보내기", + "agentManager.pr.comment.resolve": "해결", + "agentManager.pr.comment.unresolve": "해결 취소", + "agentManager.pr.comment.outdated": "오래됨", + "agentManager.pr.comment.sent": "전송됨", + "agentManager.pr.comment.copy": "댓글 복사", + "agentManager.pr.comment.openOnGitHub": "GitHub에서 열기", + "agentManager.pr.comment.resolveFailed": "해결할 수 없습니다. {{error}}", + "agentManager.pr.comment.unresolveFailed": "해결을 취소할 수 없습니다. {{error}}", "agentManager.review.collapsedOnly": "{{count}}개 접힘", "agentManager.review.collapsedWithLarge": "{{collapsed}}개 접힘, {{large}}개 대용량", "agentManager.review.largeFileCollapsed": "큰 파일(접힘)", 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 c2ef4e5d14..2cda219d4f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -196,6 +196,21 @@ export const dict = { "agentManager.review.metaFile": "Bestand", "agentManager.review.metaLine": "Regel", "agentManager.review.metaComment": "Gebruikersopmerking", + "agentManager.review.metaAuthor": "Auteur", + "agentManager.pr.comment.title": "Opmerkingen", + "agentManager.pr.comment.unresolvedCount": "{{count}} onopgelost", + "agentManager.pr.comment.resolvedGroup": "Opgelost ({{count}})", + "agentManager.pr.comment.sendAll": "{{count}} onopgeloste opmerkingen naar agent sturen", + "agentManager.pr.comment.sendAllToTerminal": "{{count}} onopgeloste opmerkingen naar terminal sturen", + "agentManager.pr.comment.send": "Naar agent sturen", + "agentManager.pr.comment.resolve": "Oplossen", + "agentManager.pr.comment.unresolve": "Oplossing ongedaan maken", + "agentManager.pr.comment.outdated": "Verouderd", + "agentManager.pr.comment.sent": "Verzonden", + "agentManager.pr.comment.copy": "Opmerking kopiëren", + "agentManager.pr.comment.openOnGitHub": "Openen op GitHub", + "agentManager.pr.comment.resolveFailed": "Kan niet oplossen. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Kan oplossing niet ongedaan maken. {{error}}", "agentManager.review.collapsedOnly": "{{count}} ingeklapt", "agentManager.review.collapsedWithLarge": "{{collapsed}} ingeklapt, {{large}} groot", "agentManager.review.largeFileCollapsed": "Groot bestand (ingeklapt)", 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 d26092e3ae..1cdaebe9ea 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -188,6 +188,21 @@ export const dict = { "agentManager.review.metaFile": "Fil", "agentManager.review.metaLine": "Linje", "agentManager.review.metaComment": "Brukerkommentar", + "agentManager.review.metaAuthor": "Forfatter", + "agentManager.pr.comment.title": "Kommentarer", + "agentManager.pr.comment.unresolvedCount": "{{count}} uløste", + "agentManager.pr.comment.resolvedGroup": "Løste ({{count}})", + "agentManager.pr.comment.sendAll": "Send {{count}} uløste til agenten", + "agentManager.pr.comment.sendAllToTerminal": "Send {{count}} uløste til terminalen", + "agentManager.pr.comment.send": "Send til agenten", + "agentManager.pr.comment.resolve": "Løs", + "agentManager.pr.comment.unresolve": "Opphev løsning", + "agentManager.pr.comment.outdated": "Utdatert", + "agentManager.pr.comment.sent": "Sendt", + "agentManager.pr.comment.copy": "Kopier kommentar", + "agentManager.pr.comment.openOnGitHub": "Åpne på GitHub", + "agentManager.pr.comment.resolveFailed": "Kunne ikke løse. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Kunne ikke oppheve løsning. {{error}}", "agentManager.review.collapsedOnly": "{{count}} kollapset", "agentManager.review.collapsedWithLarge": "{{collapsed}} kollapset, {{large}} store", "agentManager.review.largeFileCollapsed": "Stor fil (sammenfoldet)", 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 b4f8324807..2031f4f9b2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -190,6 +190,21 @@ export const dict = { "agentManager.review.metaFile": "Plik", "agentManager.review.metaLine": "Linia", "agentManager.review.metaComment": "Komentarz użytkownika", + "agentManager.review.metaAuthor": "Autor", + "agentManager.pr.comment.title": "Komentarze", + "agentManager.pr.comment.unresolvedCount": "{{count}} nierozwiązanych", + "agentManager.pr.comment.resolvedGroup": "Rozwiązane ({{count}})", + "agentManager.pr.comment.sendAll": "Wyślij {{count}} nierozwiązanych do agenta", + "agentManager.pr.comment.sendAllToTerminal": "Wyślij {{count}} nierozwiązanych do terminala", + "agentManager.pr.comment.send": "Wyślij do agenta", + "agentManager.pr.comment.resolve": "Rozwiąż", + "agentManager.pr.comment.unresolve": "Cofnij rozwiązanie", + "agentManager.pr.comment.outdated": "Nieaktualne", + "agentManager.pr.comment.sent": "Wysłano", + "agentManager.pr.comment.copy": "Kopiuj komentarz", + "agentManager.pr.comment.openOnGitHub": "Otwórz na GitHub", + "agentManager.pr.comment.resolveFailed": "Nie można rozwiązać. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Nie można cofnąć rozwiązania. {{error}}", "agentManager.review.collapsedOnly": "{{count}} zwiniętych", "agentManager.review.collapsedWithLarge": "{{collapsed}} zwiniętych, {{large}} dużych", "agentManager.review.largeFileCollapsed": "Duży plik (zwinięty)", 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 809b074b9a..e6cded8cf6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -191,6 +191,21 @@ export const dict = { "agentManager.review.metaFile": "Файл", "agentManager.review.metaLine": "Строка", "agentManager.review.metaComment": "Комментарий пользователя", + "agentManager.review.metaAuthor": "Автор", + "agentManager.pr.comment.title": "Комментарии", + "agentManager.pr.comment.unresolvedCount": "{{count}} нерешённых", + "agentManager.pr.comment.resolvedGroup": "Решённые ({{count}})", + "agentManager.pr.comment.sendAll": "Отправить {{count}} нерешённых агенту", + "agentManager.pr.comment.sendAllToTerminal": "Отправить {{count}} нерешённых в терминал", + "agentManager.pr.comment.send": "Отправить агенту", + "agentManager.pr.comment.resolve": "Решить", + "agentManager.pr.comment.unresolve": "Отменить решение", + "agentManager.pr.comment.outdated": "Устарело", + "agentManager.pr.comment.sent": "Отправлено", + "agentManager.pr.comment.copy": "Копировать комментарий", + "agentManager.pr.comment.openOnGitHub": "Открыть на GitHub", + "agentManager.pr.comment.resolveFailed": "Не удалось решить. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Не удалось отменить решение. {{error}}", "agentManager.review.collapsedOnly": "{{count}} свернуто", "agentManager.review.collapsedWithLarge": "{{collapsed}} свернуто, {{large}} больших", "agentManager.review.largeFileCollapsed": "Большой файл (свернут)", 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 e74d7dbef9..6351965b36 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -185,6 +185,21 @@ export const dict = { "agentManager.review.metaFile": "ไฟล์", "agentManager.review.metaLine": "บรรทัด", "agentManager.review.metaComment": "ความคิดเห็นของผู้ใช้", + "agentManager.review.metaAuthor": "ผู้เขียน", + "agentManager.pr.comment.title": "ความคิดเห็น", + "agentManager.pr.comment.unresolvedCount": "ยังไม่แก้ไข {{count}} รายการ", + "agentManager.pr.comment.resolvedGroup": "แก้ไขแล้ว ({{count}})", + "agentManager.pr.comment.sendAll": "ส่งความคิดเห็นที่ยังไม่แก้ไข {{count}} รายการไปยังเอเจนต์", + "agentManager.pr.comment.sendAllToTerminal": "ส่งความคิดเห็นที่ยังไม่แก้ไข {{count}} รายการไปยังเทอร์มินัล", + "agentManager.pr.comment.send": "ส่งไปยังเอเจนต์", + "agentManager.pr.comment.resolve": "ทำเครื่องหมายว่าแก้ไขแล้ว", + "agentManager.pr.comment.unresolve": "ยกเลิกการแก้ไข", + "agentManager.pr.comment.outdated": "ล้าสมัย", + "agentManager.pr.comment.sent": "ส่งแล้ว", + "agentManager.pr.comment.copy": "คัดลอกความคิดเห็น", + "agentManager.pr.comment.openOnGitHub": "เปิดบน GitHub", + "agentManager.pr.comment.resolveFailed": "แก้ไขไม่ได้ {{error}}", + "agentManager.pr.comment.unresolveFailed": "ยกเลิกการแก้ไขไม่ได้ {{error}}", "agentManager.review.collapsedOnly": "ยุบ {{count}} รายการ", "agentManager.review.collapsedWithLarge": "ยุบ {{collapsed}} รายการ, ขนาดใหญ่ {{large}} รายการ", "agentManager.review.largeFileCollapsed": "ไฟล์ขนาดใหญ่ (พับอยู่)", 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 d6d5c3012a..89b0e6f6bf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -198,6 +198,21 @@ export const dict = { "agentManager.review.metaFile": "Dosya", "agentManager.review.metaLine": "Satır", "agentManager.review.metaComment": "Kullanıcı yorumu", + "agentManager.review.metaAuthor": "Yazar", + "agentManager.pr.comment.title": "Yorumlar", + "agentManager.pr.comment.unresolvedCount": "{{count}} çözülmemiş", + "agentManager.pr.comment.resolvedGroup": "Çözüldü ({{count}})", + "agentManager.pr.comment.sendAll": "{{count}} çözülmemiş yorumu ajana gönder", + "agentManager.pr.comment.sendAllToTerminal": "{{count}} çözülmemiş yorumu terminale gönder", + "agentManager.pr.comment.send": "Ajana gönder", + "agentManager.pr.comment.resolve": "Çöz", + "agentManager.pr.comment.unresolve": "Çözümü geri al", + "agentManager.pr.comment.outdated": "Güncel değil", + "agentManager.pr.comment.sent": "Gönderildi", + "agentManager.pr.comment.copy": "Yorumu kopyala", + "agentManager.pr.comment.openOnGitHub": "GitHub'da aç", + "agentManager.pr.comment.resolveFailed": "Çözülemedi. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Çözüm geri alınamadı. {{error}}", "agentManager.review.collapsedOnly": "{{count}} daraltıldı", "agentManager.review.collapsedWithLarge": "{{collapsed}} daraltıldı, {{large}} büyük", "agentManager.review.largeFileCollapsed": "Büyük dosya (daraltıldı)", 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 9959ad6a40..233688c53d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -199,6 +199,21 @@ export const dict = { "agentManager.review.metaFile": "Файл", "agentManager.review.metaLine": "Рядок", "agentManager.review.metaComment": "Коментар користувача", + "agentManager.review.metaAuthor": "Автор", + "agentManager.pr.comment.title": "Коментарі", + "agentManager.pr.comment.unresolvedCount": "{{count}} невирішених", + "agentManager.pr.comment.resolvedGroup": "Вирішено ({{count}})", + "agentManager.pr.comment.sendAll": "Надіслати {{count}} невирішених агенту", + "agentManager.pr.comment.sendAllToTerminal": "Надіслати {{count}} невирішених до терміналу", + "agentManager.pr.comment.send": "Надіслати агенту", + "agentManager.pr.comment.resolve": "Вирішити", + "agentManager.pr.comment.unresolve": "Скасувати вирішення", + "agentManager.pr.comment.outdated": "Застарілий", + "agentManager.pr.comment.sent": "Надіслано", + "agentManager.pr.comment.copy": "Копіювати коментар", + "agentManager.pr.comment.openOnGitHub": "Відкрити на GitHub", + "agentManager.pr.comment.resolveFailed": "Не вдалося вирішити. {{error}}", + "agentManager.pr.comment.unresolveFailed": "Не вдалося скасувати вирішення. {{error}}", "agentManager.review.collapsedOnly": "{{count}} згорнуто", "agentManager.review.collapsedWithLarge": "{{collapsed}} згорнуто, {{large}} великих", "agentManager.review.largeFileCollapsed": "Великий файл (згорнуто)", 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 c8456673d0..60e9cd8132 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -183,6 +183,21 @@ export const dict = { "agentManager.review.metaFile": "文件", "agentManager.review.metaLine": "行", "agentManager.review.metaComment": "用户评论", + "agentManager.review.metaAuthor": "作者", + "agentManager.pr.comment.title": "评论", + "agentManager.pr.comment.unresolvedCount": "{{count}} 个未解决", + "agentManager.pr.comment.resolvedGroup": "已解决 ({{count}})", + "agentManager.pr.comment.sendAll": "将 {{count}} 个未解决评论发送给代理", + "agentManager.pr.comment.sendAllToTerminal": "将 {{count}} 个未解决评论发送到终端", + "agentManager.pr.comment.send": "发送给代理", + "agentManager.pr.comment.resolve": "解决", + "agentManager.pr.comment.unresolve": "取消解决", + "agentManager.pr.comment.outdated": "已过时", + "agentManager.pr.comment.sent": "已发送", + "agentManager.pr.comment.copy": "复制评论", + "agentManager.pr.comment.openOnGitHub": "在 GitHub 上打开", + "agentManager.pr.comment.resolveFailed": "无法解决。{{error}}", + "agentManager.pr.comment.unresolveFailed": "无法取消解决。{{error}}", "agentManager.review.collapsedOnly": "{{count}} 个已折叠", "agentManager.review.collapsedWithLarge": "{{collapsed}} 个已折叠,{{large}} 个过大", "agentManager.review.largeFileCollapsed": "大文件(已折叠)", 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 dd009844c7..2eb8944221 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -183,6 +183,21 @@ export const dict = { "agentManager.review.metaFile": "檔案", "agentManager.review.metaLine": "行", "agentManager.review.metaComment": "使用者評論", + "agentManager.review.metaAuthor": "作者", + "agentManager.pr.comment.title": "留言", + "agentManager.pr.comment.unresolvedCount": "{{count}} 個未解決", + "agentManager.pr.comment.resolvedGroup": "已解決 ({{count}})", + "agentManager.pr.comment.sendAll": "將 {{count}} 個未解決留言傳送給代理", + "agentManager.pr.comment.sendAllToTerminal": "將 {{count}} 個未解決留言傳送到終端機", + "agentManager.pr.comment.send": "傳送給代理", + "agentManager.pr.comment.resolve": "解決", + "agentManager.pr.comment.unresolve": "取消解決", + "agentManager.pr.comment.outdated": "已過時", + "agentManager.pr.comment.sent": "已傳送", + "agentManager.pr.comment.copy": "複製留言", + "agentManager.pr.comment.openOnGitHub": "在 GitHub 上開啟", + "agentManager.pr.comment.resolveFailed": "無法解決。{{error}}", + "agentManager.pr.comment.unresolveFailed": "無法取消解決。{{error}}", "agentManager.review.collapsedOnly": "{{count}} 個已摺疊", "agentManager.review.collapsedWithLarge": "{{collapsed}} 個已摺疊,{{large}} 個過大", "agentManager.review.largeFileCollapsed": "大型檔案(已摺疊)", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentCard.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentCard.tsx new file mode 100644 index 0000000000..142fc00c4b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentCard.tsx @@ -0,0 +1,123 @@ +/** @jsxImportSource solid-js */ +import { For, Show } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Markdown } from "@kilocode/kilo-ui/markdown" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { useLanguage } from "../../src/context/language" +import { PRCommentDiff } from "../../diff-viewer/PRCommentDiff" +import { CopyButton } from "./CopyButton" +import { prMarkdown, preview } from "./pr-comment-payload" +import type { PRComment } from "./pr-types" + +interface Props { + comment: PRComment + resolved: boolean + pending: boolean + sent: boolean + open: boolean + error?: string + onToggleOpen: () => void + onToggleResolved: () => void + onSend: () => void + onOpenFile?: () => void + onOpenUrl?: () => void +} + +export function PRCommentCard(props: Props) { + const { t } = useLanguage() + const location = () => { + const file = props.comment.file + if (!file) return "" + return props.comment.line ? `${file}:${props.comment.line}` : file + } + + return ( +
+ + + + + + +
+ +
+ + {(reply) => ( +
+ {reply.author} +
+ +
+
+ )} +
+ {(err) =>
{err()}
}
+
+ + + + + + + props.onOpenFile?.()} + /> + + + + + props.onOpenUrl?.()} + /> + + +
+
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx index 500b2d1c9a..951e458840 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx @@ -1,126 +1,182 @@ /** @jsxImportSource solid-js */ -import { Index, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js" -import { Markdown } from "@kilocode/kilo-ui/markdown" -import { Spinner } from "@kilocode/kilo-ui/spinner" +import { Index, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { useLanguage } from "../../src/context/language" import { useVSCode } from "../../src/context/vscode" import type { PRStatus } from "../../src/types/messages" +import { sendReviewComments } from "../../diff-viewer/review-annotations" +import { PRCommentCard } from "./PRCommentCard" +import { SEND_LIMIT, githubUrl, prPayload } from "./pr-comment-payload" import type { PRComment } from "./pr-types" import { SectionHeading } from "./SectionHeading" -import { CopyButton } from "./CopyButton" -import { PRCommentDiff } from "../../diff-viewer/PRCommentDiff" -function CommentCard(props: { comment: PRComment; worktreeId: string }) { +interface Props { + comments: NonNullable + worktreeId: string + activeTerminalId?: string + onOpenFile?: (file: string, line?: number) => void + onOpenUrl?: (url: string) => void +} + +type Flags = Record + +function without(map: Record, id: string): Record { + const next = { ...map } + delete next[id] + return next +} + +export function PRComments(props: Props) { + const { t } = useLanguage() const vscode = useVSCode() - // Track pending action and any error from the result - const [pendingResolved, setPendingResolved] = createSignal(undefined) - const [actionError, setActionError] = createSignal(undefined) + const [open, setOpen] = createSignal(true) + const [doneOpen, setDoneOpen] = createSignal(false) + // threadId -> resolved state requested by the user, until the next poll confirms it + const [pending, setPending] = createSignal({}) + const [errors, setErrors] = createSignal>({}) + // threadId -> expansion override; the default depends on resolved/outdated state + const [expanded, setExpanded] = createSignal({}) + const [sent, setSent] = createSignal({}) - // Resolved shows pending state if exists, otherwise server state - const resolved = createMemo(() => pendingResolved() ?? props.comment.resolved) + const resolved = (comment: PRComment) => pending()[comment.threadId] ?? comment.resolved + const expandedFor = (comment: PRComment) => expanded()[comment.threadId] ?? (!resolved(comment) && !comment.outdated) - // Clear pending when server state matches (action confirmed by poll) - createMemo(() => { - const pending = pendingResolved() - if (pending !== undefined && pending === props.comment.resolved) { - setPendingResolved(undefined) - setActionError(undefined) - } + const groups = createMemo(() => { + const list = props.comments.comments + return { todo: list.filter((item) => !resolved(item)), done: list.filter((item) => resolved(item)) } + }) + + // Drop the optimistic state once a poll reports the state the user asked for. + createEffect(() => { + const map = pending() + const settled = props.comments.comments.filter( + (item) => map[item.threadId] !== undefined && map[item.threadId] === item.resolved, + ) + if (settled.length === 0) return + setPending((prev) => { + const next = { ...prev } + for (const item of settled) delete next[item.threadId] + return next + }) }) onMount(() => { function handler(ev: MessageEvent) { const msg = ev.data - const isResult = - (msg?.type === "agentManager.resolveCommentResult" || msg?.type === "agentManager.unresolveCommentResult") && - msg.worktreeId === props.worktreeId && - msg.threadId === props.comment.threadId - if (!isResult) return - if (!msg.success) { - // Only clear on error - success waits for poll to update props.comment.resolved - setPendingResolved(undefined) - setActionError( - msg.type === "agentManager.resolveCommentResult" - ? "Failed to resolve thread." - : "Failed to unresolve thread.", - ) - } + const resolveResult = msg?.type === "agentManager.resolveCommentResult" + const unresolveResult = msg?.type === "agentManager.unresolveCommentResult" + if (!resolveResult && !unresolveResult) return + if (msg.worktreeId !== props.worktreeId) return + // Success waits for the poll to report the new server state. + if (msg.success) return + const id = msg.threadId as string + setPending((prev) => without(prev, id)) + // Keep the card open so the failure is readable instead of hidden in a collapsed row. + setExpanded((prev) => ({ ...prev, [id]: true })) + const reason = typeof msg.error === "string" && msg.error ? msg.error : t("common.requestFailed") + setErrors((prev) => ({ + ...prev, + [id]: t(resolveResult ? "agentManager.pr.comment.resolveFailed" : "agentManager.pr.comment.unresolveFailed", { + error: reason, + }), + })) } window.addEventListener("message", handler) onCleanup(() => window.removeEventListener("message", handler)) }) - function toggle() { - setActionError(undefined) - const next = !resolved() - setPendingResolved(next) + function toggleResolved(comment: PRComment) { + const next = !resolved(comment) + setErrors((prev) => without(prev, comment.threadId)) + setPending((prev) => ({ ...prev, [comment.threadId]: next })) + // A thread the user just resolved collapses, like it does on GitHub. Open the + // resolved group so the thread is visibly moved instead of just disappearing. + setExpanded((prev) => ({ ...prev, [comment.threadId]: !next })) + if (next) setDoneOpen(true) vscode.postMessage({ type: next ? "agentManager.resolveComment" : "agentManager.unresolveComment", worktreeId: props.worktreeId, - threadId: props.comment.threadId, + threadId: comment.threadId, } as never) } - return ( -
- - - -
- {props.comment.author} - - - {props.comment.file} - {`:${props.comment.line}`} - - - - Resolved - - -
- {(err) =>
{err()}
}
-
- -
-
- - - Loading -
- } - > - - -
- - ) -} + function send(list: PRComment[]) { + if (list.length === 0) return + const batch = list.slice(0, SEND_LIMIT) + sendReviewComments(batch.map(prPayload), props.activeTerminalId) + setSent((prev) => { + const next = { ...prev } + for (const item of batch) next[item.threadId] = true + return next + }) + } + + // `Index` keyed by position, not `For` keyed by identity: every poll allocates + // fresh PRComment objects, and remounting would re-run Pierre and Markdown. + const card = (comment: () => PRComment) => ( + setExpanded((prev) => ({ ...prev, [comment().threadId]: !expandedFor(comment()) }))} + onToggleResolved={() => toggleResolved(comment())} + onSend={() => send([comment()])} + onOpenFile={ + comment().file && props.onOpenFile ? () => props.onOpenFile?.(comment().file!, comment().line) : undefined + } + onOpenUrl={ + githubUrl(comment().url) && props.onOpenUrl ? () => props.onOpenUrl?.(githubUrl(comment().url)!) : undefined + } + /> + ) -export function PRComments(props: { comments: NonNullable; worktreeId: string }) { - const [open, setOpen] = createSignal(true) return ( <>
setOpen((v) => !v)} - count={props.comments.unresolved > 0 ? `${props.comments.unresolved} unresolved` : undefined} + count={ + groups().todo.length > 0 + ? t("agentManager.pr.comment.unresolvedCount", { count: groups().todo.length }) + : undefined + } countClass="am-pr-panel-unresolved" /> + 0}> + +
- - {(comment) => } - + {card}
+ 0}> +
+ setDoneOpen((v) => !v)} + /> + +
+ {card} +
+
+
+
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx index b37d72948d..989fb86b3a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -17,8 +17,11 @@ interface PRPanelProps { pr: PRStatus worktree?: WorktreeState worktreeId: string + activeTerminalId?: string onClose: () => void onOpenExternal: () => void + onOpenFile?: (file: string, line?: number) => void + onOpenUrl?: (url: string) => void } export const PRPanel: Component = (props) => { @@ -76,7 +79,13 @@ export const PRPanel: Component = (props) => { {(comments) => (
- +
)}
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-comment-payload.ts b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-comment-payload.ts new file mode 100644 index 0000000000..c3fa2caddd --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-comment-payload.ts @@ -0,0 +1,55 @@ +import { formatReviewCommentMarkdown, type PRReviewCommentData } from "../../../src/shared/review-comments" +import type { PRComment } from "./pr-types" + +/** Caps so one talkative PR cannot blow up a prompt. */ +const BODY = 4_000 +const HUNK = 40 +/** A generated or minified file can put the whole hunk on one line. */ +const HUNK_CHARS = 8_000 +const REPLIES = 5 +/** Matches the comment limit enforced by the shared review payload parser. */ +export const SEND_LIMIT = 100 + +function clip(value: string, max: number): string { + return value.length > max ? `${value.slice(0, max)}\n...` : value +} + +/** Keep the `@@` header and the tail: the commented line sits at the end. */ +function trim(value: string): string { + const lines = value.split("\n") + const cut = lines.length <= HUNK ? lines : [lines[0]!, "...", ...lines.slice(-HUNK)] + return clip(cut.join("\n"), HUNK_CHARS) +} + +export function prPayload(comment: PRComment): PRReviewCommentData { + const replies = (comment.replies ?? []) + .slice(0, REPLIES) + .map((reply) => ({ author: reply.author, body: clip(reply.body, BODY) })) + return { + id: comment.threadId, + origin: "pr", + author: comment.author, + body: clip(comment.body, BODY), + file: comment.file, + line: comment.line, + diffHunk: comment.diffHunk ? trim(comment.diffHunk) : undefined, + outdated: comment.outdated || undefined, + replies: replies.length > 0 ? replies : undefined, + } +} + +/** Only https urls reach the payload, the markdown, or `openExternal`. */ +export function githubUrl(url?: string): string | undefined { + return url?.startsWith("https://") ? url : undefined +} + +/** The whole thread as markdown, for the copy action. */ +export function prMarkdown(comment: PRComment): string { + return formatReviewCommentMarkdown(prPayload(comment)) +} + +/** First meaningful line of a comment body, for the collapsed row. */ +export function preview(body: string): string { + const line = body.split("\n").find((item) => item.trim().length > 0) ?? "" + return line.replace(/^[#>\-*\s`]+/, "").trim() +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css index e09ec0c0ac..726eb1de21 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -234,54 +234,148 @@ /* PR comment list */ .am-pr-panel-comment-list { - gap: 8px; + gap: 6px; margin-top: 6px; } -.am-pr-panel-comment { +/* PR comment card */ +.am-pr-comment { border: 1px solid var(--vscode-panel-border); border-radius: 4px; - padding: 8px; - opacity: 1; + overflow: hidden; } -.am-pr-panel-comment-resolved { - opacity: 0.5; -} - -.am-pr-panel-comment-header { +.am-pr-comment-head { + width: 100%; gap: 6px; - margin-bottom: 4px; - flex-wrap: wrap; + padding: 6px 8px; + background: none; + border: none; + cursor: pointer; + text-align: left; + color: var(--vscode-foreground); + min-width: 0; } -.am-pr-panel-comment-author { +.am-pr-comment-head:hover { + background: var(--vscode-list-hoverBackground); +} + +.am-pr-comment-head:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.am-pr-comment-open .am-pr-comment-head { + border-bottom: 1px solid var(--vscode-panel-border); +} + +.am-pr-comment-chevron { + flex-shrink: 0; + opacity: 0.6; +} + +.am-pr-comment-check { + flex-shrink: 0; + color: var(--vscode-testing-iconPassed, #34d399); +} + +.am-pr-comment-author { font-size: var(--kilo-font-size-12); font-weight: 600; color: var(--vscode-foreground); + flex-shrink: 0; } -.am-pr-panel-comment-file { +.am-pr-comment-preview { + font-size: var(--kilo-font-size-12); + color: var(--text-weak); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.am-pr-comment-file { font-size: var(--kilo-font-size-11); color: var(--text-weak); font-family: var(--font-mono, monospace); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 180px; + direction: rtl; + min-width: 0; } -.am-pr-panel-comment-resolved-badge { +.am-pr-comment-tag { font-size: var(--kilo-font-size-10); color: var(--text-weaker); border: 1px solid var(--vscode-panel-border); border-radius: 3px; padding: 1px 4px; margin-left: auto; + flex-shrink: 0; } -.am-pr-copy-btn { - margin-left: auto; +.am-pr-comment-tag-sent { + color: var(--vscode-testing-iconPassed, #34d399); +} + +.am-pr-comment-body { + padding: 6px 8px 0; +} + +.am-pr-comment-reply { + padding: 6px 8px 0 16px; + border-left: 2px solid var(--vscode-panel-border); + margin-left: 8px; +} + +.am-pr-comment-reply .am-pr-comment-body { + padding-left: 0; +} + +.am-pr-comment-error { + font-size: var(--kilo-font-size-11); + color: var(--vscode-testing-iconFailed, #f87171); + padding: 6px 8px 0; +} + +.am-pr-comment-actions { + gap: 4px; + padding: 8px; + flex-wrap: wrap; +} + +.am-pr-comment-actions-gap { + flex: 1; +} + +/* Same bordered treatment the local review comment actions use, because the + VS Code theme renders secondary kilo-ui buttons as bare text. */ +[data-component="button"].am-pr-comment-btn[data-variant="secondary"] { + border: 1px solid var(--border-base); + background: var(--surface-base); + color: var(--text-base); +} + +[data-component="button"].am-pr-comment-btn[data-variant="secondary"]:hover:not(:disabled) { + background: var(--surface-interactive-base); +} + +.am-pr-comment-spinner { + width: 12px; + height: 12px; + margin-right: 4px; +} + +.am-pr-comment-send-all { + margin-top: 6px; + align-self: flex-start; +} + +.am-pr-comment-done-group { + margin-top: 10px; } /* Pierre diff hunk preview inside comment cards */ @@ -289,58 +383,7 @@ border: 1px solid var(--vscode-panel-border); border-radius: 4px; overflow: hidden; - margin-bottom: 6px; -} - -/* Resolve button */ -.am-pr-resolve-row { - display: flex; - justify-content: flex-start; - margin-top: 15px; -} - -.am-pr-resolve-btn { - background: none; - border: 1px solid var(--vscode-panel-border); - border-radius: 3px; - color: var(--text-weak); - cursor: pointer; - font-size: var(--kilo-font-size-14); - padding: 4px 16px; -} - -.am-pr-resolve-btn:hover { - color: var(--vscode-foreground); - border-color: var(--vscode-foreground); -} -.am-pr-resolve-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.am-pr-resolve-error { - font-size: var(--kilo-font-size-11); - color: var(--vscode-testing-iconFailed, #f87171); - padding: 2px 0 4px; -} - -.am-pr-resolve-loading { - display: flex; - align-items: center; - gap: 8px; - padding: 4px 16px; - font-size: var(--kilo-font-size-14); - color: var(--text-weak); -} - -.am-pr-resolve-spinner { - width: 14px; - height: 14px; -} - -.am-pr-panel-comment-body [data-component="markdown"] { - font-size: var(--kilo-font-size-12); - color: var(--vscode-foreground); + margin: 6px 8px 0; } .am-pr-panel-description { @@ -349,7 +392,7 @@ } .am-pr-panel-description [data-component="markdown"], -.am-pr-panel-comment-body [data-component="markdown"] { +.am-pr-comment-body [data-component="markdown"] { font-size: var(--kilo-font-size-13); color: var(--vscode-foreground); } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts index 99bf2d6146..2e788e2985 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts @@ -13,6 +13,11 @@ export interface PRCheck { duration?: string } +export interface PRCommentReply { + author: string + body: string +} + export interface PRComment { id: string threadId: string @@ -23,8 +28,10 @@ export interface PRComment { line?: number url?: string resolved: boolean + outdated: boolean createdAt?: number diffHunk?: string + replies?: PRCommentReply[] } export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented" 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 17a9cf5f58..27df2ab35c 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts @@ -1,6 +1,7 @@ import type { AnnotationSide, DiffLineAnnotation } from "@pierre/diffs" import type { WorktreeFileDiff } from "../src/types/messages" import { extractLines, type ReviewComment } from "./review-comments" +import type { ReviewCommentEntry } from "../src/types/messages" export interface AnnotationLabels { commentOnLine: (line: number) => string @@ -137,7 +138,7 @@ function makeActionButton(title: string, icon: SVGSVGElement, action: () => void return button } -export function sendReviewComments(comments: ReviewComment[], activeTerminalId?: string): void { +export function sendReviewComments(comments: ReviewCommentEntry[], activeTerminalId?: string): void { window.dispatchEvent( new MessageEvent("message", { data: { diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 5bb00170cf..23a0f2b65e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -56,7 +56,7 @@ import { type SandboxDefaultState, type SandboxState, } from "./prompt-input-utils" -import type { ExtensionMessage, ReviewComment, SendMessageFailedMessage, TextPart } from "../../types/messages" +import type { ExtensionMessage, ReviewCommentEntry, SendMessageFailedMessage, TextPart } from "../../types/messages" import { formatReviewCommentsMarkdown } from "../../utils/review-comment-markdown" import { createdDraftKey, @@ -84,7 +84,7 @@ import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" import { parseMemoryCommand, type ParsedMemoryCommand } from "../../utils/memory-command" import { useMemory } from "../../context/memory" -function mergeReviewComments(current: ReviewComment[], incoming: ReviewComment[]): ReviewComment[] { +function mergeReviewComments(current: ReviewCommentEntry[], incoming: ReviewCommentEntry[]): ReviewCommentEntry[] { if (incoming.length === 0) return current const map = new Map(current.map((item) => [item.id, item])) for (const item of incoming) { @@ -227,7 +227,7 @@ export const PromptInput: Component = (props) => { const saveDraft = ( key: string, next: string, - comments: ReviewComment[], + comments: ReviewCommentEntry[], imgs: ImageAttachment[], scroll = textareaRef?.scrollTop ?? scrollDrafts.get(key) ?? 0, ) => savePromptDraft(key, next, comments, imgs, scroll) @@ -239,7 +239,7 @@ export const PromptInput: Component = (props) => { }) const [text, setText] = createSignal("") - const [reviewComments, setReviewComments] = createSignal([]) + const [reviewComments, setReviewComments] = createSignal([]) const [enhancing, setEnhancing] = createSignal(false) const [autoApprove, setAutoApprove] = createSignal(false) const [sandboxes, setSandboxes] = createSignal>({}) @@ -353,7 +353,7 @@ export const PromptInput: Component = (props) => { const speech = useSpeechToText(vscode, server, language) const speechModels = useSpeechToTextModels() - const replaceReviewComments = (next: ReviewComment[]) => { + const replaceReviewComments = (next: ReviewCommentEntry[]) => { setReviewComments(next) if (next.length === 0) { reviewDrafts.delete(draftKey()) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx index ccc0960498..cfb85b31df 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ReviewComments.tsx @@ -3,16 +3,18 @@ import { Button } from "@kilocode/kilo-ui/button" import { Dialog } from "@kilocode/kilo-ui/dialog" import { Icon } from "@kilocode/kilo-ui/icon" import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Markdown } from "@kilocode/kilo-ui/markdown" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" import { useWorktreeMode } from "../../context/worktree-mode" -import type { ReviewComment } from "../../types/messages" +import { isPRReviewComment } from "../../../../src/shared/review-comments" +import type { ReviewCommentEntry } from "../../types/messages" import { fileName } from "./prompt-input-utils" interface ReviewCommentsProps { - comments: ReviewComment[] + comments: ReviewCommentEntry[] sessionID?: string variant?: "draft" | "message" onRemove?: (id: string) => void @@ -24,10 +26,22 @@ export const ReviewComments: Component = (props) => { const vscode = useVSCode() const worktree = useWorktreeMode() const dialog = useDialog() - const side = (item: ReviewComment) => (item.side === "deletions" ? "-" : "+") - const title = (item: ReviewComment) => `${fileName(item.file)} ${side(item)}${item.line}` + const author = (item: ReviewCommentEntry) => (isPRReviewComment(item) ? item.author : "") + const side = (item: ReviewCommentEntry) => { + if (isPRReviewComment(item)) return "" + return item.side === "deletions" ? "-" : "+" + } + const line = (item: ReviewCommentEntry) => (item.line ? `${side(item)}${item.line}` : "") + const body = (item: ReviewCommentEntry) => (isPRReviewComment(item) ? item.body : item.comment) + const snippet = (item: ReviewCommentEntry) => (isPRReviewComment(item) ? item.diffHunk : item.selectedText) + const label = (item: ReviewCommentEntry) => (item.file ? fileName(item.file) : `@${author(item)}`) + const title = (item: ReviewCommentEntry) => { + const at = line(item) + return at ? `${label(item)} ${at}` : label(item) + } - const open = (item: ReviewComment) => { + const open = (item: ReviewCommentEntry) => { + if (!item.file) return if (worktree && props.sessionID) { vscode.postMessage({ type: "agentManager.openFile", @@ -42,35 +56,59 @@ export const ReviewComments: Component = (props) => { dialog.close() } - const show = (item: ReviewComment) => { + const show = (item: ReviewCommentEntry) => { dialog.show(() => (
{title(item)} - - open(item)} - /> - + + + open(item)} + /> + +
- {language.t("agentManager.review.metaFile")} - {item.file} - {language.t("agentManager.review.metaLine")} - L{item.line} + + {(login) => ( + <> + {language.t("agentManager.review.metaAuthor")} + @{login()} + + )} + + + {(file) => ( + <> + {language.t("agentManager.review.metaFile")} + {file()} + + )} + + + {(value) => ( + <> + {language.t("agentManager.review.metaLine")} + L{value()} + + )} + {language.t("agentManager.review.metaComment")} - {item.comment} + + + + +
- -
{item.selectedText}
-
+ {(value) =>
{value()}
}
)) @@ -98,15 +136,12 @@ export const ReviewComments: Component = (props) => {
diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index c42ff3fa73..90b79c3ec1 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -28,6 +28,7 @@ import { ContextMenu } from "@kilocode/kilo-ui/context-menu" import { ThinkingSelectorBase } from "../components/shared/ThinkingSelector" import { DeferredPopover } from "../components/shared/DeferredPopover" import { ProjectSelect } from "../../agent-manager/ProjectSelect" +import { PRComments } from "../../agent-manager/pr/PRComments" import { createSignal, onCleanup, onMount, type JSX } from "solid-js" import type { AgentProjectSnapshot, @@ -40,6 +41,7 @@ import type { ReviewComment } from "../../diff-viewer/review-comments" import { createModeRouter } from "../../agent-manager/mode-router" import "../../agent-manager/agent-manager.css" import "../../agent-manager/agent-manager-review.css" +import "../../agent-manager/pr/pr-panel.css" registerVscodeToolOverrides() @@ -1484,3 +1486,83 @@ export const MultiProjectSidebar: Story = { ) }, } + +// --------------------------------------------------------------------------- +// PR panel — review comments +// --------------------------------------------------------------------------- + +const prComments: NonNullable = { + total: 4, + unresolved: 2, + comments: [ + { + id: "PRRC_1", + threadId: "PRRT_1", + author: "octocat", + body: "This throws when `gh` is missing. Can we guard it and fall back to the cached status?", + file: "packages/kilo-vscode/src/agent-manager/gh.ts", + line: 42, + url: "https://github.com/org/repo/pull/8594#discussion_r1", + resolved: false, + outdated: false, + diffHunk: + '@@ -39,7 +39,7 @@ export function execGhRead(args: string[]) {\n- return execWithShellEnv("gh", args, options)\n+ return execWithShellEnv("gh", args, { ...options, env: env(options) })', + replies: [{ author: "hubot", body: "Agreed. A guard plus a log line is enough here." }], + }, + { + id: "PRRC_2", + threadId: "PRRT_2", + author: "hubot", + body: "The timeout should be a constant so the poller and the mutation cannot drift apart.", + file: "packages/kilo-vscode/src/agent-manager/pr/PRActions.ts", + line: 8, + url: "https://github.com/org/repo/pull/8594#discussion_r2", + resolved: false, + outdated: true, + }, + { + id: "PRRC_3", + threadId: "PRRT_3", + author: "octocat", + body: "nit: rename this variable to `threads`.\n\nIt reads better next to the loop below.", + file: "packages/kilo-vscode/src/agent-manager/pr/am-pr-utils.ts", + line: 71, + url: "https://github.com/org/repo/pull/8594#discussion_r3", + resolved: true, + outdated: false, + }, + { + id: "PRRC_4", + threadId: "PRRT_4", + author: "hubot", + body: "Good catch, fixed in a9f21c3.", + file: "packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx", + line: 118, + url: "https://github.com/org/repo/pull/8594#discussion_r4", + resolved: true, + outdated: false, + }, + ], +} + +export const PRPanelComments: Story = { + name: "PR panel — review comments", + render: () => ( + +
+ {}} onOpenUrl={() => {}} /> +
+
+ ), +} + +export const PRPanelComments200: Story = { + name: "PR panel — review comments (narrow)", + render: () => ( + +
+ {}} onOpenUrl={() => {}} /> +
+
+ ), +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts index 4ccb83c074..56223232b7 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts @@ -63,6 +63,7 @@ export type { AggregateCheckStatus, PRCheck, PRComment, + PRCommentReply, PRReviewer, } from "../../../agent-manager/pr/pr-types" @@ -178,7 +179,11 @@ export interface LocalGitStats { behind: number } -export type { ReviewCommentData as ReviewComment } from "../../../../src/shared/review-comments" +export type { + ReviewCommentData as ReviewComment, + ReviewCommentEntry, + PRReviewCommentData, +} from "../../../../src/shared/review-comments" /** * Maximum number of parallel worktree versions for multi-version mode. diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index fda0c747d5..d56853e82b 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -39,7 +39,7 @@ import type { LocalGitStats, ManagedSessionState, PRStatus, - ReviewComment, + ReviewCommentEntry, RunStatus, SectionState, TerminalDestination, @@ -305,13 +305,13 @@ export interface AppendChatBoxMessage { export interface AppendReviewCommentsMessage { type: "appendReviewComments" - comments: ReviewComment[] + comments: ReviewCommentEntry[] autoSend?: boolean } export interface AppendReviewCommentsToTerminalMessage { type: "appendReviewCommentsToTerminal" - comments: ReviewComment[] + comments: ReviewCommentEntry[] autoSend?: boolean targetTerminalId: string } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index c271ceaca2..851af6c66f 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -4,7 +4,7 @@ import type { MessageLoadMode } from "./sessions" import type { PermissionFileDiff } from "./permissions" import type { ModelSelection, ProviderConfig } from "./providers" import type { Config } from "./config" -import type { ModelAllocation, ReviewComment, TerminalDestination, TerminalPlacement } from "./agent-manager" +import type { ModelAllocation, ReviewCommentEntry, TerminalDestination, TerminalPlacement } from "./agent-manager" import type { ReviewMessageData } from "../../../../src/shared/review-comments" import type { WorkStyle, WorkStyleState } from "../../../../src/shared/work-style-presets" import type { AnacondaDesktopWebviewMessage } from "../../../../src/shared/anaconda-desktop-messages" @@ -1061,7 +1061,7 @@ export interface OpenDiffVirtualRequest { export interface DiffViewerSendCommentsRequest { type: "diffViewer.sendComments" - comments: ReviewComment[] + comments: ReviewCommentEntry[] autoSend: boolean } diff --git a/packages/kilo-vscode/webview-ui/src/utils/draft-store.ts b/packages/kilo-vscode/webview-ui/src/utils/draft-store.ts index 69ba06733e..c2c24dde14 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/draft-store.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/draft-store.ts @@ -1,9 +1,9 @@ -import type { ReviewComment } from "../types/messages" +import type { ReviewCommentEntry } from "../types/messages" import type { ImageAttachment } from "../hooks/useImageAttachments" import { pendingDraftKey, sessionDraftKey } from "./prompt-drafts" export const drafts = new Map() -export const reviewDrafts = new Map() +export const reviewDrafts = new Map() export const imageDrafts = new Map() export const scrollDrafts = new Map() const discarded = new Set() @@ -13,7 +13,7 @@ const sending = new Set() export function savePromptDraft( key: string, text: string, - comments: ReviewComment[], + comments: ReviewCommentEntry[], images: ImageAttachment[], scroll = 0, ) { diff --git a/plans/agent-manager-pr-comments-ux.md b/plans/agent-manager-pr-comments-ux.md new file mode 100644 index 0000000000..7ce5340a07 --- /dev/null +++ b/plans/agent-manager-pr-comments-ux.md @@ -0,0 +1,418 @@ +# Plan: GitHub-style PR review comments in the Agent Manager PR panel + +## Problem + +The PR panel in Agent Manager renders every GitHub review thread as one flat, +always-expanded card. The card has weak actions and no way to hand a comment to +the agent. Nine concrete defects: + +1. **The unresolve button looks disabled.** A resolved card gets + `opacity: 0.5` on the whole card + ([`pr-panel.css:248`](../packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css#L248)), + which dims the enabled `Unresolve comment` button + ([`PRComments.tsx:96`](../packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx#L96)). + No `disabled` attribute is ever set, so the control works but reads as dead. + This is the reported bug, and dimming is the wrong fix for "this thread is + done". +2. **Nothing collapses.** Every thread, resolved or not, renders its full diff + hunk plus its full Markdown body + ([`PRComments.tsx:118-124`](../packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx#L118-L124)). + On a PR with 20 resolved threads, the 2 threads that still need work are + buried behind hundreds of pixels of settled discussion. +3. **The actions are not prominent.** Copy is one ghost icon pushed into the + header row + ([`PRComments.tsx:80`](../packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx#L80)), + and resolve is a low-contrast bordered text button at the bottom + ([`pr-panel.css:302`](../packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css#L302)). + The local review comment cards are the opposite: a real action row with a + primary send action. +4. **A PR comment cannot be sent to the agent.** Local diff review comments have + `Send to chat` per comment and `Send all to chat` for the batch + ([`review-annotations.ts:425-432`](../packages/kilo-vscode/webview-ui/diff-viewer/review-annotations.ts#L425-L432), + [`DiffPanel.tsx:443-459`](../packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx#L443-L459)). + GitHub PR comments have no equivalent, so the user copies text by hand and + pastes it into the prompt as raw text. +5. **Only the first comment of a thread is fetched.** + `comments(first: 1)` in + [`PRStatusPoller.ts:492`](../packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts#L492) + and the matching parser + ([`am-pr-utils.ts:69-89`](../packages/kilo-vscode/src/agent-manager/pr/am-pr-utils.ts#L69-L89)) + drop every reply. The reply usually holds the decision ("agreed, guard it"), + so both the UI and any send-to-agent payload lose the important half of the + conversation. +6. **No outdated state.** `isOutdated` is not queried, so a comment against code + that no longer exists looks identical to a live one. +7. **Resolve failures are opaque.** The bridge drops the real `gh` error and + posts `success: false` without the `error` field that the message type + already declares + ([`pr-status-bridge.ts:120-130`](../packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts#L120-L130), + [`types.ts:399-405`](../packages/kilo-vscode/src/agent-manager/types.ts#L399-L405)). + The card shows `Failed to resolve thread.` for a permission error, a network + error, and a stale thread ID alike. +8. **Strings are hardcoded English** in a webview that ships 21 locales. +9. **`createMemo` is used for a side effect** + ([`PRComments.tsx:23`](../packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx#L23)) + to reconcile the optimistic state with the poll result. + +## Reference behavior: how GitHub does it + +The target is GitHub's own review-thread model, not a new invention. + +| GitHub behavior | Detail | +|---|---| +| Resolved thread collapses | In **Files changed**, a resolved thread renders as one compact row: avatar, author, the first line of the comment truncated, and a `Resolved` label. The body, the diff context, and the reply box are hidden. | +| The row is the disclosure | Clicking the collapsed row expands the full thread in place. Nothing is dimmed after it expands. | +| Unresolve is always live | The expanded thread keeps a normal, fully enabled `Unresolve conversation` button. GitHub never greys it out. | +| Resolve collapses immediately | `Resolve conversation` collapses the thread as soon as the mutation succeeds, which is the feedback that the action worked. | +| Outdated threads collapse too | A thread whose code changed gets an `Outdated` badge and starts collapsed. | +| Timeline summary | In **Conversation**, a resolved thread shows ` marked this conversation as resolved` with a `Show resolved` button. | +| Counts are visible | The header shows unresolved conversation counts, and the filter menu offers `Unresolved` / `Resolved` / `All`. | +| Replies stay in the thread | A collapsed row hints at thread size; expanding shows every reply in order. | + +Two GitHub affordances are deliberately **not** copied: the conversation filter +dropdown (a 320px inspector panel does not have room for it, and two grouped +sections carry the same information), and reply composition (out of scope, see +Non-goals). + +## Proposed UX + +### Layout + +``` +────────────────────────────────────────────── +▾ COMMENTS 3 unresolved + ┌────────────────────────────────────────┐ + │ ➤ Send 3 unresolved to agent │ primary, full width + └────────────────────────────────────────┘ + + ┌────────────────────────────────────────┐ + │ src/agent-manager/gh.ts:42 │ meta row + │ ┌──────────── diff hunk ─────────────┐ │ + │ │ - const x = 1 │ │ + │ │ + const x = 2 │ │ + │ └────────────────────────────────────┘ │ + │ @alice │ + │ This throws when gh is missing. │ Markdown body + │ ▸ Show 2 replies │ + │ ────────────────────────────────────── │ + │ [➤ Send] [Resolve] ⧉ ↗ ⇥ │ action row + └────────────────────────────────────────┘ + + ┌────────────────────────────────────────┐ + │ src/pr/PRActions.ts:8 [Outdated] │ outdated: collapsed by default + │ ▸ @bob this mutation needs a timeout │ + └────────────────────────────────────────┘ + +▸ Resolved (5) +``` + +With the resolved group open: + +``` +▾ Resolved (5) + ▸ ✓ @bob nit: rename this variable + ▸ ✓ @alice can we extract this helper? + ▸ ✓ @bob good catch, fixed in a9f21c3 +``` + +### Interaction rules + +1. **Grouping is derived from server state.** Unresolved threads render first, + then a collapsible `Resolved (N)` group that starts closed. The section + heading keeps the existing `N unresolved` count. +2. **Unresolved threads render expanded**, except outdated ones, which render + collapsed with an `Outdated` badge (GitHub parity). +3. **Resolved threads render as one-line rows.** The row is a `