Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sync-inspector-width.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Persist the Agent Manager inspector width and share it between the terminal and diff viewer.
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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)
})
33 changes: 12 additions & 21 deletions packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<PersistedProjectTabs & { sidebarWidth?: number }>()
const persisted = vscode.getState<PersistedProjectTabs & { sidebarWidth?: number; sidePanelWidth?: number }>()
const registry = createProjectRegistry({
persisted: persisted ?? {},
activeId: () => currentProjectId() ?? "single",
Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -642,6 +632,7 @@ const AgentManagerContent: Component = () => {
},
key: () => registry.active().id,
width: sidebarWidth,
panelWidth,
get: () => vscode.getState<Record<string, unknown>>(),
set: (value) => vscode.setState(value),
})
Expand Down Expand Up @@ -2637,16 +2628,16 @@ const AgentManagerContent: Component = () => {
<Show when={sidePanel() !== null || terms.sides().length > 0}>
<div
class={`am-diff-resize ${sidePanel() === null ? "am-side-host-hidden" : ""}`}
style={{ width: `${hostWidth()}px` }}
style={{ width: `${panelWidth()}px` }}
inert={sidePanel() === null}
>
<Show when={sidePanel() !== null}>
<ResizeHandle
direction="horizontal"
edge="start"
size={hostWidth()}
min={sideMin()}
max={Math.round(window.innerWidth * 0.8)}
size={panelWidth()}
min={minPanelWidth(window.innerWidth)}
max={maxPanelWidth(window.innerWidth)}
onResize={resizeSide}
/>
</Show>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]>
key: () => string
width: () => number
panelWidth?: () => number
get: () => Record<string, unknown> | undefined
set: (value: Record<string, unknown>) => void
}): void {
Expand All @@ -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))
Expand Down
25 changes: 25 additions & 0 deletions packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts
Original file line number Diff line number Diff line change
@@ -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))))
}
Loading