mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #12961 from Kilo-Org/feature/am-pr-view
feat(vscode): Agent Manager PR View Panel
This commit is contained in:
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197
|
||||
size 3694
|
||||
oid sha256:33ee6eb7070237487d1931f32ac77a2549e18e664b993895e9a9d6619527d0aa
|
||||
size 4063
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197
|
||||
size 3694
|
||||
oid sha256:33ee6eb7070237487d1931f32ac77a2549e18e664b993895e9a9d6619527d0aa
|
||||
size 4063
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user