Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
9a7a677
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 3, 2026
6e10464
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 3, 2026
039d233
feat(pr-viewer): View PR with mock data in AM
cosi-conda Aug 3, 2026
0a78fed
feat(pr-viewer): Update styles and add markdown renderer for PR descr…
cosi-conda Aug 3, 2026
eb74496
feat(pr-viewer): Update styles section headers
cosi-conda Aug 3, 2026
291ac08
fix(pr-view): Move toggle pr panel function
cosi-conda Aug 3, 2026
8663e5b
fix(pr-view): Organizing files and components, and improving types
cosi-conda Aug 3, 2026
f01d5fd
feat(pr-view): Improve agent code practices
cosi-conda Aug 3, 2026
6e1d7e6
feat(pr-view): Improve agent code practices, add types and styles
cosi-conda Aug 3, 2026
ffacdc6
feat(pr-view): PR summary
cosi-conda Aug 3, 2026
c7213b2
feat(pr-view): Use PR icon on badges and update summary to remove dis…
cosi-conda Aug 4, 2026
93fde67
feat(pr-view): Use PR icon on worktree item and remove pr number
cosi-conda Aug 4, 2026
bd67e85
feat(pr-view): Styling and copy/paste on PR comments
cosi-conda Aug 4, 2026
3c3cf69
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 4, 2026
a773275
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 4, 2026
70c37d6
feat(pr-view): show code -X/+Y on code diff and remove PR number
cosi-conda Aug 4, 2026
6bcb68e
feat(pr-view): fix styles to add variables and light theme fix for sv…
cosi-conda Aug 4, 2026
cc900b2
feat(pr-view): Organizing code; Adjust colors for themes; Improve typ…
cosi-conda Aug 5, 2026
907a5f6
feat(pr-view): Handle gh auth errors in the ui
cosi-conda Aug 6, 2026
9bc0cb1
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 6, 2026
7415a47
fix(pr-view): merge 'main' causes line count linter error so need to …
cosi-conda Aug 6, 2026
0979a57
feat(pr-view): organize types
cosi-conda Aug 6, 2026
0c22c49
feat(pr-view): add unit tests for status bridge and pr utils
cosi-conda Aug 6, 2026
816dc7a
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 7, 2026
a2919e1
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 7, 2026
e876334
feat(pr-view): Re-add pr number on worktree item
cosi-conda Aug 7, 2026
f94512d
Merge branch 'feature/am-pr-view' of https://github.com/Kilo-Org/kilo…
cosi-conda Aug 7, 2026
46dafba
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 7, 2026
e056794
Merge branch 'feature/am-pr-view' of https://github.com/Kilo-Org/kilo…
cosi-conda Aug 7, 2026
d0585fa
fix(pr-view): unit tests
cosi-conda Aug 7, 2026
d6e4182
chore: update kilo-vscode visual regression baselines
kilo-maintainer[bot] Aug 7, 2026
2e97f0c
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 7, 2026
1a0cc86
fix(pr-view): update reviewers summary and types
cosi-conda Aug 7, 2026
61a57dd
Merge branch 'feature/am-pr-view' of https://github.com/Kilo-Org/kilo…
cosi-conda Aug 7, 2026
c875fb0
fix(pr-view): split reviewers out of comments; fix pr summary to show…
cosi-conda Aug 7, 2026
c20ccad
fix(pr-view): close pr window if wt has not pr
cosi-conda Aug 7, 2026
e1f0643
feat(pr-view): move file changes
cosi-conda Aug 7, 2026
a357c71
feat(pr-view): Update pr button style
cosi-conda Aug 7, 2026
1a65a9d
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 7, 2026
05301da
fix(pr-view): Formatting and update css variable name for unit test
cosi-conda Aug 7, 2026
db13315
feat(pr-view): i18n for agent manager pr messaging
cosi-conda Aug 7, 2026
baf5685
fix(pr-view): don't include skipped prs in pr summary and also remove…
cosi-conda Aug 7, 2026
c313dc4
fix(pr-view): default pr status state
cosi-conda Aug 7, 2026
2dc7442
fix(pr-view): add kilocode change
cosi-conda Aug 7, 2026
0a6b39f
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 10, 2026
d074e0e
fix(pr-view): Update mergeMessages to main
cosi-conda Aug 10, 2026
cc397bb
feat(pr-view): new icons in kilo-ui for circle x, pull request, and r…
cosi-conda Aug 10, 2026
cf490b9
fix(pr-view): Update hash for pr status poller to include reviewers s…
cosi-conda Aug 10, 2026
95c361e
fix(pr-view): clear error on successful status received
cosi-conda Aug 10, 2026
1b2ab46
fix(pr-view): Adjust link out pattern to use vs code post message
cosi-conda Aug 10, 2026
a1daaad
feat(pr-view): remove excess markedprovider on pr
cosi-conda Aug 10, 2026
7bf1e54
feat(pr-view): close review tab when pr panel opens
cosi-conda Aug 10, 2026
1afd4e7
format(pr-view): prettier
cosi-conda Aug 10, 2026
b87f4da
feat(pr-view): add title to pr hash
cosi-conda Aug 10, 2026
e62e92d
fix(pr-view): fetch the most recent 20 reviews
cosi-conda Aug 10, 2026
e532519
fix(pr-view): error handling for pr reviewer fetch
cosi-conda Aug 10, 2026
10762a4
Merge branch 'main' into feature/am-pr-view
cosi-conda Aug 10, 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions packages/kilo-ui/src/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import { Icon as Upstream, type IconProps as Props } from "@opencode-ai/ui/icon"
import { splitProps } from "solid-js"

