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 185e7770c8..f195876afa 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,7 +1,12 @@ 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" +import { + clampPanelWidth, + createPanelResize, + 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") @@ -22,14 +27,28 @@ test("xterm owns the padding used by FitAddon", () => { test("uses one persisted width for the diff and terminal inspector", () => { expect(app).toContain("persisted?.sidePanelWidth") - expect(app).toContain("setPanelWidth(pendingSideWidth!)") + expect(app).toContain("createPanelResize(setPanelWidth") expect(app).not.toContain("diffWidth") expect(app).not.toContain("terminalWidth") }) test("limits inspector layout updates during resize", () => { - expect(app).toContain("SIDE_RESIZE_INTERVAL_MS = 32") - expect(app).toContain("time - sideResizeTime < SIDE_RESIZE_INTERVAL_MS") + const frames: ((time: number) => void)[] = [] + const widths: number[] = [] + const resize = createPanelResize( + (width) => widths.push(width), + () => 1200, + (frame) => frames.push(frame), + ) + + resize(700) + resize(720) + expect(frames).toHaveLength(1) + frames.shift()!(16) + expect(frames).toHaveLength(1) + expect(widths).toEqual([]) + frames.shift()!(32) + expect(widths).toEqual([720]) }) test("does not refit hidden terminal buffers during resize", () => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 615ebb350d..3386334d07 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -177,7 +177,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 { clampPanelWidth, createPanelResize, maxPanelWidth, minPanelWidth } from "./side-panel-layout" import { buildShortcutCategories } from "./shortcuts" import { tracker } from "./telemetry" import { createChatFocus, hasQuestionOption } from "./focus" @@ -201,7 +201,6 @@ type SidePanel = "diff" | "pr" | "terminal" | null const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) // Fallback keybindings before extension sends resolved ones const MAX_JUMP_INDEX = 9 -const SIDE_RESIZE_INTERVAL_MS = 32 const defaultBindings: Record = { previousSession: isMac ? "⌘⌥↑" : "Ctrl+Alt+↑", @@ -322,9 +321,6 @@ const AgentManagerContent: Component = () => { // rAF coalescing for resize handlers — at most one signal write per frame let sidebarRaf: number | undefined let pendingSidebarWidth: number | undefined - let sideRaf: number | undefined - let pendingSideWidth: number | undefined - let sideResizeTime = 0 const [history, setHistory] = createSignal(false) const [sidePanel, setSidePanel] = createSignal(null) @@ -337,20 +333,7 @@ const AgentManagerContent: Component = () => { // 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 = clampPanelWidth(width, window.innerWidth) - if (sideRaf !== undefined) return - const flush = (time: number) => { - if (time - sideResizeTime < SIDE_RESIZE_INTERVAL_MS) { - sideRaf = requestAnimationFrame(flush) - return - } - sideRaf = undefined - sideResizeTime = time - setPanelWidth(pendingSideWidth!) - } - sideRaf = requestAnimationFrame(flush) - } + const resizeSide = createPanelResize(setPanelWidth, () => window.innerWidth) const showSideTerminal = () => { setHistory(false) setReviewActive(false) 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 index 8cfaafe4f9..3b74f85d3d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts @@ -1,6 +1,9 @@ export const MIN_PANEL_WIDTH = 360 const DEFAULT_PANEL_WIDTH_RATIO = 0.5 const MAX_PANEL_WIDTH_RATIO = 0.8 +export const SIDE_RESIZE_INTERVAL_MS = 32 + +type Frame = (callback: (time: number) => void) => number function viewportWidth(viewport: number): number { return Number.isFinite(viewport) && viewport > 0 ? viewport : MIN_PANEL_WIDTH @@ -23,3 +26,27 @@ export function clampPanelWidth(value: unknown, viewport: number): number { const candidate = typeof value === "number" && Number.isFinite(value) ? value : fallback return Math.round(Math.max(minPanelWidth(width), Math.min(candidate, maxPanelWidth(width)))) } + +export function createPanelResize( + update: (width: number) => void, + viewport: () => number, + frame: Frame = requestAnimationFrame, +) { + let raf: number | undefined + let pending = 0 + let time = 0 + const flush = (now: number) => { + if (now - time < SIDE_RESIZE_INTERVAL_MS) { + raf = frame(flush) + return + } + raf = undefined + time = now + update(pending) + } + return (width: number) => { + pending = clampPanelWidth(width, viewport()) + if (raf !== undefined) return + raf = frame(flush) + } +}