diff --git a/.changeset/pr-reviewer-avatars.md b/.changeset/pr-reviewer-avatars.md new file mode 100644 index 00000000000..2a187794bc3 --- /dev/null +++ b/.changeset/pr-reviewer-avatars.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show reviewer avatars and GitHub-style review states in the Agent Manager PR sidebar, and remove the redundant review decision row. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-panel-reviewers-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-panel-reviewers-chromium-linux.png new file mode 100644 index 00000000000..a262682186b --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-panel-reviewers-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbcb83899ae47fa6209a7faf5fe7468f807bef462853389e233aea457dae7a71 +size 9112 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-panel-summary-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-panel-summary-chromium-linux.png index 98198f79547..7a097a7d4b0 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-panel-summary-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-panel-summary-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5130bd50a42da51614edd0d3a12d17da6e0706d3b52c41e1e73be3910c98ea9 -size 43752 +oid sha256:3e7cb45784f1ef9e0e3e4d2ed62d046aebabd1e0b94287d57d5b770e99da8582 +size 42788 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png index 9fe4f1a6677..788bf55d832 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d24842272978a93de953c739a62fb55bdfd6c9eb26665db44b4a932f63eb874e -size 746976 +oid sha256:adbd7f9e9a98585cf00acb7600a37481ffa97ea4a16731fa430092b91287dda8 +size 746956 diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 7be756b96bd..019f81aceaf 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -58,6 +58,9 @@ export class PRStatusPoller { private activeWorktreeId: string | undefined private cachedRepo: { owner: string; name: string; root: string } | undefined private prCache = new Map() + /** Reviewer avatars are stable, so look them up once per login and reuse them. */ + private readonly avatars = new Map() + private readonly resolvedAvatars = new Set() private lastFullSync = 0 // timestamp of last full (all-worktree) sync private readonly intervalMs: number private readonly semaphore: Semaphore | undefined @@ -138,6 +141,8 @@ export class PRStatusPoller { this.rich = true this.cachedRepo = undefined this.prCache.clear() + this.avatars.clear() + this.resolvedAvatars.clear() this.lastFullSync = 0 } @@ -317,7 +322,28 @@ export class PRStatusPoller { } private extras(pr: PRResult, cwd: string) { - return [pr.checks ?? this.fetchChecks(pr.number, cwd), pr.reviewers ?? this.fetchReviewers(pr.number, cwd)] as const + return [pr.checks ?? this.fetchChecks(pr.number, cwd), this.reviewers(pr, cwd)] as const + } + + /** + * `gh pr view --json reviews` returns reviewer logins without avatars, so + * merge avatar URLs from the GraphQL query and cache them per login. States + * from `gh pr view` stay authoritative. + */ + private async reviewers(pr: PRResult, cwd: string): Promise { + if (pr.reviewers === undefined) return (await this.fetchReviewers(pr.number, cwd)).items + const list = pr.reviewers + if (list.length === 0 || list.every((item) => item.avatar || this.resolvedAvatars.has(item.login))) + return list.map((item) => (item.avatar ? item : { ...item, avatar: this.avatars.get(item.login) })) + const fetched = await this.fetchReviewers(pr.number, cwd) + if (fetched.ok) { + for (const item of list) this.resolvedAvatars.add(item.login) + for (const item of fetched.items) { + this.resolvedAvatars.add(item.login) + if (item.avatar) this.avatars.set(item.login, item.avatar) + } + } + return list.map((item) => (item.avatar ? item : { ...item, avatar: this.avatars.get(item.login) })) } private handleError(worktreeId: string, branch: string | undefined, cwd: string, err: unknown): void { @@ -459,7 +485,7 @@ export class PRStatusPoller { return info } - private async fetchReviewers(prNumber: number, cwd: string): Promise { + private async fetchReviewers(prNumber: number, cwd: string): Promise<{ items: PRReviewer[]; ok: boolean }> { try { const repo = await this.getRepoInfo(cwd) const query = `query($owner: String!, $repo: String!, $number: Int!) { @@ -490,13 +516,16 @@ export class PRStatusPoller { { cwd, timeout: 15_000 }, ) const pr = JSON.parse(stdout)?.data?.repository?.pullRequest - return parseReviewers( - (pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[], - (pr?.reviews?.nodes ?? []) as GhReview[], - ) + return { + items: parseReviewers( + (pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[], + (pr?.reviews?.nodes ?? []) as GhReview[], + ), + ok: true, + } } catch (err) { this.options.log("Failed to fetch PR reviewers:", err) - return [] + return { items: [], ok: false } } } 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 797307fe106..6d96ffa9091 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 @@ -370,7 +370,7 @@ export function signature(pr: PRStatus): string { pr.checks.total, pr.checks.checks.map((check) => [check.name, check.status, check.url ?? "", check.duration ?? ""]), ], - pr.reviewers.map((r) => [r.login, r.state]), + pr.reviewers.map((r) => [r.login, r.state, r.avatar ?? ""]), pr.body ?? "", [ pr.comments?.total ?? null, diff --git a/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts index 24e84df709b..815a7c723bf 100644 --- a/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts +++ b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts @@ -166,6 +166,46 @@ describe("PRStatusPoller batched GitHub queries", () => { poller.stop() }) + it("merges reviewer avatars from the GraphQL query and caches them per login", async () => { + const poller = new PRStatusPoller({ + getWorktrees: () => [], + getWorkspaceRoot: () => "/repo", + onStatus: () => undefined, + log: () => undefined, + }) + const internal = poller as unknown as { + reviewers: ( + pr: { number: number; reviewers?: Array<{ login: string; state: string; avatar?: string }> }, + cwd: string, + ) => Promise> + fetchReviewers: ( + number: number, + cwd: string, + ) => Promise<{ + items: Array<{ login: string; state: string; avatar?: string }> + ok: boolean + }> + } + const fetches: number[] = [] + internal.fetchReviewers = async (number) => { + fetches.push(number) + return { + items: [{ login: "eshurakov", state: "commented", avatar: "https://avatar/eshurakov" }], + ok: true, + } + } + const result = [{ login: "eshurakov", state: "approved", avatar: "https://avatar/eshurakov" }] + + expect( + await internal.reviewers({ number: 7, reviewers: [{ login: "eshurakov", state: "approved" }] }, "/repo"), + ).toEqual(result) + expect( + await internal.reviewers({ number: 7, reviewers: [{ login: "eshurakov", state: "approved" }] }, "/repo"), + ).toEqual(result) + expect(fetches).toEqual([7]) + poller.stop() + }) + it("forwards the actual branch for null PR results", async () => { const values: Array<{ pr: PRStatus | null; branch?: string }> = [] const branches: string[] = [] 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 63fdcce2684..784fe8c20a5 100644 --- a/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts @@ -640,6 +640,13 @@ describe("PR signature", () => { expect(signature({ ...pr, title: "A:B", body: "C" })).not.toBe(signature({ ...pr, title: "A", body: "B:C" })) }) + it("changes when a reviewer avatar becomes available", () => { + const before = signature(pr) + expect(signature({ ...pr, reviewers: [{ login: "alice", state: "pending", avatar: "https://avatar" }] })).not.toBe( + before, + ) + }) + it("changes when either captured PR ref changes", () => { const refs = { baseRefOid: "a".repeat(40), headRefOid: "b".repeat(40) } const before = signature({ ...pr, ...refs }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx index 577dd4a8e7d..ccb152de4f6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx @@ -11,12 +11,6 @@ const STATE_LABEL: Record = { closed: "Closed", } -const REVIEW_LABEL: Partial, string>> = { - approved: "Approved", - changes_requested: "Changes Requested", - pending: "Review Pending", -} - export function PROverview(props: { pr: PRStatus; worktree?: WorktreeState }) { return (
@@ -38,14 +32,6 @@ export function PROverview(props: { pr: PRStatus; worktree?: WorktreeState }) { {STATE_LABEL[props.pr.state]}
- - {(review) => ( -
- Review - {REVIEW_LABEL[review()]} -
- )} -
) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx index 071da1f119d..418783e6d91 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx @@ -1,13 +1,15 @@ /** @jsxImportSource solid-js */ import { For, Show, createSignal } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" import type { PRReviewer, ReviewerState } from "./pr-types" +import { PRAvatar } from "./PRAvatar" import { SectionHeading } from "./SectionHeading" const REVIEWER_ICON: Record = { approved: "circle-check", - changes_requested: "refresh", - commented: "edit", + changes_requested: "circle-x-outline", + commented: "comment", pending: "dash", } @@ -30,9 +32,13 @@ export function PRReviewers(props: { reviewers: PRReviewer[] }) { {(reviewer) => (
- + - {REVIEWER_LABEL[reviewer.state]} + + + + +
)}
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 f94e2c03f6a..44418e6d0a8 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 @@ -768,11 +768,15 @@ } .am-pr-panel-reviewer { - gap: 6px; + gap: 8px; padding: 3px 0; font-size: var(--kilo-font-size-12); } +.am-pr-panel-reviewer [data-component="avatar"] { + border-radius: 50%; +} + .am-pr-reviewer-icon { flex-shrink: 0; } @@ -781,7 +785,7 @@ color: var(--vscode-testing-iconPassed, #34d399); } .am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon { - color: var(--vscode-charts-yellow, #fbbf24); + color: var(--vscode-testing-iconFailed, #f87171); } .am-pr-panel-reviewer[data-state="commented"] .am-pr-reviewer-icon { color: var(--text-weak); @@ -793,11 +797,14 @@ .am-pr-reviewer-login { flex: 1; color: var(--vscode-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .am-pr-reviewer-state { - color: var(--text-weak); flex-shrink: 0; + color: var(--text-weak); } .am-pr-panel-checks { 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 6ac8d1f35aa..605862bd88f 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 @@ -34,8 +34,9 @@ import { DeferredPopover } from "../components/shared/DeferredPopover" import { ProjectSelect } from "../../agent-manager/ProjectSelect" import { PRComments } from "../../agent-manager/pr/PRComments" import { PRConversation } from "../../agent-manager/pr/PRConversation" -import type { PRComment, PRTimelineItem } from "../../agent-manager/pr/pr-types" import { PRPanel } from "../../agent-manager/pr/PRPanel" +import { PRReviewers } from "../../agent-manager/pr/PRReviewers" +import type { PRComment, PRReviewer, PRTimelineItem } from "../../agent-manager/pr/pr-types" import { For, createSignal, onCleanup, onMount, type JSX } from "solid-js" import type { AgentProjectSnapshot, @@ -2068,6 +2069,24 @@ const prComments: NonNullable = { ], } +const prReviewers: PRReviewer[] = [ + { login: "marius-kilocode", state: "approved" }, + { login: "reviewer-changes", state: "changes_requested" }, + { login: "reviewer-comment", state: "commented" }, + { login: "reviewer-pending", state: "pending" }, +] + +export const PRPanelReviewers: Story = { + name: "PR panel — reviewers", + render: () => ( + +
+ +
+
+ ), +} + export const PRPanelComments: Story = { name: "PR panel — review comments", render: () => (