diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 09db8d3227..9c67a03b3a 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -395,6 +395,7 @@ export class AgentManagerProvider implements Disposable { this.panelSessions.clear() void ctx.sessions.abortSessions(ids).catch((err) => this.log("Failed to abort sessions on panel close:", err)) this.statsPoller.stop() + this.projectPollers.dispose() this.prBridge.poller.stop() this.diffs.stop() this.activeSessionId = undefined @@ -450,12 +451,14 @@ export class AgentManagerProvider implements Disposable { /** Initialize an expanded background project and push its state (no panel wiring). */ private initExpanded(ctx: ProjectContext): void { - void initContextState(ctx, (...args) => this.log(...args)).then((result) => { - if (!result.current) return - registerProjectSessions(ctx, this.panel?.sessions) - this.pushState(ctx) - this.projectPollers.sync(this.contexts) - }) + void initContextState(ctx, (...args) => this.log(...args)) + .then((result) => { + if (!result.current) return + registerProjectSessions(ctx, this.panel?.sessions) + this.pushState(ctx) + this.projectPollers.sync(this.contexts) + }) + .catch((err) => this.log("Failed to initialize expanded project:", err)) } // Message interceptor diff --git a/packages/kilo-vscode/src/agent-manager/project/contexts.ts b/packages/kilo-vscode/src/agent-manager/project/contexts.ts index ddc2914f83..95a16d7050 100644 --- a/packages/kilo-vscode/src/agent-manager/project/contexts.ts +++ b/packages/kilo-vscode/src/agent-manager/project/contexts.ts @@ -141,8 +141,12 @@ export class ProjectContexts { } isExpanded(id: string): boolean { - this.rememberExpansion(id, false) - return this.expansion.get(id) === true + const value = this.expansion.get(id) + if (value !== undefined) return value + const stored = this.opts.registry.expanded?.(id) + if (stored !== undefined) return stored + const root = this.opts.workspaceRoot() + return root !== undefined && projectIdFor(canonicalizePath(root)) === id } /** Make a project the active context and expand it. Returns undefined when not allowed. */ @@ -244,16 +248,17 @@ export class ProjectContexts { const id = ctx?.id ?? stored!.id const root = ctx?.root ?? stored!.root const pinned = ctx?.pinned ?? false + const missing = ctx ? ctx.missing() : !(this.opts.deps.exists ?? fs.existsSync)(root) return { id, root, label: stored?.label || path.basename(root) || root, pinned, active: this.isActive(id), - expanded: this.isExpanded(id), + expanded: !missing && this.isExpanded(id), initialized: ctx?.loaded ?? false, trusted: pinned || (stored?.trusted ?? false), - missing: ctx ? ctx.missing() : !(this.opts.deps.exists ?? fs.existsSync)(root), + missing, } } diff --git a/packages/kilo-vscode/src/agent-manager/project/messages.ts b/packages/kilo-vscode/src/agent-manager/project/messages.ts index 28e4c3bde8..4349f709c4 100644 --- a/packages/kilo-vscode/src/agent-manager/project/messages.ts +++ b/packages/kilo-vscode/src/agent-manager/project/messages.ts @@ -196,8 +196,8 @@ async function setExpanded(id: string, expanded: boolean, deps: ProjectMessageDe } await deps.registry.setExpanded(id, expanded) if (expanded) { - deps.contexts.expand(id) - deps.expand(ctx) + const next = deps.contexts.expand(id) + if (next) deps.expand(next) } if (!expanded) deps.contexts.collapse(id) deps.push() diff --git a/packages/kilo-vscode/tests/unit/agent-project-contexts.test.ts b/packages/kilo-vscode/tests/unit/agent-project-contexts.test.ts index 126c0ca546..9c8b6239e9 100644 --- a/packages/kilo-vscode/tests/unit/agent-project-contexts.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-project-contexts.test.ts @@ -210,6 +210,12 @@ describe("ProjectContexts", () => { expect(list[1]!.initialized).toBe(false) }) + it("keeps the pinned project expanded before active state is initialized", () => { + const { contexts } = setup({ workspace: WORKSPACE }) + + expect(contexts.isExpanded(PINNED)).toBe(true) + }) + it("hydrates persisted project expansion without initializing the project", () => { const extra = stored("prj-extra", true) const { contexts } = setup({ diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx index 55dfd4f5aa..5dbe5f7147 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx @@ -96,7 +96,7 @@ export const ProjectsSection: Component = (props) => ( } const expanded = !project().expanded props.onExpand(project().id, expanded) - if (!project().active && project().trusted) props.onSelect(project().id) + if (!project().active && project().trusted && !project().missing) props.onSelect(project().id) }} /> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SectionHeader.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SectionHeader.tsx index 475c8192e4..3bcd0ecbce 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SectionHeader.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SectionHeader.tsx @@ -31,16 +31,23 @@ const SectionHeader: Component = (props) => { const { t } = useLanguage() const [renaming, setRenaming] = createSignal(false) const [value, setValue] = createSignal("") + let cancelled = false const border = () => colorCss(props.section.color) ?? "var(--vscode-panel-border)" const startRename = () => { + cancelled = false setValue(props.section.name) setRenaming(true) } const commit = () => { + if (cancelled) { + cancelled = false + return + } const trimmed = value().trim() + cancelled = true setRenaming(false) props.onRenameEnd?.() if (trimmed && trimmed !== props.section.name) { @@ -49,6 +56,7 @@ const SectionHeader: Component = (props) => { } const cancel = () => { + cancelled = true setRenaming(false) props.onRenameEnd?.() } @@ -83,6 +91,7 @@ const SectionHeader: Component = (props) => { if (e.key === "Enter") commit() if (e.key === "Escape") cancel() }} + onBlur={commit} onClick={(e) => e.stopPropagation()} ref={(el) => requestAnimationFrame(() => diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarSectionHeader.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSectionHeader.tsx index aefcd95ffd..ebbc69446c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SidebarSectionHeader.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSectionHeader.tsx @@ -14,31 +14,30 @@ interface Props { /** Shared layout for sidebar headings with a fixed leading control column. */ export const SidebarSectionHeader: Component = (props) => { - const keydown = (event: KeyboardEvent) => { - if (!props.onToggle || event.target !== event.currentTarget) return - if (event.key !== "Enter" && event.key !== " ") return - event.preventDefault() - props.onToggle() - } - return (
{ if (event.button === 0) props.onToggle?.() }} - onKeyDown={keydown} >
- +
{props.label}
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/UnassignedSessionsSection.tsx b/packages/kilo-vscode/webview-ui/agent-manager/UnassignedSessionsSection.tsx index 22aae23187..625780bc1c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/UnassignedSessionsSection.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/UnassignedSessionsSection.tsx @@ -33,6 +33,7 @@ export const UnassignedSessionsSection: Component = (props) => { {t("agentManager.section.sessions")}} onToggle={props.onToggle} /> 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 c1f150256f..a94cf39d14 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -190,7 +190,7 @@ html[data-theme="kilo-vscode"] box-sizing: border-box; } -.am-sidebar-header[role="button"] { +.am-sidebar-header-toggleable { cursor: pointer; user-select: none; } @@ -213,6 +213,26 @@ html[data-theme="kilo-vscode"] color: var(--text-weak); } +.am-sidebar-header-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; + padding: 0; + color: inherit; + background: none; + border: 0; + border-radius: var(--radius-sm); + cursor: pointer; +} + +.am-sidebar-header-toggle:hover { + color: var(--text-base); + background: var(--surface-inset-base-hover); +} + .am-sidebar-header-label { display: flex; align-items: center; @@ -244,6 +264,9 @@ html[data-theme="kilo-vscode"] } .am-section-header { + display: flex; + align-items: center; + justify-content: space-between; padding: 4px 8px 2px; } diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 9ebef0f876..2ce729af07 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -16,6 +16,7 @@ import { SessionContext } from "../context/session" import { ServerContext } from "../context/server" import { WorktreeModeProvider } from "../context/worktree-mode" import { SidebarSearchMenu } from "../../agent-manager/SidebarSearchMenu" +import { SidebarSectionHeader } from "../../agent-manager/SidebarSectionHeader" import { SidebarToggleButton } from "../../agent-manager/SidebarToggleButton" import { SideTerminalPanel, createTerminalState } from "../../agent-manager/terminal" import { LOCAL } from "../../agent-manager/navigate" @@ -1162,27 +1163,30 @@ export const SidebarSearchOpen: Story = { return (
-
- -
- sidebarSearchItems} - keybind="⌘F" - current={() => sidebarSearchItems.find((item) => item.key === selected())} - labels={{ - search: "Search worktrees and sessions", - scope: "Searches the local workspace, local sessions, worktrees, and their sessions", - contexts: "LOCAL & WORKTREES", - sessions: "SESSIONS", - waiting: "Wait", - retry: "Retry", - }} - onSelect={(item) => setSelected(item.key)} - defaultOpen - portal={false} - /> -
-
+ WORKTREES} + actions={ +
+ sidebarSearchItems} + keybind="⌘F" + current={() => sidebarSearchItems.find((item) => item.key === selected())} + labels={{ + search: "Search worktrees and sessions", + scope: "Searches the local workspace, local sessions, worktrees, and their sessions", + contexts: "LOCAL & WORKTREES", + sessions: "SESSIONS", + waiting: "Wait", + retry: "Retry", + }} + onSelect={(item) => setSelected(item.key)} + defaultOpen + portal={false} + /> +
+ } + /> {selected()}