const icons: Record<string, { path: string; viewBox: string }> = {
"circle-x-outline": {
viewBox: "0 0 20 20",
path: `<path d="M7.5 7.5L12.5 12.5M12.5 7.5L7.5 12.5M18.3333 10C18.3333 14.6024 14.6024 18.3333 10 18.3333C5.39763 18.3333 1.66667 14.6024 1.66667 10C1.66667 5.39763 5.39763 1.66667 10 1.66667C14.6024 1.66667 18.3333 5.39763 18.3333 10Z" stroke="currentColor" stroke-linecap="square"/>`,
},
"pull-request": {
viewBox: "0 0 20 20",
path: `<path d="M1.875 4.0625a2.8125 2.8125 0 1 1 3.75 2.6525v6.57a2.8138 2.8138 0 1 1-1.875 0V6.715A2.8125 2.8125 0 0 1 1.875 4.0625Zm7.096-.22125L11.96625.84625A.3125.3125 0 0 1 12.5 1.0675V3.125h1.25A3.125 3.125 0 0 1 16.875 6.25v7.035a2.8138 2.8138 0 1 1-1.875 0V6.25a1.25 1.25 0 0 0-1.25-1.25H12.5v2.0575a.3125.3125 0 0 1-.53375.22125L8.97125 4.34125a.3125.3125 0 0 1 0-.4425ZM4.6875 3.125a.9375.9375 0 1 0 0 1.875.9375.9375 0 0 0 0-1.875Zm0 11.875a.9375.9375 0 1 0 0 1.875.9375.9375 0 0 0 0-1.875Zm10.3125.9375a.9375.9375 0 1 0 1.875 0 .9375.9375 0 0 0-1.875 0Z" fill="currentColor"/>`,
},
refresh: {
viewBox: "0 0 20 20",
path: `<path d="M17.0837 10.0003C17.0837 13.9123 13.9123 17.0837 10.0003 17.0837C6.08833 17.0837 2.91699 13.9123 2.91699 10.0003C2.91699 6.08833 6.08833 2.91699 10.0003 2.91699C12.3717 2.91699 14.4722 4.07428 15.7698 5.83366M15.7698 5.83366V2.91699M15.7698 5.83366H12.8532" stroke="currentColor" stroke-linecap="square"/>`,
},
memory: {
viewBox: "0 0 24 24",
path: `<path d="M2 7h4M2 12h4M2 17h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><rect width="16" height="20" x="4" y="2" rx="2" stroke="currentColor" stroke-width="2"/>`,
Expand Down
183 changes: 71 additions & 112 deletions packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { ExecFileOptionsWithStringEncoding } from "child_process"
import type { Worktree } from "./WorktreeStateManager"
import type { PRStatus, PRCheck, PRComment, CheckStatus, AggregateCheckStatus, PRState, ReviewDecision } from "./types"
import type { PRStatus, PRCheck, PRComment, PRReviewer, AggregateCheckStatus } from "./types"
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"

interface PRStatusPollerOptions {
getWorktrees: () => Worktree[]
Expand All @@ -17,6 +19,7 @@ interface PRStatusPollerOptions {
}

const GH_PROBE_TTL = 300_000 // 5 minutes — gh installation state rarely changes at runtime
const GH_PROBE_FAILURE_TTL = 30_000 // 30 seconds — retry faster after a failed probe
const MAX_BACKOFF = 120_000 // 2 minutes — cap for exponential backoff on repeated errors
const BACKOFF_MULTIPLIER = 2
const PR_LOOKUP_TTL = 10_000 // 10 seconds — short TTL; only the active worktree polls so this stays cheap
Expand Down Expand Up @@ -171,7 +174,8 @@ export class PRStatusPoller {

private async probeGh(): Promise<boolean> {
const now = Date.now()
if (this.ghAvailable !== undefined && now - this.ghProbeTime < GH_PROBE_TTL) {
const ttl = this.ghAvailable === false ? GH_PROBE_FAILURE_TTL : GH_PROBE_TTL
if (this.ghAvailable !== undefined && now - this.ghProbeTime < ttl) {
return this.ghAvailable
}
try {
Expand Down Expand Up @@ -244,26 +248,32 @@ export class PRStatusPoller {
return
}

const [checks, comments] = await Promise.all([
const [checks, reviewers, comments] = await Promise.all([
this.fetchChecks(pr.number, wt.path),
this.fetchReviewers(pr.number, wt.path),
this.activeWorktreeId === worktreeId ? this.fetchComments(pr.number, wt.path) : undefined,
])
if (this.stale(generation)) return

const status: PRStatus = {
number: pr.number,
title: pr.title,
body: pr.body,
url: pr.url,
state: pr.state,
review: pr.review,
checks,
...(comments && { comments }),
reviewers,
...(comments && {
comments: { total: comments.total, unresolved: comments.unresolved, comments: comments.comments },
}),
additions: pr.additions,
deletions: pr.deletions,
files: pr.files,
}

const hash = `${worktreeId}:${pr.number}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}`
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 ?? ""}`
if (this.lastHash.get(worktreeId) === hash) return
this.lastHash.set(worktreeId, hash)

Expand Down Expand Up @@ -292,7 +302,7 @@ export class PRStatusPoller {
}

private static readonly PR_JSON_FIELDS =
"number,title,url,state,isDraft,reviewDecision,additions,deletions,changedFiles,headRefName,headRefOid"
"number,title,body,url,state,isDraft,reviewDecision,additions,deletions,changedFiles,headRefName,headRefOid"

/** Return a cached PR lookup if still fresh, otherwise fetch and cache.
* Keyed by branch name so multiple worktrees on the same branch share
Expand Down Expand Up @@ -374,7 +384,7 @@ export class PRStatusPoller {
passed: number
failed: number
pending: number
items: PRCheck[]
checks: PRCheck[]
}> {
try {
const { stdout } = await this.gh(
Expand All @@ -389,24 +399,24 @@ export class PRStatusPoller {
completedAt?: string
}>

const items: PRCheck[] = data.map((c) => ({
const checks: PRCheck[] = data.map((c) => ({
name: c.name,
status: mapCheckStatus(c.state),
status: checkStatus(c.state),
url: c.link,
duration: formatCheckDuration(c.startedAt, c.completedAt),
}))

const total = items.length
const passed = items.filter((c) => c.status === "success").length
const failed = items.filter((c) => c.status === "failure").length
const pending = items.filter((c) => c.status === "pending").length
const total = checks.filter((c) => c.status !== "skipped").length
const passed = checks.filter((c) => c.status === "success").length
const failed = checks.filter((c) => c.status === "failure").length
const pending = checks.filter((c) => c.status === "pending").length

const status: AggregateCheckStatus =
total === 0 ? "none" : failed > 0 ? "failure" : pending > 0 ? "pending" : "success"

return { status, total, passed, failed, pending, items }
return { status, total, passed, failed, pending, checks }
} catch {
return { status: "none", total: 0, passed: 0, failed: 0, pending: 0, items: [] }
return { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] }
}
}

Expand All @@ -424,13 +434,53 @@ export class PRStatusPoller {
return info
}

private async fetchReviewers(prNumber: number, cwd: string): Promise<PRReviewer[]> {
try {
const repo = await this.getRepoInfo(cwd)
const query = `query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewRequests(first: 20) {
nodes { requestedReviewer { ... on User { login avatarUrl } } }
}
reviews(last: 20, states: [APPROVED, CHANGES_REQUESTED, COMMENTED]) {
nodes { author { login avatarUrl } state }
}
}
}
}`
const { stdout } = await this.gh(
[
"api",
"graphql",
"-f",
`query=${query}`,
"-F",
`owner=${repo.owner}`,
"-F",
`repo=${repo.name}`,
"-F",
`number=${prNumber}`,
],
{ cwd, timeout: 15_000 },
)
const pr = JSON.parse(stdout)?.data?.repository?.pullRequest
return parseReviewers(
(pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[],
(pr?.reviews?.nodes ?? []) as GhReview[],
)
} catch (err) {
this.options.log("Failed to fetch PR reviewers:", err)
return []
}
}

private async fetchComments(
prNumber: number,
cwd: string,
): Promise<{ total: number; unresolved: number; items: PRComment[] }> {
): Promise<{ total: number; unresolved: number; comments: PRComment[] }> {
try {
const repo = await this.getRepoInfo(cwd)

const query = `query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
Expand Down Expand Up @@ -469,107 +519,16 @@ export class PRStatusPoller {
],
{ cwd, timeout: 15_000 },
)
const result = JSON.parse(stdout)
const threads = result?.data?.repository?.pullRequest?.reviewThreads?.nodes ?? []

const items: PRComment[] = []
for (const thread of threads) {
const first = thread.comments?.nodes?.[0]
if (!first) continue
items.push({
id: first.id,
author: first.author?.login ?? "unknown",
avatar: first.author?.avatarUrl,
body: first.body ?? "",
file: first.path,
line: first.line,
url: first.url,
resolved: thread.isResolved ?? false,
createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined,
})
}

const total = items.length
const unresolved = items.filter((c) => !c.resolved).length
return { total, unresolved, items }
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 }
} catch (err) {
this.options.log("Failed to fetch PR comments:", err)
return { total: 0, unresolved: 0, items: [] }
return { total: 0, unresolved: 0, comments: [] }
}
}
}

