Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ada60ca
feat(pr-diff): show diff hunk in comments on pr panel
cosi-conda Aug 10, 2026
feb7fc2
feat(pr-diff): organize files
cosi-conda Aug 10, 2026
3ce2ef1
feat(pr-diff): Log if no cwd for wt
cosi-conda Aug 10, 2026
695e685
feat(pr-diff): Move resolve button / styles and add error message
cosi-conda Aug 10, 2026
66b78db
feat(pr-diff): Move consts to file and add catch to resolve comment
cosi-conda Aug 10, 2026
5996834
feat(pr-diff): Jump to comments section; Add unresolve comment function
cosi-conda Aug 10, 2026
91773d6
feat(pr-actions): Resolve/unresolve comment functionality with error …
cosi-conda Aug 11, 2026
13dd6de
feat(pr-actions): Jump to comments
cosi-conda Aug 11, 2026
de6bb95
feat(pr-actions): Prevent scroll on update
cosi-conda Aug 11, 2026
dbf7796
feat(pr-actions): Scroll to top floating button
cosi-conda Aug 11, 2026
1ac66c3
feat(pr-actions): Refresh data on open pr panel
cosi-conda Aug 11, 2026
15d13f3
feat(pr-actions): Match colors to diff panel and consolidate types
cosi-conda Aug 11, 2026
954ee41
feat(pr-actions): Clear state if index changes
cosi-conda Aug 11, 2026
95b2e18
feat(pr-actions): Unit tests
cosi-conda Aug 11, 2026
8a9990d
feat(pr-actions): Unit tests
cosi-conda Aug 11, 2026
7010e7e
Merge branch 'main' into feature/pr-actions-diff-hunk
cosi-conda Aug 11, 2026
6f5b531
feat(pr-actions): Unit tests
cosi-conda Aug 11, 2026
c7da8a6
feat(pr-actions): Unit tests
cosi-conda Aug 11, 2026
58ca25f
feat(pr-actions): Unit tests
cosi-conda Aug 11, 2026
b42d834
Merge branch 'main' into feature/pr-actions-diff-hunk
cosi-conda Aug 11, 2026
5451451
feat(pr-actions): Prettier
cosi-conda Aug 11, 2026
0be9469
feat(pr-actions): Unit test mocks
cosi-conda Aug 11, 2026
851d372
feat(pr-actions): Unit test mocks
cosi-conda Aug 11, 2026
9fde682
feat(pr-actions): Unit test mocks
cosi-conda Aug 11, 2026
3746ae5
fix(pr-actions): post to webview if no cwd
cosi-conda Aug 11, 2026
4af09d1
fix(pr-actions): Use button for clickable pr summary item
cosi-conda Aug 11, 2026
3e3dd3d
fix(pr-actions): Add changeset md
cosi-conda Aug 11, 2026
328dbd3
fix(pr-actions): Adjust typing for better type safety in pr status br…
cosi-conda Aug 11, 2026
d96d357
fix(pr-actions): Adjust comment total count and loading style; Fetch …
cosi-conda Aug 11, 2026
4ee3934
fix(pr-actions): Adjut fallback for thread id in parseComments
cosi-conda Aug 11, 2026
ec94d8b
feat(pr-actions): Prettier
cosi-conda Aug 11, 2026
7d2ba0a
Merge branch 'main' into feature/pr-actions-diff-hunk
cosi-conda Aug 12, 2026
938105d
fix(pr-view): Address bot comment about timing of refreshing pr data
cosi-conda Aug 12, 2026
4794595
Merge branch 'main' into feature/pr-actions-diff-hunk
cosi-conda Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pr-comment-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Add Agent Manager PR comment actions: resolve/unresolve review threads, jump to comments section, and scroll-to-top for PR diff view.
13 changes: 9 additions & 4 deletions packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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 "./am-pr-utils"
import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-types"
import { parsePRResult, checkStatus, formatCheckDuration, parseComments, parseReviewers } from "./pr/am-pr-utils"
import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./pr/am-pr-types"

