Merge pull request #12961 from Kilo-Org/feature/am-pr-view

feat(vscode): Agent Manager PR View Panel
This commit is contained in:
cosi-conda
2026-08-10 12:00:15 -07:00
committed by GitHub
59 changed files with 2188 additions and 340 deletions
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197
size 3694
oid sha256:33ee6eb7070237487d1931f32ac77a2549e18e664b993895e9a9d6619527d0aa
size 4063
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197
size 3694
oid sha256:33ee6eb7070237487d1931f32ac77a2549e18e664b993895e9a9d6619527d0aa
size 4063
+12
View File
@@ -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"/>`,
@@ -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[]
@@ -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
@@ -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 {
@@ -244,8 +248,9 @@ 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
@@ -253,17 +258,22 @@ export class PRStatusPoller {
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)
@@ -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
@@ -374,7 +384,7 @@ export class PRStatusPoller {
passed: number
failed: number
pending: number
items: PRCheck[]
checks: PRCheck[]
}> {
try {
const { stdout } = await this.gh(
@@ -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: [] }
}
}
@@ -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) {
@@ -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)
@@ -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
}
@@ -0,0 +1,105 @@
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 {
const data = JSON.parse(json)
if (!data.number) return null
const state = data.isDraft ? "draft" : (data.state?.toLowerCase() ?? "open")
const decision = data.reviewDecision as string | undefined
const review =
decision === "APPROVED"
? "approved"
: decision === "CHANGES_REQUESTED"
? "changes_requested"
: decision === "REVIEW_REQUIRED"
? "pending"
: null
return {
number: data.number,
title: data.title ?? "",
body: data.body ?? "",
url: data.url ?? "",
state,
review,
additions: data.additions ?? 0,
deletions: data.deletions ?? 0,
files: data.changedFiles ?? 0,
}
}
export function checkStatus(state: string): CheckStatus {
switch (state.toUpperCase()) {
case "SUCCESS":
case "NEUTRAL":
return "success"
case "FAILURE":
case "ERROR":
case "ACTION_REQUIRED":
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"
}
}
export 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`
}
const REVIEWER_STATE: Record<string, ReviewerState> = {
APPROVED: "approved",
CHANGES_REQUESTED: "changes_requested",
COMMENTED: "commented",
}
export function parseComments(threads: GhThread[]): PRComment[] {
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,
})
}
return items
}
export function parseReviewers(requests: GhReviewRequest[], reviews: GhReview[]): PRReviewer[] {
const map = new Map<string, PRReviewer>()
for (const node of requests) {
const user = node.requestedReviewer
if (!user?.login) continue
map.set(user.login, { login: user.login, avatar: user.avatarUrl, state: "pending" })
}
for (const node of reviews) {
const login = node.author?.login
if (!login) continue
const state = REVIEWER_STATE[node.state ?? ""] ?? "pending"
if (!map.has(login) || state !== "commented") {
map.set(login, { login, avatar: node.author?.avatarUrl, state })
}
}
return [...map.values()]
}
@@ -31,6 +31,7 @@ export class PRStatusBridge {
readonly poller: PRStatusPoller
private readonly cache = new Map<string, AgentManagerOutMessage>()
private readonly host: PRBridgeHost
private lastErrorNotified: "gh_missing" | "gh_auth" | "fetch_failed" | undefined
constructor(host: PRBridgeHost) {
this.host = host
@@ -61,6 +62,8 @@ export class PRStatusBridge {
/** Replay cached PR statuses to a freshly-connected webview. */
replay(): void {
this.cache.forEach((msg) => this.host.postToWebview(msg))
if (this.lastErrorNotified === "gh_auth" || this.lastErrorNotified === "gh_missing")
this.host.postToWebview({ type: "agentManager.prError", error: this.lastErrorNotified } as AgentManagerOutMessage)
}
snapshot(): Map<string, PRStatus> {
@@ -78,8 +81,8 @@ export class PRStatusBridge {
return true
}
if (m.type === "agentManager.openPR") {
const wt = this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)
if (wt?.prUrl) this.host.openExternal(wt.prUrl)
const url = (m.url as string) ?? this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)?.prUrl
if (url) this.host.openExternal(url)
return true
}
return false
@@ -93,6 +96,13 @@ export class PRStatusBridge {
reset(): void {
this.poller.stop()
this.cache.clear()
this.lastErrorNotified = undefined
}
notifyError(err: "gh_missing" | "gh_auth" | "fetch_failed"): void {
if (this.lastErrorNotified === err) return
this.lastErrorNotified = err
this.host.postToWebview({ type: "agentManager.prError", error: err } as AgentManagerOutMessage)
}
}
@@ -115,10 +125,15 @@ function bridgePollerOpts(bridge: PRStatusBridge, host: PRBridgeHost) {
pr: null,
error: err,
} as AgentManagerOutMessage)
// Always forward auth/missing errors so the webview can show a toast,
// regardless of whether prior data exists. Deduplicate per error type
// so multiple failing worktrees don't produce multiple toasts.
if (err === "gh_auth" || err === "gh_missing") bridge.notifyError(err)
return
}
const msg = { type: "agentManager.prStatus", worktreeId: id, pr, error: err } as AgentManagerOutMessage
bridge["cache"].set(id, msg)
bridge["lastErrorNotified"] = undefined
host.postToWebview(msg)
host.updateWorktreePR(id, pr?.number, pr?.url, pr?.state)
},
@@ -71,9 +71,18 @@ export interface PRComment {
createdAt?: number
}
export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented"
export interface PRReviewer {
login: string
avatar?: string
state: ReviewerState
}
export interface PRStatus {
number: number
title: string
body?: string
url: string
state: PRState
review: ReviewDecision | null
@@ -83,12 +92,13 @@ export interface PRStatus {
passed: number
failed: number
pending: number
items: PRCheck[]
checks: PRCheck[]
}
reviewers: PRReviewer[]
comments?: {
total: number
unresolved: number
items: PRComment[]
comments: PRComment[]
}
additions: number
deletions: number
@@ -379,6 +389,11 @@ interface PRStatusOutMessage {
error?: "gh_missing" | "gh_auth" | "fetch_failed"
}
interface PRErrorOutMessage {
type: "agentManager.prError"
error: "gh_missing" | "gh_auth" | "fetch_failed"
}
interface ActionOutMessage {
type: "action"
action: string
@@ -418,6 +433,7 @@ export type AgentManagerOutMessage =
| RevertWorktreeFileResultMessage
| DiffBranchesMessage
| PRStatusOutMessage
| PRErrorOutMessage
| ActionOutMessage
| RunStatusMessage
| TerminalCreatedMessage
@@ -739,6 +755,7 @@ interface OpenPRIn {
type: "agentManager.openPR"
projectId?: string
worktreeId: string
url?: string
}
interface OpenSessionsIn {
@@ -38,6 +38,7 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"),
path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"),
path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"),
path.join(ROOT, "webview-ui/agent-manager/pr/PRBadge.tsx"),
path.join(ROOT, "webview-ui/agent-manager/SectionHeader.tsx"),
path.join(ROOT, "webview-ui/agent-manager/SidebarSectionHeader.tsx"),
path.join(ROOT, "webview-ui/agent-manager/SidebarSearchMenu.tsx"),
@@ -86,8 +86,8 @@ describe("Agent Manager orchestration domain", () => {
url: "https://example.com/pr/42",
state: "open",
review: "approved",
checks: { status: "success", total: 1, passed: 1, failed: 0, pending: 0, items: [] },
comments: { total: 2, unresolved: 1, items: [] },
checks: { status: "success", total: 1, passed: 1, failed: 0, pending: 0, checks: [] },
comments: { total: 2, unresolved: 1, comments: [], reviewers: [] },
additions: 10,
deletions: 2,
files: 1,
@@ -0,0 +1,188 @@
import { describe, expect, it } from "bun:test"
import { PRStatusBridge } from "../../src/agent-manager/pr-status-bridge"
import type { AgentManagerOutMessage, PRStatus } from "../../src/agent-manager/types"
const pr: PRStatus = {
number: 1,
title: "my PR",
url: "https://github.com/x/y/pull/1",
state: "open",
review: null,
checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] },
additions: 0,
deletions: 0,
files: 0,
}
function harness(opts: { hasPersisted?: boolean } = {}) {
const sent: AgentManagerOutMessage[] = []
const worktrees: { id: string; prUrl?: string }[] = []
const bridge = PRStatusBridge.create({
getWorktrees: () => worktrees as never,
getWorkspaceRoot: () => "/repo",
postToWebview: (msg) => sent.push(msg),
updateWorktreePR: () => {},
hasPersistedPR: () => opts.hasPersisted ?? false,
openExternal: () => {},
log: () => {},
})
const onStatus = (bridge.poller as unknown as { options: { onStatus: (...a: unknown[]) => void } }).options.onStatus
return { bridge, sent, onStatus }
}
// --- error deduplication ---
describe("PRStatusBridge.notifyError", () => {
it("sends the first error notification", () => {
const { bridge, sent } = harness()
bridge.notifyError("gh_missing")
expect(sent).toHaveLength(1)
expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prError", error: "gh_missing" }))
})
it("deduplicates the same error type", () => {
const { bridge, sent } = harness()
bridge.notifyError("gh_auth")
bridge.notifyError("gh_auth")
expect(sent).toHaveLength(1)
})
it("sends again when error type changes", () => {
const { bridge, sent } = harness()
bridge.notifyError("gh_missing")
bridge.notifyError("gh_auth")
expect(sent).toHaveLength(2)
})
})
// --- onStatus cache suppression ---
describe("PRStatusBridge onStatus", () => {
it("forwards a successful status to the webview", () => {
const { sent, onStatus } = harness()
onStatus("wt1", pr)
expect(sent).toHaveLength(1)
expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1", pr }))
})
it("forwards pr:null error when no cache entry and no persisted PR", () => {
const { sent, onStatus } = harness()
onStatus("wt1", null, "fetch_failed")
expect(sent).toHaveLength(1)
expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1", pr: null }))
})
it("suppresses pr:null error when cache entry exists", () => {
const { sent, onStatus } = harness()
onStatus("wt1", pr)
sent.length = 0
onStatus("wt1", null, "fetch_failed")
expect(sent).toHaveLength(0)
})
it("suppresses pr:null error when persisted PR exists", () => {
const { sent, onStatus } = harness({ hasPersisted: true })
onStatus("wt1", null, "fetch_failed")
expect(sent).toHaveLength(0)
})
it("forwards gh_auth error even when cache entry exists", () => {
const { sent, onStatus } = harness()
onStatus("wt1", pr)
sent.length = 0
onStatus("wt1", null, "gh_auth")
const errorMsg = sent.find((m) => m.type === "agentManager.prError")
expect(errorMsg).toEqual(expect.objectContaining({ error: "gh_auth" }))
})
it("forwards gh_missing error even when cache entry exists", () => {
const { sent, onStatus } = harness()
onStatus("wt1", pr)
sent.length = 0
onStatus("wt1", null, "gh_missing")
const errorMsg = sent.find((m) => m.type === "agentManager.prError")
expect(errorMsg).toEqual(expect.objectContaining({ error: "gh_missing" }))
})
})
// --- replay ---
describe("PRStatusBridge.replay", () => {
it("replays cached status messages", () => {
const { bridge, sent, onStatus } = harness()
onStatus("wt1", pr)
sent.length = 0
bridge.replay()
expect(sent).toHaveLength(1)
expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1" }))
})
it("replays the last auth error on reconnect", () => {
const { bridge, sent, onStatus } = harness()
onStatus("wt1", null, "gh_auth")
sent.length = 0
bridge.replay()
expect(
sent.some((m) => m.type === "agentManager.prError" && (m as never as { error: string }).error === "gh_auth"),
).toBe(true)
})
it("does not replay fetch_failed errors", () => {
const { bridge, sent, onStatus } = harness()
onStatus("wt1", null, "fetch_failed")
sent.length = 0
bridge.replay()
expect(sent).toHaveLength(0)
})
})
// --- snapshot ---
describe("PRStatusBridge.snapshot", () => {
it("returns only entries with a non-null pr", () => {
const { bridge, onStatus } = harness()
onStatus("wt1", pr)
onStatus("wt2", pr)
expect(bridge.snapshot().size).toBe(2)
})
it("excludes entries where pr was null", () => {
const { bridge, onStatus } = harness({ hasPersisted: true })
onStatus("wt1", null, "fetch_failed")
expect(bridge.snapshot().size).toBe(0)
})
})
// --- remove / reset ---
describe("PRStatusBridge.remove", () => {
it("removes a cached entry so it is no longer replayed", () => {
const { bridge, sent, onStatus } = harness()
onStatus("wt1", pr)
bridge.remove("wt1")
sent.length = 0
bridge.replay()
expect(sent).toHaveLength(0)
})
})
describe("PRStatusBridge.reset", () => {
it("clears cache and error state so replay sends nothing", () => {
const { bridge, sent, onStatus } = harness()
onStatus("wt1", pr)
bridge.notifyError("gh_auth")
bridge.reset()
sent.length = 0
bridge.replay()
expect(sent).toHaveLength(0)
})
it("allows the same error to be sent again after reset", () => {
const { bridge, sent } = harness()
bridge.notifyError("gh_auth")
bridge.reset()
sent.length = 0
bridge.notifyError("gh_auth")
expect(sent).toHaveLength(1)
})
})
@@ -0,0 +1,324 @@
import { describe, expect, it } from "bun:test"
import {
parsePRResult,
checkStatus,
formatCheckDuration,
parseComments,
parseReviewers,
} from "../../src/agent-manager/am-pr-utils"
import type { GhThread, GhReviewRequest, GhReview } from "../../src/agent-manager/am-pr-types"
// --- parsePRResult ---
describe("parsePRResult", () => {
it("returns null when number is missing", () => {
expect(parsePRResult(JSON.stringify({ title: "foo" }))).toBeNull()
})
it("parses an open PR", () => {
const raw = {
number: 42,
title: "my PR",
body: "desc",
url: "https://github.com/x/y/pull/42",
state: "OPEN",
isDraft: false,
reviewDecision: null,
additions: 10,
deletions: 3,
changedFiles: 2,
}
expect(parsePRResult(JSON.stringify(raw))).toEqual({
number: 42,
title: "my PR",
body: "desc",
url: "https://github.com/x/y/pull/42",
state: "open",
review: null,
additions: 10,
deletions: 3,
files: 2,
})
})
it("maps isDraft to draft state regardless of gh state field", () => {
const raw = {
number: 1,
title: "",
body: "",
url: "",
state: "OPEN",
isDraft: true,
reviewDecision: null,
additions: 0,
deletions: 0,
changedFiles: 0,
}
expect(parsePRResult(JSON.stringify(raw))?.state).toBe("draft")
})
it("maps MERGED state", () => {
const raw = {
number: 1,
title: "",
body: "",
url: "",
state: "MERGED",
isDraft: false,
reviewDecision: null,
additions: 0,
deletions: 0,
changedFiles: 0,
}
expect(parsePRResult(JSON.stringify(raw))?.state).toBe("merged")
})
it("maps CLOSED state", () => {
const raw = {
number: 1,
title: "",
body: "",
url: "",
state: "CLOSED",
isDraft: false,
reviewDecision: null,
additions: 0,
deletions: 0,
changedFiles: 0,
}
expect(parsePRResult(JSON.stringify(raw))?.state).toBe("closed")
})
it("maps APPROVED review decision", () => {
const raw = {
number: 1,
title: "",
body: "",
url: "",
state: "OPEN",
isDraft: false,
reviewDecision: "APPROVED",
additions: 0,
deletions: 0,
changedFiles: 0,
}
expect(parsePRResult(JSON.stringify(raw))?.review).toBe("approved")
})
it("maps CHANGES_REQUESTED review decision", () => {
const raw = {
number: 1,
title: "",
body: "",
url: "",
state: "OPEN",
isDraft: false,
reviewDecision: "CHANGES_REQUESTED",
additions: 0,
deletions: 0,
changedFiles: 0,
}
expect(parsePRResult(JSON.stringify(raw))?.review).toBe("changes_requested")
})
it("maps REVIEW_REQUIRED review decision to pending", () => {
const raw = {
number: 1,
title: "",
body: "",
url: "",
state: "OPEN",
isDraft: false,
reviewDecision: "REVIEW_REQUIRED",
additions: 0,
deletions: 0,
changedFiles: 0,
}
expect(parsePRResult(JSON.stringify(raw))?.review).toBe("pending")
})
it("returns null review for unknown decision", () => {
const raw = {
number: 1,
title: "",
body: "",
url: "",
state: "OPEN",
isDraft: false,
reviewDecision: "SOMETHING_ELSE",
additions: 0,
deletions: 0,
changedFiles: 0,
}
expect(parsePRResult(JSON.stringify(raw))?.review).toBeNull()
})
it("defaults missing fields to empty strings and zeros", () => {
const result = parsePRResult(JSON.stringify({ number: 5 }))
expect(result).toEqual(
expect.objectContaining({ title: "", body: "", url: "", additions: 0, deletions: 0, files: 0 }),
)
})
})
// --- checkStatus ---
describe("checkStatus", () => {
it("maps SUCCESS", () => expect(checkStatus("SUCCESS")).toBe("success"))
it("maps FAILURE", () => expect(checkStatus("FAILURE")).toBe("failure"))
it("maps ERROR to failure", () => expect(checkStatus("ERROR")).toBe("failure"))
it("maps PENDING", () => expect(checkStatus("PENDING")).toBe("pending"))
it("maps QUEUED to pending", () => expect(checkStatus("QUEUED")).toBe("pending"))
it("maps IN_PROGRESS to pending", () => expect(checkStatus("IN_PROGRESS")).toBe("pending"))
it("maps REQUESTED to pending", () => expect(checkStatus("REQUESTED")).toBe("pending"))
it("maps WAITING to pending", () => expect(checkStatus("WAITING")).toBe("pending"))
it("maps SKIPPED", () => expect(checkStatus("SKIPPED")).toBe("skipped"))
it("maps CANCELLED", () => expect(checkStatus("CANCELLED")).toBe("cancelled"))
it("maps TIMED_OUT to cancelled", () => expect(checkStatus("TIMED_OUT")).toBe("cancelled"))
it("maps STALE to cancelled", () => expect(checkStatus("STALE")).toBe("cancelled"))
it("maps STARTUP_FAILURE to cancelled", () => expect(checkStatus("STARTUP_FAILURE")).toBe("cancelled"))
it("maps unknown state to pending", () => expect(checkStatus("WHATEVER")).toBe("pending"))
it("is case-insensitive", () => expect(checkStatus("success")).toBe("success"))
})
// --- formatCheckDuration ---
describe("formatCheckDuration", () => {
it("returns undefined when startedAt is missing", () => {
expect(formatCheckDuration(undefined, "2024-01-01T00:01:00Z")).toBeUndefined()
})
it("returns undefined when completedAt is missing", () => {
expect(formatCheckDuration("2024-01-01T00:00:00Z", undefined)).toBeUndefined()
})
it("formats sub-minute durations in seconds", () => {
expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:00:45Z")).toBe("45s")
})
it("formats durations over a minute as m/s", () => {
expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:02:30Z")).toBe("2m 30s")
})
it("formats exactly 60 seconds as 1m 0s", () => {
expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:01:00Z")).toBe("1m 0s")
})
})
// --- parseComments ---
describe("parseComments", () => {
it("returns empty array for empty threads", () => {
expect(parseComments([])).toEqual([])
})
it("skips threads with no comments", () => {
const threads: GhThread[] = [{ isResolved: false, comments: { nodes: [] } }]
expect(parseComments(threads)).toHaveLength(0)
})
it("parses a resolved thread", () => {
const threads: GhThread[] = [
{
isResolved: true,
comments: {
nodes: [
{
id: "c1",
author: { login: "alice", avatarUrl: "https://avatar" },
body: "looks good",
path: "src/foo.ts",
line: 10,
url: "https://url",
createdAt: "2024-01-01T00:00:00Z",
},
],
},
},
]
expect(parseComments(threads)).toEqual([
{
id: "c1",
author: "alice",
avatar: "https://avatar",
body: "looks good",
file: "src/foo.ts",
line: 10,
url: "https://url",
resolved: true,
createdAt: new Date("2024-01-01T00:00:00Z").getTime(),
},
])
})
it("defaults missing author to 'unknown'", () => {
const threads: GhThread[] = [{ isResolved: false, comments: { nodes: [{ id: "c2", body: "note" }] } }]
expect(parseComments(threads)[0]?.author).toBe("unknown")
})
it("only uses the first comment of each thread", () => {
const threads: GhThread[] = [
{
isResolved: false,
comments: {
nodes: [
{ id: "first", body: "first comment" },
{ id: "second", body: "second comment" },
],
},
},
]
const result = parseComments(threads)
expect(result).toHaveLength(1)
expect(result[0]?.id).toBe("first")
})
})
// --- parseReviewers ---
describe("parseReviewers", () => {
it("returns empty array with no requests or reviews", () => {
expect(parseReviewers([], [])).toEqual([])
})
it("adds pending reviewer from request", () => {
const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice", avatarUrl: "https://avatar" } }]
expect(parseReviewers(requests, [])).toEqual([{ login: "alice", avatar: "https://avatar", state: "pending" }])
})
it("skips review requests without a login", () => {
const requests: GhReviewRequest[] = [{ requestedReviewer: {} }]
expect(parseReviewers(requests, [])).toHaveLength(0)
})
it("adds reviewer from review when not in requests", () => {
const reviews: GhReview[] = [{ author: { login: "bob" }, state: "APPROVED" }]
expect(parseReviewers([], reviews)).toEqual([{ login: "bob", avatar: undefined, state: "approved" }])
})
it("upgrades pending request to approved when review arrives", () => {
const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }]
const reviews: GhReview[] = [{ author: { login: "alice" }, state: "APPROVED" }]
expect(parseReviewers(requests, reviews)).toEqual([{ login: "alice", avatar: undefined, state: "approved" }])
})
it("does not downgrade approved to commented", () => {
const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }]
const reviews: GhReview[] = [
{ author: { login: "alice" }, state: "APPROVED" },
{ author: { login: "alice" }, state: "COMMENTED" },
]
expect(parseReviewers(requests, reviews)[0]?.state).toBe("approved")
})
it("does upgrade pending to changes_requested", () => {
const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }]
const reviews: GhReview[] = [{ author: { login: "alice" }, state: "CHANGES_REQUESTED" }]
expect(parseReviewers(requests, reviews)[0]?.state).toBe("changes_requested")
})
it("skips reviews without a login", () => {
const reviews: GhReview[] = [{ author: {}, state: "APPROVED" }]
expect(parseReviewers([], reviews)).toHaveLength(0)
})
})
@@ -36,6 +36,7 @@ import type {
RunStatus,
PRStatus,
AgentManagerPRStatusMessage,
AgentManagerPRErrorMessage,
AgentManagerProjectsMessage,
AgentProjectSnapshot,
ManagedSessionState,
@@ -143,6 +144,7 @@ import {
import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering"
import { useTabScroll } from "./tab-scroll"
import { DiffPanel } from "./DiffPanel"
import { PRPanel } from "./pr/PRPanel"
import { createRevertFile } from "./revert-file"
import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView"
import { createApplyToLocal } from "./apply-to-local"
@@ -152,6 +154,8 @@ import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review
import type { SidebarSearchMenuRef } from "./SidebarSearchMenu"
import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search"
import { randomColor } from "./section-colors"
import { createMarkdownRender } from "./review-preferences"
import { createSidebarCollapse } from "./sidebar-collapse"
import { createNewTaskDrafts } from "./new-task-drafts"
import {
buildTopLevelItems,
@@ -170,11 +174,9 @@ import { DiffScopeControls } from "../diff-viewer/DiffScopeControls"
import { scopeCapabilities } from "./diff-scope-state"
import { createDiffReviewScope } from "./diff-review-scope"
import { initialMessage, seedInitialVariant } from "./initial-message"
import { createMarkdownRender } from "./review-preferences"
import { createSidebarCollapse } from "./sidebar-collapse"
import { SidebarToggleButton } from "./SidebarToggleButton"
import { setTabWidths } from "./tab-widths"
import { clampPanelWidth, maxPanelWidth, minPanelWidth } from "./side-panel-layout"
import { clampPanelWidth, maxPanelWidth, minPanelWidth, SidePanel } from "./side-panel-layout"
import { buildShortcutCategories } from "./shortcuts"
import { tracker } from "./telemetry"
import { createChatFocus, hasQuestionOption } from "./focus"
@@ -195,7 +197,7 @@ interface SetupState {
/** Sidebar selection: LOCAL for local repo, worktree ID for a worktree, or null for an unassigned session. */
type SidebarSelection = typeof LOCAL | string | null
type SidePanel = "diff" | "pr" | "terminal" | null
export type SidePanelState = SidePanel | null
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const SIDE_RESIZE_INTERVAL_MS = 32
@@ -297,8 +299,16 @@ const AgentManagerContent: Component = () => {
let sideResizeTime = 0
const [history, setHistory] = createSignal(false)
const [sidePanel, setSidePanel] = createSignal<SidePanel>(null)
const diffOpen = () => sidePanel() === "diff"
const [sidePanel, setSidePanel] = createSignal<SidePanelState>(null)
const diffOpen = () => sidePanel() === SidePanel.Diff
const prOpen = () => sidePanel() === SidePanel.PR
const activePR = createMemo(() => {
const selected = selection()
if (!selected || selected === LOCAL) return undefined
const pr = prStatuses()[selected]
if (!pr) return undefined
return { pr, selected, wt: worktrees().find((w) => w.id === selected) }
})
const diffs = createWorktreeDiffs(vscode)
const diffDatas = diffs.diffDatas
const diffLoading = diffs.diffLoading
@@ -324,7 +334,7 @@ const AgentManagerContent: Component = () => {
const showSideTerminal = () => {
setHistory(false)
setReviewActive(false)
setSidePanel("terminal")
setSidePanel(SidePanel.Terminal)
}
const [reviewOpenByContext, setReviewOpenByContext] = createSignal<Record<string, boolean>>({})
@@ -336,7 +346,6 @@ const AgentManagerContent: Component = () => {
// Per-worktree git stats (diff additions/deletions, commits missing from origin)
const worktreeStats = () => registry.active().worktreeStats()
// Per-worktree PR status data
const prStatuses = () => registry.active().prStatuses()
const runStatuses = () => registry.active().runStatuses()
@@ -421,7 +430,7 @@ const AgentManagerContent: Component = () => {
const rememberPromptFocus = (focused: boolean) => {
if (focused) focusMemory.set(focusKey(), "prompt")
}
const terminalVisible = () => sidePanel() === "terminal" && !history() && !reviewActive()
const terminalVisible = () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive()
const focusOnDraftChange = () => {
const key = focusKey()
const owner = focusMemory.get(key)
@@ -490,6 +499,15 @@ const AgentManagerContent: Component = () => {
}
createEffect(on(selection, () => cancelPendingDelete(), { defer: true }))
createEffect(on(selection, () => clearReviewComposer(reviewComposer), { defer: true }))
createEffect(
on(
selection,
() => {
if (!activePR()) setSidePanel((p) => (p === SidePanel.PR ? null : p))
},
{ defer: true },
),
)
onCleanup(() => clearTimeout(pendingDeleteTimer))
// Per-context tab memory lives in the active project's store: maps sidebar
@@ -547,11 +565,17 @@ const AgentManagerContent: Component = () => {
}
const openWindow = metrics.click("open_worktree_window", "tab_toolbar", openWorktreeDirectory)
const togglePRPanel = () => {
setHistory(false)
if (reviewActive()) closeReviewTab()
setSidePanel((prev) => (prev === SidePanel.PR ? null : SidePanel.PR))
}
const openSelectedPR = () => {
const sel = selection()
if (!sel || sel === LOCAL || !prStatuses()[sel]) return
metrics.track("open_pull_request", "keyboard_shortcut")
vscode.postMessage({ type: "agentManager.openPR", worktreeId: sel })
togglePRPanel()
}
const runWorktree = (id: string, destination: TerminalDestination) => {
@@ -1183,8 +1207,8 @@ const AgentManagerContent: Component = () => {
} else if (msg.action === "toggleDiff") {
if (reviewActive()) {
closeReviewTab()
setSidePanel("diff")
} else setSidePanel((prev) => (prev === "diff" ? null : "diff"))
setSidePanel(SidePanel.Diff)
} else setSidePanel((prev) => (prev === SidePanel.Diff ? null : SidePanel.Diff))
} else if (msg.action === "newTab") handleNewTabForCurrentSelection()
else if (msg.action === "closeTab") closeActiveTab()
else if (msg.action === "newWorktree") showNewWorktreeDialog()
@@ -1345,6 +1369,7 @@ const AgentManagerContent: Component = () => {
showToast({ variant: "error", title: t("agentManager.terminal.errorTitle"), description: message }),
postMessage: (message) => vscode.postMessage(message as never),
onCreated: (contextKey, terminalId) => appendToTabOrder(contextKey, terminalId),
onSideClosed: (_contextKey, terminalId) => forgetTerminalFocus(terminalId),
onScriptRunning: (contextKey, terminalId) => {
if (terms.sideKey() !== contextKey) return
@@ -1412,7 +1437,7 @@ const AgentManagerContent: Component = () => {
if (!isActivePayload(ev.projectId)) return
// Close diff/review panels — nothing to show during setup.
// Terminal panels keep live setup output, so they stay open.
if (sidePanel() === "diff") setSidePanel(null)
if (sidePanel() === SidePanel.Diff) setSidePanel(null)
setReviewActive(false)
setSetup({ active: true, message: ev.message, branch: ev.branch, worktreeId: ev.worktreeId })
}
@@ -1562,6 +1587,15 @@ const AgentManagerContent: Component = () => {
managedSession: focusManagedSession,
})
if (msg.type === "agentManager.prError") {
const ev = msg as AgentManagerPRErrorMessage
showToast({
variant: "error",
title: t(`agentManager.pr.error.${ev.error}.title`),
description: t(`agentManager.pr.error.${ev.error}.description`),
})
}
if (projectLive.apply(msg)) return
})
@@ -2011,7 +2045,7 @@ const AgentManagerContent: Component = () => {
const sideCtl = createSideTerminal({
handlers: termHandlers,
visible: () => sidePanel() === "terminal" && !history() && !reviewActive(),
visible: () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive(),
focusedId: () => terms.sideFocusedId(),
count: () => terms.sidesForContext(terms.sideKey()).length,
isScript: terms.isScript,
@@ -2144,7 +2178,7 @@ const AgentManagerContent: Component = () => {
const closeActiveTab = () => {
// A focused side terminal owns Cmd+W while its panel is visible.
// Closing a chat tab out from under the user's cursor would be surprising.
if (sidePanel() === "terminal" && terms.sideFocusedId()) {
if (sidePanel() === SidePanel.Terminal && terms.sideFocusedId()) {
if (sideCtl.close()) return
}
if (termHandlers.closeFocused()) {
@@ -2200,10 +2234,10 @@ const AgentManagerContent: Component = () => {
})
if (reviewActive()) {
closeReviewTab()
setSidePanel("diff")
setSidePanel(SidePanel.Diff)
return
}
setSidePanel((prev) => (prev === "diff" ? null : "diff"))
setSidePanel((prev) => (prev === SidePanel.Diff ? null : SidePanel.Diff))
}
const renderTabById = (id: string) =>
@@ -2399,8 +2433,11 @@ const AgentManagerContent: Component = () => {
reviewActive={reviewActive}
onToggleDiff={toggleDiffPanel}
onToggleReview={metrics.click("fullscreen_review", "tab_toolbar", toggleReviewTab)}
prStatus={() => activePR()?.pr}
prOpen={prOpen}
onTogglePR={togglePRPanel}
terminalDestination={sideCtl.destination}
terminalDestinationActive={() => sidePanel() === "terminal"}
terminalDestinationActive={() => sidePanel() === SidePanel.Terminal}
terminalKeybind={() => kb().showTerminal ?? ""}
onTerminalDestinationOpen={() => {
cancelAmbientSetup()
@@ -2588,7 +2625,7 @@ const AgentManagerContent: Component = () => {
/>
</Show>
<div class="am-diff-panel-wrapper">
<Show when={sidePanel() === "diff"}>
<Show when={sidePanel() === SidePanel.Diff}>
<DiffPanel
diffs={reviewDiffs()}
loading={diffLoading()}
@@ -2623,10 +2660,29 @@ const AgentManagerContent: Component = () => {
activeTerminalId={terms.activeId()}
/>
</Show>
<Show when={sidePanel() === SidePanel.PR && activePR()}>
{(() => {
const data = activePR()!
return (
<PRPanel
pr={data.pr}
worktree={data.wt}
onClose={() => setSidePanel(null)}
onOpenExternal={() =>
vscode.postMessage({
type: "agentManager.openPR",
worktreeId: data.selected,
url: data.pr.url,
})
}
/>
)
})()}
</Show>
<SideTerminalPanel
state={terms}
contextKey={terms.sideKey}
visible={() => sidePanel() === "terminal"}
visible={() => sidePanel() === SidePanel.Terminal}
nextKeybind={kb().nextTerminal ?? ""}
closeKeybind={kb().closeTab ?? ""}
onFocusPrompt={focusPrompt}
@@ -13,7 +13,7 @@ import {
closestCenter,
type DragEvent,
} from "@thisbeyond/solid-dnd"
import type { LocalGitStats, RunStatus, WorktreeGitStats } from "../src/types/messages"
import type { LocalGitStats, RunStatus, WorktreeGitStats, PRStatus } from "../src/types/messages"
import type { LanguageContextValue } from "../src/context/language"
import { LOCAL } from "./navigate"
import { ConstrainDragYAxis } from "../src/components/chat/TabDnd"
@@ -55,6 +55,9 @@ export interface TabBarProps {
reviewActive: () => boolean
onToggleDiff: () => void
onToggleReview: () => void
prStatus: () => PRStatus | undefined
prOpen: () => boolean
onTogglePR: () => void
terminalDestination: () => TerminalDestination
terminalDestinationActive: () => boolean
terminalKeybind: () => string
@@ -215,6 +218,20 @@ export const TabBar: Component<TabBarProps> = (props) => (
)
})()}
</Show>
<Show when={props.prStatus()}>
{(pr) => (
<Tooltip value={`PR #${pr().number}`} placement="bottom">
<IconButton
icon="pull-request"
size="small"
variant="ghost"
label={`PR #${pr().number}`}
class={props.prOpen() ? "am-tab-diff-btn-active" : ""}
onClick={props.onTogglePR}
/>
</Tooltip>
)}
</Show>
<TooltipKeybind
title={props.t("agentManager.diff.toggle")}
keybind={props.bindings().toggleDiff ?? ""}
@@ -226,14 +243,19 @@ export const TabBar: Component<TabBarProps> = (props) => (
title={props.t("agentManager.diff.toggle")}
>
<Icon name="layers" size="small" />
<Show when={hasChanges()}>
<span class="am-diff-toggle-stats">
<Show when={stats()!.files > 0}>
<span class="am-stat-files">{stats()!.files}f</span>
<Show when={props.prStatus()}>
{(pr) => (
<Show when={pr().additions > 0 || pr().deletions > 0}>
<span class="am-diff-toggle-stats">
<Show when={pr().additions > 0}>
<span class="am-stat-additions">+{pr().additions}</span>
</Show>
<Show when={pr().deletions > 0}>
<span class="am-stat-deletions">−{pr().deletions}</span>
</Show>
</span>
</Show>
<span class="am-stat-additions">+{stats()!.additions}</span>
<span class="am-stat-deletions">−{stats()!.deletions}</span>
</span>
)}
</Show>
</button>
</TooltipKeybind>
@@ -10,7 +10,8 @@ import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
import { HoverCard } from "@kilocode/kilo-ui/hover-card"
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
import { Button } from "@kilocode/kilo-ui/button"
import type { WorktreeState, WorktreeGitStats, PRStatus, SectionState, RunStatus } from "../src/types/messages"
import type { WorktreeState, WorktreeGitStats, SectionState, RunStatus } from "../src/types/messages"
import type { PRStatus } from "../src/types/messages"
import { colorCss } from "./section-colors"
import { useLanguage } from "../src/context/language"
import { formatRelativeDate } from "../src/utils/date"
@@ -57,7 +58,7 @@ interface WorktreeItemProps {
/** Keybinding string for the open-in-vscode action. */
openKeybind: string
/** PR status for this worktree's branch, or null if no PR. */
pr?: PRStatus | null
pr?: PRStatus
runStatus?: RunStatus
/** Callback when the PR badge is clicked. */
onOpenPR?: () => void
@@ -94,14 +95,6 @@ const hasStats = (s: WorktreeGitStats | undefined): s is WorktreeGitStats =>
* and review results are conveyed by a separate status icon (see prBadgeIndicator)
* so a failing check is not mistaken for a closed PR.
*/
export function prAccentColor(pr: PRStatus): string {
if (pr.state === "draft") return "var(--text-weaker)"
if (pr.state === "merged") return "#a78bfa"
if (pr.state === "closed") return "#f87171"
if (pr.checks.status === "pending") return "#fbbf24"
return "#34d399"
}
/** True while an open PR's checks are still running — drives the pulsing amber badge. */
export function prChecksRunning(pr: PRStatus): boolean {
return pr.state === "open" && pr.checks.status === "pending"
@@ -349,18 +342,30 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
}
>
{(pr) => {
const accent = () => prAccentColor(pr())
const indicator = () => prBadgeIndicator(pr())
return (
<span
class="am-pr-badge"
style={{ "--pr-accent": accent() }}
data-pending={prChecksRunning(pr()) ? "" : undefined}
classList={{
"am-pr-accent-draft": pr().state === "draft",
"am-pr-accent-merged": pr().state === "merged",
"am-pr-accent-closed": pr().state === "closed",
"am-pr-accent-pending": pr().state === "open" && pr().checks.status === "pending",
"am-pr-accent-open": pr().state === "open" && pr().checks.status !== "pending",
"am-pr-badge-pending": prChecksRunning(pr()),
}}
onClick={handleOpenPR}
>
<Switch fallback={<Icon name="branch" size="small" />}>
<Icon name="pull-request" size="small" />
<span class="am-pr-badge-number">#{pr().number}</span>
<Switch>
<Match when={indicator() === "failure"}>
<Icon name="circle-x" size="small" class="am-pr-badge-status" data-status="failure" />
<Icon
name="circle-x-outline"
size="small"
class="am-pr-badge-status"
data-status="failure"
/>
</Match>
<Match when={indicator() === "changes"}>
<Icon name="warning" size="small" class="am-pr-badge-status" data-status="changes" />
@@ -374,7 +379,6 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
/>
</Match>
</Switch>
<span class="am-pr-badge-number">#{pr().number}</span>
</span>
)
}}
@@ -840,7 +840,7 @@ html[data-theme="kilo-vscode"]
}
.am-run-badge[data-run-state="stopping"] {
--am-run-accent: #fbbf24;
--am-run-accent: var(--vscode-charts-yellow, #fbbf24);
}
.am-run-badge[data-run-state="success"] {
@@ -848,7 +848,7 @@ html[data-theme="kilo-vscode"]
}
.am-run-badge[data-run-state="error"] {
--am-run-accent: #f87171;
--am-run-accent: var(--vscode-testing-iconFailed, #f87171);
}
@keyframes am-run-pulse {
@@ -898,24 +898,44 @@ html[data-theme="kilo-vscode"]
}
.am-stat-additions {
color: #34d399;
color: var(--vscode-testing-iconPassed, #34d399);
}
.am-stat-deletions {
color: #f87171;
color: var(--vscode-testing-iconFailed, #f87171);
}
/* PR badge accent colors via classList */
.am-pr-accent-open,
.am-pr-panel-badge.am-pr-accent-open {
--am-pr-accent: var(--vscode-testing-iconPassed, #34d399);
}
.am-pr-accent-pending,
.am-pr-panel-badge.am-pr-accent-pending {
--am-pr-accent: var(--vscode-charts-yellow, #fbbf24);
}
.am-pr-accent-draft,
.am-pr-panel-badge.am-pr-accent-draft {
--am-pr-accent: var(--text-weaker);
}
.am-pr-accent-merged,
.am-pr-panel-badge.am-pr-accent-merged {
--am-pr-accent: var(--vscode-charts-purple, var(--vscode-foreground));
}
.am-pr-accent-closed,
.am-pr-panel-badge.am-pr-accent-closed {
--am-pr-accent: var(--vscode-testing-iconFailed, #f87171);
}
/* PR pill badge — second row inside am-wt-row2.
Uses --pr-accent custom property (set inline) to derive all colors. */
.am-pr-badge {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px 7px 2px 4px;
padding: 2px 4px;
border-radius: 6px;
border: none;
background: color-mix(in srgb, var(--pr-accent) 12%, transparent);
color: var(--pr-accent);
background: color-mix(in srgb, var(--am-pr-accent) 12%, transparent);
color: var(--am-pr-accent);
cursor: pointer;
font-size: var(--kilo-font-size-11);
font-weight: 500;
@@ -924,34 +944,34 @@ html[data-theme="kilo-vscode"]
margin-left: auto;
}
.am-pr-badge:hover {
background: color-mix(in srgb, var(--pr-accent) 25%, transparent);
background: color-mix(in srgb, var(--am-pr-accent) 25%, transparent);
}
.am-pr-badge [data-component="icon"] {
width: 12px;
height: 12px;
color: var(--pr-accent);
color: var(--am-pr-accent);
}
.am-pr-badge-number {
line-height: 1;
color: var(--text-muted);
}
.am-pr-badge:hover .am-pr-badge-number {
color: var(--pr-accent);
color: var(--am-pr-accent);
}
/* Status icon overlaid on the badge — colored by CI/review result, independent
of the badge's PR-state accent color. */
.am-pr-badge-status[data-status="failure"] {
color: #ef4444;
color: var(--vscode-testing-iconFailed, #f87171);
}
.am-pr-badge-status[data-status="changes"] {
color: #fbbf24;
color: var(--vscode-charts-yellow, #fbbf24);
}
.am-pr-badge-status[data-status="approved"] {
color: #34d399;
color: var(--vscode-testing-iconPassed, #34d399);
}
/* Checks still running — pulse the whole amber badge. */
.am-pr-badge[data-pending] {
.am-pr-badge.am-pr-badge-pending {
animation: am-pr-pulse 1.5s ease-in-out infinite;
}
@keyframes am-pr-pulse {
@@ -965,17 +985,17 @@ html[data-theme="kilo-vscode"]
}
@media (prefers-reduced-motion: reduce) {
.am-pr-badge[data-pending] {
.am-pr-badge.am-pr-badge-pending {
animation: none;
opacity: 0.9;
}
}
.am-worktree-item-active .am-pr-badge {
background: color-mix(in srgb, var(--pr-accent) 18%, transparent);
background: color-mix(in srgb, var(--am-pr-accent) 18%, transparent);
}
.am-worktree-item-active .am-pr-badge:hover {
background: color-mix(in srgb, var(--pr-accent) 30%, transparent);
background: color-mix(in srgb, var(--am-pr-accent) 30%, transparent);
}
/* Clickable link icon in hover card to open PR on GitHub */
@@ -996,35 +1016,35 @@ html[data-theme="kilo-vscode"]
}
.am-worktree-item-active .am-stat-files {
color: rgba(255, 255, 255, 0.7);
color: var(--text-weak);
}
.am-worktree-item-active .am-stat-additions {
color: #6ee7b7;
color: var(--vscode-testing-iconPassed, #6ee7b7);
}
.am-worktree-item-active .am-stat-deletions {
color: #fca5a5;
color: var(--vscode-testing-iconFailed, #fca5a5);
}
.am-worktree-commits {
color: #34d399;
color: var(--vscode-testing-iconPassed, #34d399);
}
.am-worktree-behind {
color: #f87171;
color: var(--vscode-testing-iconFailed, #f87171);
}
.am-worktree-item-active .am-worktree-commits {
color: #6ee7b7;
color: var(--vscode-testing-iconPassed, #6ee7b7);
}
.am-worktree-item-active .am-worktree-behind {
color: #fca5a5;
color: var(--vscode-testing-iconFailed, #fca5a5);
}
.am-local-item-active .am-worktree-behind {
color: #fca5a5;
color: var(--vscode-testing-iconFailed, #fca5a5);
}
.am-local-item:hover .am-worktree-stats,
@@ -1699,7 +1719,7 @@ body.am-wt-dragging-active * {
}
.am-sidebar-search-status {
color: #fbbf24;
color: var(--vscode-charts-yellow, #fbbf24);
text-transform: uppercase;
}
@@ -1794,9 +1814,10 @@ body.am-wt-dragging-active * {
color: var(--text-base);
}
.am-diff-toggle-btn.am-tab-diff-btn-active {
background: var(--surface-interactive-base) !important;
color: var(--text-base) !important;
.am-diff-toggle-btn.am-tab-diff-btn-active,
.am-diff-toggle-btn.am-tab-diff-btn-active:hover {
background: var(--surface-interactive-base);
color: var(--text-on-interactive-base);
}
.am-diff-toggle-btn.am-diff-toggle-has-changes {
@@ -1810,20 +1831,24 @@ body.am-wt-dragging-active * {
}
.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-files {
color: rgba(255, 255, 255, 0.7);
color: var(--text-weak);
}
.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-additions {
color: #6ee7b7;
color: var(--vscode-testing-iconPassed, #6ee7b7);
}
.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-deletions {
color: #fca5a5;
color: var(--vscode-testing-iconFailed, #fca5a5);
}
.am-tab-diff-btn-active {
background: var(--surface-interactive-base) !important;
color: var(--text-base) !important;
color: var(--text-on-interactive-base) !important;
}
.am-tab-diff-btn-active [data-slot="icon-svg"] {
color: var(--text-on-interactive-base) !important;
}
.am-detail-content {
@@ -2947,9 +2972,10 @@ body.am-wt-dragging-active * {
color: var(--text-base);
}
.am-nv-pill-active {
background: var(--border-focus, #007fd4) !important;
color: #fff !important;
.am-nv-pill-active,
.am-nv-pill-active:hover {
background: var(--border-focus, #007fd4);
color: var(--text-on-interactive-base);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
+8 -3
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Worktree جديد",
"agentManager.worktree.setupScript": "سكربت إعداد Worktree",
"agentManager.worktree.delete": "حذف Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "حذف؟",
"agentManager.worktree.stale": "قديم",
"agentManager.worktree.staleTooltip": "مفقود على القرص أو لم يعد git worktree يتتبعه",
"agentManager.worktree.removeStale": "إزالة Worktree القديم",
@@ -104,8 +104,8 @@ export const dict = {
"agentManager.run.options": "خيارات التشغيل",
"agentManager.run.configure": "تكوين سكربت التشغيل",
"agentManager.shortcuts.openAgentManager": "فتح Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "وضع الوكيل التالي",
"agentManager.shortcuts.cyclePreviousAgentMode": "وضع الوكيل السابق",
"agentManager.shortcuts.showShortcuts": "إظهار اختصارات لوحة المفاتيح",
"agentManager.dialog.removeStaleWorktree.title": "إزالة Worktree القديم",
"agentManager.dialog.removeStaleWorktree.messagePre": "إزالة Worktree القديم ",
@@ -211,4 +211,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "لم يتم العثور على فروع.",
"agentManager.import.noBranchesHint": "الصق رابط PR أعلاه أو أنشئ Worktree جديدًا.",
"agentManager.import.failed": "فشل الاستيراد",
"agentManager.pr.error.gh_auth.title": "مصادقة GitHub مطلوبة",
"agentManager.pr.error.gh_auth.description": "شغّل 'gh auth login' في الطرفية لاستعادة حالة PR.",
"agentManager.pr.error.gh_missing.title": "لم يتم تثبيت GitHub CLI",
"agentManager.pr.error.gh_missing.description": "قم بتثبيت GitHub CLI لتفعيل حالة PR.",
}
+8 -3
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Novo Worktree",
"agentManager.worktree.setupScript": "Script de configuração do Worktree",
"agentManager.worktree.delete": "Excluir Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "Excluir?",
"agentManager.worktree.stale": "Obsoleto",
"agentManager.worktree.staleTooltip": "Ausente no disco ou não é mais rastreado pelo git worktree",
"agentManager.worktree.removeStale": "Remover Worktree obsoleto",
@@ -106,8 +106,8 @@ export const dict = {
"agentManager.run.options": "Opções de execução",
"agentManager.run.configure": "Configurar script de execução",
"agentManager.shortcuts.openAgentManager": "Abrir Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Próximo modo de agente",
"agentManager.shortcuts.cyclePreviousAgentMode": "Modo de agente anterior",
"agentManager.shortcuts.showShortcuts": "Mostrar atalhos de teclado",
"agentManager.dialog.removeStaleWorktree.title": "Remover Worktree obsoleto",
"agentManager.dialog.removeStaleWorktree.messagePre": "Remover Worktree obsoleto ",
@@ -215,4 +215,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "Nenhum branch encontrado.",
"agentManager.import.noBranchesHint": "Cole uma URL de PR acima ou crie um novo Worktree.",
"agentManager.import.failed": "Falha na importação",
"agentManager.pr.error.gh_auth.title": "Autenticação do GitHub necessária",
"agentManager.pr.error.gh_auth.description": "Execute 'gh auth login' no terminal para restaurar o status do PR.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI não instalado",
"agentManager.pr.error.gh_missing.description": "Instale o GitHub CLI para habilitar o status do PR.",
}
+9 -4
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Novi Worktree",
"agentManager.worktree.setupScript": "Skripta za postavljanje Worktree-a",
"agentManager.worktree.delete": "Obriši Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "Izbrisati?",
"agentManager.worktree.stale": "Zastario",
"agentManager.worktree.staleTooltip": "Nedostaje na disku ili ga git worktree više ne prati",
"agentManager.worktree.removeStale": "Ukloni zastarjeli Worktree",
@@ -106,8 +106,8 @@ export const dict = {
"agentManager.run.options": "Opcije pokretanja",
"agentManager.run.configure": "Konfiguriši skriptu za pokretanje",
"agentManager.shortcuts.openAgentManager": "Otvori Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Sljedeći način rada agenta",
"agentManager.shortcuts.cyclePreviousAgentMode": "Prethodni način rada agenta",
"agentManager.shortcuts.showShortcuts": "Prikaži prečice na tastaturi",
"agentManager.dialog.removeStaleWorktree.title": "Ukloni zastarjeli Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Ukloni zastarjeli Worktree ",
@@ -203,7 +203,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "Pregled slike nije dostupan za ovaj snimak sesije.",
"agentManager.review.endOfLongDiff": "Došli ste do kraja!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "Zahtjev za povlačenje",
"agentManager.import.pastePrUrl": "Zalijepite PR URL...",
"agentManager.import.open": "Otvori",
"agentManager.import.branches": "Branchevi",
@@ -214,4 +214,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "Nisu pronađeni branchevi.",
"agentManager.import.noBranchesHint": "Zalijepite PR URL iznad ili kreirajte novi Worktree.",
"agentManager.import.failed": "Uvoz neuspješan",
"agentManager.pr.error.gh_auth.title": "Potrebna GitHub autentikacija",
"agentManager.pr.error.gh_auth.description": "Pokrenite 'gh auth login' u terminalu da vratite status PR-a.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI nije instaliran",
"agentManager.pr.error.gh_missing.description": "Instalirajte GitHub CLI da omogućite status PR-a.",
}
+10 -5
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Nyt Worktree",
"agentManager.worktree.setupScript": "Worktree-opsætningsscript",
"agentManager.worktree.delete": "Slet Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "Slet?",
"agentManager.worktree.stale": "Forældet",
"agentManager.worktree.staleTooltip": "Mangler på disken eller spores ikke længere af git worktree",
"agentManager.worktree.removeStale": "Fjern forældet Worktree",
@@ -107,8 +107,8 @@ export const dict = {
"agentManager.run.options": "Kørselsindstillinger",
"agentManager.run.configure": "Konfigurer kørselsscript",
"agentManager.shortcuts.openAgentManager": "Åbn Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Næste agenttilstand",
"agentManager.shortcuts.cyclePreviousAgentMode": "Forrige agenttilstand",
"agentManager.shortcuts.showShortcuts": "Vis tastaturgenveje",
"agentManager.dialog.removeStaleWorktree.title": "Fjern forældet Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Fjern forældet Worktree ",
@@ -205,10 +205,10 @@ export const dict = {
"Forhåndsvisning af billedet er ikke tilgængelig for dette snapshot af sessionen.",
"agentManager.review.endOfLongDiff": "Du nåede slutningen!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "Pull request",
"agentManager.import.pastePrUrl": "Indsæt PR URL...",
"agentManager.import.open": "Åbn",
"agentManager.import.branches": "Branches",
"agentManager.import.branches": "Grene",
"agentManager.import.selectBranch": "Vælg branch...",
"agentManager.import.loading": "Indlæser...",
"agentManager.import.loadingBranches": "Indlæser branches...",
@@ -216,4 +216,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "Ingen branches fundet.",
"agentManager.import.noBranchesHint": "Indsæt en PR URL ovenfor eller opret et nyt Worktree.",
"agentManager.import.failed": "Import mislykkedes",
"agentManager.pr.error.gh_auth.title": "GitHub-godkendelse påkrævet",
"agentManager.pr.error.gh_auth.description": "Kør 'gh auth login' i din terminal for at gendanne PR-status.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI er ikke installeret",
"agentManager.pr.error.gh_missing.description": "Installer GitHub CLI for at aktivere PR-status.",
}
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Neuer Worktree",
"agentManager.worktree.setupScript": "Worktree-Einrichtungsskript",
"agentManager.worktree.delete": "Worktree löschen",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "Löschen?",
"agentManager.worktree.stale": "Veraltet",
"agentManager.worktree.staleTooltip": "Fehlt auf der Festplatte oder wird nicht mehr von git worktree verfolgt",
"agentManager.worktree.removeStale": "Veralteten Worktree entfernen",
@@ -108,8 +108,8 @@ export const dict = {
"agentManager.run.options": "Ausführungsoptionen",
"agentManager.run.configure": "Ausführungsskript konfigurieren",
"agentManager.shortcuts.openAgentManager": "Agent Manager öffnen",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Nächster Agentenmodus",
"agentManager.shortcuts.cyclePreviousAgentMode": "Vorheriger Agentenmodus",
"agentManager.shortcuts.showShortcuts": "Tastenkürzel anzeigen",
"agentManager.dialog.removeStaleWorktree.title": "Veralteten Worktree entfernen",
"agentManager.dialog.removeStaleWorktree.messagePre": "Veralteten Worktree entfernen ",
@@ -208,7 +208,7 @@ export const dict = {
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pastePrUrl": "PR-URL einfügen...",
"agentManager.import.open": "Öffnen",
"agentManager.import.branches": "Branches",
"agentManager.import.branches": "Zweige",
"agentManager.import.selectBranch": "Branch auswählen...",
"agentManager.import.loading": "Laden...",
"agentManager.import.loadingBranches": "Branches werden geladen...",
@@ -216,4 +216,10 @@ export const dict = {
"agentManager.import.noBranchesFound": "Keine Branches gefunden.",
"agentManager.import.noBranchesHint": "Fügen Sie oben eine PR-URL ein oder erstellen Sie einen neuen Worktree.",
"agentManager.import.failed": "Import fehlgeschlagen",
"agentManager.pr.error.gh_auth.title": "GitHub-Authentifizierung erforderlich",
"agentManager.pr.error.gh_auth.description":
"Führen Sie 'gh auth login' in Ihrem Terminal aus, um den PR-Status wiederherzustellen.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI nicht installiert",
"agentManager.pr.error.gh_missing.description": "Installieren Sie die GitHub CLI, um den PR-Status zu aktivieren.",
}
@@ -217,4 +217,8 @@ export const dict = {
"agentManager.import.noBranchesFound": "No branches found.",
"agentManager.import.noBranchesHint": "Paste a PR URL above or create a new worktree.",
"agentManager.import.failed": "Import failed",
"agentManager.pr.error.gh_auth.title": "GitHub authentication required",
"agentManager.pr.error.gh_auth.description": "Run 'gh auth login' in your terminal to restore PR status.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI not installed",
"agentManager.pr.error.gh_missing.description": "Install the GitHub CLI to enable PR status.",
}
+10 -4
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Nuevo Worktree",
"agentManager.worktree.setupScript": "Script de configuración de Worktree",
"agentManager.worktree.delete": "Eliminar Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "¿Eliminar?",
"agentManager.worktree.stale": "Obsoleto",
"agentManager.worktree.staleTooltip": "No está en disco o ya no está seguido por git worktree",
"agentManager.worktree.removeStale": "Eliminar Worktree obsoleto",
@@ -107,8 +107,8 @@ export const dict = {
"agentManager.run.options": "Opciones de ejecución",
"agentManager.run.configure": "Configurar script de ejecución",
"agentManager.shortcuts.openAgentManager": "Abrir Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Siguiente modo de agente",
"agentManager.shortcuts.cyclePreviousAgentMode": "Modo de agente anterior",
"agentManager.shortcuts.showShortcuts": "Mostrar atajos de teclado",
"agentManager.dialog.removeStaleWorktree.title": "Eliminar Worktree obsoleto",
"agentManager.dialog.removeStaleWorktree.messagePre": "¿Eliminar Worktree obsoleto ",
@@ -208,7 +208,7 @@ export const dict = {
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pastePrUrl": "Pegar URL del PR...",
"agentManager.import.open": "Abrir",
"agentManager.import.branches": "Branches",
"agentManager.import.branches": "Ramas",
"agentManager.import.selectBranch": "Seleccionar branch...",
"agentManager.import.loading": "Cargando...",
"agentManager.import.loadingBranches": "Cargando branches...",
@@ -216,4 +216,10 @@ export const dict = {
"agentManager.import.noBranchesFound": "No se encontraron branches.",
"agentManager.import.noBranchesHint": "Pega una URL de PR arriba o crea un nuevo Worktree.",
"agentManager.import.failed": "Error en la importación",
"agentManager.pr.error.gh_auth.title": "Se requiere autenticación de GitHub",
"agentManager.pr.error.gh_auth.description":
"Ejecuta 'gh auth login' en tu terminal para restaurar el estado del PR.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI no está instalado",
"agentManager.pr.error.gh_missing.description": "Instala GitHub CLI para habilitar el estado del PR.",
}
+8 -3
View File
@@ -3,8 +3,8 @@ export const dict = {
"agentManager.sidebar.collapse": "بستن نوار کناری",
"agentManager.sidebar.expand": "نمایش نوار کناری",
"agentManager.section.worktrees": "WORKTREES",
"agentManager.section.sessions": "SESSIONS",
"agentManager.projects": "PROJECTS",
"agentManager.section.sessions": "جلسات",
"agentManager.projects": "پروژه‌ها",
"agentManager.project.add": "افزودن پروژه",
"agentManager.project.remove": "حذف از Agent Manager",
"agentManager.project.trust": "اعتماد",
@@ -207,7 +207,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "پیش‌نمایش تصویر برای این نمونه جلسه در دسترس نیست.",
"agentManager.review.endOfLongDiff": "به انتها رسیدید!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "درخواست ادغام",
"agentManager.import.pastePrUrl": "URL درخواست PR را وارد کنید...",
"agentManager.import.open": "باز کردن",
"agentManager.import.branches": "شاخه‌ها",
@@ -218,4 +218,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "هیچ شاخه‌ای یافت نشد.",
"agentManager.import.noBranchesHint": "یک URL درخواست ادغام را در بالا جای‌گذاری کنید یا یک worktree جدید بسازید.",
"agentManager.import.failed": "وارد کردن ناموفق بود",
"agentManager.pr.error.gh_auth.title": "احراز هویت GitHub لازم است",
"agentManager.pr.error.gh_auth.description": "برای بازیابی وضعیت PR، دستور 'gh auth login' را در ترمینال اجرا کنید.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI نصب نشده است",
"agentManager.pr.error.gh_missing.description": "برای فعال کردن وضعیت PR، GitHub CLI را نصب کنید.",
}
+10 -4
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Nouveau Worktree",
"agentManager.worktree.setupScript": "Script de configuration du Worktree",
"agentManager.worktree.delete": "Supprimer le Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "Supprimer ?",
"agentManager.worktree.stale": "Obsolète",
"agentManager.worktree.staleTooltip": "Absent du disque ou n'est plus suivi par git worktree",
"agentManager.worktree.removeStale": "Supprimer le Worktree obsolète",
@@ -60,7 +60,7 @@ export const dict = {
"agentManager.sidebarSearch.label": "Rechercher des Worktrees et des sessions",
"agentManager.sidebarSearch.scope":
"Recherche dans l'espace de travail local, les sessions locales, les Worktrees et leurs sessions",
"agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES",
"agentManager.sidebarSearch.contexts": "LOCAL ET WORKTREES",
"agentManager.terminal.new": "Nouvel onglet de terminal",
"agentManager.terminal.ended": "terminal terminé — fermez l'onglet pour ignorer",
@@ -107,8 +107,8 @@ export const dict = {
"agentManager.run.options": "Options d'exécution",
"agentManager.run.configure": "Configurer le script d'exécution",
"agentManager.shortcuts.openAgentManager": "Ouvrir Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Mode d'agent suivant",
"agentManager.shortcuts.cyclePreviousAgentMode": "Mode d'agent précédent",
"agentManager.shortcuts.showShortcuts": "Afficher les raccourcis clavier",
"agentManager.dialog.removeStaleWorktree.title": "Supprimer le Worktree obsolète",
"agentManager.dialog.removeStaleWorktree.messagePre": "Supprimer le Worktree obsolète ",
@@ -216,4 +216,10 @@ export const dict = {
"agentManager.import.noBranchesFound": "Aucune branche trouvée.",
"agentManager.import.noBranchesHint": "Collez une URL de PR ci-dessus ou créez un nouveau Worktree.",
"agentManager.import.failed": "Échec de l'importation",
"agentManager.pr.error.gh_auth.title": "Authentification GitHub requise",
"agentManager.pr.error.gh_auth.description":
"Exécutez 'gh auth login' dans votre terminal pour restaurer le statut de la PR.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI non installé",
"agentManager.pr.error.gh_missing.description": "Installez GitHub CLI pour activer le statut de la PR.",
}
+7 -1
View File
@@ -36,7 +36,7 @@ export const dict = {
"agentManager.section.moveUp": "Sposta su",
"agentManager.section.moveDown": "Sposta giù",
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.branch": "Ramo",
"agentManager.hoverCard.worktree": "Worktree",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sessioni",
@@ -222,4 +222,10 @@ export const dict = {
"agentManager.import.noBranchesFound": "Nessun branch trovato.",
"agentManager.import.noBranchesHint": "Incolla un URL PR sopra o crea un nuovo worktree.",
"agentManager.import.failed": "Importazione non riuscita",
"agentManager.pr.error.gh_auth.title": "Autenticazione GitHub richiesta",
"agentManager.pr.error.gh_auth.description":
"Esegui 'gh auth login' nel terminale per ripristinare lo stato della PR.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI non installato",
"agentManager.pr.error.gh_missing.description": "Installa GitHub CLI per abilitare lo stato della PR.",
}
+11 -4
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "新しいWorktree",
"agentManager.worktree.setupScript": "Worktreeセットアップスクリプト",
"agentManager.worktree.delete": "Worktreeを削除",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "削除しますか?",
"agentManager.worktree.stale": "無効",
"agentManager.worktree.staleTooltip": "ディスク上に存在しないか、git worktree で追跡されていません",
"agentManager.worktree.removeStale": "無効な Worktree を削除",
@@ -107,8 +107,8 @@ export const dict = {
"agentManager.run.options": "実行オプション",
"agentManager.run.configure": "実行スクリプトを設定",
"agentManager.shortcuts.openAgentManager": "Agent Managerを開く",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "次のエージェントモード",
"agentManager.shortcuts.cyclePreviousAgentMode": "前のエージェントモード",
"agentManager.shortcuts.showShortcuts": "キーボードショートカットを表示",
"agentManager.dialog.removeStaleWorktree.title": "無効な Worktree を削除",
"agentManager.dialog.removeStaleWorktree.messagePre": "無効な Worktree を削除 ",
@@ -204,7 +204,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "このセッションスナップショットでは画像プレビューを利用できません。",
"agentManager.review.endOfLongDiff": "最後まで到達しました!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "プルリクエスト",
"agentManager.import.pastePrUrl": "PR URLを貼り付け...",
"agentManager.import.open": "開く",
"agentManager.import.branches": "ブランチ",
@@ -215,4 +215,11 @@ export const dict = {
"agentManager.import.noBranchesFound": "ブランチが見つかりません。",
"agentManager.import.noBranchesHint": "上にPR URLを貼り付けるか、新しいWorktreeを作成してください。",
"agentManager.import.failed": "インポートに失敗しました",
"agentManager.pr.error.gh_auth.title": "GitHub認証が必要です",
"agentManager.pr.error.gh_auth.description":
"PRステータスを復元するには、ターミナルで 'gh auth login' を実行してください。",
"agentManager.pr.error.gh_missing.title": "GitHub CLIがインストールされていません",
"agentManager.pr.error.gh_missing.description":
"PRステータスを有効にするには、GitHub CLIをインストールしてください。",
}
+9 -4
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "새 Worktree",
"agentManager.worktree.setupScript": "Worktree 설정 스크립트",
"agentManager.worktree.delete": "Worktree 삭제",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "삭제하시겠습니까?",
"agentManager.worktree.stale": "오래됨",
"agentManager.worktree.staleTooltip": "디스크에 없거나 더 이상 git worktree에서 추적되지 않습니다",
"agentManager.worktree.removeStale": "오래된 Worktree 제거",
@@ -105,8 +105,8 @@ export const dict = {
"agentManager.run.options": "실행 옵션",
"agentManager.run.configure": "실행 스크립트 구성",
"agentManager.shortcuts.openAgentManager": "Agent Manager 열기",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "다음 에이전트 모드",
"agentManager.shortcuts.cyclePreviousAgentMode": "이전 에이전트 모드",
"agentManager.shortcuts.showShortcuts": "키보드 단축키 표시",
"agentManager.dialog.removeStaleWorktree.title": "오래된 Worktree 제거",
"agentManager.dialog.removeStaleWorktree.messagePre": "오래된 Worktree 제거 ",
@@ -202,7 +202,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "이 세션 스냅샷에서는 이미지 미리보기를 사용할 수 없습니다.",
"agentManager.review.endOfLongDiff": "끝까지 도달했습니다!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "풀 리퀘스트",
"agentManager.import.pastePrUrl": "PR URL 붙여넣기...",
"agentManager.import.open": "열기",
"agentManager.import.branches": "브랜치",
@@ -213,4 +213,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "브랜치를 찾을 수 없습니다.",
"agentManager.import.noBranchesHint": "위에 PR URL을 붙여넣거나 새 Worktree를 만드세요.",
"agentManager.import.failed": "가져오기 실패",
"agentManager.pr.error.gh_auth.title": "GitHub 인증 필요",
"agentManager.pr.error.gh_auth.description": "PR 상태를 복원하려면 터미널에서 'gh auth login'을 실행하세요.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI가 설치되지 않음",
"agentManager.pr.error.gh_missing.description": "PR 상태를 활성화하려면 GitHub CLI를 설치하세요.",
}
+7 -2
View File
@@ -138,7 +138,7 @@ export const dict = {
"agentManager.dialog.baseBranch": "Basis branch",
"agentManager.dialog.searchBranches": "Zoeken naar branches...",
"agentManager.dialog.branchBadge.default": "standaard",
"agentManager.dialog.branchBadge.remote": "remote",
"agentManager.dialog.branchBadge.remote": "extern",
"agentManager.dialog.versions": "Versies",
"agentManager.dialog.versionHint": "{{count}} worktrees zullen parallel draaien",
"agentManager.dialog.compareModels": "Modellen vergelijken",
@@ -209,7 +209,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "Afbeeldingsvoorbeeld is niet beschikbaar voor deze sessiesnapshot.",
"agentManager.review.endOfLongDiff": "Je hebt het einde bereikt!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "Pull request",
"agentManager.import.pastePrUrl": "Plak PR URL...",
"agentManager.import.open": "Openen",
"agentManager.import.branches": "Branches",
@@ -220,4 +220,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "Geen branches gevonden.",
"agentManager.import.noBranchesHint": "Plak hierboven een PR URL of maak een nieuwe worktree.",
"agentManager.import.failed": "Importeren mislukt",
"agentManager.pr.error.gh_auth.title": "GitHub-authenticatie vereist",
"agentManager.pr.error.gh_auth.description": "Voer 'gh auth login' uit in je terminal om de PR-status te herstellen.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI niet geïnstalleerd",
"agentManager.pr.error.gh_missing.description": "Installeer de GitHub CLI om de PR-status in te schakelen.",
}
+10 -5
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Nytt Worktree",
"agentManager.worktree.setupScript": "Worktree-oppsettskript",
"agentManager.worktree.delete": "Slett Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "Slette?",
"agentManager.worktree.stale": "Utdatert",
"agentManager.worktree.staleTooltip": "Mangler på disken eller spores ikke lenger av git worktree",
"agentManager.worktree.removeStale": "Fjern utdatert Worktree",
@@ -105,8 +105,8 @@ export const dict = {
"agentManager.run.options": "Kjøringsalternativer",
"agentManager.run.configure": "Konfigurer kjøreskript",
"agentManager.shortcuts.openAgentManager": "Åpne Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Neste agentmodus",
"agentManager.shortcuts.cyclePreviousAgentMode": "Forrige agentmodus",
"agentManager.shortcuts.showShortcuts": "Vis tastatursnarveier",
"agentManager.dialog.removeStaleWorktree.title": "Fjern utdatert Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Fjern utdatert Worktree ",
@@ -202,10 +202,10 @@ export const dict = {
"Forhåndsvisning av bildet er ikke tilgjengelig for dette øyeblikksbildet av økten.",
"agentManager.review.endOfLongDiff": "Du nådde slutten!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "Pull request",
"agentManager.import.pastePrUrl": "Lim inn PR URL...",
"agentManager.import.open": "Åpne",
"agentManager.import.branches": "Branches",
"agentManager.import.branches": "Grener",
"agentManager.import.selectBranch": "Velg branch...",
"agentManager.import.loading": "Laster...",
"agentManager.import.loadingBranches": "Laster branches...",
@@ -213,4 +213,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "Ingen branches funnet.",
"agentManager.import.noBranchesHint": "Lim inn en PR URL ovenfor eller opprett et nytt Worktree.",
"agentManager.import.failed": "Import mislyktes",
"agentManager.pr.error.gh_auth.title": "GitHub-autentisering kreves",
"agentManager.pr.error.gh_auth.description": "Kjør 'gh auth login' i terminalen for å gjenopprette PR-status.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI er ikke installert",
"agentManager.pr.error.gh_missing.description": "Installer GitHub CLI for å aktivere PR-status.",
}
+8 -3
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Nowy Worktree",
"agentManager.worktree.setupScript": "Skrypt konfiguracji Worktree",
"agentManager.worktree.delete": "Usuń Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "Usunąć?",
"agentManager.worktree.stale": "Nieaktualny",
"agentManager.worktree.staleTooltip": "Brak na dysku lub nie jest już śledzony przez git worktree",
"agentManager.worktree.removeStale": "Usuń nieaktualny Worktree",
@@ -107,8 +107,8 @@ export const dict = {
"agentManager.run.options": "Opcje uruchamiania",
"agentManager.run.configure": "Konfiguruj skrypt uruchamiania",
"agentManager.shortcuts.openAgentManager": "Otwórz Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Następny tryb agenta",
"agentManager.shortcuts.cyclePreviousAgentMode": "Poprzedni tryb agenta",
"agentManager.shortcuts.showShortcuts": "Pokaż skróty klawiszowe",
"agentManager.dialog.removeStaleWorktree.title": "Usuń nieaktualny Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Usunąć nieaktualny Worktree ",
@@ -214,4 +214,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "Nie znaleziono branchy.",
"agentManager.import.noBranchesHint": "Wklej URL PR powyżej lub utwórz nowy Worktree.",
"agentManager.import.failed": "Import nie powiódł się",
"agentManager.pr.error.gh_auth.title": "Wymagana autoryzacja GitHub",
"agentManager.pr.error.gh_auth.description": "Uruchom 'gh auth login' w terminalu, aby przywrócić status PR.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI nie jest zainstalowane",
"agentManager.pr.error.gh_missing.description": "Zainstaluj GitHub CLI, aby włączyć status PR.",
}
+8 -3
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Новый Worktree",
"agentManager.worktree.setupScript": "Скрипт настройки Worktree",
"agentManager.worktree.delete": "Удалить Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "Удалить?",
"agentManager.worktree.stale": "Устаревший",
"agentManager.worktree.staleTooltip": "Отсутствует на диске или больше не отслеживается git worktree",
"agentManager.worktree.removeStale": "Удалить устаревший Worktree",
@@ -107,8 +107,8 @@ export const dict = {
"agentManager.run.options": "Параметры запуска",
"agentManager.run.configure": "Настроить скрипт запуска",
"agentManager.shortcuts.openAgentManager": "Открыть Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "Следующий режим агента",
"agentManager.shortcuts.cyclePreviousAgentMode": "Предыдущий режим агента",
"agentManager.shortcuts.showShortcuts": "Показать сочетания клавиш",
"agentManager.dialog.removeStaleWorktree.title": "Удалить устаревший Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "Удалить устаревший Worktree ",
@@ -215,4 +215,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "Ветки не найдены.",
"agentManager.import.noBranchesHint": "Вставьте URL PR выше или создайте новый Worktree.",
"agentManager.import.failed": "Ошибка импорта",
"agentManager.pr.error.gh_auth.title": "Требуется аутентификация GitHub",
"agentManager.pr.error.gh_auth.description": "Выполните 'gh auth login' в терминале, чтобы восстановить статус PR.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI не установлен",
"agentManager.pr.error.gh_missing.description": "Установите GitHub CLI, чтобы включить статус PR.",
}
+10 -5
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "Worktree ใหม่",
"agentManager.worktree.setupScript": "สคริปต์ตั้งค่า Worktree",
"agentManager.worktree.delete": "ลบ Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "ลบ?",
"agentManager.worktree.stale": "ล้าสมัย",
"agentManager.worktree.staleTooltip": "ไม่พบบนดิสก์หรือไม่ได้ถูกติดตามโดย git worktree อีกต่อไป",
"agentManager.worktree.removeStale": "ลบ Worktree ที่ล้าสมัย",
@@ -102,8 +102,8 @@ export const dict = {
"agentManager.run.options": "ตัวเลือกการเรียกใช้",
"agentManager.run.configure": "กำหนดค่าสคริปต์การเรียกใช้",
"agentManager.shortcuts.openAgentManager": "เปิด Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "โหมดเอเจนต์ถัดไป",
"agentManager.shortcuts.cyclePreviousAgentMode": "โหมดเอเจนต์ก่อนหน้า",
"agentManager.shortcuts.showShortcuts": "แสดงปุ่มลัดแป้นพิมพ์",
"agentManager.dialog.removeStaleWorktree.title": "ลบ Worktree ที่ล้าสมัย",
"agentManager.dialog.removeStaleWorktree.messagePre": "ลบ Worktree ที่ล้าสมัย ",
@@ -198,10 +198,10 @@ export const dict = {
"agentManager.review.imageUnavailable": "ไม่สามารถแสดงตัวอย่างรูปภาพสำหรับสแนปช็อตของเซสชันนี้ได้",
"agentManager.review.endOfLongDiff": "คุณมาถึงท้ายสุดแล้ว!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "คำขอรวมโค้ด",
"agentManager.import.pastePrUrl": "วาง URL ของ PR...",
"agentManager.import.open": "เปิด",
"agentManager.import.branches": "Branches",
"agentManager.import.branches": "สาขา",
"agentManager.import.selectBranch": "เลือก Branch...",
"agentManager.import.loading": "กำลังโหลด...",
"agentManager.import.loadingBranches": "กำลังโหลด Branches...",
@@ -209,4 +209,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "ไม่พบ Branch",
"agentManager.import.noBranchesHint": "วาง URL ของ PR ด้านบนหรือสร้าง Worktree ใหม่",
"agentManager.import.failed": "นำเข้าล้มเหลว",
"agentManager.pr.error.gh_auth.title": "ต้องยืนยันตัวตน GitHub",
"agentManager.pr.error.gh_auth.description": "รันคำสั่ง 'gh auth login' ในเทอร์มินัลเพื่อกู้คืนสถานะ PR",
"agentManager.pr.error.gh_missing.title": "ไม่ได้ติดตั้ง GitHub CLI",
"agentManager.pr.error.gh_missing.description": "ติดตั้ง GitHub CLI เพื่อเปิดใช้งานสถานะ PR",
}
+7 -1
View File
@@ -211,7 +211,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "Bu oturum anlık görüntüsü için görsel önizlemesi kullanılamıyor.",
"agentManager.review.endOfLongDiff": "Sonuna geldiniz!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "Çekme İsteği",
"agentManager.import.pastePrUrl": "PR URL'sini yapıştırın...",
"agentManager.import.open": "Aç",
"agentManager.import.branches": "Dallar",
@@ -222,4 +222,10 @@ export const dict = {
"agentManager.import.noBranchesFound": "Hiçbir dal bulunamadı.",
"agentManager.import.noBranchesHint": "Yukarıya bir PR URL'si yapıştırın veya yeni bir worktree oluşturun.",
"agentManager.import.failed": "İçe aktarma başarısız oldu",
"agentManager.pr.error.gh_auth.title": "GitHub kimlik doğrulaması gerekli",
"agentManager.pr.error.gh_auth.description":
"PR durumunu geri yüklemek için terminalinizde 'gh auth login' komutunu çalıştırın.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI yüklü değil",
"agentManager.pr.error.gh_missing.description": "PR durumunu etkinleştirmek için GitHub CLI'yi yükleyin.",
}
+6 -1
View File
@@ -212,7 +212,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "Попередній перегляд зображення недоступний для цього знімка сеансу.",
"agentManager.review.endOfLongDiff": "Ви дійшли до кінця!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "Пул-реквест",
"agentManager.import.pastePrUrl": "Вставте URL PR...",
"agentManager.import.open": "Відкрити",
"agentManager.import.branches": "Гілки",
@@ -223,4 +223,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "Гілок не знайдено.",
"agentManager.import.noBranchesHint": "Вставте URL PR вище або створіть нове робоче дерево.",
"agentManager.import.failed": "Імпортування не вдалося",
"agentManager.pr.error.gh_auth.title": "Потрібна автентифікація GitHub",
"agentManager.pr.error.gh_auth.description": "Виконайте 'gh auth login' у терміналі, щоб відновити статус PR.",
"agentManager.pr.error.gh_missing.title": "GitHub CLI не встановлено",
"agentManager.pr.error.gh_missing.description": "Встановіть GitHub CLI, щоб увімкнути статус PR.",
}
+9 -4
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "新建 Worktree",
"agentManager.worktree.setupScript": "Worktree 设置脚本",
"agentManager.worktree.delete": "删除 Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "删除?",
"agentManager.worktree.stale": "已失效",
"agentManager.worktree.staleTooltip": "在磁盘上缺失,或不再由 git worktree 跟踪",
"agentManager.worktree.removeStale": "移除失效 Worktree",
@@ -102,8 +102,8 @@ export const dict = {
"agentManager.run.options": "运行选项",
"agentManager.run.configure": "配置运行脚本",
"agentManager.shortcuts.openAgentManager": "打开 Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "下一个代理模式",
"agentManager.shortcuts.cyclePreviousAgentMode": "上一个代理模式",
"agentManager.shortcuts.showShortcuts": "显示键盘快捷键",
"agentManager.dialog.removeStaleWorktree.title": "移除失效 Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "移除失效 Worktree ",
@@ -196,7 +196,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "此会话快照无法提供图片预览。",
"agentManager.review.endOfLongDiff": "你已经到末尾了!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "拉取请求",
"agentManager.import.pastePrUrl": "粘贴 PR URL...",
"agentManager.import.open": "打开",
"agentManager.import.branches": "分支",
@@ -207,4 +207,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "未找到分支。",
"agentManager.import.noBranchesHint": "在上方粘贴 PR URL 或创建新的 Worktree。",
"agentManager.import.failed": "导入失败",
"agentManager.pr.error.gh_auth.title": "需要 GitHub 身份验证",
"agentManager.pr.error.gh_auth.description": "在终端中运行 'gh auth login' 以恢复 PR 状态。",
"agentManager.pr.error.gh_missing.title": "未安装 GitHub CLI",
"agentManager.pr.error.gh_missing.description": "安装 GitHub CLI 以启用 PR 状态。",
}
+9 -4
View File
@@ -14,7 +14,7 @@ export const dict = {
"agentManager.worktree.new": "新建 Worktree",
"agentManager.worktree.setupScript": "Worktree 設定指令碼",
"agentManager.worktree.delete": "刪除 Worktree",
"agentManager.worktree.confirmDelete": "Delete?",
"agentManager.worktree.confirmDelete": "刪除?",
"agentManager.worktree.stale": "已失效",
"agentManager.worktree.staleTooltip": "磁碟上不存在,或不再由 git worktree 追蹤",
"agentManager.worktree.removeStale": "移除失效 Worktree",
@@ -102,8 +102,8 @@ export const dict = {
"agentManager.run.options": "執行選項",
"agentManager.run.configure": "設定執行指令碼",
"agentManager.shortcuts.openAgentManager": "開啟 Agent Manager",
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
"agentManager.shortcuts.cycleAgentMode": "下一個代理模式",
"agentManager.shortcuts.cyclePreviousAgentMode": "上一個代理模式",
"agentManager.shortcuts.showShortcuts": "顯示鍵盤快捷鍵",
"agentManager.dialog.removeStaleWorktree.title": "移除失效 Worktree",
"agentManager.dialog.removeStaleWorktree.messagePre": "移除失效 Worktree ",
@@ -196,7 +196,7 @@ export const dict = {
"agentManager.review.imageUnavailable": "此工作階段快照無法預覽圖片。",
"agentManager.review.endOfLongDiff": "你已經到最後了!",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pullRequest": "提取請求",
"agentManager.import.pastePrUrl": "貼上 PR URL...",
"agentManager.import.open": "開啟",
"agentManager.import.branches": "分支",
@@ -207,4 +207,9 @@ export const dict = {
"agentManager.import.noBranchesFound": "找不到分支。",
"agentManager.import.noBranchesHint": "在上方貼上 PR URL 或建立新的 Worktree。",
"agentManager.import.failed": "匯入失敗",
"agentManager.pr.error.gh_auth.title": "需要 GitHub 驗證",
"agentManager.pr.error.gh_auth.description": "在終端機中執行 'gh auth login' 以還原 PR 狀態。",
"agentManager.pr.error.gh_missing.title": "未安裝 GitHub CLI",
"agentManager.pr.error.gh_missing.description": "安裝 GitHub CLI 以啟用 PR 狀態。",
}
@@ -0,0 +1,22 @@
/** @jsxImportSource solid-js */
import { createSignal } from "solid-js"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
export function CopyButton(props: { text: string; label?: string; class?: string }) {
const [copied, setCopied] = createSignal(false)
const copy = () => {
navigator.clipboard.writeText(props.text)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}
return (
<IconButton
icon={copied() ? "check" : "copy"}
size="small"
variant="ghost"
label={props.label ?? "Copy"}
class={props.class}
onClick={copy}
/>
)
}
@@ -0,0 +1,33 @@
/** @jsxImportSource solid-js */
import { Show } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import type { PRStatus } from "../../src/types/messages"
import { prBadgeIndicator, prChecksRunning } from "../WorktreeItem"
const INDICATOR_ICON: Record<string, string> = {
failure: "circle-x-outline",
changes: "warning",
approved: "circle-check",
}
export function PRBadge(props: { pr: PRStatus }) {
const indicator = () => prBadgeIndicator(props.pr)
return (
<span
class="am-pr-panel-badge am-pr-row"
classList={{
"am-pr-accent-draft": props.pr.state === "draft",
"am-pr-accent-merged": props.pr.state === "merged",
"am-pr-accent-closed": props.pr.state === "closed",
"am-pr-accent-pending": props.pr.state === "open" && props.pr.checks.status === "pending",
"am-pr-accent-open": props.pr.state === "open" && props.pr.checks.status !== "pending",
"am-pr-badge-pending": prChecksRunning(props.pr),
}}
>
<Icon name="pull-request" size="small" />
<Show when={indicator() !== "none"}>
<Icon name={INDICATOR_ICON[indicator()]} size="small" class="am-pr-badge-status" data-status={indicator()} />
</Show>
</span>
)
}
@@ -0,0 +1,62 @@
/** @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 { PRStatus } from "../../src/types/messages"
import type { PRCheck, CheckStatus } from "./pr-types"
import { SectionHeading } from "./SectionHeading"
import { useVSCode } from "../../src/context/vscode"
const CHECK: Record<CheckStatus, { icon: string; label: string }> = {
success: { icon: "circle-check", label: "Passed" },
failure: { icon: "circle-x-outline", label: "Failed" },
cancelled: { icon: "circle-x-outline", label: "Cancelled" },
skipped: { icon: "circle-x-outline", label: "Skipped" },
pending: { icon: "play", label: "Running" },
}
export function PRChecks(props: { checks: PRStatus["checks"] }) {
const vscode = useVSCode()
const [open, setOpen] = createSignal(true)
return (
<>
<div class="am-pr-panel-divider" />
<div class="am-pr-panel-section">
<SectionHeading
title="Checks"
open={open()}
onToggle={() => setOpen((v) => !v)}
count={`${props.checks.passed}/${props.checks.total} passed`}
countClass={`am-pr-checks-count-${props.checks.status}`}
/>
<Show when={open()}>
<div class="am-pr-panel-checks am-pr-col">
<For each={props.checks.checks}>
{(check: PRCheck) => (
<div class="am-pr-panel-check-item am-pr-row" data-status={check.status}>
<Icon name={CHECK[check.status].icon} size="small" class="am-pr-check-icon" />
<span class="am-pr-check-name">{check.name}</span>
<span class="am-pr-check-status">{CHECK[check.status].label}</span>
<Show when={check.duration}>
<span class="am-pr-check-duration">{check.duration}</span>
</Show>
<Show when={check.url}>
<Tooltip value="Open in browser" placement="bottom">
<button
class="am-pr-check-link"
aria-label="Open check in browser"
onClick={() => vscode.postMessage({ type: "openExternal", url: check.url! })}
>
<Icon name="link" size="small" />
</button>
</Tooltip>
</Show>
</div>
)}
</For>
</div>
</Show>
</div>
</>
)
}
@@ -0,0 +1,51 @@
/** @jsxImportSource solid-js */
import { For, Show, createSignal } from "solid-js"
import { Markdown } from "@kilocode/kilo-ui/markdown"
import type { PRStatus } from "../../src/types/messages"
import type { PRComment } from "./pr-types"
import { SectionHeading } from "./SectionHeading"
import { CopyButton } from "./CopyButton"
export function PRComments(props: { comments: NonNullable<PRStatus["comments"]> }) {
const [open, setOpen] = createSignal(true)
return (
<>
<div class="am-pr-panel-divider" />
<div class="am-pr-panel-section">
<SectionHeading
title="Comments"
open={open()}
onToggle={() => setOpen((v) => !v)}
count={props.comments.unresolved > 0 ? `${props.comments.unresolved} unresolved` : undefined}
countClass="am-pr-panel-unresolved"
/>
<Show when={open()}>
<div class="am-pr-panel-comment-list am-pr-col">
<For each={props.comments.comments}>
{(comment: PRComment) => (
<div class="am-pr-panel-comment" classList={{ "am-pr-panel-comment-resolved": comment.resolved }}>
<div class="am-pr-panel-comment-header am-pr-row">
<span class="am-pr-panel-comment-author">{comment.author}</span>
<Show when={comment.file}>
<span class="am-pr-panel-comment-file">
{comment.file}
<Show when={comment.line}>{`:${comment.line}`}</Show>
</span>
</Show>
<Show when={comment.resolved}>
<span class="am-pr-panel-comment-resolved-badge">Resolved</span>
</Show>
<CopyButton text={comment.body} class="am-pr-copy-btn" />
</div>
<div class="am-pr-panel-comment-body">
<Markdown text={comment.body} />
</div>
</div>
)}
</For>
</div>
</Show>
</div>
</>
)
}
@@ -0,0 +1,21 @@
/** @jsxImportSource solid-js */
import { Show, createSignal } from "solid-js"
import { Markdown } from "@kilocode/kilo-ui/markdown"
import { SectionHeading } from "./SectionHeading"
export function PRDescription(props: { body: string }) {
const [open, setOpen] = createSignal(true)
return (
<>
<div class="am-pr-panel-divider" />
<div class="am-pr-panel-section">
<SectionHeading title="Description" open={open()} onToggle={() => setOpen((v) => !v)} />
<Show when={open()}>
<div class="am-pr-panel-description">
<Markdown text={props.body} />
</div>
</Show>
</div>
</>
)
}
@@ -0,0 +1,51 @@
/** @jsxImportSource solid-js */
import { Show } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import type { WorktreeState } from "../../src/types/messages"
import type { PRStatus } from "../../src/types/messages"
const STATE_LABEL: Record<PRStatus["state"], string> = {
open: "Open",
draft: "Draft",
merged: "Merged",
closed: "Closed",
}
const REVIEW_LABEL: Partial<Record<NonNullable<PRStatus["review"]>, string>> = {
approved: "Approved",
changes_requested: "Changes Requested",
pending: "Review Pending",
}
export function PROverview(props: { pr: PRStatus; worktree?: WorktreeState }) {
return (
<div class="am-pr-panel-section">
<Show when={props.worktree}>
{(wt) => (
<div class="am-pr-panel-row am-pr-row">
<span class="am-pr-panel-label">Branch</span>
<span class="am-pr-panel-value am-pr-panel-branch am-pr-row">
<span class="am-pr-branch-name">{wt().branch}</span>
<Icon name="arrow-right" size="small" class="am-pr-branch-arrow" />
<span class="am-pr-branch-name">{wt().parentBranch}</span>
</span>
</div>
)}
</Show>
<div class="am-pr-panel-row am-pr-row">
<span class="am-pr-panel-label">Status</span>
<span class="am-pr-panel-value" data-pr-state={props.pr.state}>
{STATE_LABEL[props.pr.state]}
</span>
</div>
<Show when={props.pr.review}>
{(review) => (
<div class="am-pr-panel-row am-pr-row">
<span class="am-pr-panel-label">Review</span>
<span class="am-pr-panel-value">{REVIEW_LABEL[review()]}</span>
</div>
)}
</Show>
</div>
)
}
@@ -0,0 +1,63 @@
/** @jsxImportSource solid-js */
import { Component, Show } from "solid-js"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import type { WorktreeState } from "../../src/types/messages"
import type { PRStatus } from "../../src/types/messages"
import { PRBadge } from "./PRBadge"
import { PROverview } from "./PROverview"
import { PRReviewers } from "./PRReviewers"
import { PRDescription } from "./PRDescription"
import { PRChecks } from "./PRChecks"
import { PRComments } from "./PRComments"
import { PRSummary } from "./PRSummary"
import "./pr-panel.css"
interface PRPanelProps {
pr: PRStatus
worktree?: WorktreeState
onClose: () => void
onOpenExternal: () => void
}
export const PRPanel: Component<PRPanelProps> = (props) => {
return (
<div class="am-pr-panel am-pr-col">
<div class="am-pr-panel-header am-pr-row">
<div class="am-pr-panel-title-row am-pr-row">
<PRBadge pr={props.pr} />
<span class="am-pr-panel-title">{props.pr.title}</span>
<span class="am-pr-panel-number">#{props.pr.number}</span>
</div>
<div class="am-pr-panel-actions am-pr-row">
<Tooltip value="Open in browser" placement="bottom">
<IconButton
icon="link"
size="small"
variant="ghost"
label="Open in browser"
onClick={props.onOpenExternal}
/>
</Tooltip>
<Tooltip value="Close" placement="bottom">
<IconButton icon="close" size="small" variant="ghost" label="Close PR panel" onClick={props.onClose} />
</Tooltip>
</div>
</div>
<div class="am-pr-panel-body">
<PRSummary pr={props.pr} />
<PROverview pr={props.pr} worktree={props.worktree} />
<Show when={(props.pr.reviewers ?? []).length > 0}>
<PRReviewers reviewers={props.pr.reviewers ?? []} />
</Show>
<Show when={props.pr.body}>{(body) => <PRDescription body={body()} />}</Show>
<Show when={props.pr.checks.total > 0}>
<PRChecks checks={props.pr.checks} />
</Show>
<Show when={props.pr.comments?.total ? props.pr.comments : undefined}>
{(comments) => <PRComments comments={comments()} />}
</Show>
</div>
</div>
)
}
@@ -0,0 +1,44 @@
/** @jsxImportSource solid-js */
import { For, Show, createSignal } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import type { PRReviewer, ReviewerState } from "./pr-types"
import { SectionHeading } from "./SectionHeading"
const REVIEWER_ICON: Record<ReviewerState, string> = {
approved: "circle-check",
changes_requested: "refresh",
commented: "edit",
pending: "dash",
}
const REVIEWER_LABEL: Record<ReviewerState, string> = {
approved: "Approved",
changes_requested: "Changes requested",
commented: "Commented",
pending: "Awaiting",
}
export function PRReviewers(props: { reviewers: PRReviewer[] }) {
const [open, setOpen] = createSignal(true)
return (
<>
<div class="am-pr-panel-divider" />
<div class="am-pr-panel-section">
<SectionHeading title="Reviewers" open={open()} onToggle={() => setOpen((v) => !v)} />
<Show when={open()}>
<div class="am-pr-panel-reviewers am-pr-col">
<For each={props.reviewers}>
{(reviewer) => (
<div class="am-pr-panel-reviewer am-pr-row" data-state={reviewer.state}>
<Icon name={REVIEWER_ICON[reviewer.state]} size="small" class="am-pr-reviewer-icon" />
<span class="am-pr-reviewer-login">{reviewer.login}</span>
<span class="am-pr-reviewer-state">{REVIEWER_LABEL[reviewer.state]}</span>
</div>
)}
</For>
</div>
</Show>
</div>
</>
)
}
@@ -0,0 +1,72 @@
/** @jsxImportSource solid-js */
import { Show } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import type { PRStatus } from "../../src/types/messages"
interface PRSummaryProps {
pr: PRStatus
}
function summaryRows(pr: PRStatus): Array<{ icon: string; label: string; status: string }> {
const rows = []
if (pr.checks.total > 0) {
const { passed, total, status } = pr.checks
rows.push({
icon: status === "success" ? "circle-check" : status === "failure" ? "circle-x-outline" : "play",
label: status === "success" ? "All checks passing" : `${passed}/${total} checks passed`,
status,
})
}
if (pr.review) {
const status = pr.review === "approved" ? "success" : pr.review === "changes_requested" ? "failure" : "pending"
rows.push({
icon: status === "success" ? "circle-check" : status === "failure" ? "circle-x-outline" : "play",
label: status === "success" ? "Approved" : status === "failure" ? "Changes requested" : "Review pending",
status,
})
}
if (pr.comments && pr.comments.unresolved > 0) {
rows.push({
icon: "comment",
label: `${pr.comments.unresolved} unresolved comment${pr.comments.unresolved > 1 ? "s" : ""}`,
status: "warning",
})
}
return rows
}
export function PRSummary(props: PRSummaryProps) {
const rows = () => summaryRows(props.pr)
return (
<Show when={rows().length > 0}>
<div class="am-pr-summary">
<div class="am-pr-summary-header am-pr-row">
<span class="am-pr-summary-title">PR Summary</span>
<span class="am-pr-panel-section-count am-pr-panel-diff am-pr-row">
<Show when={props.pr.files > 0}>
<span class="am-stat-files">{props.pr.files}f</span>
</Show>
<Show when={props.pr.additions > 0}>
<span class="am-stat-additions">+{props.pr.additions}</span>
</Show>
<Show when={props.pr.deletions > 0}>
<span class="am-stat-deletions">−{props.pr.deletions}</span>
</Show>
</span>
</div>
<div class="am-pr-summary-rows am-pr-col">
{rows().map((row) => (
<div class="am-pr-summary-row am-pr-row" data-status={row.status}>
<Icon name={row.icon} size="small" class="am-pr-summary-icon" />
<span class="am-pr-summary-label">{row.label}</span>
</div>
))}
</div>
</div>
</Show>
)
}
@@ -0,0 +1,23 @@
/** @jsxImportSource solid-js */
import { Show } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
export function SectionHeading(props: {
title: string
open: boolean
onToggle: () => void
count?: string
countClass?: string
}) {
return (
<button class="am-pr-panel-section-heading am-pr-panel-section-toggle am-pr-row" onClick={props.onToggle}>
<span class="am-pr-panel-section-heading-left am-pr-row">
<Icon name={props.open ? "chevron-down" : "chevron-right"} size="small" class="am-pr-section-chevron" />
{props.title}
</span>
<Show when={props.count}>
<span class={`am-pr-panel-section-count ${props.countClass ?? ""}`}>{props.count}</span>
</Show>
</button>
)
}
@@ -0,0 +1,457 @@
/* PR Panel */
.am-pr-panel {
height: 100%;
overflow: hidden;
}
.am-pr-row {
display: flex;
align-items: center;
}
.am-pr-col {
display: flex;
flex-direction: column;
}
.am-pr-panel-header {
align-items: flex-start;
justify-content: space-between;
gap: 8px;
padding: 10px 12px 8px;
border-bottom: 1px solid var(--vscode-panel-border);
flex-shrink: 0;
}
.am-pr-panel-title-row {
gap: 8px;
min-width: 0;
}
.am-pr-panel-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 4px;
border-radius: 4px;
font-size: var(--kilo-font-size-10);
font-variant-numeric: tabular-nums;
background: color-mix(in srgb, var(--am-pr-accent) 15%, transparent);
color: var(--am-pr-accent);
border: 1px solid color-mix(in srgb, var(--am-pr-accent) 40%, transparent);
white-space: nowrap;
flex-shrink: 0;
}
.am-pr-panel-badge.am-pr-badge-pending {
animation: am-pr-badge-pulse 1.5s ease-in-out infinite;
}
.am-pr-panel-title {
font-size: var(--kilo-font-size-14);
color: var(--vscode-foreground);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.am-pr-panel-number {
font-size: var(--kilo-font-size-12);
color: var(--vscode-descriptionForeground);
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
.am-pr-panel-actions {
gap: 2px;
flex-shrink: 0;
}
.am-pr-panel-body {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.am-pr-panel-section {
padding: 4px 12px;
}
.am-pr-panel-section-heading {
justify-content: space-between;
font-size: var(--font-size-small);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-weak);
padding-bottom: 6px;
}
.am-pr-panel-section-toggle {
width: 100%;
background: none;
border: none;
cursor: pointer;
padding: 0;
text-align: left;
border-radius: 3px;
}
.am-pr-panel-section-toggle:hover {
color: var(--vscode-foreground);
}
.am-pr-panel-section-heading-left {
gap: 4px;
}
.am-pr-section-chevron {
flex-shrink: 0;
opacity: 0.6;
}
/* Branch → base row */
.am-pr-panel-branch {
gap: 4px;
flex-wrap: wrap;
}
.am-pr-branch-name {
font-family: var(--font-mono, monospace);
font-size: var(--kilo-font-size-11);
}
.am-pr-branch-arrow {
opacity: 0.5;
flex-shrink: 0;
}
/* Check count color by status */
.am-pr-checks-count-success {
color: var(--vscode-testing-iconPassed, #34d399);
}
.am-pr-checks-count-failure {
color: var(--vscode-testing-iconFailed, #f87171);
}
.am-pr-checks-count-pending {
color: var(--vscode-charts-yellow, #fbbf24);
}
.am-pr-panel-section-count {
font-size: var(--kilo-font-size-11);
font-weight: 400;
text-transform: none;
letter-spacing: 0;
color: var(--text-weak);
}
.am-pr-panel-divider {
height: 1px;
background: var(--vscode-panel-border);
margin: 6px 0;
}
.am-pr-panel-row {
justify-content: space-between;
gap: 8px;
padding: 3px 0;
min-height: 22px;
}
.am-pr-panel-label {
font-size: var(--kilo-font-size-11);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-weaker);
flex-shrink: 0;
}
.am-pr-panel-value {
font-size: var(--kilo-font-size-12);
color: var(--vscode-foreground);
text-align: right;
}
.am-pr-panel-value[data-pr-state="open"] {
color: var(--vscode-testing-iconPassed, #34d399);
}
.am-pr-panel-value[data-pr-state="merged"] {
color: var(--vscode-charts-purple, #a78bfa);
}
.am-pr-panel-value[data-pr-state="closed"] {
color: var(--vscode-testing-iconFailed, #f87171);
}
.am-pr-panel-value[data-pr-state="draft"] {
color: var(--text-weaker);
}
.am-pr-panel-value[data-checks="failure"] {
color: var(--vscode-testing-iconFailed, #f87171);
}
.am-pr-panel-value[data-checks="pending"] {
color: var(--vscode-charts-yellow, #fbbf24);
}
.am-pr-panel-value[data-checks="success"] {
color: var(--vscode-testing-iconPassed, #34d399);
}
.am-pr-panel-diff {
gap: 6px;
}
.am-pr-panel-unresolved {
color: var(--vscode-charts-yellow, #fbbf24);
}
/* PR comment list */
.am-pr-panel-comment-list {
gap: 8px;
margin-top: 6px;
}
.am-pr-panel-comment {
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
padding: 8px;
opacity: 1;
}
.am-pr-panel-comment-resolved {
opacity: 0.5;
}
.am-pr-panel-comment-header {
gap: 6px;
margin-bottom: 4px;
flex-wrap: wrap;
}
.am-pr-panel-comment-author {
font-size: var(--kilo-font-size-12);
font-weight: 600;
color: var(--vscode-foreground);
}
.am-pr-panel-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;
}
.am-pr-panel-comment-resolved-badge {
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;
}
.am-pr-copy-btn {
margin-left: auto;
}
.am-pr-panel-comment-body [data-component="markdown"] {
font-size: var(--kilo-font-size-12);
color: var(--vscode-foreground);
}
.am-pr-panel-description {
line-height: 1.5;
padding: 2px 0 4px;
}
.am-pr-panel-description [data-component="markdown"],
.am-pr-panel-comment-body [data-component="markdown"] {
font-size: var(--kilo-font-size-13);
color: var(--vscode-foreground);
}
.am-pr-panel-description [data-component="markdown"] h1,
.am-pr-panel-description [data-component="markdown"] h2,
.am-pr-panel-description [data-component="markdown"] h3,
.am-pr-panel-description [data-component="markdown"] h4,
.am-pr-panel-description [data-component="markdown"] h5,
.am-pr-panel-description [data-component="markdown"] h6 {
font-weight: 600;
color: var(--vscode-foreground);
margin-top: 0.75em;
margin-bottom: 0.25em;
}
.am-pr-panel-description [data-component="markdown"] h1 {
font-size: var(--kilo-font-size-16);
}
.am-pr-panel-description [data-component="markdown"] h2 {
font-size: var(--kilo-font-size-14);
}
.am-pr-panel-description [data-component="markdown"] h3 {
font-size: var(--kilo-font-size-13);
}
/* PR Reviewers */
.am-pr-panel-reviewers {
gap: 2px;
}
.am-pr-panel-reviewer {
gap: 6px;
padding: 3px 0;
font-size: var(--kilo-font-size-12);
}
.am-pr-reviewer-icon {
flex-shrink: 0;
}
.am-pr-panel-reviewer[data-state="approved"] .am-pr-reviewer-icon {
color: var(--vscode-testing-iconPassed, #34d399);
}
.am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon {
color: var(--vscode-charts-yellow, #fbbf24);
}
.am-pr-panel-reviewer[data-state="commented"] .am-pr-reviewer-icon {
color: var(--text-weak);
}
.am-pr-panel-reviewer[data-state="pending"] .am-pr-reviewer-icon {
color: var(--text-weaker);
}
.am-pr-reviewer-login {
flex: 1;
color: var(--vscode-foreground);
}
.am-pr-reviewer-state {
color: var(--text-weak);
flex-shrink: 0;
}
.am-pr-panel-checks {
gap: 2px;
margin-top: 4px;
}
.am-pr-panel-check-item {
gap: 6px;
padding: 3px 0;
font-size: var(--kilo-font-size-12);
}
.am-pr-check-icon {
flex-shrink: 0;
}
.am-pr-panel-check-item[data-status="success"] .am-pr-check-icon {
color: var(--vscode-testing-iconPassed, #34d399);
}
.am-pr-panel-check-item[data-status="failure"] .am-pr-check-icon {
color: var(--vscode-testing-iconFailed, #f87171);
}
.am-pr-panel-check-item[data-status="cancelled"] .am-pr-check-icon {
color: var(--text-weaker);
}
.am-pr-panel-check-item[data-status="skipped"] .am-pr-check-icon {
color: var(--text-weaker);
}
.am-pr-panel-check-item[data-status="pending"] .am-pr-check-icon {
color: var(--vscode-charts-yellow, #fbbf24);
}
.am-pr-check-name {
flex: 1;
color: var(--vscode-foreground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.am-pr-check-status {
color: var(--text-weak);
flex-shrink: 0;
}
.am-pr-check-duration {
color: var(--text-weaker);
font-size: var(--kilo-font-size-10);
flex-shrink: 0;
}
.am-pr-check-link {
color: var(--text-weak);
display: flex;
align-items: center;
flex-shrink: 0;
cursor: pointer;
text-decoration: none;
}
.am-pr-check-link:hover {
color: var(--text-base);
}
/* PR toggle button in tab bar */
.am-pr-toggle-number {
font-size: var(--kilo-font-size-10);
font-variant-numeric: tabular-nums;
}
/* PR Summary banner */
.am-pr-summary {
margin: 8px 12px;
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
overflow: hidden;
}
.am-pr-summary-header {
justify-content: space-between;
padding: 6px 6px 6px 10px;
border-bottom: 1px solid var(--vscode-panel-border);
background: var(--vscode-editor-background);
}
.am-pr-summary-title {
font-size: var(--font-size-small);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-weak);
}
.am-pr-summary-rows {
padding: 6px 10px;
gap: 4px;
background: var(--vscode-editor-background);
}
.am-pr-summary-row {
gap: 7px;
padding: 2px 0;
font-size: var(--kilo-font-size-12);
}
.am-pr-summary-icon {
flex-shrink: 0;
}
.am-pr-summary-row[data-status="success"] .am-pr-summary-icon {
color: var(--vscode-testing-iconPassed, #34d399);
}
.am-pr-summary-row[data-status="failure"] .am-pr-summary-icon {
color: var(--vscode-testing-iconFailed, #f87171);
}
.am-pr-summary-row[data-status="pending"] .am-pr-summary-icon {
color: var(--vscode-charts-yellow, #fbbf24);
}
.am-pr-summary-row[data-status="warning"] .am-pr-summary-icon {
color: var(--vscode-charts-yellow, #fbbf24);
}
.am-pr-summary-label {
color: var(--vscode-foreground);
}
@@ -0,0 +1,34 @@
// PR sub-types — source of truth for all PR-related types used in the PR panel.
// PRStatus lives in src/types/messages/agent-manager.ts for broad consumption.
export type PRState = "open" | "draft" | "merged" | "closed"
export type ReviewDecision = "approved" | "changes_requested" | "pending"
export type CheckStatus = "success" | "failure" | "pending" | "skipped" | "cancelled"
export type AggregateCheckStatus = "success" | "failure" | "pending" | "none"
export interface PRCheck {
name: string
status: CheckStatus
url?: string
duration?: string
}
export interface PRComment {
id: string
author: string
avatar?: string
body: string
file?: string
line?: number
url?: string
resolved: boolean
createdAt?: number
}
export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented"
export interface PRReviewer {
login: string
avatar?: string
state: ReviewerState
}
@@ -2,6 +2,12 @@ export const MIN_PANEL_WIDTH = 360
const DEFAULT_PANEL_WIDTH_RATIO = 0.5
const MAX_PANEL_WIDTH_RATIO = 0.8
export enum SidePanel {
Diff = "diff",
PR = "pr",
Terminal = "terminal",
}
function viewportWidth(viewport: number): number {
return Number.isFinite(viewport) && viewport > 0 ? viewport : MIN_PANEL_WIDTH
}
@@ -13,11 +13,12 @@
import { createEffect, createSignal, type Accessor } from "solid-js"
import type { ScriptTerminalStatus, TerminalStateControls } from "./state"
import type { TerminalTabStateWithContext } from "./state"
import type { SidePanelState } from "../AgentManagerApp"
interface AmbientSetupDeps {
terms: TerminalStateControls
selection: Accessor<string | null>
sidePanel: Accessor<string | null>
sidePanel: Accessor<SidePanelState>
setSidePanel(panel: null): void
}
@@ -52,6 +52,8 @@ const Root: ParentComponent = (props) => (
<MermaidDownloadBridge />
<ServerProvider>
<LanguageBridge>
{/* MarkedProvider is required here for all markdown consumers in the tree,
including PRPanel's PRDescription and PRComments components. Do not remove. */}
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>
@@ -0,0 +1,43 @@
import type { Message, MessageLoadMode, Part } from "../types/messages"
import { sameParts } from "./session-parts"
import { withoutResolvedSessionErrors } from "./session-errors"
export function mergeMessages(current: Message[], incoming: Message[], mode: Exclude<MessageLoadMode, "focus">) {
const kept = withoutResolvedSessionErrors(current, incoming)
if (mode === "reconcile") {
// Tail reconcile: incoming is the authoritative newest-N snapshot.
// Local state may already hold some of those IDs and may also hold
// newer optimistic entries created after the fetch was taken. Merge
// by id (server wins on collision) then sort by createdAt so new
// server messages land in the right position and optimistic tail
// entries stay at the end.
const byId = new Map<string, Message>()
for (const msg of kept) byId.set(msg.id, msg)
for (const msg of incoming) byId.set(msg.id, msg)
return [...byId.values()].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
}
const seen = new Set<string>()
const source = mode === "prepend" ? [...incoming, ...kept] : incoming
return source.filter((msg) => {
if (seen.has(msg.id)) return false
seen.add(msg.id)
return true
})
}
// Cheap tail check: same ids in the same order and no visible streamed-part
// correction to apply. It skips store churn when SSE already matches the
// snapshot, but lets reconcile heal part removals and finalized text.
export function sameReconcileShape(
current: Message[],
incoming: Message[],
getParts: (messageID: string) => Part[] | undefined,
): boolean {
if (current.length !== incoming.length) return false
for (const [i, n] of incoming.entries()) {
const c = current[i]!
if (c.id !== n.id) return false
if (!sameParts(getParts(c.id) ?? c.parts, n.parts)) return false
}
return true
}
@@ -72,7 +72,8 @@ import { getAgentModel } from "./session-model-store"
import { resolveMessagePrefs } from "./session-preferences"
import { errorIDs, preserveSessionErrors, withoutResolvedSessionErrors } from "./session-errors"
import { PartStash } from "./part-stash"
import { mergeParts, sameParts } from "./session-parts"
import { mergeParts } from "./session-parts"
import { mergeMessages, sameReconcileShape } from "./session-merge"
import { state as todoState } from "./todo-revert"
import { sessionVariantKeys, transferVariants, variantKey } from "./session-variant-store"
import { createSessionVariants } from "./session-variants"
@@ -1381,25 +1382,6 @@ export const SessionProvider: ParentComponent = (props) => {
setPages(sessionID, { ...(pages[sessionID] ?? emptyPageState), ...patch })
}
function mergeMessages(current: Message[], incoming: Message[], mode: Exclude<MessageLoadMode, "focus">) {
const kept = withoutResolvedSessionErrors(current, incoming)
if (mode === "reconcile") {
// Merge the authoritative newest-N snapshot by ID, then sort so newer
// optimistic entries stay at the end.
const byId = new Map<string, Message>()
for (const msg of kept) byId.set(msg.id, msg)
for (const msg of incoming) byId.set(msg.id, msg)
return [...byId.values()].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
}
const seen = new Set<string>()
const source = mode === "prepend" ? [...incoming, ...kept] : incoming
return source.filter((msg) => {
if (seen.has(msg.id)) return false
seen.add(msg.id)
return true
})
}
function recoverPrefs(sessionID: string, messages: Message[], names = agentNames()) {
const prefs = resolveMessagePrefs(messages, names)
if (prefs.agent && !store.agentSelections[sessionID]) {
@@ -1425,19 +1407,6 @@ export const SessionProvider: ParentComponent = (props) => {
return [...merged, ...orphans]
}
// Cheap tail check: same ids in the same order and no visible streamed-part
// correction to apply. It skips store churn when SSE already matches the
// snapshot, but lets reconcile heal part removals and finalized text.
function sameReconcileShape(current: Message[], incoming: Message[]): boolean {
if (current.length !== incoming.length) return false
for (const [i, n] of incoming.entries()) {
const c = current[i]!
if (c.id !== n.id) return false
if (!sameParts(store.parts[c.id] ?? c.parts, n.parts)) return false
}
return true
}
function setTools(sessionID: string, tools: ToolPart[]) {
setStore("toolParts", sessionID, reconcileSessionToolParts(tools))
}
@@ -1486,7 +1455,10 @@ export const SessionProvider: ParentComponent = (props) => {
// Reconcile fast-path: if the tail matches local state shape-wise, every
// message+part-count already agrees with the server. Skip the reactive
// store churn entirely — virtualizer and rendering stay untouched.
if (mode === "reconcile" && sameReconcileShape(store.messages[sessionID] ?? [], messages)) {
if (
mode === "reconcile" &&
sameReconcileShape(store.messages[sessionID] ?? [], messages, (id) => store.parts[id])
) {
const parts = messageParts(messages)
for (const msg of messages) {
if (store.parts[msg.id]) delete parts[msg.id]
@@ -612,7 +612,8 @@ const basePR: PRStatus = {
url: "https://github.com/org/repo/pull/8594",
state: "open",
review: null,
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] },
reviewers: [],
additions: 978,
deletions: 202,
files: 12,
@@ -593,7 +593,8 @@ export const WithPRBadges: Story = {
additions: 120,
deletions: 30,
files: 5,
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
reviewers: [],
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] },
}}
/>
{/* Open + failing checks → green badge, red ✗ (no longer confusable with closed) */}
@@ -611,7 +612,8 @@ export const WithPRBadges: Story = {
additions: 15,
deletions: 8,
files: 3,
checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, items: [] },
reviewers: [],
checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, checks: [] },
}}
/>
{/* Open + changes requested → green badge, amber warning */}
@@ -629,7 +631,8 @@ export const WithPRBadges: Story = {
additions: 60,
deletions: 12,
files: 4,
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
reviewers: [],
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] },
}}
/>
{/* Open + checks running → pulsing amber badge (animation disabled in snapshots) */}
@@ -647,7 +650,8 @@ export const WithPRBadges: Story = {
additions: 200,
deletions: 0,
files: 8,
checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, items: [] },
reviewers: [],
checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, checks: [] },
}}
/>
{/* Draft → gray badge */}
@@ -665,7 +669,8 @@ export const WithPRBadges: Story = {
additions: 30,
deletions: 5,
files: 2,
checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, items: [] },
reviewers: [],
checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] },
}}
/>
{/* Merged → purple badge, no status icon */}
@@ -683,7 +688,8 @@ export const WithPRBadges: Story = {
additions: 90,
deletions: 20,
files: 6,
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] },
reviewers: [],
checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] },
}}
/>
{/* Closed → red badge, no status icon (distinct from a failing open PR) */}
@@ -701,7 +707,8 @@ export const WithPRBadges: Story = {
additions: 10,
deletions: 4,
files: 1,
checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, items: [] },
reviewers: [],
checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, checks: [] },
}}
/>
</div>
@@ -45,36 +45,31 @@ export interface SectionState {
}
// ---------------------------------------------------------------------------
// PR status types (mirrored from extension types.ts)
// PR status types — sub-types live in agent-manager/pr/pr-types.ts
// ---------------------------------------------------------------------------
export type PRState = "open" | "draft" | "merged" | "closed"
export type ReviewDecision = "approved" | "changes_requested" | "pending"
export type CheckStatus = "success" | "failure" | "pending" | "skipped" | "cancelled"
export type AggregateCheckStatus = "success" | "failure" | "pending" | "none"
export interface PRCheck {
name: string
status: CheckStatus
url?: string
duration?: string
}
export interface PRComment {
id: string
author: string
avatar?: string
body: string
file?: string
line?: number
url?: string
resolved: boolean
createdAt?: number
}
import type {
PRState,
ReviewDecision,
AggregateCheckStatus,
PRCheck,
PRComment,
PRReviewer,
} from "../../../agent-manager/pr/pr-types"
export type {
PRState,
ReviewDecision,
CheckStatus,
AggregateCheckStatus,
PRCheck,
PRComment,
PRReviewer,
} from "../../../agent-manager/pr/pr-types"
export interface PRStatus {
number: number
title: string
body?: string
url: string
state: PRState
review: ReviewDecision | null
@@ -84,12 +79,13 @@ export interface PRStatus {
passed: number
failed: number
pending: number
items: PRCheck[]
checks: PRCheck[]
}
reviewers: PRReviewer[]
comments?: {
total: number
unresolved: number
items: PRComment[]
comments: PRComment[]
}
additions: number
deletions: number
@@ -1047,6 +1047,11 @@ export interface AgentManagerPRStatusMessage {
error?: "gh_missing" | "gh_auth" | "fetch_failed"
}
export interface AgentManagerPRErrorMessage {
type: "agentManager.prError"
error: "gh_missing" | "gh_auth" | "fetch_failed"
}
// Sidebar: Live worktree diff stats (extension → webview)
export interface WorktreeStatsLoadedMessage {
type: "worktreeStatsLoaded"
@@ -1412,6 +1417,7 @@ export type ExtensionMessage =
| AgentManagerWorktreeStatsMessage
| AgentManagerLocalStatsMessage
| AgentManagerPRStatusMessage
| AgentManagerPRErrorMessage
| AgentManagerTerminalCreatedMessage
| AgentManagerTerminalRestartedMessage
| AgentManagerTerminalFontChangedMessage
@@ -988,6 +988,7 @@ export interface OpenPRMessage {
type: "agentManager.openPR"
projectId?: string
worktreeId: string
url?: string
}
export interface ApplyWorktreeDiffMessage {