interface PRResult {
number: number
title: string
url: string
state: PRState
review: ReviewDecision | null
additions: number
deletions: number
files: number
}

function parsePRResult(json: string): PRResult | null {
const data = JSON.parse(json)
if (!data.number) return null
return {
number: data.number,
title: data.title ?? "",
url: data.url ?? "",
state: parsePRState(data.isDraft, data.state),
review: parseReviewDecision(data.reviewDecision),
additions: data.additions ?? 0,
deletions: data.deletions ?? 0,
files: data.changedFiles ?? 0,
}
}

function parsePRState(isDraft: boolean, ghState: string): PRState {
if (isDraft) return "draft"
if (ghState === "MERGED") return "merged"
if (ghState === "CLOSED") return "closed"
return "open"
}

function parseReviewDecision(decision: string | undefined): ReviewDecision | null {
if (decision === "APPROVED") return "approved"
if (decision === "CHANGES_REQUESTED") return "changes_requested"
if (decision === "REVIEW_REQUIRED") return "pending"
return null
}

function mapCheckStatus(state: string): CheckStatus {
switch (state.toUpperCase()) {
case "SUCCESS":
return "success"
case "FAILURE":
case "ERROR":
return "failure"
case "PENDING":
case "QUEUED":
case "IN_PROGRESS":
case "REQUESTED":
case "WAITING":
return "pending"
case "SKIPPED":
return "skipped"
case "CANCELLED":
case "TIMED_OUT":
case "STALE":
case "STARTUP_FAILURE":
return "cancelled"
default:
return "pending"
}
}

function formatCheckDuration(startedAt?: string, completedAt?: string): string | undefined {
if (!startedAt || !completedAt) return undefined
const secs = Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1000)
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`
}

/** Run async thunks with bounded concurrency, returning settled results. */
async function settled<T>(thunks: (() => Promise<T>)[], concurrency: number): Promise<PromiseSettledResult<T>[]> {
const results: PromiseSettledResult<T>[] = new Array(thunks.length)
Expand Down
40 changes: 40 additions & 0 deletions packages/kilo-vscode/src/agent-manager/am-pr-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { PRState, ReviewDecision } from "./types"

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

export interface GhAuthor {
login?: string
avatarUrl?: string
}
export interface GhComment {
id: string
author?: GhAuthor
body?: string
path?: string
line?: number
url?: string
createdAt?: string
}
export interface GhThread {
isResolved?: boolean
comments?: { nodes?: GhComment[] }
}
export interface GhReviewRequest {
requestedReviewer?: GhAuthor
}
export interface GhReview {
author?: GhAuthor
state?: string
}

export interface PRResult {
number: number
title: string
body: string
url: string
state: PRState
review: ReviewDecision | null
additions: number
deletions: number
files: number
}
Loading
Loading