mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
feat: keyboard shortcuts for agent manager tab/worktree management (#495)
* feat: add keyboard shortcuts for agent manager tab/worktree management * fix: use mock.module in git-context tests to avoid cross-file mock leakage * feat: shared worktree delete confirmation dialog with Enter/Esc, Cmd+W fallthrough, Cmd+T race fix * fix: Enter on delete dialog, VS Code theme colors, Cmd+N auto-switch, shortcut hints, smooth neighbor selection on worktree delete * refactor: move confirm dialog styles to kilo-ui, replace 'local' magic string with LOCAL constant * revert: restore upstream commit-message tests, drop our git.ts/GitRunner changes * revert: remove out-of-scope kilo-ui/SessionList/chat.css changes, keep confirm styles in agent-manager.css * fix: remove stale /P reference from comment * fix: move keybinding entries from submenus to keybindings array
This commit is contained in:
@@ -101,6 +101,26 @@
|
||||
"title": "Agent Manager: Next Tab",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.newTab",
|
||||
"title": "Agent Manager: New Tab",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.closeTab",
|
||||
"title": "Agent Manager: Close Tab",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.newWorktree",
|
||||
"title": "Agent Manager: New Worktree",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.closeWorktree",
|
||||
"title": "Agent Manager: Close Worktree",
|
||||
"category": "Kilo Code"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.generateCommitMessage",
|
||||
"title": "Generate Commit Message",
|
||||
@@ -288,6 +308,30 @@
|
||||
"key": "ctrl+right",
|
||||
"mac": "cmd+right",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.newTab",
|
||||
"key": "ctrl+t",
|
||||
"mac": "cmd+t",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.closeTab",
|
||||
"key": "ctrl+w",
|
||||
"mac": "cmd+w",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.newWorktree",
|
||||
"key": "ctrl+n",
|
||||
"mac": "cmd+n",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.closeWorktree",
|
||||
"key": "ctrl+shift+w",
|
||||
"mac": "cmd+shift+w",
|
||||
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
|
||||
@@ -82,6 +82,18 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.nextTab", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "tabNext" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.newTab", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "newTab" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.closeTab", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "closeTab" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.newWorktree", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "newWorktree" })
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.agentManager.closeWorktree", () => {
|
||||
agentManagerProvider.postMessage({ type: "action", action: "closeWorktree" })
|
||||
}),
|
||||
)
|
||||
|
||||
// Register autocomplete provider
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { resolveNavigation, validateLocalSession } from "../../webview-ui/agent-manager/navigate"
|
||||
import { resolveNavigation, validateLocalSession, LOCAL } from "../../webview-ui/agent-manager/navigate"
|
||||
|
||||
const ids = ["a", "b", "c", "d"]
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("resolveNavigation", () => {
|
||||
|
||||
describe("from first session", () => {
|
||||
it("up → local", () => {
|
||||
expect(resolveNavigation("up", "a", ids)).toEqual({ action: "local" })
|
||||
expect(resolveNavigation("up", "a", ids)).toEqual({ action: LOCAL })
|
||||
})
|
||||
|
||||
it("down → selects second session", () => {
|
||||
@@ -68,7 +68,7 @@ describe("resolveNavigation", () => {
|
||||
})
|
||||
|
||||
it("up from only session → local", () => {
|
||||
expect(resolveNavigation("up", "x", ["x"])).toEqual({ action: "local" })
|
||||
expect(resolveNavigation("up", "x", ["x"])).toEqual({ action: LOCAL })
|
||||
})
|
||||
|
||||
it("down from only session → none", () => {
|
||||
@@ -95,20 +95,20 @@ describe("resolveNavigation", () => {
|
||||
expect(trail).toEqual(["s1", "s2", "s3"])
|
||||
|
||||
// Navigate back up through all sessions to local
|
||||
const upTrail: (string | "local")[] = []
|
||||
const upTrail: (string | typeof LOCAL)[] = []
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const result = resolveNavigation("up", current, sessions)
|
||||
if (result.action === "select") {
|
||||
current = result.id
|
||||
upTrail.push(current)
|
||||
} else if (result.action === "local") {
|
||||
} else if (result.action === LOCAL) {
|
||||
current = undefined
|
||||
upTrail.push("local")
|
||||
upTrail.push(LOCAL)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
expect(upTrail).toEqual(["s2", "s1", "local"])
|
||||
expect(upTrail).toEqual(["s2", "s1", LOCAL])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { nextSelectionAfterDelete, LOCAL } from "../../webview-ui/agent-manager/navigate"
|
||||
|
||||
describe("nextSelectionAfterDelete", () => {
|
||||
it("selects the worktree below when deleting from the middle", () => {
|
||||
expect(nextSelectionAfterDelete("b", ["a", "b", "c"])).toBe("c")
|
||||
})
|
||||
|
||||
it("selects the worktree above when deleting the last item", () => {
|
||||
expect(nextSelectionAfterDelete("c", ["a", "b", "c"])).toBe("b")
|
||||
})
|
||||
|
||||
it("selects the worktree below when deleting the first item", () => {
|
||||
expect(nextSelectionAfterDelete("a", ["a", "b", "c"])).toBe("b")
|
||||
})
|
||||
|
||||
it("falls back to LOCAL when deleting the only worktree", () => {
|
||||
expect(nextSelectionAfterDelete("a", ["a"])).toBe(LOCAL)
|
||||
})
|
||||
|
||||
it("falls back to LOCAL when ID is not found", () => {
|
||||
expect(nextSelectionAfterDelete("x", ["a", "b"])).toBe(LOCAL)
|
||||
})
|
||||
|
||||
it("falls back to LOCAL when list is empty", () => {
|
||||
expect(nextSelectionAfterDelete("a", [])).toBe(LOCAL)
|
||||
})
|
||||
|
||||
it("handles two-item list deleting first", () => {
|
||||
expect(nextSelectionAfterDelete("a", ["a", "b"])).toBe("b")
|
||||
})
|
||||
|
||||
it("handles two-item list deleting second", () => {
|
||||
expect(nextSelectionAfterDelete("b", ["a", "b"])).toBe("a")
|
||||
})
|
||||
})
|
||||
@@ -11,7 +11,8 @@ import type {
|
||||
SessionInfo,
|
||||
} from "../src/types/messages"
|
||||
import { ThemeProvider } from "@kilocode/kilo-ui/theme"
|
||||
import { DialogProvider } from "@kilocode/kilo-ui/context/dialog"
|
||||
import { DialogProvider, useDialog } from "@kilocode/kilo-ui/context/dialog"
|
||||
import { Dialog } from "@kilocode/kilo-ui/dialog"
|
||||
import { MarkedProvider } from "@kilocode/kilo-ui/context/marked"
|
||||
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
|
||||
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
|
||||
@@ -32,7 +33,7 @@ import { WorktreeModeProvider } from "../src/context/worktree-mode"
|
||||
import { ChatView } from "../src/components/chat"
|
||||
import { LanguageBridge, DataBridge } from "../src/App"
|
||||
import { formatRelativeDate } from "../src/utils/date"
|
||||
import { validateLocalSession } from "./navigate"
|
||||
import { validateLocalSession, nextSelectionAfterDelete, LOCAL } from "./navigate"
|
||||
import "./agent-manager.css"
|
||||
|
||||
interface SetupState {
|
||||
@@ -42,17 +43,21 @@ interface SetupState {
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
/** Sidebar selection: "local" for workspace, worktree ID for a worktree, or null for an unassigned session. */
|
||||
type SidebarSelection = "local" | string | null
|
||||
/** Sidebar selection: LOCAL for workspace, worktree ID for a worktree, or null for an unassigned session. */
|
||||
type SidebarSelection = typeof LOCAL | string | null
|
||||
|
||||
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
|
||||
const modKey = isMac ? "\u2318" : "Ctrl+"
|
||||
|
||||
const AgentManagerContent: Component = () => {
|
||||
const session = useSession()
|
||||
const vscode = useVSCode()
|
||||
const dialog = useDialog()
|
||||
|
||||
const [setup, setSetup] = createSignal<SetupState>({ active: false, message: "" })
|
||||
const [worktrees, setWorktrees] = createSignal<WorktreeState[]>([])
|
||||
const [managedSessions, setManagedSessions] = createSignal<ManagedSessionState[]>([])
|
||||
const [selection, setSelection] = createSignal<SidebarSelection>("local")
|
||||
const [selection, setSelection] = createSignal<SidebarSelection>(LOCAL)
|
||||
const [repoBranch, setRepoBranch] = createSignal<string | undefined>()
|
||||
|
||||
// Recover persisted local session IDs from webview state
|
||||
@@ -129,7 +134,7 @@ const AgentManagerContent: Component = () => {
|
||||
// Sessions for the currently selected worktree (tab bar), sorted by creation date
|
||||
const activeWorktreeSessions = createMemo((): SessionInfo[] => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === "local") return []
|
||||
if (!sel || sel === LOCAL) return []
|
||||
const managed = managedSessions().filter((ms) => ms.worktreeId === sel)
|
||||
const ids = new Set(managed.map((ms) => ms.id))
|
||||
return session
|
||||
@@ -141,7 +146,7 @@ const AgentManagerContent: Component = () => {
|
||||
// Active tab sessions: local sessions when on "local", worktree sessions otherwise
|
||||
const activeTabs = createMemo((): SessionInfo[] => {
|
||||
const sel = selection()
|
||||
if (sel === "local") return localSessions()
|
||||
if (sel === LOCAL) return localSessions()
|
||||
if (sel) return activeWorktreeSessions()
|
||||
return []
|
||||
})
|
||||
@@ -149,7 +154,7 @@ const AgentManagerContent: Component = () => {
|
||||
// Whether the selected context has zero sessions
|
||||
const contextEmpty = createMemo(() => {
|
||||
const sel = selection()
|
||||
if (sel === "local") return localSessionIDs().length === 0
|
||||
if (sel === LOCAL) return localSessionIDs().length === 0
|
||||
if (sel) return activeWorktreeSessions().length === 0
|
||||
return false
|
||||
})
|
||||
@@ -171,8 +176,8 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
// Navigate sidebar items with arrow keys
|
||||
const navigate = (direction: "up" | "down") => {
|
||||
const flat: { type: "local" | "wt" | "session"; id: string }[] = [
|
||||
{ type: "local", id: "local" },
|
||||
const flat: { type: typeof LOCAL | "wt" | "session"; id: string }[] = [
|
||||
{ type: LOCAL, id: LOCAL },
|
||||
...worktrees().map((wt) => ({ type: "wt" as const, id: wt.id })),
|
||||
...unassignedSessions().map((s) => ({ type: "session" as const, id: s.id })),
|
||||
]
|
||||
@@ -184,7 +189,7 @@ const AgentManagerContent: Component = () => {
|
||||
if (next < 0 || next >= flat.length) return
|
||||
|
||||
const item = flat[next]!
|
||||
if (item.type === "local") {
|
||||
if (item.type === LOCAL) {
|
||||
selectLocal()
|
||||
} else if (item.type === "wt") {
|
||||
selectWorktree(item.id)
|
||||
@@ -217,7 +222,7 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
|
||||
const selectLocal = () => {
|
||||
setSelection("local")
|
||||
setSelection(LOCAL)
|
||||
vscode.postMessage({ type: "agentManager.requestRepoInfo" })
|
||||
const locals = localSessions()
|
||||
const first = locals[0]
|
||||
@@ -253,17 +258,29 @@ const AgentManagerContent: Component = () => {
|
||||
else if (msg.action === "sessionNext") navigate("down")
|
||||
else if (msg.action === "tabPrevious") navigateTab("left")
|
||||
else if (msg.action === "tabNext") navigateTab("right")
|
||||
else if (msg.action === "newTab") handleNewTabForCurrentSelection()
|
||||
else if (msg.action === "closeTab") closeActiveTab()
|
||||
else if (msg.action === "newWorktree") handleNewWorktreeOrPromote()
|
||||
else if (msg.action === "closeWorktree") closeSelectedWorktree()
|
||||
}
|
||||
window.addEventListener("message", handler)
|
||||
|
||||
// Prevent Cmd+Up/Down/Left/Right from triggering native scroll
|
||||
const preventScroll = (e: KeyboardEvent) => {
|
||||
// Prevent Cmd+Arrow/T/W/N from triggering native browser actions
|
||||
const preventDefaults = (e: KeyboardEvent) => {
|
||||
if (!(e.metaKey || e.ctrlKey)) return
|
||||
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
// Prevent browser defaults for our shortcuts (new tab, close tab, new window)
|
||||
if (["t", "w", "n"].includes(e.key.toLowerCase()) && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
}
|
||||
// Prevent defaults for shift variants (close worktree)
|
||||
if (e.key.toLowerCase() === "w" && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", preventScroll)
|
||||
window.addEventListener("keydown", preventDefaults)
|
||||
|
||||
// When the panel regains focus (e.g. returning from terminal), focus the prompt
|
||||
const onWindowFocus = () => window.dispatchEvent(new Event("focusPrompt"))
|
||||
@@ -272,7 +289,7 @@ const AgentManagerContent: Component = () => {
|
||||
// When a session is created while on local, replace the current pending tab with the real session.
|
||||
// Guard against duplicate sessionCreated events (HTTP response + SSE can both fire).
|
||||
const unsubCreate = vscode.onMessage((msg) => {
|
||||
if (msg.type === "sessionCreated" && selection() === "local") {
|
||||
if (msg.type === "sessionCreated" && selection() === LOCAL) {
|
||||
const created = msg as { type: string; session: { id: string } }
|
||||
if (localSessionIDs().includes(created.session.id)) return
|
||||
const pending = activePendingId()
|
||||
@@ -299,6 +316,9 @@ const AgentManagerContent: Component = () => {
|
||||
globalThis.setTimeout(() => setSetup({ active: false, message: "" }), error ? 3000 : 500)
|
||||
if (!error && ev.sessionId) {
|
||||
session.selectSession(ev.sessionId)
|
||||
// Auto-switch sidebar to the worktree containing this session
|
||||
const ms = managedSessions().find((s) => s.id === ev.sessionId)
|
||||
if (ms?.worktreeId) setSelection(ms.worktreeId)
|
||||
}
|
||||
} else {
|
||||
setSetup({ active: true, message: ev.message, branch: ev.branch })
|
||||
@@ -324,7 +344,7 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("message", handler)
|
||||
window.removeEventListener("keydown", preventScroll)
|
||||
window.removeEventListener("keydown", preventDefaults)
|
||||
window.removeEventListener("focus", onWindowFocus)
|
||||
unsubCreate()
|
||||
unsub()
|
||||
@@ -344,10 +364,47 @@ const AgentManagerContent: Component = () => {
|
||||
vscode.postMessage({ type: "agentManager.createWorktree" })
|
||||
}
|
||||
|
||||
const confirmDeleteWorktree = (worktreeId: string) => {
|
||||
const wt = worktrees().find((w) => w.id === worktreeId)
|
||||
if (!wt) return
|
||||
const doDelete = () => {
|
||||
vscode.postMessage({ type: "agentManager.deleteWorktree", worktreeId: wt.id })
|
||||
if (selection() === wt.id) {
|
||||
const next = nextSelectionAfterDelete(
|
||||
wt.id,
|
||||
worktrees().map((w) => w.id),
|
||||
)
|
||||
if (next === LOCAL) selectLocal()
|
||||
else selectWorktree(next)
|
||||
}
|
||||
dialog.close()
|
||||
}
|
||||
dialog.show(() => (
|
||||
<Dialog title="Delete Worktree" fit>
|
||||
<div class="am-confirm">
|
||||
<div class="am-confirm-message">
|
||||
<Icon name="trash" size="small" />
|
||||
<span>
|
||||
Delete worktree <code class="am-confirm-branch">{wt.branch}</code>? This removes the worktree from disk
|
||||
and dissociates all sessions.
|
||||
</span>
|
||||
</div>
|
||||
<div class="am-confirm-actions">
|
||||
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="large" class="am-confirm-delete" onClick={doDelete} autofocus>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
))
|
||||
}
|
||||
|
||||
const handleDeleteWorktree = (worktreeId: string, e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
vscode.postMessage({ type: "agentManager.deleteWorktree", worktreeId })
|
||||
if (selection() === worktreeId) selectLocal()
|
||||
confirmDeleteWorktree(worktreeId)
|
||||
}
|
||||
|
||||
const handlePromote = (sessionId: string, e: MouseEvent) => {
|
||||
@@ -357,7 +414,7 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
const handleAddSession = () => {
|
||||
const sel = selection()
|
||||
if (sel === "local") {
|
||||
if (sel === LOCAL) {
|
||||
addPendingTab()
|
||||
} else if (sel) {
|
||||
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
|
||||
@@ -399,12 +456,61 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Close the currently active tab via keyboard shortcut.
|
||||
// If no tabs remain, fall through to close the selected worktree.
|
||||
const closeActiveTab = () => {
|
||||
const tabs = activeTabs()
|
||||
if (tabs.length === 0) {
|
||||
closeSelectedWorktree()
|
||||
return
|
||||
}
|
||||
const current = session.currentSessionID()
|
||||
const pending = activePendingId()
|
||||
const target = current
|
||||
? tabs.find((s) => s.id === current)
|
||||
: pending
|
||||
? tabs.find((s) => s.id === pending)
|
||||
: undefined
|
||||
if (!target) return
|
||||
const synthetic = new MouseEvent("click")
|
||||
handleCloseTab(target.id, synthetic)
|
||||
}
|
||||
|
||||
// Cmd+T: add a new tab strictly to the current selection (no side effects)
|
||||
const handleNewTabForCurrentSelection = () => {
|
||||
const sel = selection()
|
||||
if (sel === LOCAL) {
|
||||
addPendingTab()
|
||||
} else if (sel) {
|
||||
// Pass the captured worktree ID directly to avoid race conditions
|
||||
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
|
||||
}
|
||||
}
|
||||
|
||||
// Cmd+N: if an unassigned session is selected, promote it; otherwise create a new worktree
|
||||
const handleNewWorktreeOrPromote = () => {
|
||||
const sel = selection()
|
||||
const sid = session.currentSessionID()
|
||||
if (sel === null && sid && !worktreeSessionIds().has(sid)) {
|
||||
vscode.postMessage({ type: "agentManager.promoteSession", sessionId: sid })
|
||||
return
|
||||
}
|
||||
handleCreateWorktree()
|
||||
}
|
||||
|
||||
// Close the currently selected worktree with a confirmation dialog
|
||||
const closeSelectedWorktree = () => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) return
|
||||
confirmDeleteWorktree(sel)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="am-layout">
|
||||
<div class="am-sidebar">
|
||||
{/* Local workspace item */}
|
||||
<button
|
||||
class={`am-local-item ${selection() === "local" ? "am-local-item-active" : ""}`}
|
||||
class={`am-local-item ${selection() === LOCAL ? "am-local-item-active" : ""}`}
|
||||
data-sidebar-id="local"
|
||||
onClick={() => selectLocal()}
|
||||
>
|
||||
@@ -538,7 +644,7 @@ const AgentManagerContent: Component = () => {
|
||||
icon="plus"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label="New session"
|
||||
label={`New session (${modKey}T)`}
|
||||
class="am-tab-add"
|
||||
onClick={handleAddSession}
|
||||
/>
|
||||
@@ -554,6 +660,7 @@ const AgentManagerContent: Component = () => {
|
||||
<div class="am-empty-state-text">No sessions open</div>
|
||||
<Button variant="primary" size="small" onClick={handleAddSession}>
|
||||
New session
|
||||
<span class="am-shortcut-hint">{modKey}T</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -479,3 +479,59 @@
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Confirmation dialog */
|
||||
|
||||
.am-confirm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 0 24px 16px 24px;
|
||||
}
|
||||
|
||||
.am-confirm-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--text-base);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.am-confirm-message > [data-component="icon"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--icon-critical-base);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.am-confirm-branch {
|
||||
font-family: var(--font-family-mono, monospace);
|
||||
font-size: var(--font-size-small);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-inset-base);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.am-confirm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.am-confirm-delete {
|
||||
background: var(--surface-critical-strong) !important;
|
||||
color: var(--text-on-brand-base) !important;
|
||||
}
|
||||
|
||||
.am-confirm-delete:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Keyboard shortcut hint inside buttons */
|
||||
|
||||
.am-shortcut-hint {
|
||||
font-size: var(--font-size-small);
|
||||
opacity: 0.6;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* Returns the action to take: select a session by ID, go to local, or do nothing.
|
||||
*/
|
||||
|
||||
export type NavResult = { action: "select"; id: string } | { action: "local" } | { action: "none" }
|
||||
/** Sentinel value for the local workspace selection. */
|
||||
export const LOCAL = "local" as const
|
||||
|
||||
export type NavResult = { action: "select"; id: string } | { action: typeof LOCAL } | { action: "none" }
|
||||
|
||||
export function resolveNavigation(direction: "up" | "down", current: string | undefined, ids: string[]): NavResult {
|
||||
// Determine current position: -1 = local, 0..N-1 = session index
|
||||
@@ -25,7 +28,7 @@ export function resolveNavigation(direction: "up" | "down", current: string | un
|
||||
const next = direction === "up" ? idx - 1 : idx + 1
|
||||
|
||||
// Moving up past the first session → go to local
|
||||
if (next === -1) return { action: "local" }
|
||||
if (next === -1) return { action: LOCAL }
|
||||
|
||||
// At the bottom boundary
|
||||
if (next >= ids.length) return { action: "none" }
|
||||
@@ -42,3 +45,16 @@ export function validateLocalSession(persisted: string | undefined, ids: string[
|
||||
if (ids.indexOf(persisted) === -1) return undefined
|
||||
return persisted
|
||||
}
|
||||
|
||||
/**
|
||||
* After removing a worktree, pick the nearest remaining sidebar neighbor.
|
||||
* Order: the worktree just below → the one above → LOCAL.
|
||||
*/
|
||||
export function nextSelectionAfterDelete(deletedId: string, worktreeIds: string[]): typeof LOCAL | string {
|
||||
const idx = worktreeIds.indexOf(deletedId)
|
||||
if (idx === -1) return LOCAL
|
||||
const remaining = worktreeIds.filter((id) => id !== deletedId)
|
||||
if (remaining.length === 0) return LOCAL
|
||||
// Prefer the item that was below (same index in the shortened list), else the one above
|
||||
return remaining[Math.min(idx, remaining.length - 1)]!
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user