From 8cca3d8784dbb1159c1c96f890dc030fb87adc4e Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 6 Mar 2026 14:01:20 +0100 Subject: [PATCH] feat(agent-manager): default base branch selection and BranchSelect component (#6701) - Add persistent default base branch with picker dialog - Extract BranchSelect reusable component from duplicated branch list UI - Add base-branch.ts pure logic module with normalizeBaseBranch/chooseBaseBranch - Add isCheckedOut flag to branch list items - Update WorktreeStateManager to persist defaultBaseBranch - Add i18n keys for all 16 locales - Add unit tests for base-branch logic - Update architecture tests --- .../src/agent-manager/AgentManagerProvider.ts | 60 +++- .../src/agent-manager/WorktreeStateManager.ts | 19 ++ .../src/agent-manager/base-branch.ts | 34 ++ .../src/agent-manager/git-import.ts | 1 + .../tests/unit/agent-manager-arch.test.ts | 2 + .../tests/unit/base-branch.test.ts | 59 ++++ .../agent-manager/AgentManagerApp.tsx | 320 ++++++++++-------- .../webview-ui/agent-manager/BranchSelect.tsx | 128 +++++++ .../agent-manager/agent-manager.css | 16 + .../webview-ui/agent-manager/i18n/ar.ts | 2 + .../webview-ui/agent-manager/i18n/br.ts | 2 + .../webview-ui/agent-manager/i18n/bs.ts | 2 + .../webview-ui/agent-manager/i18n/da.ts | 2 + .../webview-ui/agent-manager/i18n/de.ts | 2 + .../webview-ui/agent-manager/i18n/en.ts | 3 +- .../webview-ui/agent-manager/i18n/es.ts | 2 + .../webview-ui/agent-manager/i18n/fr.ts | 2 + .../webview-ui/agent-manager/i18n/ja.ts | 2 + .../webview-ui/agent-manager/i18n/ko.ts | 2 + .../webview-ui/agent-manager/i18n/no.ts | 2 + .../webview-ui/agent-manager/i18n/pl.ts | 2 + .../webview-ui/agent-manager/i18n/ru.ts | 2 + .../webview-ui/agent-manager/i18n/th.ts | 2 + .../webview-ui/agent-manager/i18n/zh.ts | 2 + .../webview-ui/agent-manager/i18n/zht.ts | 2 + .../webview-ui/src/types/messages.ts | 10 + 26 files changed, 541 insertions(+), 141 deletions(-) create mode 100644 packages/kilo-vscode/src/agent-manager/base-branch.ts create mode 100644 packages/kilo-vscode/tests/unit/base-branch.test.ts create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/BranchSelect.tsx diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 7c368ab63c..b8b1275adb 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -9,6 +9,7 @@ import { KiloProvider } from "../KiloProvider" import { buildWebviewHtml } from "../utils" import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager" import { WorktreeStateManager } from "./WorktreeStateManager" +import { chooseBaseBranch, normalizeBaseBranch } from "./base-branch" import { GitStatsPoller, type WorktreePresenceResult } from "./GitStatsPoller" import { GitOps, type ApplyConflict } from "./GitOps" import { versionedName } from "./branch-name" @@ -281,6 +282,12 @@ export class AgentManagerProvider implements vscode.Disposable { return null } + if (type === "agentManager.setDefaultBaseBranch") { + const branch = normalizeBaseBranch(msg.branch as string | undefined) + this.state?.setDefaultBaseBranch(branch) + this.pushState() + return null + } if (type === "agentManager.requestExternalWorktrees") { void this.onRequestExternalWorktrees() return null @@ -386,6 +393,31 @@ export class AgentManagerProvider implements vscode.Disposable { // Shared helpers // --------------------------------------------------------------------------- + /** Resolve the effective base branch using the configured default, explicit override, and existence check. */ + private async resolveBaseBranch( + manager: WorktreeManager, + state: WorktreeStateManager, + explicit?: string, + ): Promise { + const configured = state.getDefaultBaseBranch() + if (!configured && !explicit) return undefined + + const configuredExists = configured ? await manager.branchExists(configured) : false + const result = chooseBaseBranch({ explicit, configured, configuredExists }) + + if (result.stale) { + this.clearStaleDefaultBaseBranch(state, result.stale) + } + return result.branch + } + + /** Reset a stale default base branch and notify the webview. */ + private clearStaleDefaultBaseBranch(state: WorktreeStateManager, stale: string): void { + this.log(`Default base branch "${stale}" no longer exists, clearing`) + state.setDefaultBaseBranch(undefined) + this.pushState() + } + /** Create a git worktree on disk and register it in state. Returns null on failure. */ private async createWorktreeOnDisk(opts?: { groupId?: string @@ -411,11 +443,16 @@ export class AgentManagerProvider implements vscode.Disposable { this.postToWebview({ type: "agentManager.worktreeSetup", status: "creating", message: "Creating git worktree..." }) + // Resolve effective base branch using configured default + const effectiveBase = opts?.existingBranch + ? undefined + : await this.resolveBaseBranch(manager, state, opts?.baseBranch) + let result: CreateWorktreeResult try { result = await manager.createWorktree({ prompt: opts?.name || "kilo", - baseBranch: opts?.baseBranch, + baseBranch: effectiveBase ?? opts?.baseBranch, branchName: opts?.branchName, existingBranch: opts?.existingBranch, }) @@ -940,10 +977,23 @@ export class AgentManagerProvider implements vscode.Disposable { try { const result = await manager.listBranches() const checkedOut = await manager.checkedOutBranches() - const filtered = result.branches.filter((b) => !checkedOut.has(b.name)) + + // Include isCheckedOut flag on each branch — let the webview decide how to filter + const branches = result.branches.map((b) => ({ + ...b, + isCheckedOut: checkedOut.has(b.name), + })) + + // Validate configured default branch still exists + const state = this.getStateManager() + const configured = state?.getDefaultBaseBranch() + if (configured && !branches.some((b) => b.name === configured)) { + this.clearStaleDefaultBaseBranch(state!, configured) + } + this.postToWebview({ type: "agentManager.branches", - branches: filtered, + branches, defaultBranch: result.defaultBranch, }) } catch (error) { @@ -1336,7 +1386,8 @@ export class AgentManagerProvider implements vscode.Disposable { if (!manager) return try { const branch = await manager.currentBranch() - this.postToWebview({ type: "agentManager.repoInfo", branch }) + const defaultBranch = await manager.defaultBranch() + this.postToWebview({ type: "agentManager.repoInfo", branch, defaultBranch }) } catch (error) { this.log(`Failed to get current branch: ${error}`) } @@ -1409,6 +1460,7 @@ export class AgentManagerProvider implements vscode.Disposable { sessionsCollapsed: state.getSessionsCollapsed(), reviewDiffStyle: state.getReviewDiffStyle(), isGitRepo: true, + defaultBaseBranch: state.getDefaultBaseBranch(), }) this.statsPoller.setEnabled(worktrees.length > 0 || this.panel !== undefined) diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts index 9b135fb07b..02a23db75f 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts @@ -37,6 +37,7 @@ interface StateFile { tabOrder?: Record sessionsCollapsed?: boolean reviewDiffStyle?: "unified" | "split" + defaultBaseBranch?: string } const STATE_FILE = "agent-manager.json" @@ -55,6 +56,7 @@ export class WorktreeStateManager { private tabOrder: Record = {} private collapsed = false private reviewDiffStyle: "unified" | "split" = "unified" + private defaultBase: string | undefined private readonly log: (msg: string) => void private saving: Promise | undefined private pendingSave = false @@ -245,6 +247,19 @@ export class WorktreeStateManager { void this.save() } + // --------------------------------------------------------------------------- + // Default base branch + // --------------------------------------------------------------------------- + + getDefaultBaseBranch(): string | undefined { + return this.defaultBase + } + + setDefaultBaseBranch(value: string | undefined): void { + this.defaultBase = value + void this.save() + } + // --------------------------------------------------------------------------- // Persistence // --------------------------------------------------------------------------- @@ -271,6 +286,7 @@ export class WorktreeStateManager { if (data.reviewDiffStyle === "split") { this.reviewDiffStyle = "split" } + this.defaultBase = data.defaultBaseBranch this.log(`Loaded state: ${this.worktrees.size} worktrees, ${this.sessions.size} sessions`) } catch (error) { const code = (error as NodeJS.ErrnoException).code @@ -343,6 +359,9 @@ export class WorktreeStateManager { if (this.reviewDiffStyle === "split") { data.reviewDiffStyle = "split" } + if (this.defaultBase) { + data.defaultBaseBranch = this.defaultBase + } try { const dir = path.dirname(this.file) diff --git a/packages/kilo-vscode/src/agent-manager/base-branch.ts b/packages/kilo-vscode/src/agent-manager/base-branch.ts new file mode 100644 index 0000000000..9d67f6f26f --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/base-branch.ts @@ -0,0 +1,34 @@ +/** + * Pure logic for resolving the effective base branch. + * + * Priority chain: + * 1. Explicit (per-worktree override, from dialog) + * 2. Configured default (persisted in agent-manager.json), if it still exists + * 3. Auto-detect (undefined → WorktreeManager.defaultBranch() at call site) + */ + +/** Trim whitespace, return undefined for empty/blank. */ +export function normalizeBaseBranch(value: string | undefined): string | undefined { + if (!value) return undefined + const trimmed = value.trim() + return trimmed.length > 0 ? trimmed : undefined +} + +/** + * Pick the effective base branch given an explicit override, a configured + * default, and whether that configured branch still exists in the repo. + * + * Returns `{ branch }` with the winner, and optionally `{ stale }` when the + * configured branch no longer exists (so the caller can clear it). + */ +export function chooseBaseBranch(opts: { explicit?: string; configured?: string; configuredExists?: boolean }): { + branch?: string + stale?: string +} { + if (opts.explicit) return { branch: opts.explicit } + if (opts.configured) { + if (opts.configuredExists) return { branch: opts.configured } + return { stale: opts.configured } + } + return {} +} diff --git a/packages/kilo-vscode/src/agent-manager/git-import.ts b/packages/kilo-vscode/src/agent-manager/git-import.ts index 81432295db..5685f96aa8 100644 --- a/packages/kilo-vscode/src/agent-manager/git-import.ts +++ b/packages/kilo-vscode/src/agent-manager/git-import.ts @@ -4,6 +4,7 @@ export interface BranchListItem { isRemote: boolean isDefault: boolean lastCommitDate?: string + isCheckedOut?: boolean } export interface PRUrlParts { diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index 1a88fefc75..34b4bf0f21 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -25,6 +25,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/review-annotations.ts"), 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/BranchSelect.tsx"), path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"), ] const TSX_FILE = TSX_FILES[0] @@ -171,6 +172,7 @@ describe("Agent Manager Provider — onMessage routing", () => { "agentManager.requestRepoInfo", "agentManager.requestState", "agentManager.setTabOrder", + "agentManager.setDefaultBaseBranch", ] for (const msg of expected) { expect(text, `onMessage should handle "${msg}"`).toContain(msg) diff --git a/packages/kilo-vscode/tests/unit/base-branch.test.ts b/packages/kilo-vscode/tests/unit/base-branch.test.ts new file mode 100644 index 0000000000..27e17c14a5 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/base-branch.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from "bun:test" +import { normalizeBaseBranch, chooseBaseBranch } from "../../src/agent-manager/base-branch" + +describe("normalizeBaseBranch", () => { + it("returns undefined for undefined input", () => { + expect(normalizeBaseBranch(undefined)).toBeUndefined() + }) + + it("returns undefined for empty string", () => { + expect(normalizeBaseBranch("")).toBeUndefined() + }) + + it("returns undefined for whitespace-only string", () => { + expect(normalizeBaseBranch(" ")).toBeUndefined() + expect(normalizeBaseBranch("\t\n")).toBeUndefined() + }) + + it("trims whitespace from valid input", () => { + expect(normalizeBaseBranch(" main ")).toBe("main") + expect(normalizeBaseBranch(" develop\n")).toBe("develop") + }) + + it("returns the value as-is when already trimmed", () => { + expect(normalizeBaseBranch("main")).toBe("main") + expect(normalizeBaseBranch("feature/branch")).toBe("feature/branch") + }) +}) + +describe("chooseBaseBranch", () => { + it("prefers explicit over configured", () => { + const result = chooseBaseBranch({ explicit: "release", configured: "develop", configuredExists: true }) + expect(result).toEqual({ branch: "release" }) + }) + + it("uses configured when it exists and no explicit is given", () => { + const result = chooseBaseBranch({ configured: "develop", configuredExists: true }) + expect(result).toEqual({ branch: "develop" }) + }) + + it("marks configured as stale when it no longer exists", () => { + const result = chooseBaseBranch({ configured: "old-branch", configuredExists: false }) + expect(result).toEqual({ stale: "old-branch" }) + }) + + it("falls back to auto-detect (undefined) when nothing is configured", () => { + const result = chooseBaseBranch({}) + expect(result).toEqual({}) + }) + + it("falls back to auto-detect when configured is undefined", () => { + const result = chooseBaseBranch({ configured: undefined, configuredExists: false }) + expect(result).toEqual({}) + }) + + it("explicit takes priority even when configured is stale", () => { + const result = chooseBaseBranch({ explicit: "main", configured: "deleted", configuredExists: false }) + expect(result).toEqual({ branch: "main" }) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 845befa913..9dbc195940 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -56,7 +56,6 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Spinner } from "@kilocode/kilo-ui/spinner" import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { Popover } from "@kilocode/kilo-ui/popover" -import { HoverCard } from "@kilocode/kilo-ui/hover-card" import { VSCodeProvider, useVSCode } from "../src/context/vscode" import { ServerProvider } from "../src/context/server" import { ProviderProvider } from "../src/context/provider" @@ -85,6 +84,7 @@ import { FullScreenDiffView } from "./FullScreenDiffView" import { ApplyDialog } from "./ApplyDialog" import { groupApplyConflicts } from "./apply-conflicts" import type { ReviewComment } from "./review-comments" +import { BranchSelect } from "./BranchSelect" import { WorktreeItem } from "./WorktreeItem" import "./agent-manager.css" @@ -313,6 +313,11 @@ const AgentManagerContent: Component = () => { const [worktreesLoaded, setWorktreesLoaded] = createSignal(false) const [sessionsLoaded, setSessionsLoaded] = createSignal(false) const [isGitRepo, setIsGitRepo] = createSignal(true) + const [repoDetectedBranch, setRepoDetectedBranch] = createSignal() + const [defaultBaseBranch, setDefaultBaseBranch] = createSignal() + + const repoDefaultBranch = () => defaultBaseBranch() ?? repoDetectedBranch() ?? "main" + const hasConfiguredBranch = () => !!defaultBaseBranch() const DEFAULT_SIDEBAR_WIDTH = 260 const MIN_SIDEBAR_WIDTH = 200 @@ -1055,6 +1060,7 @@ const AgentManagerContent: Component = () => { if (msg.type === "agentManager.repoInfo") { const info = msg as AgentManagerRepoInfoMessage setRepoBranch(info.branch) + if (info.defaultBranch) setRepoDetectedBranch(info.defaultBranch) } if (msg.type === "agentManager.worktreeSetup") { @@ -1114,6 +1120,7 @@ const AgentManagerContent: Component = () => { if (state.reviewDiffStyle === "split" || state.reviewDiffStyle === "unified") { setReviewDiffStyle(state.reviewDiffStyle) } + if ("defaultBaseBranch" in state) setDefaultBaseBranch(state.defaultBaseBranch || undefined) const current = session.currentSessionID() if (current) { const ms = state.sessions.find((s) => s.id === current) @@ -1412,6 +1419,101 @@ const AgentManagerContent: Component = () => { vscode.postMessage({ type: "agentManager.configureSetupScript" }) } + const handleChangeDefaultBaseBranch = () => { + const [search, setSearch] = createSignal("") + const [branches, setBranches] = createSignal([]) + const [loading, setLoading] = createSignal(true) + const [highlighted, setHighlighted] = createSignal(-1) + + const unsub = vscode.onMessage((msg) => { + if (msg.type === "agentManager.branches") { + const ev = msg as AgentManagerBranchesMessage + setBranches(ev.branches) + if (ev.defaultBranch) setRepoDetectedBranch(ev.defaultBranch) + setLoading(false) + } + }) + + vscode.postMessage({ type: "agentManager.requestBranches" }) + + const filtered = createMemo(() => { + const s = search().toLowerCase() + if (!s) return branches() + return branches().filter((b) => b.name.toLowerCase().includes(s)) + }) + + const selectBranch = (name: string | undefined) => { + vscode.postMessage({ type: "agentManager.setDefaultBaseBranch", branch: name }) + setDefaultBaseBranch(name) + unsub() + dialog.close() + } + + const handleKeyDown = (e: KeyboardEvent) => { + const items = filtered() + // offset by 1 for auto-detect option (-1 = auto-detect) + const total = items.length + 1 + if (e.key === "ArrowDown") { + e.preventDefault() + e.stopPropagation() + setHighlighted((prev) => Math.min(prev + 1, total - 2)) + } else if (e.key === "ArrowUp") { + e.preventDefault() + e.stopPropagation() + setHighlighted((prev) => Math.max(prev - 1, -1)) + } else if (e.key === "Enter") { + e.preventDefault() + e.stopPropagation() + const idx = highlighted() + if (idx === -1) { + selectBranch(undefined) + } else { + const branch = items[idx] + if (branch) selectBranch(branch.name) + } + } else if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + unsub() + dialog.close() + } + } + + dialog.show(() => ( + +
+ { + setSearch(v) + setHighlighted(-1) + }} + onSelect={(b) => selectBranch(b.name)} + onSearchKeyDown={handleKeyDown} + selected={defaultBaseBranch()} + highlighted={highlighted()} + onHighlight={setHighlighted} + searchPlaceholder={t("agentManager.dialog.searchBranches")} + emptyLabel={t("agentManager.import.noMatchingBranches")} + loadingLabel={t("agentManager.import.loadingBranches")} + defaultLabel={t("agentManager.dialog.branchBadge.default")} + remoteLabel={t("agentManager.dialog.branchBadge.remote")} + defaultName={defaultBaseBranch()} + autoOption={{ + label: t("agentManager.worktree.defaultBaseBranchAuto"), + hint: repoDetectedBranch(), + active: !hasConfiguredBranch(), + highlighted: highlighted() === -1, + onSelect: () => selectBranch(undefined), + }} + /> +
+
+ )) + } + const handleShowKeyboardShortcuts = () => { const categories = buildShortcutCategories(kb(), t) dialog.show(() => ( @@ -1449,7 +1551,7 @@ const AgentManagerContent: Component = () => { // Advanced worktree dialog — opens a full dialog with prompt, versions, model, mode const showAdvancedWorktreeDialog = () => { - dialog.show(() => dialog.close()} />) + dialog.show(() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />) } const confirmDeleteWorktree = (worktreeId: string) => { @@ -1849,6 +1951,12 @@ const AgentManagerContent: Component = () => { {t("agentManager.worktree.setupScript")} + + + + {t("agentManager.worktree.defaultBaseBranch")}: {repoDefaultBranch()} + + @@ -2435,7 +2543,7 @@ function sanitizeBranchName(name: string): string { .join("/") } -const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => { +const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBranch?: string }> = (props) => { const { t } = useLanguage() const vscode = useVSCode() const session = useSession() @@ -2445,7 +2553,7 @@ const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => { // --- Shared branch data (used by both New tab's base branch selector and Import tab) --- const [branches, setBranches] = createSignal([]) const [branchesLoading, setBranchesLoading] = createSignal(false) - const [defaultBranch, setDefaultBranch] = createSignal("main") + const [defaultBranch, setDefaultBranch] = createSignal(props.defaultBaseBranch ?? "main") const [branchSearch, setBranchSearch] = createSignal("") // --- New tab state --- @@ -2552,7 +2660,7 @@ const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => { if (msg.type === "agentManager.branches") { const ev = msg as AgentManagerBranchesMessage setBranches(ev.branches) - setDefaultBranch(ev.defaultBranch) + if (!props.defaultBaseBranch) setDefaultBranch(ev.defaultBranch) setBranchesLoading(false) } if (msg.type === "agentManager.importResult") { @@ -2730,98 +2838,69 @@ const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => { } > - -
- - {(branch, index) => ( - - )} - -
+ } else if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + setBaseBranchOpen(false) + setBranchSearch("") + setHighlightedIndex(0) + } + }} + selected={effectiveBaseBranch()} + highlighted={highlightedIndex()} + onHighlight={setHighlightedIndex} + searchPlaceholder={t("agentManager.dialog.searchBranches")} + emptyLabel={t("agentManager.import.noMatchingBranches")} + defaultLabel={t("agentManager.dialog.branchBadge.default")} + remoteLabel={t("agentManager.dialog.branchBadge.remote")} + defaultName={defaultBranch()} + /> @@ -3000,49 +3079,18 @@ const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => { } > - -
- 0} - fallback={ -
- {branchesLoading() - ? t("agentManager.import.loadingBranches") - : t("agentManager.import.noMatchingBranches")} -
- } - > - - {(branch) => ( -
handleBranchSelect(branch.name)}> - - - {branch.name} - - {t("agentManager.dialog.branchBadge.default")} - - - {t("agentManager.dialog.branchBadge.remote")} - - - - {formatRelativeDate(branch.lastCommitDate!)} - -
- )} -
-
-
+ !b.isCheckedOut)} + loading={branchesLoading()} + search={branchSearch()} + onSearch={setBranchSearch} + onSelect={(b) => handleBranchSelect(b.name)} + searchPlaceholder={t("agentManager.dialog.searchBranches")} + loadingLabel={t("agentManager.import.loadingBranches")} + emptyLabel={t("agentManager.import.noMatchingBranches")} + defaultLabel={t("agentManager.dialog.branchBadge.default")} + remoteLabel={t("agentManager.dialog.branchBadge.remote")} + /> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/BranchSelect.tsx b/packages/kilo-vscode/webview-ui/agent-manager/BranchSelect.tsx new file mode 100644 index 0000000000..cf470a2664 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/BranchSelect.tsx @@ -0,0 +1,128 @@ +// Reusable branch selector: search input + scrollable list with keyboard navigation + +import { Component, For, Show } from "solid-js" +import type { BranchInfo } from "../src/types/messages" +import { Icon } from "@kilocode/kilo-ui/icon" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import { formatRelativeDate } from "../src/utils/date" + +export interface AutoOption { + label: string + hint?: string + active: boolean + highlighted?: boolean + onSelect: () => void +} + +export interface BranchSelectProps { + branches: BranchInfo[] + loading?: boolean + search: string + onSearch: (value: string) => void + onSelect: (branch: BranchInfo) => void + onSearchKeyDown?: (event: KeyboardEvent) => void + selected?: string + highlighted?: number + onHighlight?: (index: number) => void + defaultName?: string + searchPlaceholder: string + loadingLabel?: string + emptyLabel: string + defaultLabel: string + remoteLabel: string + autoOption?: AutoOption +} + +export const BranchSelect: Component = (props) => { + const isDefault = (branch: BranchInfo) => { + if (props.defaultName) return branch.name === props.defaultName + return branch.isDefault + } + + return ( + <> + +
+ + {(opt) => ( + + )} + + 0} + fallback={ + +
+ {props.loading ? (props.loadingLabel ?? props.emptyLabel) : props.emptyLabel} +
+
+ } + > + + {(branch, index) => ( + + )} + +
+ +
+ + {props.loadingLabel} +
+
+
+ + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 1abea483c1..e3ac7a3a92 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -3132,3 +3132,19 @@ button.am-section-toggle:hover .am-section-label { .am-file-tree-summary-dels { color: var(--syntax-diff-delete, #da3319); } + +/* ============ Default base branch picker dialog ============ */ + +.am-default-base-branch { + min-width: 360px; +} + +.am-default-base-branch .am-dropdown-list { + max-height: 320px; +} + +.am-branch-hint { + margin-left: auto; + color: var(--text-weaker); + font-size: 11px; +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 011f9922e2..3f996594dc 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "انقر مرتين لإعادة التسمية", "agentManager.worktree.versions": "{{count}} نسخ", "agentManager.worktree.advancedOptions": "خيارات Worktree متقدمة", + "agentManager.worktree.defaultBaseBranch": "الفرع الأساسي الافتراضي", + "agentManager.worktree.defaultBaseBranchAuto": "اكتشاف تلقائي", "agentManager.hoverCard.branch": "الفرع", "agentManager.hoverCard.base": "الأساس", "agentManager.hoverCard.sessions": "الجلسات", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 0694b95e2b..82791aaf2a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Clique duplo para renomear", "agentManager.worktree.versions": "{{count}} versões", "agentManager.worktree.advancedOptions": "Opções avançadas de Worktree", + "agentManager.worktree.defaultBaseBranch": "Branch base padrão", + "agentManager.worktree.defaultBaseBranchAuto": "Detecção automática", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessões", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index a5bad37507..94ec21efac 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Dvostruki klik za preimenovanje", "agentManager.worktree.versions": "{{count}} verzija", "agentManager.worktree.advancedOptions": "Napredne opcije Worktree-a", + "agentManager.worktree.defaultBaseBranch": "Zadana bazna grana", + "agentManager.worktree.defaultBaseBranchAuto": "Automatsko otkrivanje", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Baza", "agentManager.hoverCard.sessions": "Sesije", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 129adb37fe..567fe4236f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Dobbeltklik for at omdøbe", "agentManager.worktree.versions": "{{count}} versioner", "agentManager.worktree.advancedOptions": "Avancerede Worktree-indstillinger", + "agentManager.worktree.defaultBaseBranch": "Standard basisgren", + "agentManager.worktree.defaultBaseBranchAuto": "Automatisk registrering", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessioner", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 5fc2827da6..d73c9bf44a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Doppelklick zum Umbenennen", "agentManager.worktree.versions": "{{count}} Versionen", "agentManager.worktree.advancedOptions": "Erweiterte Worktree-Optionen", + "agentManager.worktree.defaultBaseBranch": "Standard-Basiszweig", + "agentManager.worktree.defaultBaseBranchAuto": "Automatisch erkennen", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Basis", "agentManager.hoverCard.sessions": "Sitzungen", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index da37cfb515..80d6f222cc 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -15,6 +15,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Double-click to rename", "agentManager.worktree.versions": "{{count}} versions", "agentManager.worktree.advancedOptions": "Advanced worktree options", + "agentManager.worktree.defaultBaseBranch": "Default Base Branch", + "agentManager.worktree.defaultBaseBranchAuto": "Auto-detect", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", @@ -37,7 +39,6 @@ export const dict = { "agentManager.setup.failed": "Workspace setup failed", "agentManager.setup.settingUp": "Setting up workspace", - "agentManager.shortcuts.title": "Keyboard Shortcuts", "agentManager.shortcuts.category.sidebar": "Sidebar", "agentManager.shortcuts.category.tabs": "Tabs", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index fd12e0665e..3cbc5188c6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Doble clic para renombrar", "agentManager.worktree.versions": "{{count}} versiones", "agentManager.worktree.advancedOptions": "Opciones avanzadas de Worktree", + "agentManager.worktree.defaultBaseBranch": "Rama base predeterminada", + "agentManager.worktree.defaultBaseBranchAuto": "Detección automática", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sesiones", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index ba3e899adc..e25a4d01e9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Double-cliquez pour renommer", "agentManager.worktree.versions": "{{count}} versions", "agentManager.worktree.advancedOptions": "Options avancées du Worktree", + "agentManager.worktree.defaultBaseBranch": "Branche de base par défaut", + "agentManager.worktree.defaultBaseBranchAuto": "Détection automatique", "agentManager.hoverCard.branch": "BRANCHE", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessions", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index b8a7b98e53..1f7d8415b4 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "ダブルクリックで名前を変更", "agentManager.worktree.versions": "{{count}}バージョン", "agentManager.worktree.advancedOptions": "Worktreeの詳細オプション", + "agentManager.worktree.defaultBaseBranch": "デフォルトベースブランチ", + "agentManager.worktree.defaultBaseBranchAuto": "自動検出", "agentManager.hoverCard.branch": "ブランチ", "agentManager.hoverCard.base": "ベース", "agentManager.hoverCard.sessions": "セッション", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index 3f309f5589..4d5d7632f5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "더블 클릭하여 이름 변경", "agentManager.worktree.versions": "{{count}}개 버전", "agentManager.worktree.advancedOptions": "Worktree 고급 옵션", + "agentManager.worktree.defaultBaseBranch": "기본 베이스 브랜치", + "agentManager.worktree.defaultBaseBranchAuto": "자동 감지", "agentManager.hoverCard.branch": "브랜치", "agentManager.hoverCard.base": "베이스", "agentManager.hoverCard.sessions": "세션", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 4e8cfec637..c4f4f55db7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Dobbeltklikk for å gi nytt navn", "agentManager.worktree.versions": "{{count}} versjoner", "agentManager.worktree.advancedOptions": "Avanserte Worktree-alternativer", + "agentManager.worktree.defaultBaseBranch": "Standard basisgren", + "agentManager.worktree.defaultBaseBranchAuto": "Automatisk gjenkjenning", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Økter", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index fce70508fc..37458df20d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Kliknij dwukrotnie, aby zmienić nazwę", "agentManager.worktree.versions": "{{count}} wersji", "agentManager.worktree.advancedOptions": "Zaawansowane opcje Worktree", + "agentManager.worktree.defaultBaseBranch": "Domyślna gałąź bazowa", + "agentManager.worktree.defaultBaseBranchAuto": "Automatyczne wykrywanie", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Baza", "agentManager.hoverCard.sessions": "Sesje", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 297a307346..cb1c0ea0d6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "Двойной клик для переименования", "agentManager.worktree.versions": "{{count}} версий", "agentManager.worktree.advancedOptions": "Расширенные настройки Worktree", + "agentManager.worktree.defaultBaseBranch": "Базовая ветка по умолчанию", + "agentManager.worktree.defaultBaseBranchAuto": "Автоопределение", "agentManager.hoverCard.branch": "ВЕТКА", "agentManager.hoverCard.base": "Основа", "agentManager.hoverCard.sessions": "Сессии", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 329fb247d0..6ecf1b06c9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "ดับเบิลคลิกเพื่อเปลี่ยนชื่อ", "agentManager.worktree.versions": "{{count}} เวอร์ชัน", "agentManager.worktree.advancedOptions": "ตัวเลือก Worktree ขั้นสูง", + "agentManager.worktree.defaultBaseBranch": "สาขาฐานเริ่มต้น", + "agentManager.worktree.defaultBaseBranchAuto": "ตรวจจับอัตโนมัติ", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "ฐาน", "agentManager.hoverCard.sessions": "เซสชัน", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 9bdd0e9e9b..241503f9be 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "双击重命名", "agentManager.worktree.versions": "{{count}} 个版本", "agentManager.worktree.advancedOptions": "Worktree 高级选项", + "agentManager.worktree.defaultBaseBranch": "默认基础分支", + "agentManager.worktree.defaultBaseBranchAuto": "自动检测", "agentManager.hoverCard.branch": "分支", "agentManager.hoverCard.base": "基础", "agentManager.hoverCard.sessions": "会话", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 61a722f3c6..375dca6ba9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -14,6 +14,8 @@ export const dict = { "agentManager.worktree.doubleClickRename": "按兩下以重新命名", "agentManager.worktree.versions": "{{count}} 個版本", "agentManager.worktree.advancedOptions": "Worktree 進階選項", + "agentManager.worktree.defaultBaseBranch": "預設基礎分支", + "agentManager.worktree.defaultBaseBranchAuto": "自動偵測", "agentManager.hoverCard.branch": "分支", "agentManager.hoverCard.base": "基底", "agentManager.hoverCard.sessions": "工作階段", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index b83f918284..259d3d1f36 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -626,6 +626,7 @@ export interface AgentManagerSessionMetaMessage { export interface AgentManagerRepoInfoMessage { type: "agentManager.repoInfo" branch: string + defaultBranch?: string } // Agent Manager worktree setup progress @@ -674,6 +675,7 @@ export interface AgentManagerStateMessage { sessionsCollapsed?: boolean reviewDiffStyle?: "unified" | "split" isGitRepo?: boolean + defaultBaseBranch?: string } // Resolved keybindings for agent manager actions @@ -703,6 +705,7 @@ export interface BranchInfo { isRemote: boolean isDefault: boolean lastCommitDate?: string + isCheckedOut?: boolean } export interface AgentManagerBranchesMessage { @@ -1450,6 +1453,12 @@ export interface EnhancePromptRequest { requestId: string } +// Set default base branch (webview → extension) +export interface SetDefaultBaseBranchRequest { + type: "agentManager.setDefaultBaseBranch" + branch?: string +} + export type WebviewMessage = | SendMessageRequest | AbortRequest @@ -1532,6 +1541,7 @@ export type WebviewMessage = // legacy-migration end | ApplyWorktreeDiffMessage | EnhancePromptRequest + | SetDefaultBaseBranchRequest // ============================================ // VS Code API type