diff --git a/.changeset/sync-inspector-width.md b/.changeset/sync-inspector-width.md new file mode 100644 index 00000000000..02eeb4e7239 --- /dev/null +++ b/.changeset/sync-inspector-width.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Persist the Agent Manager inspector width and share it between the terminal and diff viewer. diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-layout.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-layout.test.ts index f30ba800e52..a38ffbb7b99 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-layout.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-layout.test.ts @@ -1,8 +1,10 @@ import { expect, test } from "bun:test" import { readFileSync } from "node:fs" import { resolve } from "node:path" +import { clampPanelWidth, maxPanelWidth, minPanelWidth } from "../../webview-ui/agent-manager/side-panel-layout" const css = readFileSync(resolve(import.meta.dir, "../../webview-ui/agent-manager/agent-manager.css"), "utf8") +const app = readFileSync(resolve(import.meta.dir, "../../webview-ui/agent-manager/AgentManagerApp.tsx"), "utf8") test("xterm owns the padding used by FitAddon", () => { const host = css.match(/\.am-terminal-host\s*\{([^}]*)\}/)?.[1] @@ -13,3 +15,22 @@ test("xterm owns the padding used by FitAddon", () => { expect(host).not.toMatch(/\bpadding\s*:/) expect(term).toMatch(/\bpadding\s*:\s*8px\s*;/) }) + +test("uses one persisted width for the diff and terminal inspector", () => { + expect(app).toContain("persisted?.sidePanelWidth") + expect(app).toContain("setPanelWidth(pendingSideWidth!)") + expect(app).not.toContain("diffWidth") + expect(app).not.toContain("terminalWidth") +}) + +test("clamps the restored inspector width to the shared layout bounds", () => { + expect(clampPanelWidth(undefined, 1200)).toBe(600) + expect(clampPanelWidth(500, 1200)).toBe(500) + expect(clampPanelWidth(1000, 1000)).toBe(maxPanelWidth(1000)) + expect(clampPanelWidth(100, 1200)).toBe(minPanelWidth(1200)) + expect(clampPanelWidth("invalid", 1200)).toBe(600) + expect(minPanelWidth(400)).toBe(200) + expect(maxPanelWidth(400)).toBe(320) + expect(clampPanelWidth(undefined, 400)).toBe(200) + expect(clampPanelWidth(360, 400)).toBe(320) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 939a8e505be..969fa6f5b89 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -173,6 +173,7 @@ import { createMarkdownRender } from "./review-preferences" import { createSidebarCollapse } from "./sidebar-collapse" import { SidebarToggleButton } from "./SidebarToggleButton" import { setTabWidths } from "./tab-widths" +import { clampPanelWidth, maxPanelWidth, minPanelWidth } from "./side-panel-layout" import { buildShortcutCategories } from "./shortcuts" import { tracker } from "./telemetry" import { createChatFocus, hasQuestionOption } from "./focus" @@ -273,7 +274,7 @@ const AgentManagerContent: Component = () => { const MAX_SIDEBAR_WIDTH_RATIO = 0.4 // Recover persisted local session IDs from webview state - const persisted = vscode.getState() + const persisted = vscode.getState() const registry = createProjectRegistry({ persisted: persisted ?? {}, activeId: () => currentProjectId() ?? "single", @@ -313,26 +314,15 @@ const AgentManagerContent: Component = () => { const diffLoading = diffs.diffLoading const setDiffLoading = diffs.setDiffLoading const diffNotices = diffs.diffNotices - // The diff and terminal panels each remember their own width: a diff - // benefits from half the window, a terminal only needs about a third. - const TERMINAL_MIN_WIDTH = 360 - const TERMINAL_MAX_WIDTH = 640 - const [diffWidth, setDiffWidth] = createSignal(Math.round(window.innerWidth * 0.5)) - const [terminalWidth, setTerminalWidth] = createSignal( - Math.min(TERMINAL_MAX_WIDTH, Math.max(TERMINAL_MIN_WIDTH, Math.round(window.innerWidth / 3))), - ) - // The hidden-but-mounted host still fits the terminal, so pick the - // terminal's width whenever one is alive and no other mode is showing. - const widthMode = () => sidePanel() ?? (terms.sides().length > 0 ? "terminal" : null) - const hostWidth = () => (widthMode() === "terminal" ? terminalWidth() : diffWidth()) - const sideMin = () => (widthMode() === "terminal" ? TERMINAL_MIN_WIDTH : 200) + // Diff and terminal views share one inspector width, restored from webview + // state so the user's divider position survives panel reloads. + const [panelWidth, setPanelWidth] = createSignal(clampPanelWidth(persisted?.sidePanelWidth, window.innerWidth)) const resizeSide = (width: number) => { - pendingSideWidth = Math.max(sideMin(), Math.min(width, window.innerWidth * 0.8)) + pendingSideWidth = clampPanelWidth(width, window.innerWidth) if (sideRaf !== undefined) return sideRaf = requestAnimationFrame(() => { sideRaf = undefined - if (widthMode() === "terminal") setTerminalWidth(pendingSideWidth!) - else setDiffWidth(pendingSideWidth!) + setPanelWidth(pendingSideWidth!) }) } const showSideTerminal = () => { @@ -642,6 +632,7 @@ const AgentManagerContent: Component = () => { }, key: () => registry.active().id, width: sidebarWidth, + panelWidth, get: () => vscode.getState>(), set: (value) => vscode.setState(value), }) @@ -2637,16 +2628,16 @@ const AgentManagerContent: Component = () => { 0}>
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/project/local-tabs.ts b/packages/kilo-vscode/webview-ui/agent-manager/project/local-tabs.ts index ea57612e34e..9bcf91a378b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/project/local-tabs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/project/local-tabs.ts @@ -2,13 +2,14 @@ import { createEffect, createMemo, onCleanup } from "solid-js" import type { SessionInfo } from "../../src/types/messages/sessions" /** - * Persist open tabs and the sidebar width to webview state for recovery. + * Persist open tabs and panel widths to webview state for recovery. * Debounced so a resize drag does not serialize state on every pixel. */ export function persistLocalTabs(opts: { tabs: () => Record key: () => string width: () => number + panelWidth?: () => number get: () => Record | undefined set: (value: Record) => void }): void { @@ -18,9 +19,16 @@ export function persistLocalTabs(opts: { const tabs = opts.tabs() const key = opts.key() const width = opts.width() + const panel = opts.panelWidth?.() clearTimeout(timer) timer = setTimeout(() => { - opts.set({ ...(opts.get() ?? {}), localTabs: tabs, localSessionIDs: tabs[key] ?? [], sidebarWidth: width }) + opts.set({ + ...(opts.get() ?? {}), + localTabs: tabs, + localSessionIDs: tabs[key] ?? [], + sidebarWidth: width, + ...(panel === undefined ? {} : { sidePanelWidth: panel }), + }) }, 300) }) onCleanup(() => clearTimeout(timer)) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts b/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts new file mode 100644 index 00000000000..8cfaafe4f9e --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts @@ -0,0 +1,25 @@ +export const MIN_PANEL_WIDTH = 360 +const DEFAULT_PANEL_WIDTH_RATIO = 0.5 +const MAX_PANEL_WIDTH_RATIO = 0.8 + +function viewportWidth(viewport: number): number { + return Number.isFinite(viewport) && viewport > 0 ? viewport : MIN_PANEL_WIDTH +} + +export function minPanelWidth(viewport: number): number { + const width = viewportWidth(viewport) + return Math.min(MIN_PANEL_WIDTH, Math.round(width * DEFAULT_PANEL_WIDTH_RATIO)) +} + +export function maxPanelWidth(viewport: number): number { + const width = viewportWidth(viewport) + return Math.max(minPanelWidth(width), Math.round(width * MAX_PANEL_WIDTH_RATIO)) +} + +/** Restore or constrain the shared inspector width without trusting saved state. */ +export function clampPanelWidth(value: unknown, viewport: number): number { + const width = viewportWidth(viewport) + const fallback = Math.round(width * DEFAULT_PANEL_WIDTH_RATIO) + const candidate = typeof value === "number" && Number.isFinite(value) ? value : fallback + return Math.round(Math.max(minPanelWidth(width), Math.min(candidate, maxPanelWidth(width)))) +}