interface PRStatusPollerOptions {
getWorktrees: () => Worktree[]
Expand Down Expand Up @@ -485,7 +485,9 @@ export class PRStatusPoller {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 100) {
totalCount
nodes {
id
isResolved
comments(first: 1) {
nodes {
Expand All @@ -496,6 +498,7 @@ export class PRStatusPoller {
line
url
createdAt
diffHunk
}
}
}
Expand All @@ -520,8 +523,10 @@ export class PRStatusPoller {
{ cwd, timeout: 15_000 },
)
const pr = JSON.parse(stdout)?.data?.repository?.pullRequest
const comments = parseComments((pr?.reviewThreads?.nodes ?? []) as GhThread[])
return { total: comments.length, unresolved: comments.filter((c) => !c.resolved).length, comments }
const threads = pr?.reviewThreads
const comments = parseComments((threads?.nodes ?? []) as GhThread[])
const totalCount = threads?.totalCount ?? comments.length
return { total: totalCount, unresolved: comments.filter((c) => !c.resolved).length, comments }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: total (from totalCount) and unresolved (from first 100 nodes) can disagree

totalCount counts all review threads, but unresolved is still computed only from the first 100 fetched nodes. On a PR with >100 threads the summary could show e.g. "103 comments" with a success status while unresolved threads exist beyond the first page (and comments passed to the webview is also capped at 100). Probably rare in practice, but worth either paginating (reviewThreads cursor) or clamping/annotating the cap so the counts stay consistent.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having over 100 threads seems very unlikely on a human reviewed PR

} catch (err) {
this.options.log("Failed to fetch PR comments:", err)
return { total: 0, unresolved: 0, comments: [] }
Expand Down
44 changes: 44 additions & 0 deletions packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { AgentManagerOutMessage, PRStatus } from "./types"
import type { Disposable } from "./host"
import type { Semaphore } from "./semaphore"
import { PRStatusPoller } from "./PRStatusPoller"
import { resolveComment, unresolveComment } from "./pr/PRActions"

interface PRBridgeHost {
getWorktrees(): Worktree[]
Expand Down Expand Up @@ -85,6 +86,49 @@ export class PRStatusBridge {
if (url) this.host.openExternal(url)
return true
}
const isResolve = m.type === "agentManager.resolveComment"
const isUnresolve = m.type === "agentManager.unresolveComment"
if (isResolve || isUnresolve) {
const id = m.worktreeId as string
const threadId = m.threadId as string
const wt = this.host.getWorktrees().find((w: Worktree) => w.id === id)
const cwd = wt?.path ?? this.host.getWorkspaceRoot()
const resultType = isResolve ? "agentManager.resolveCommentResult" : "agentManager.unresolveCommentResult"
if (!cwd) {
Comment thread
cosi-conda marked this conversation as resolved.
this.host.log("resolveComment: no cwd for worktree", id)
this.host.postToWebview({
type: resultType,
worktreeId: id,
threadId,
success: false,
})
return true
}
const action = isResolve ? resolveComment : unresolveComment
action(threadId, cwd).then(
() => {
this.host.postToWebview({
type: resultType,
worktreeId: id,
threadId,
success: true,
})
// Refresh PR data after successful mutation to get updated comment state
this.poller.refresh(id)
},
(err: unknown) => {
const msg = err instanceof Error ? err.message : String(err)
this.host.log(`${resultType} failed: ${msg}`)
this.host.postToWebview({
type: resultType,
worktreeId: id,
threadId,
success: false,
})
},
)
return true
}
return false
}

Expand Down
30 changes: 30 additions & 0 deletions packages/kilo-vscode/src/agent-manager/pr/PRActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { execGhRead } from "../gh"
Comment thread
cosi-conda marked this conversation as resolved.
import { GH_MUTATION_TIMEOUT } from "./pr-constants"

export async function resolveComment(threadId: string, cwd: string): Promise<void> {
const mutation = `mutation($id: ID!) { resolveReviewThread(input: { threadId: $id }) { thread { isResolved } } }`
try {
await execGhRead(["api", "graphql", "-f", `query=${mutation}`, "-F", `id=${threadId}`], {
cwd,
timeout: GH_MUTATION_TIMEOUT,
})
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
const stderr = (err as Record<string, unknown>).stderr
throw new Error(`Could not resolve thread: ${msg}${stderr ? ` — ${stderr}` : ""}`)
}
}

export async function unresolveComment(threadId: string, cwd: string): Promise<void> {
const mutation = `mutation($id: ID!) { unresolveReviewThread(input: { threadId: $id }) { thread { isResolved } } }`
try {
await execGhRead(["api", "graphql", "-f", `query=${mutation}`, "-F", `id=${threadId}`], {
cwd,
timeout: GH_MUTATION_TIMEOUT,
})
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
const stderr = (err as Record<string, unknown>).stderr
throw new Error(`Could not unresolve thread: ${msg}${stderr ? ` — ${stderr}` : ""}`)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PRState, ReviewDecision } from "./types"
import type { PRState, ReviewDecision } from "../types"

// Raw shapes returned by `gh pr view --json`

Expand All @@ -14,8 +14,10 @@ export interface GhComment {
line?: number
url?: string
createdAt?: string
diffHunk?: string
}
export interface GhThread {
id?: string
isResolved?: boolean
comments?: { nodes?: GhComment[] }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CheckStatus, PRComment, PRReviewer, ReviewerState } from "./types"
import type { CheckStatus, PRComment, PRReviewer, ReviewerState } from "../types"
import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-types"

export function parsePRResult(json: string): PRResult | null {
Expand Down Expand Up @@ -73,6 +73,7 @@ export function parseComments(threads: GhThread[]): PRComment[] {
if (!first) continue
items.push({
id: first.id,
threadId: thread.id ?? first.id,
author: first.author?.login ?? "unknown",
avatar: first.author?.avatarUrl,
body: first.body ?? "",
Expand All @@ -81,6 +82,7 @@ export function parseComments(threads: GhThread[]): PRComment[] {
url: first.url,
resolved: thread.isResolved ?? false,
createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined,
diffHunk: first.diffHunk,
})
}
return items
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/src/agent-manager/pr/pr-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Timeouts for gh CLI and GraphQL calls in PR actions
export const GH_MUTATION_TIMEOUT = 15_000 // 15 seconds — gh api graphql mutations
18 changes: 18 additions & 0 deletions packages/kilo-vscode/src/agent-manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface PRCheck {

export interface PRComment {
id: string
threadId: string
author: string
avatar?: string
body: string
Expand All @@ -69,6 +70,7 @@ export interface PRComment {
url?: string
resolved: boolean
createdAt?: number
diffHunk?: string
}

export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented"
Expand Down Expand Up @@ -394,6 +396,14 @@ interface PRErrorOutMessage {
error: "gh_missing" | "gh_auth" | "fetch_failed"
}

interface CommentActionResultMessage {
type: "agentManager.resolveCommentResult" | "agentManager.unresolveCommentResult"
worktreeId: string
threadId: string
success: boolean
error?: string
}

interface ActionOutMessage {
type: "action"
action: string
Expand Down Expand Up @@ -434,6 +444,7 @@ export type AgentManagerOutMessage =
| DiffBranchesMessage
| PRStatusOutMessage
| PRErrorOutMessage
| CommentActionResultMessage
| ActionOutMessage
| RunStatusMessage
| TerminalCreatedMessage
Expand Down Expand Up @@ -758,6 +769,12 @@ interface OpenPRIn {
url?: string
}

interface CommentActionIn {
type: "agentManager.resolveComment" | "agentManager.unresolveComment"
worktreeId: string
threadId: string
}

interface OpenSessionsIn {
type: "agentManager.openSessions"
sessionIDs: string[]
Expand Down Expand Up @@ -1050,6 +1067,7 @@ export type AgentManagerInMessage =
| SetDiffBaseBranchIn
| RefreshPRIn
| OpenPRIn
| CommentActionIn
| OpenSessionsIn
| VisibleSessionIn
| OpenFileIn
Expand Down
86 changes: 84 additions & 2 deletions packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { describe, expect, it } from "bun:test"
import { describe, expect, it, mock, beforeEach } from "bun:test"

const resolveComment = mock(async (_threadId: string, _cwd: string) => {})
const unresolveComment = mock(async (_threadId: string, _cwd: string) => {})

mock.module("../../src/agent-manager/pr/PRActions", () => ({ resolveComment, unresolveComment }))

import { PRStatusBridge } from "../../src/agent-manager/pr-status-bridge"
import type { AgentManagerOutMessage, PRStatus } from "../../src/agent-manager/types"

Expand All @@ -9,14 +15,15 @@ const pr: PRStatus = {
state: "open",
review: null,
checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] },
reviewers: [],
additions: 0,
deletions: 0,
files: 0,
}

function harness(opts: { hasPersisted?: boolean } = {}) {
const sent: AgentManagerOutMessage[] = []
const worktrees: { id: string; prUrl?: string }[] = []
const worktrees: { id: string; path: string; prUrl?: string }[] = [{ id: "wt1", path: "/repo/wt1" }]
const bridge = PRStatusBridge.create({
getWorktrees: () => worktrees as never,
getWorkspaceRoot: () => "/repo",
Expand Down Expand Up @@ -186,3 +193,78 @@ describe("PRStatusBridge.reset", () => {
expect(sent).toHaveLength(1)
})
})

// --- resolveComment / unresolveComment message handling ---

describe("PRStatusBridge.handleMessage resolveComment", () => {
beforeEach(() => {
resolveComment.mockReset()
unresolveComment.mockReset()
})

it("returns true for agentManager.resolveComment", () => {
const { bridge } = harness()
resolveComment.mockResolvedValueOnce(undefined)
expect(bridge.handleMessage({ type: "agentManager.resolveComment", worktreeId: "wt1", threadId: "PRT_1" })).toBe(
true,
)
})

it("returns true for agentManager.unresolveComment", () => {
const { bridge } = harness()
unresolveComment.mockResolvedValueOnce(undefined)
expect(bridge.handleMessage({ type: "agentManager.unresolveComment", worktreeId: "wt1", threadId: "PRT_1" })).toBe(
true,
)
})

it("posts resolveCommentResult with success:true on resolve success", async () => {
const { bridge, sent } = harness()
resolveComment.mockResolvedValueOnce(undefined)
bridge.handleMessage({ type: "agentManager.resolveComment", worktreeId: "wt1", threadId: "PRT_1" })
await Promise.resolve()
const result = sent.find((m) => m.type === "agentManager.resolveCommentResult")
expect(result).toEqual(
expect.objectContaining({
type: "agentManager.resolveCommentResult",
worktreeId: "wt1",
threadId: "PRT_1",
success: true,
}),
)
})

it("posts unresolveCommentResult with success:true on unresolve success", async () => {
const { bridge, sent } = harness()
unresolveComment.mockResolvedValueOnce(undefined)
bridge.handleMessage({ type: "agentManager.unresolveComment", worktreeId: "wt1", threadId: "PRT_1" })
await Promise.resolve()
const result = sent.find((m) => m.type === "agentManager.unresolveCommentResult")
expect(result).toEqual(expect.objectContaining({ success: true }))
})

it("posts resolveCommentResult with success:false on failure", async () => {
const { bridge, sent } = harness()
resolveComment.mockRejectedValueOnce(new Error("gh: Not Found"))
bridge.handleMessage({ type: "agentManager.resolveComment", worktreeId: "wt1", threadId: "PRT_1" })
await Promise.resolve()
const result = sent.find((m) => m.type === "agentManager.resolveCommentResult")
expect(result).toEqual(expect.objectContaining({ success: false }))
})

it("logs and returns early when no cwd found", () => {
const logged: unknown[] = []
const bridge = PRStatusBridge.create({
getWorktrees: () => [] as never,
getWorkspaceRoot: () => undefined,
postToWebview: () => {},
updateWorktreePR: () => {},
hasPersistedPR: () => false,
openExternal: () => {},
log: (...args) => logged.push(args),
})
bridge.handleMessage({ type: "agentManager.resolveComment", worktreeId: "wt-missing", threadId: "PRT_1" })
expect(resolveComment).not.toHaveBeenCalled()
expect(logged.length).toBeGreaterThan(0)
})
})
Loading
Loading