fix(vscode): extract side panel resize scheduler

This commit is contained in:
Johnny Eric Amancio
2026-08-06 15:41:25 +02:00
parent 80124bc483
commit 3a572d6c19
3 changed files with 52 additions and 23 deletions
@@ -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", () => {
@@ -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<string, string> = {
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<SidePanel>(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)
@@ -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)
}
}