fix(agent-manager): address review findings

This commit is contained in:
marius-kilocode
2026-08-04 11:34:46 +02:00
parent 245ada532e
commit cf4a8f2f74
10 changed files with 101 additions and 51 deletions
@@ -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
@@ -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,
}
}
@@ -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()
@@ -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({
@@ -96,7 +96,7 @@ export const ProjectsSection: Component<ProjectsSectionProps> = (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)
}}
/>
<Show when={project().expanded}>
@@ -31,16 +31,23 @@ const SectionHeader: Component<Props> = (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> = (props) => {
}
const cancel = () => {
cancelled = true
setRenaming(false)
props.onRenameEnd?.()
}
@@ -83,6 +91,7 @@ const SectionHeader: Component<Props> = (props) => {
if (e.key === "Enter") commit()
if (e.key === "Escape") cancel()
}}
onBlur={commit}
onClick={(e) => e.stopPropagation()}
ref={(el) =>
requestAnimationFrame(() =>
@@ -14,31 +14,30 @@ interface Props {
/** Shared layout for sidebar headings with a fixed leading control column. */
export const SidebarSectionHeader: Component<Props> = (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 (
<div
class={`am-sidebar-header${props.class ? ` ${props.class}` : ""}`}
role={props.onToggle ? "button" : undefined}
tabIndex={props.onToggle ? 0 : undefined}
aria-expanded={props.onToggle ? props.expanded : undefined}
aria-label={props.ariaLabel}
class={`am-sidebar-header${props.onToggle ? " am-sidebar-header-toggleable" : ""}${props.class ? ` ${props.class}` : ""}`}
title={props.title}
onClick={(event) => {
if (event.button === 0) props.onToggle?.()
}}
onKeyDown={keydown}
>
<div class="am-sidebar-header-main">
<Show when={props.onToggle}>
<span class="am-sidebar-header-chevron" aria-hidden="true">
<Icon name={props.expanded ? "chevron-down" : "chevron-right"} size="small" />
</span>
<button
class="am-sidebar-header-toggle"
type="button"
aria-expanded={props.expanded}
aria-label={props.ariaLabel}
onClick={(event) => {
event.stopPropagation()
props.onToggle?.()
}}
>
<span class="am-sidebar-header-chevron" aria-hidden="true">
<Icon name={props.expanded ? "chevron-down" : "chevron-right"} size="small" />
</span>
</button>
</Show>
<div class="am-sidebar-header-label">{props.label}</div>
</div>
@@ -33,6 +33,7 @@ export const UnassignedSessionsSection: Component<Props> = (props) => {
<SidebarSectionHeader
class="am-section-header am-section-toggle"
expanded={!props.collapsed()}
ariaLabel={t("agentManager.section.sessions")}
label={<span class="am-section-label">{t("agentManager.section.sessions")}</span>}
onToggle={props.onToggle}
/>
@@ -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;
}
@@ -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 (
<StoryProviders noPadding>
<div style={{ "min-height": "430px", padding: "16px", background: "var(--surface-base)" }}>
<div class="am-section-header">
<span class="am-section-label">WORKTREES</span>
<div class="am-section-actions">
<SidebarSearchMenu
items={() => 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}
/>
</div>
</div>
<SidebarSectionHeader
class="am-section-header"
label={<span class="am-section-label">WORKTREES</span>}
actions={
<div class="am-section-actions">
<SidebarSearchMenu
items={() => 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}
/>
</div>
}
/>
<output class="sr-only" data-slot="sidebar-search-selection">
{selected()}
</output>