feat(agent-manager): add keyboard shortcut tooltips and worktree hover cards (#516)

* feat(agent-manager): add keyboard shortcut tooltips and worktree hover cards

- Add TooltipKeybind to all agent manager buttons (tabs, new session,
  close tab, terminal, new worktree, delete worktree, promote to worktree)
- Add HoverCard popovers on worktree items showing branch name, base
  branch, session count, creation date, and navigation shortcut hint
- Show directional keybind hints only for directly adjacent items
  (tabs: ⌘←/⌘→, worktrees: ⌘↑/⌘↓)
- Resolve keybindings from package.json at runtime per platform
  (Mac symbols vs Windows/Linux Ctrl+key format)
- Send resolved keybindings from extension to webview via new
  agentManager.keybindings message instead of hardcoding shortcuts
- Prevent multiple HoverCards from appearing simultaneously via
  shared hover state lifted above the For loop
- Suppress HoverCard when hovering the delete button

* refactor: extract formatKeybinding and adjacentHint into testable modules

- Extract formatKeybinding into format-keybinding.ts (pure function,
  no vscode dependency, takes mac boolean parameter)
- Extract adjacentHint into navigate.ts as a reusable pure function
  for computing directional keybind hints
- Add tests for adjacentHint (12 cases) and formatKeybinding (13 cases)
- Simplify tab/worktree direction logic in AgentManagerApp to use
  adjacentHint instead of inline index arithmetic
This commit is contained in:
Marius
2026-02-20 13:26:04 +01:00
committed by GitHub
parent 658878a362
commit cb18296f88
9 changed files with 477 additions and 65 deletions
@@ -7,6 +7,7 @@ import { WorktreeStateManager } from "./WorktreeStateManager"
import { SetupScriptService } from "./SetupScriptService"
import { SetupScriptRunner } from "./SetupScriptRunner"
import { SessionTerminalManager } from "./SessionTerminalManager"
import { formatKeybinding } from "./format-keybinding"
/**
* AgentManagerProvider opens the Agent Manager panel.
@@ -75,6 +76,7 @@ export class AgentManagerProvider implements vscode.Disposable {
void this.initializeState()
void this.sendRepoInfo()
this.sendKeybindings()
this.panel.onDidDispose(() => {
this.log("Panel disposed")
@@ -393,6 +395,28 @@ export class AgentManagerProvider implements vscode.Disposable {
return null
}
// ---------------------------------------------------------------------------
// Keybindings
// ---------------------------------------------------------------------------
private sendKeybindings(): void {
const ext = vscode.extensions.getExtension("kilocode.kilo-code")
const keybindings: Array<{ command: string; key?: string; mac?: string }> =
ext?.packageJSON?.contributes?.keybindings ?? []
const mac = process.platform === "darwin"
const prefix = "kilo-code.new.agentManager."
const bindings: Record<string, string> = {}
for (const kb of keybindings) {
if (!kb.command.startsWith(prefix)) continue
const action = kb.command.slice(prefix.length)
const raw = mac ? (kb.mac ?? kb.key) : kb.key
if (raw) bindings[action] = formatKeybinding(raw, mac)
}
this.postToWebview({ type: "agentManager.keybindings", bindings })
}
// ---------------------------------------------------------------------------
// Setup script
// ---------------------------------------------------------------------------
@@ -0,0 +1,34 @@
const KEY_SYMBOLS: Record<string, { mac: string; other: string }> = {
ctrl: { mac: "⌃", other: "Ctrl" },
cmd: { mac: "⌘", other: "Ctrl" },
shift: { mac: "⇧", other: "Shift" },
alt: { mac: "⌥", other: "Alt" },
}
const SPECIAL_KEYS: Record<string, string> = {
left: "←",
right: "→",
up: "↑",
down: "↓",
backspace: "⌫",
delete: "Del",
enter: "↵",
escape: "Esc",
}
/**
* Format a VS Code keybinding string (e.g. "cmd+shift+w") into
* a display string using platform-appropriate symbols.
* Mac: "⌘⇧W" Windows/Linux: "Ctrl+Shift+W"
*/
export function formatKeybinding(raw: string, mac: boolean): string {
const symbols = raw
.split("+")
.map((p) => p.trim().toLowerCase())
.map((part) => {
const mod = KEY_SYMBOLS[part]
if (mod) return mac ? mod.mac : mod.other
return SPECIAL_KEYS[part] ?? part.toUpperCase()
})
return mac ? symbols.join("") : symbols.join("+")
}
@@ -0,0 +1,72 @@
import { describe, it, expect } from "bun:test"
import { formatKeybinding } from "../../src/agent-manager/format-keybinding"
describe("formatKeybinding", () => {
describe("mac", () => {
it("formats cmd as ⌘", () => {
expect(formatKeybinding("cmd+w", true)).toBe("⌘W")
})
it("formats cmd+shift as ⌘⇧", () => {
expect(formatKeybinding("cmd+shift+w", true)).toBe("⌘⇧W")
})
it("formats ctrl as ⌃", () => {
expect(formatKeybinding("ctrl+c", true)).toBe("⌃C")
})
it("formats alt as ⌥", () => {
expect(formatKeybinding("alt+f", true)).toBe("⌥F")
})
it("formats arrow keys as symbols", () => {
expect(formatKeybinding("cmd+left", true)).toBe("⌘←")
expect(formatKeybinding("cmd+right", true)).toBe("⌘→")
expect(formatKeybinding("cmd+up", true)).toBe("⌘↑")
expect(formatKeybinding("cmd+down", true)).toBe("⌘↓")
})
it("formats special keys", () => {
expect(formatKeybinding("cmd+backspace", true)).toBe("⌘⌫")
expect(formatKeybinding("cmd+enter", true)).toBe("⌘↵")
expect(formatKeybinding("escape", true)).toBe("Esc")
})
it("joins without separator on mac", () => {
expect(formatKeybinding("cmd+shift+alt+t", true)).toBe("⌘⇧⌥T")
})
it("formats plain key", () => {
expect(formatKeybinding("cmd+/", true)).toBe("⌘/")
})
})
describe("windows/linux", () => {
it("formats cmd as Ctrl", () => {
expect(formatKeybinding("cmd+w", false)).toBe("Ctrl+W")
})
it("formats ctrl as Ctrl", () => {
expect(formatKeybinding("ctrl+w", false)).toBe("Ctrl+W")
})
it("formats ctrl+shift", () => {
expect(formatKeybinding("ctrl+shift+w", false)).toBe("Ctrl+Shift+W")
})
it("formats alt as Alt", () => {
expect(formatKeybinding("alt+f", false)).toBe("Alt+F")
})
it("formats arrow keys as symbols", () => {
expect(formatKeybinding("ctrl+left", false)).toBe("Ctrl+←")
expect(formatKeybinding("ctrl+right", false)).toBe("Ctrl+→")
expect(formatKeybinding("ctrl+up", false)).toBe("Ctrl+↑")
expect(formatKeybinding("ctrl+down", false)).toBe("Ctrl+↓")
})
it("joins with + separator on non-mac", () => {
expect(formatKeybinding("ctrl+shift+alt+t", false)).toBe("Ctrl+Shift+Alt+T")
})
})
})
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test"
import { resolveNavigation, validateLocalSession, LOCAL } from "../../webview-ui/agent-manager/navigate"
import { resolveNavigation, validateLocalSession, adjacentHint, LOCAL } from "../../webview-ui/agent-manager/navigate"
const ids = ["a", "b", "c", "d"]
@@ -134,3 +134,49 @@ describe("validateLocalSession", () => {
expect(validateLocalSession(undefined, [])).toBeUndefined()
})
})
describe("adjacentHint", () => {
const flat = [LOCAL, "wt1", "wt2", "wt3", "s1"]
it("returns prev hint when item is directly above active", () => {
expect(adjacentHint("wt1", "wt2", flat, "⌘↑", "⌘↓")).toBe("⌘↑")
})
it("returns next hint when item is directly below active", () => {
expect(adjacentHint("wt3", "wt2", flat, "⌘↑", "⌘↓")).toBe("⌘↓")
})
it("returns empty string for the active item itself", () => {
expect(adjacentHint("wt2", "wt2", flat, "⌘↑", "⌘↓")).toBe("")
})
it("returns empty string for non-adjacent items", () => {
expect(adjacentHint("wt1", "wt3", flat, "⌘↑", "⌘↓")).toBe("")
expect(adjacentHint("s1", "wt1", flat, "⌘↑", "⌘↓")).toBe("")
})
it("returns empty string when active is undefined", () => {
expect(adjacentHint("wt1", undefined, flat, "⌘↑", "⌘↓")).toBe("")
})
it("returns empty string when active is not in list", () => {
expect(adjacentHint("wt1", "unknown", flat, "⌘↑", "⌘↓")).toBe("")
})
it("returns empty string when item is not in list", () => {
expect(adjacentHint("unknown", "wt2", flat, "⌘↑", "⌘↓")).toBe("")
})
it("works at boundaries — first item with LOCAL active", () => {
expect(adjacentHint("wt1", LOCAL, flat, "⌘↑", "⌘↓")).toBe("⌘↓")
})
it("works at boundaries — LOCAL with first item active", () => {
expect(adjacentHint(LOCAL, "wt1", flat, "⌘↑", "⌘↓")).toBe("⌘↑")
})
it("works with single-item list", () => {
expect(adjacentHint("a", "b", ["a", "b"], "prev", "next")).toBe("prev")
expect(adjacentHint("b", "a", ["a", "b"], "prev", "next")).toBe("next")
})
})
@@ -16,6 +16,7 @@ import type {
AgentManagerRepoInfoMessage,
AgentManagerWorktreeSetupMessage,
AgentManagerStateMessage,
AgentManagerKeybindingsMessage,
WorktreeState,
ManagedSessionState,
SessionInfo,
@@ -36,7 +37,8 @@ import { Icon } from "@kilocode/kilo-ui/icon"
import { Button } from "@kilocode/kilo-ui/button"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
import { HoverCard } from "@kilocode/kilo-ui/hover-card"
import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu"
import { VSCodeProvider, useVSCode } from "../src/context/vscode"
import { ServerProvider } from "../src/context/server"
@@ -47,7 +49,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, nextSelectionAfterDelete, LOCAL } from "./navigate"
import { validateLocalSession, nextSelectionAfterDelete, adjacentHint, LOCAL } from "./navigate"
import { reorderTabs, applyTabOrder, firstOrderedTitle } from "./tab-order"
import { ConstrainDragYAxis, SortableTab } from "./sortable-tab"
import "./agent-manager.css"
@@ -63,7 +65,19 @@ interface SetupState {
type SidebarSelection = typeof LOCAL | string | null
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const modKey = isMac ? "\u2318" : "Ctrl+"
// Fallback keybindings before extension sends resolved ones
const defaultBindings: Record<string, string> = {
previousSession: isMac ? "⌘↑" : "Ctrl+↑",
nextSession: isMac ? "⌘↓" : "Ctrl+↓",
previousTab: isMac ? "⌘←" : "Ctrl+←",
nextTab: isMac ? "⌘→" : "Ctrl+→",
showTerminal: isMac ? "⌘/" : "Ctrl+/",
newTab: isMac ? "⌘T" : "Ctrl+T",
closeTab: isMac ? "⌘W" : "Ctrl+W",
newWorktree: isMac ? "⌘N" : "Ctrl+N",
closeWorktree: isMac ? "⌘⇧W" : "Ctrl+Shift+W",
}
/** Manages horizontal scroll for the tab list: hides the scrollbar, converts
* vertical wheel events to horizontal scroll, tracks overflow to show/hide
@@ -135,6 +149,8 @@ const AgentManagerContent: Component = () => {
const vscode = useVSCode()
const dialog = useDialog()
const [kb, setKb] = createSignal<Record<string, string>>(defaultBindings)
const [setup, setSetup] = createSignal<SetupState>({ active: false, message: "" })
const [worktrees, setWorktrees] = createSignal<WorktreeState[]>([])
const [managedSessions, setManagedSessions] = createSignal<ManagedSessionState[]>([])
@@ -455,6 +471,11 @@ const AgentManagerContent: Component = () => {
session.selectSession(ev.sessionId)
}
if (msg.type === "agentManager.keybindings") {
const ev = msg as AgentManagerKeybindingsMessage
setKb(ev.bindings)
}
if (msg.type === "agentManager.state") {
const state = msg as AgentManagerStateMessage
setWorktrees(state.worktrees)
@@ -754,40 +775,107 @@ const AgentManagerContent: Component = () => {
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<IconButton
icon="plus"
size="small"
variant="ghost"
label="New Worktree"
onClick={handleCreateWorktree}
/>
<TooltipKeybind title="New worktree" keybind={kb().newWorktree ?? ""} placement="bottom">
<IconButton
icon="plus"
size="small"
variant="ghost"
label="New worktree"
onClick={handleCreateWorktree}
/>
</TooltipKeybind>
</div>
</div>
<div class="am-worktree-list">
<For each={worktrees()}>
{(wt) => (
<div
class={`am-worktree-item ${selection() === wt.id ? "am-worktree-item-active" : ""}`}
data-sidebar-id={wt.id}
onClick={() => selectWorktree(wt.id)}
>
<Icon name="branch" size="small" />
<span class="am-worktree-branch" title={wt.branch}>
{worktreeLabel(wt)}
</span>
<Show when={!deletingWorktrees().has(wt.id)} fallback={<Spinner class="am-worktree-spinner" />}>
<IconButton
icon="close-small"
size="small"
variant="ghost"
label="Close worktree"
class="am-worktree-close"
onClick={(e: MouseEvent) => handleDeleteWorktree(wt.id, e)}
/>
</Show>
</div>
)}
</For>
{(() => {
const [hoveredWt, setHoveredWt] = createSignal<string | null>(null)
const [overClose, setOverClose] = createSignal(false)
return (
<For each={worktrees()}>
{(wt, wtIdx) => {
const sessions = createMemo(() => managedSessions().filter((ms) => ms.worktreeId === wt.id))
const navHint = () => {
const flat = [
LOCAL as string,
...worktrees().map((w) => w.id),
...unassignedSessions().map((s) => s.id),
]
const active = selection() ?? session.currentSessionID() ?? ""
return adjacentHint(wt.id, active, flat, kb().previousSession ?? "", kb().nextSession ?? "")
}
return (
<HoverCard
openDelay={100}
closeDelay={100}
placement="right-start"
gutter={8}
open={hoveredWt() === wt.id && !overClose()}
onOpenChange={(open) => setHoveredWt(open ? wt.id : null)}
trigger={
<div
class={`am-worktree-item ${selection() === wt.id ? "am-worktree-item-active" : ""}`}
data-sidebar-id={wt.id}
onClick={() => selectWorktree(wt.id)}
>
<Icon name="branch" size="small" />
<span class="am-worktree-branch">{worktreeLabel(wt)}</span>
<Show
when={!deletingWorktrees().has(wt.id)}
fallback={<Spinner class="am-worktree-spinner" />}
>
<div
class="am-worktree-close"
onMouseEnter={() => setOverClose(true)}
onMouseLeave={() => setOverClose(false)}
>
<TooltipKeybind
title="Delete worktree"
keybind={kb().closeWorktree ?? ""}
placement="top"
>
<IconButton
icon="close-small"
size="small"
variant="ghost"
label="Delete worktree"
onClick={(e: MouseEvent) => handleDeleteWorktree(wt.id, e)}
/>
</TooltipKeybind>
</div>
</Show>
</div>
}
>
<div class="am-hover-card">
<div class="am-hover-card-header">
<div>
<div class="am-hover-card-label">BRANCH</div>
<div class="am-hover-card-branch">{wt.branch}</div>
<div class="am-hover-card-meta">{formatRelativeDate(wt.createdAt)}</div>
</div>
<Show when={navHint()}>
<span class="am-hover-card-keybind">{navHint()}</span>
</Show>
</div>
<Show when={wt.parentBranch}>
<div class="am-hover-card-divider" />
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">Base</span>
<span class="am-hover-card-row-value">{wt.parentBranch}</span>
</div>
</Show>
<div class="am-hover-card-divider" />
<div class="am-hover-card-row">
<span class="am-hover-card-row-label">Sessions</span>
<span class="am-hover-card-row-value">{sessions().length}</span>
</div>
</div>
</HoverCard>
)
}}
</For>
)
})()}
<Show when={worktrees().length === 0}>
<button class="am-worktree-create" onClick={handleCreateWorktree}>
<Icon name="plus" size="small" />
@@ -816,14 +904,17 @@ const AgentManagerContent: Component = () => {
>
<span class="am-item-title">{s.title || "Untitled"}</span>
<span class="am-item-time">{formatRelativeDate(s.updatedAt)}</span>
<IconButton
icon="branch"
size="small"
variant="ghost"
label="Open in worktree"
class="am-item-promote"
onClick={(e: MouseEvent) => handlePromote(s.id, e)}
/>
<div class="am-item-promote">
<TooltipKeybind title="Open in worktree" keybind={kb().newWorktree ?? ""} placement="right">
<IconButton
icon="branch"
size="small"
variant="ghost"
label="Open in worktree"
onClick={(e: MouseEvent) => handlePromote(s.id, e)}
/>
</TooltipKeybind>
</div>
</button>
)}
</For>
@@ -854,10 +945,18 @@ const AgentManagerContent: Component = () => {
pending
? s.id === activePendingId() && !session.currentSessionID()
: s.id === session.currentSessionID()
const tabDirection = () => {
if (active()) return ""
const ids = activeTabs().map((t) => t.id)
const activeId = session.currentSessionID() ?? activePendingId() ?? ""
return adjacentHint(s.id, activeId, ids, kb().previousTab ?? "", kb().nextTab ?? "")
}
return (
<SortableTab
tab={s}
active={active()}
keybind={tabDirection()}
closeKeybind={kb().closeTab ?? ""}
onSelect={() => {
if (pending) {
setActivePendingId(s.id)
@@ -877,16 +976,18 @@ const AgentManagerContent: Component = () => {
</div>
<div class={`am-tab-fade am-tab-fade-right ${tabScroll.showRight() ? "am-tab-fade-visible" : ""}`} />
</div>
<IconButton
icon="plus"
size="small"
variant="ghost"
label={`New session (${modKey}T)`}
class="am-tab-add"
onClick={handleAddSession}
/>
<TooltipKeybind title="New session" keybind={kb().newTab ?? ""} placement="bottom">
<IconButton
icon="plus"
size="small"
variant="ghost"
label="New session"
class="am-tab-add"
onClick={handleAddSession}
/>
</TooltipKeybind>
<div class="am-tab-terminal">
<Tooltip value="Open Terminal" placement="bottom">
<TooltipKeybind title="Terminal" keybind={kb().showTerminal ?? ""} placement="bottom">
<IconButton
icon="console"
size="small"
@@ -897,7 +998,7 @@ const AgentManagerContent: Component = () => {
if (id) vscode.postMessage({ type: "agentManager.showTerminal", sessionId: id })
}}
/>
</Tooltip>
</TooltipKeybind>
</div>
</div>
<DragOverlay>
@@ -921,7 +1022,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>
<span class="am-shortcut-hint">{kb().newTab ?? ""}</span>
</Button>
</div>
</Show>
@@ -122,11 +122,16 @@
flex-direction: column;
gap: 2px;
overflow-y: auto;
overflow-x: hidden;
max-height: 50vh;
}
/* Worktree item — larger card style */
.am-worktree-list [data-slot="hover-card-trigger"] {
min-width: 0;
}
.am-worktree-item {
position: relative;
display: flex;
@@ -137,6 +142,8 @@
cursor: pointer;
font-size: var(--font-size-base);
color: var(--text-base);
min-width: 0;
width: 100%;
}
.am-worktree-item:hover {
@@ -628,3 +635,87 @@
opacity: 0.6;
margin-left: 6px;
}
/* HoverCard popover for worktree items */
.am-hover-card {
padding: 10px 12px;
min-width: 160px;
max-width: 240px;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
.am-hover-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.am-hover-card-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-weaker);
line-height: 1.4;
}
.am-hover-card-branch {
font-size: 13px;
font-weight: 600;
color: var(--text-strong);
line-height: 1.4;
word-break: break-all;
}
.am-hover-card-meta {
font-size: 12px;
color: var(--text-weaker);
line-height: 1.4;
}
.am-hover-card-keybind {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
height: 20px;
padding: 0 6px;
border-radius: 3px;
background: var(--surface-inset-base);
border: 1px solid var(--border-weak-base);
font-family: var(--font-family-sans);
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--text-weak);
white-space: nowrap;
}
.am-hover-card-divider {
height: 1px;
background: var(--border-weak-base);
margin: 6px 0;
}
.am-hover-card-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.am-hover-card-row-label {
font-size: 12px;
color: var(--text-weaker);
}
.am-hover-card-row-value {
font-size: 12px;
font-weight: 500;
color: var(--text-base);
}
@@ -46,6 +46,34 @@ export function validateLocalSession(persisted: string | undefined, ids: string[
return persisted
}
/**
* Return the keybinding hint for an item adjacent to the active item.
* Only returns a hint when the item is exactly one step away in the flat list.
* Returns empty string for non-adjacent items or the active item itself.
*
* @param itemId - The item being hovered
* @param activeId - The currently selected/active item (or undefined for LOCAL)
* @param flatIds - The full ordered sidebar list (LOCAL first, then worktrees, then sessions)
* @param prev - Display string for "go up" (e.g. "⌘↑" or keybinding)
* @param next - Display string for "go down" (e.g. "⌘↓" or keybinding)
*/
export function adjacentHint(
itemId: string,
activeId: string | undefined,
flatIds: string[],
prev: string,
next: string,
): string {
if (!activeId || itemId === activeId) return ""
const activeIdx = flatIds.indexOf(activeId)
const itemIdx = flatIds.indexOf(itemId)
if (activeIdx === -1 || itemIdx === -1) return ""
const diff = itemIdx - activeIdx
if (diff === -1) return prev
if (diff === 1) return next
return ""
}
/**
* After removing a worktree, pick the nearest remaining sidebar neighbor.
* Order: the worktree just below → the one above → LOCAL.
@@ -8,7 +8,7 @@ import type { Transformer } from "@thisbeyond/solid-dnd"
import { createRoot } from "solid-js"
import type { SessionInfo } from "../src/types/messages"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
/** Lock drag movement to the X axis (horizontal-only tab dragging). */
export const ConstrainDragYAxis: Component = () => {
@@ -33,6 +33,8 @@ export const ConstrainDragYAxis: Component = () => {
export const SortableTab: Component<{
tab: SessionInfo
active: boolean
keybind?: string
closeKeybind?: string
onSelect: () => void
onMiddleClick: (e: MouseEvent) => void
onClose: (e: MouseEvent) => void
@@ -47,23 +49,30 @@ export const SortableTab: Component<{
class={`am-tab-sortable ${sortable.isActiveDraggable ? "am-tab-dragging" : ""}`}
data-tab-id={props.tab.id}
>
<Tooltip value={props.tab.title || "Untitled"} placement="bottom">
<TooltipKeybind
title={props.tab.title || "Untitled"}
keybind={props.keybind ?? ""}
placement="bottom"
inactive={props.active}
>
<div
class={`am-tab ${props.active ? "am-tab-active" : ""}`}
onClick={props.onSelect}
onMouseDown={props.onMiddleClick}
>
<span class="am-tab-label">{props.tab.title || "Untitled"}</span>
<IconButton
icon="close-small"
size="small"
variant="ghost"
label="Close tab"
class="am-tab-close"
onClick={props.onClose}
/>
<TooltipKeybind title="Close" keybind={props.closeKeybind ?? ""} placement="bottom">
<IconButton
icon="close-small"
size="small"
variant="ghost"
label="Close tab"
class="am-tab-close"
onClick={props.onClose}
/>
</TooltipKeybind>
</div>
</Tooltip>
</TooltipKeybind>
</div>
)
}
@@ -566,6 +566,12 @@ export interface AgentManagerStateMessage {
tabOrder?: Record<string, string[]>
}
// Resolved keybindings for agent manager actions
export interface AgentManagerKeybindingsMessage {
type: "agentManager.keybindings"
bindings: Record<string, string>
}
export type ExtensionMessage =
| ReadyMessage
| ConnectionStateMessage
@@ -604,6 +610,7 @@ export type ExtensionMessage =
| AgentManagerWorktreeSetupMessage
| AgentManagerSessionAddedMessage
| AgentManagerStateMessage
| AgentManagerKeybindingsMessage
| SetChatBoxMessage
| TriggerTaskMessage