feat(vscode): show deterministic worktree diff and origin commit stats (#6383)

* tmp

* fix(vscode): prevent overlapping stats polling and localize hover labels

* fix(vscode): harden worktree stats polling and i18n lint

* refactor(vscode): extract worktree stats poller
This commit is contained in:
Marius
2026-02-26 13:00:56 +00:00
committed by GitHub
parent 33a285b857
commit 0fe8edc2b8
22 changed files with 644 additions and 20 deletions
@@ -5,6 +5,7 @@ import { KiloProvider } from "../KiloProvider"
import { buildWebviewHtml } from "../utils"
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
import { WorktreeStateManager } from "./WorktreeStateManager"
import { WorktreeStatsPoller } from "./WorktreeStatsPoller"
import { versionedName } from "./branch-name"
import { normalizePath } from "./git-import"
import { SetupScriptService } from "./SetupScriptService"
@@ -40,6 +41,7 @@ export class AgentManagerProvider implements vscode.Disposable {
private diffInterval: ReturnType<typeof setInterval> | undefined
private diffSessionId: string | undefined
private lastDiffHash: string | undefined
private statsPoller: WorktreeStatsPoller
private cachedDiffTarget: { directory: string; baseBranch: string } | undefined
constructor(
@@ -50,6 +52,14 @@ export class AgentManagerProvider implements vscode.Disposable {
this.terminalManager = new SessionTerminalManager((msg) =>
this.outputChannel.appendLine(`[SessionTerminal] ${msg}`),
)
this.statsPoller = new WorktreeStatsPoller({
getWorktrees: () => this.state?.getWorktrees() ?? [],
getHttpClient: () => this.connectionService.getHttpClient(),
onStats: (stats) => {
this.postToWebview({ type: "agentManager.worktreeStats", stats })
},
log: (...args) => this.log(...args),
})
}
private log(...args: unknown[]) {
@@ -95,6 +105,8 @@ export class AgentManagerProvider implements vscode.Disposable {
this.panel.onDidDispose(() => {
this.log("Panel disposed")
this.statsPoller.stop()
this.stopDiffPolling()
this.provider?.dispose()
this.provider = undefined
this.panel = undefined
@@ -1212,6 +1224,10 @@ export class AgentManagerProvider implements vscode.Disposable {
sessionsCollapsed: state.getSessionsCollapsed(),
isGitRepo: true,
})
// Keep stats polling in sync with worktree count
const worktrees = state.getWorktrees()
this.statsPoller.setEnabled(worktrees.length > 0)
}
/** Push empty state when the workspace is not a git repo or has no workspace folder. */
@@ -1463,6 +1479,7 @@ export class AgentManagerProvider implements vscode.Disposable {
public dispose(): void {
this.stopDiffPolling()
this.statsPoller.stop()
this.terminalManager.dispose()
this.provider?.dispose()
this.panel?.dispose()
@@ -0,0 +1,214 @@
import * as cp from "child_process"
import * as nodePath from "path"
import type { HttpClient } from "../services/cli-backend"
import type { Worktree } from "./WorktreeStateManager"
export interface WorktreeStats {
worktreeId: string
additions: number
deletions: number
commits: number
}
interface WorktreeStatsPollerOptions {
getWorktrees: () => Worktree[]
getHttpClient: () => HttpClient
onStats: (stats: WorktreeStats[]) => void
log: (...args: unknown[]) => void
intervalMs?: number
refreshMs?: number
runGit?: (args: string[], cwd: string) => Promise<string>
}
export class WorktreeStatsPoller {
private timer: ReturnType<typeof setTimeout> | undefined
private active = false
private busy = false
private lastHash: string | undefined
private lastStats: Record<string, { additions: number; deletions: number; commits: number }> = {}
private lastFetch = new Map<string, number>()
private inflightFetch = new Map<string, Promise<void>>()
private readonly intervalMs: number
private readonly refreshMs: number
private readonly runGit: (args: string[], cwd: string) => Promise<string>
constructor(private readonly options: WorktreeStatsPollerOptions) {
this.intervalMs = options.intervalMs ?? 5000
this.refreshMs = options.refreshMs ?? 120000
this.runGit =
options.runGit ??
((args, cwd) =>
new Promise((resolve, reject) => {
cp.execFile("git", args, { cwd, timeout: 10000 }, (err, stdout) => {
if (err) reject(err)
else resolve(stdout.trim())
})
}))
}
setEnabled(enabled: boolean): void {
if (enabled) {
if (this.active) return
this.start()
return
}
this.stop()
}
stop(): void {
this.active = false
if (this.timer) {
clearTimeout(this.timer)
this.timer = undefined
}
this.busy = false
this.lastHash = undefined
this.lastStats = {}
}
private start(): void {
this.stop()
this.active = true
void this.poll()
}
private schedule(delay: number): void {
if (!this.active) return
this.timer = setTimeout(() => {
void this.poll()
}, delay)
}
private poll(): Promise<void> {
if (!this.active) return Promise.resolve()
if (this.busy) return Promise.resolve()
this.busy = true
return this.fetch().finally(() => {
this.busy = false
this.schedule(this.intervalMs)
})
}
private async fetch(): Promise<void> {
const worktrees = this.options.getWorktrees()
if (worktrees.length === 0) return
const client = (() => {
try {
return this.options.getHttpClient()
} catch (err) {
this.options.log("Failed to get HTTP client for worktree stats:", err)
return undefined
}
})()
if (!client) return
const stats = (
await Promise.all(
worktrees.map(async (wt) => {
try {
const diffs = await client.getWorktreeDiff(wt.path, wt.parentBranch)
const additions = diffs.reduce((sum, diff) => sum + diff.additions, 0)
const deletions = diffs.reduce((sum, diff) => sum + diff.deletions, 0)
const commits = await this.countMissingOriginCommits(wt.path, wt.parentBranch)
return { worktreeId: wt.id, additions, deletions, commits }
} catch (err) {
this.options.log(`Failed to fetch worktree stats for ${wt.branch} (${wt.path}):`, err)
const prev = this.lastStats[wt.id]
if (!prev) return undefined
return {
worktreeId: wt.id,
additions: prev.additions,
deletions: prev.deletions,
commits: prev.commits,
}
}
}),
)
).filter((item): item is WorktreeStats => !!item)
if (stats.length === 0) return
const hash = stats.map((item) => `${item.worktreeId}:${item.additions}:${item.deletions}:${item.commits}`).join("|")
if (hash === this.lastHash) return
this.lastHash = hash
this.lastStats = stats.reduce(
(acc, item) => {
acc[item.worktreeId] = {
additions: item.additions,
deletions: item.deletions,
commits: item.commits,
}
return acc
},
{} as Record<string, { additions: number; deletions: number; commits: number }>,
)
this.options.onStats(stats)
}
private gitExec(args: string[], cwd: string): Promise<string> {
return this.runGit(args, cwd)
}
private hasRemoteRef(cwd: string, ref: string): Promise<boolean> {
return this.gitExec(["rev-parse", "--verify", "--quiet", `refs/remotes/${ref}`], cwd)
.then(() => true)
.catch(() => false)
}
private async refreshRemote(cwd: string, remote: string): Promise<void> {
if (!remote) return
const commonRaw = await this.gitExec(["rev-parse", "--git-common-dir"], cwd).catch(() => cwd)
const common = nodePath.isAbsolute(commonRaw) ? commonRaw : nodePath.resolve(cwd, commonRaw)
const key = `${common}:${remote}`
const existing = this.inflightFetch.get(key)
if (existing) return existing
const prev = this.lastFetch.get(key) ?? 0
const now = Date.now()
if (now - prev < this.refreshMs) return
this.lastFetch.set(key, now)
const job = this.gitExec(["fetch", "--quiet", "--no-tags", remote], cwd)
.catch((err) => {
this.options.log(`Failed to refresh remote refs for ${cwd}:`, err)
})
.then(() => undefined)
.finally(() => {
this.inflightFetch.delete(key)
})
this.inflightFetch.set(key, job)
return job
}
private async countMissingOriginCommits(cwd: string, parentBranch: string): Promise<number> {
const upstream = await this.gitExec(
["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
cwd,
).catch(() => "")
const branch = await this.gitExec(["branch", "--show-current"], cwd).catch(() => "")
const branchRemote = branch ? await this.gitExec(["config", `branch.${branch}.remote`], cwd).catch(() => "") : ""
const upstreamRemote = upstream.includes("/") ? upstream.split("/")[0] : ""
const remote = upstreamRemote || branchRemote || "origin"
await this.refreshRemote(cwd, remote)
if (upstream) {
const count = await this.gitExec(["rev-list", "--count", `${upstream}..HEAD`], cwd).catch(() => "0")
return parseInt(count, 10) || 0
}
const remoteBranch = branch ? `${remote}/${branch}` : ""
const hasRemoteBranch = remoteBranch ? await this.hasRemoteRef(cwd, remoteBranch) : false
const remoteParent = `${remote}/${parentBranch}`
const hasRemoteParent = await this.hasRemoteRef(cwd, remoteParent)
const ref = hasRemoteBranch ? remoteBranch : hasRemoteParent ? remoteParent : parentBranch
const count = await this.gitExec(["rev-list", "--count", `${ref}..HEAD`], cwd).catch(() => "0")
return parseInt(count, 10) || 0
}
}
@@ -0,0 +1,148 @@
import { describe, it, expect } from "bun:test"
import type { HttpClient } from "../../src/services/cli-backend"
import { WorktreeStatsPoller } from "../../src/agent-manager/WorktreeStatsPoller"
import type { Worktree } from "../../src/agent-manager/WorktreeStateManager"
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function waitFor(check: () => boolean, timeout = 500): Promise<void> {
const start = Date.now()
while (!check()) {
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
await sleep(5)
}
}
function worktree(id: string): Worktree {
return {
id,
branch: `branch-${id}`,
path: `/tmp/${id}`,
parentBranch: "main",
createdAt: "2026-01-01T00:00:00.000Z",
}
}
function diff(additions: number, deletions: number) {
return [{ file: "file.ts", before: "", after: "", additions, deletions, status: "modified" as const }]
}
describe("WorktreeStatsPoller", () => {
it("does not overlap polling runs", async () => {
let running = 0
let max = 0
let calls = 0
const client = {
getWorktreeDiff: async () => {
calls += 1
running += 1
max = Math.max(max, running)
await sleep(40)
running -= 1
return diff(2, 1)
},
} as unknown as HttpClient
const poller = new WorktreeStatsPoller({
getWorktrees: () => [worktree("a")],
getHttpClient: () => client,
onStats: () => undefined,
log: () => undefined,
intervalMs: 5,
runGit: async (args) => {
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
if (args[0] === "fetch") return ""
if (args[0] === "rev-list") return "1"
return ""
},
})
poller.setEnabled(true)
await waitFor(() => calls >= 2)
poller.stop()
expect(max).toBe(1)
})
it("keeps last-known stats when a later poll fails", async () => {
let calls = 0
const emitted: Array<Array<{ worktreeId: string; additions: number; deletions: number; commits: number }>> = []
const client = {
getWorktreeDiff: async () => {
calls += 1
if (calls === 1) return diff(7, 3)
throw new Error("transient backend failure")
},
} as unknown as HttpClient
const poller = new WorktreeStatsPoller({
getWorktrees: () => [worktree("a")],
getHttpClient: () => client,
onStats: (stats) => emitted.push(stats),
log: () => undefined,
intervalMs: 5,
runGit: async (args) => {
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git"
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main"
if (args[0] === "fetch") return ""
if (args[0] === "rev-list") return "2"
return ""
},
})
poller.setEnabled(true)
await waitFor(() => calls >= 2)
poller.stop()
expect(emitted.length).toBeGreaterThan(0)
const first = emitted[0]
if (!first) throw new Error("expected emitted stats")
expect(first[0]).toEqual({ worktreeId: "a", additions: 7, deletions: 3, commits: 2 })
const hasZeros = emitted.some((batch) =>
batch.some((item) => item.additions === 0 && item.deletions === 0 && item.commits === 0),
)
expect(hasZeros).toBe(false)
})
it("refreshes upstream remote once for concurrent worktrees", async () => {
const commands: string[][] = []
const emitted: Array<Array<{ worktreeId: string; additions: number; deletions: number; commits: number }>> = []
const client = {
getWorktreeDiff: async () => diff(0, 0),
} as unknown as HttpClient
const poller = new WorktreeStatsPoller({
getWorktrees: () => [worktree("a"), worktree("b")],
getHttpClient: () => client,
onStats: (stats) => emitted.push(stats),
log: () => undefined,
intervalMs: 500,
runGit: async (args) => {
commands.push(args)
if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return "/repo/.git"
if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "upstream/main"
if (args[0] === "branch") return "feature"
if (args[0] === "config") return "origin"
if (args[0] === "fetch") return ""
if (args[0] === "rev-list") return "0"
return ""
},
})
poller.setEnabled(true)
await waitFor(() => emitted.length >= 1)
poller.stop()
const fetches = commands.filter((cmd) => cmd[0] === "fetch")
expect(fetches.length).toBe(1)
const fetch = fetches[0]
if (!fetch) throw new Error("expected fetch command")
expect(fetch[3]).toBe("upstream")
})
})
@@ -23,7 +23,9 @@ import type {
AgentManagerImportResultMessage,
AgentManagerWorktreeDiffMessage,
AgentManagerWorktreeDiffLoadingMessage,
AgentManagerWorktreeStatsMessage,
WorktreeFileDiff,
WorktreeGitStats,
WorktreeState,
ManagedSessionState,
SessionInfo,
@@ -304,6 +306,9 @@ const AgentManagerContent: Component = () => {
const [diffLoading, setDiffLoading] = createSignal(false)
const [diffWidth, setDiffWidth] = createSignal(Math.round(window.innerWidth * 0.5))
// Per-worktree git stats (diff additions/deletions, commits missing from origin)
const [worktreeStats, setWorktreeStats] = createSignal<Record<string, WorktreeGitStats>>({})
// Pending local tab counter for generating unique IDs
let pendingCounter = 0
const PENDING_PREFIX = "pending:"
@@ -842,6 +847,13 @@ const AgentManagerContent: Component = () => {
const ev = msg as AgentManagerWorktreeDiffLoadingMessage
setDiffLoading(ev.loading)
}
if (msg.type === "agentManager.worktreeStats") {
const ev = msg as AgentManagerWorktreeStatsMessage
const map: Record<string, WorktreeGitStats> = {}
for (const s of ev.stats) map[s.worktreeId] = s
setWorktreeStats(map)
}
})
onCleanup(() => {
@@ -1378,13 +1390,38 @@ const AgentManagerContent: Component = () => {
</Show>
{(() => {
const num = idx() + 2
const stats = () => worktreeStats()[wt.id]
return (
<Show when={num <= MAX_JUMP_INDEX}>
<span class="am-shortcut-badge">
{isMac ? "⌘" : "Ctrl+"}
{num}
</span>
</Show>
<>
<Show when={num <= MAX_JUMP_INDEX}>
<span class="am-shortcut-badge">
{isMac ? "⌘" : "Ctrl+"}
{num}
</span>
</Show>
<Show
when={
stats() &&
(stats()!.additions > 0 || stats()!.deletions > 0 || stats()!.commits > 0)
}
>
<div class="am-worktree-stats">
<Show when={stats()!.additions > 0 || stats()!.deletions > 0}>
<span class="am-worktree-diff-stats">
<Show when={stats()!.additions > 0}>
<span class="am-stat-additions">+{stats()!.additions}</span>
</Show>
<Show when={stats()!.deletions > 0}>
<span class="am-stat-deletions">{stats()!.deletions}</span>
</Show>
</span>
</Show>
<Show when={stats()!.commits > 0}>
<span class="am-worktree-commits">{stats()!.commits}</span>
</Show>
</div>
</Show>
</>
)
})()}
<Show when={!busyWorktrees().has(wt.id)}>
@@ -1434,6 +1471,44 @@ const AgentManagerContent: Component = () => {
<span class="am-hover-card-row-label">{t("agentManager.hoverCard.sessions")}</span>
<span class="am-hover-card-row-value">{sessions().length}</span>
</div>
{(() => {
const hoverStats = () => worktreeStats()[wt.id]
return (
<Show
when={
hoverStats() &&
(hoverStats()!.additions > 0 ||
hoverStats()!.deletions > 0 ||
hoverStats()!.commits > 0)
}
>
<div class="am-hover-card-divider" />
<Show when={hoverStats()!.additions > 0 || hoverStats()!.deletions > 0}>
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">
{t("agentManager.hoverCard.changes")}
</span>
<span class="am-hover-card-row-value am-hover-card-diff-stats">
<Show when={hoverStats()!.additions > 0}>
<span class="am-stat-additions">+{hoverStats()!.additions}</span>
</Show>
<Show when={hoverStats()!.deletions > 0}>
<span class="am-stat-deletions">{hoverStats()!.deletions}</span>
</Show>
</span>
</div>
</Show>
<Show when={hoverStats()!.commits > 0}>
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">
{t("agentManager.hoverCard.commits")}
</span>
<span class="am-hover-card-row-value">{hoverStats()!.commits}</span>
</div>
</Show>
</Show>
)
})()}
</div>
</HoverCard>
</>
@@ -1595,20 +1670,36 @@ const AgentManagerContent: Component = () => {
/>
</TooltipKeybind>
<div class="am-tab-actions">
<TooltipKeybind
title={t("agentManager.diff.toggle")}
keybind={kb().toggleDiff ?? ""}
placement="bottom"
>
<IconButton
icon="layers"
size="small"
variant="ghost"
label={t("agentManager.diff.toggle")}
class={diffOpen() ? "am-tab-diff-btn-active" : ""}
onClick={() => setDiffOpen((prev) => !prev)}
/>
</TooltipKeybind>
{(() => {
const sel = () => selection()
const stats = () =>
typeof sel() === "string" && sel() !== LOCAL ? worktreeStats()[sel() as string] : undefined
const hasChanges = () => {
const s = stats()
return s && (s.additions > 0 || s.deletions > 0)
}
return (
<TooltipKeybind
title={t("agentManager.diff.toggle")}
keybind={kb().toggleDiff ?? ""}
placement="bottom"
>
<button
class={`am-diff-toggle-btn ${diffOpen() ? "am-tab-diff-btn-active" : ""} ${hasChanges() ? "am-diff-toggle-has-changes" : ""}`}
onClick={() => setDiffOpen((prev) => !prev)}
title={t("agentManager.diff.toggle")}
>
<Icon name="layers" size="small" />
<Show when={hasChanges()}>
<span class="am-diff-toggle-stats">
<span class="am-stat-additions">+{stats()!.additions}</span>
<span class="am-stat-deletions">{stats()!.deletions}</span>
</span>
</Show>
</button>
</TooltipKeybind>
)
})()}
<TooltipKeybind
title={t("agentManager.tab.terminal")}
keybind={kb().showTerminal ?? ""}
@@ -296,6 +296,60 @@ button.am-section-toggle:hover .am-section-label {
flex-shrink: 0;
}
/* Per-worktree git stats (diff lines + commits missing from origin) */
.am-worktree-stats {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
font-family: var(--font-mono, monospace);
font-size: 10px;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.am-worktree-diff-stats {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
border-radius: var(--radius-sm);
background: var(--surface-inset-base);
}
.am-worktree-item-active .am-worktree-diff-stats {
background: rgba(255, 255, 255, 0.12);
}
.am-stat-additions {
color: #34d399;
}
.am-stat-deletions {
color: #f87171;
}
.am-worktree-item-active .am-stat-additions {
color: #6ee7b7;
}
.am-worktree-item-active .am-stat-deletions {
color: #fca5a5;
}
.am-worktree-commits {
color: #34d399;
}
.am-worktree-item-active .am-worktree-commits {
color: #6ee7b7;
}
.am-worktree-item:hover .am-worktree-stats {
opacity: 0;
}
/* Grouped worktrees — visual grouping with header and left accent */
.am-wt-group-header {
@@ -665,6 +719,51 @@ button.am-section-toggle:hover .am-section-label {
/* Diff Panel */
.am-diff-toggle-btn {
display: inline-flex;
align-items: center;
gap: 5px;
border: none;
background: transparent;
color: var(--text-weak);
cursor: pointer;
padding: 4px 6px;
border-radius: var(--radius-sm);
font-family: var(--font-mono, monospace);
font-size: 10px;
font-variant-numeric: tabular-nums;
line-height: 1;
white-space: nowrap;
}
.am-diff-toggle-btn:hover {
background: var(--surface-inset-base-hover);
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-diff-toggle-has-changes {
color: var(--text-base);
}
.am-diff-toggle-stats {
display: inline-flex;
align-items: center;
gap: 4px;
}
.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-additions {
color: #6ee7b7;
}
.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-deletions {
color: #fca5a5;
}
.am-tab-diff-btn-active {
background: var(--surface-interactive-base) !important;
color: var(--text-base) !important;
@@ -1720,6 +1819,14 @@ button.am-section-toggle:hover .am-section-label {
color: var(--text-base);
}
.am-hover-card-diff-stats {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono, monospace);
font-variant-numeric: tabular-nums;
}
/* Keyboard shortcuts dialog */
.am-shortcuts {
@@ -953,6 +953,8 @@ export const dict = {
"agentManager.hoverCard.branch": "الفرع",
"agentManager.hoverCard.base": "الأساس",
"agentManager.hoverCard.sessions": "الجلسات",
"agentManager.hoverCard.changes": "التغييرات",
"agentManager.hoverCard.commits": "عمليات الالتزام",
"agentManager.session.new": "جلسة جديدة",
"agentManager.session.untitled": "بدون عنوان",
"agentManager.session.newSession": "جلسة جديدة",
@@ -967,6 +967,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sessões",
"agentManager.hoverCard.changes": "Alterações",
"agentManager.hoverCard.commits": "Commits",
"agentManager.session.new": "Nova sessão",
"agentManager.session.untitled": "Sem título",
"agentManager.session.newSession": "Nova Sessão",
@@ -989,6 +989,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Baza",
"agentManager.hoverCard.sessions": "Sesije",
"agentManager.hoverCard.changes": "Promjene",
"agentManager.hoverCard.commits": "Commiti",
"agentManager.session.new": "Nova sesija",
"agentManager.session.untitled": "Bez naslova",
"agentManager.session.newSession": "Nova sesija",
@@ -962,6 +962,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sessioner",
"agentManager.hoverCard.changes": "Ændringer",
"agentManager.hoverCard.commits": "Commits",
"agentManager.session.new": "Ny session",
"agentManager.session.untitled": "Uden titel",
"agentManager.session.newSession": "Ny session",
@@ -975,6 +975,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Basis",
"agentManager.hoverCard.sessions": "Sitzungen",
"agentManager.hoverCard.changes": "Änderungen",
"agentManager.hoverCard.commits": "Commits",
"agentManager.session.new": "Neue Sitzung",
"agentManager.session.untitled": "Unbenannt",
"agentManager.session.newSession": "Neue Sitzung",
@@ -1011,6 +1011,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sessions",
"agentManager.hoverCard.changes": "Changes",
"agentManager.hoverCard.commits": "Commits",
"agentManager.session.new": "New session",
"agentManager.session.untitled": "Untitled",
@@ -969,6 +969,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sesiones",
"agentManager.hoverCard.changes": "Cambios",
"agentManager.hoverCard.commits": "Commits",
"agentManager.session.new": "Nueva sesión",
"agentManager.session.untitled": "Sin título",
"agentManager.session.newSession": "Nueva Sesión",
@@ -977,6 +977,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCHE",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Sessions",
"agentManager.hoverCard.changes": "Modifications",
"agentManager.hoverCard.commits": "Commits",
"agentManager.session.new": "Nouvelle session",
"agentManager.session.untitled": "Sans titre",
"agentManager.session.newSession": "Nouvelle session",
@@ -957,6 +957,8 @@ export const dict = {
"agentManager.hoverCard.branch": "ブランチ",
"agentManager.hoverCard.base": "ベース",
"agentManager.hoverCard.sessions": "セッション",
"agentManager.hoverCard.changes": "変更",
"agentManager.hoverCard.commits": "コミット",
"agentManager.session.new": "新しいセッション",
"agentManager.session.untitled": "無題",
"agentManager.session.newSession": "新しいセッション",
@@ -958,6 +958,8 @@ export const dict = {
"agentManager.hoverCard.branch": "브랜치",
"agentManager.hoverCard.base": "베이스",
"agentManager.hoverCard.sessions": "세션",
"agentManager.hoverCard.changes": "변경 사항",
"agentManager.hoverCard.commits": "커밋",
"agentManager.session.new": "새 세션",
"agentManager.session.untitled": "제목 없음",
"agentManager.session.newSession": "새 세션",
@@ -963,6 +963,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Base",
"agentManager.hoverCard.sessions": "Økter",
"agentManager.hoverCard.changes": "Endringer",
"agentManager.hoverCard.commits": "Commits",
"agentManager.session.new": "Ny økt",
"agentManager.session.untitled": "Uten tittel",
"agentManager.session.newSession": "Ny økt",
@@ -964,6 +964,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "Baza",
"agentManager.hoverCard.sessions": "Sesje",
"agentManager.hoverCard.changes": "Zmiany",
"agentManager.hoverCard.commits": "Commity",
"agentManager.session.new": "Nowa sesja",
"agentManager.session.untitled": "Bez tytułu",
"agentManager.session.newSession": "Nowa sesja",
@@ -966,6 +966,8 @@ export const dict = {
"agentManager.hoverCard.branch": "ВЕТКА",
"agentManager.hoverCard.base": "Основа",
"agentManager.hoverCard.sessions": "Сессии",
"agentManager.hoverCard.changes": "Изменения",
"agentManager.hoverCard.commits": "Коммиты",
"agentManager.session.new": "Новая сессия",
"agentManager.session.untitled": "Без названия",
"agentManager.session.newSession": "Новая сессия",
@@ -950,6 +950,8 @@ export const dict = {
"agentManager.hoverCard.branch": "BRANCH",
"agentManager.hoverCard.base": "ฐาน",
"agentManager.hoverCard.sessions": "เซสชัน",
"agentManager.hoverCard.changes": "การเปลี่ยนแปลง",
"agentManager.hoverCard.commits": "คอมมิต",
"agentManager.session.new": "เซสชันใหม่",
"agentManager.session.untitled": "ไม่มีชื่อ",
"agentManager.session.newSession": "เซสชันใหม่",
@@ -953,6 +953,8 @@ export const dict = {
"agentManager.hoverCard.branch": "分支",
"agentManager.hoverCard.base": "基础",
"agentManager.hoverCard.sessions": "会话",
"agentManager.hoverCard.changes": "更改",
"agentManager.hoverCard.commits": "提交",
"agentManager.session.new": "新建会话",
"agentManager.session.untitled": "无标题",
"agentManager.session.newSession": "新建会话",
@@ -949,6 +949,8 @@ export const dict = {
"agentManager.hoverCard.branch": "分支",
"agentManager.hoverCard.base": "基底",
"agentManager.hoverCard.sessions": "工作階段",
"agentManager.hoverCard.changes": "變更",
"agentManager.hoverCard.commits": "提交",
"agentManager.session.new": "新建工作階段",
"agentManager.session.untitled": "未命名",
"agentManager.session.newSession": "新建工作階段",
@@ -732,6 +732,20 @@ export interface AgentManagerWorktreeDiffLoadingMessage {
loading: boolean
}
// Per-worktree git stats: diff additions/deletions and commits missing from origin
export interface WorktreeGitStats {
worktreeId: string
additions: number
deletions: number
commits: number
}
// Agent Manager: Worktree git stats push (extension → webview)
export interface AgentManagerWorktreeStatsMessage {
type: "agentManager.worktreeStats"
stats: WorktreeGitStats[]
}
// Request webview to send initial prompt to a newly created session (extension → webview)
export interface AgentManagerSendInitialMessage {
type: "agentManager.sendInitialMessage"
@@ -801,6 +815,7 @@ export type ExtensionMessage =
| WorkspaceDirectoryChangedMessage
| AgentManagerWorktreeDiffMessage
| AgentManagerWorktreeDiffLoadingMessage
| AgentManagerWorktreeStatsMessage
// ============================================
// Messages FROM webview TO extension