diff --git a/.changeset/agent-manager-terminal-render-performance.md b/.changeset/agent-manager-terminal-render-performance.md index 3b45e13d54..b3cc4d10e1 100644 --- a/.changeset/agent-manager-terminal-render-performance.md +++ b/.changeset/agent-manager-terminal-render-performance.md @@ -2,4 +2,4 @@ "kilo-code": patch --- -Render Agent Manager terminal output with the WebGL renderer instead of the DOM renderer, and pause rendering for terminals hidden in the background +Use the DOM renderer for Agent Manager terminals to avoid WebGL context failures, while batching output and pausing hidden-terminal rendering diff --git a/bun.lock b/bun.lock index dd9b6393d3..31cb7c0978 100644 --- a/bun.lock +++ b/bun.lock @@ -461,7 +461,6 @@ "@xterm/addon-fit": "0.11.0", "@xterm/addon-unicode-graphemes": "0.4.0", "@xterm/addon-web-links": "0.12.0", - "@xterm/addon-webgl": "0.19.0", "@xterm/xterm": "6.0.0", "diff": "8.0.4", "fastest-levenshtein": "^1.0.16", @@ -2795,8 +2794,6 @@ "@xterm/addon-web-links": ["@xterm/addon-web-links@0.12.0", "", {}, "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw=="], - "@xterm/addon-webgl": ["@xterm/addon-webgl@0.19.0", "", {}, "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A=="], - "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], "@xyflow/react": ["@xyflow/react@12.10.2", "", { "dependencies": { "@xyflow/system": "0.0.76", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ=="], diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 01ffea141c..e6f1a336c3 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -1327,7 +1327,6 @@ "@xterm/addon-fit": "0.11.0", "@xterm/addon-unicode-graphemes": "0.4.0", "@xterm/addon-web-links": "0.12.0", - "@xterm/addon-webgl": "0.19.0", "@xterm/xterm": "6.0.0", "diff": "8.0.4", "fastest-levenshtein": "^1.0.16", diff --git a/packages/kilo-vscode/tests/unit/agent-manager-ambient-setup.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-ambient-setup.test.ts index b7fff2af69..003d780626 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-ambient-setup.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-ambient-setup.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it } from "bun:test" import { createRoot, createSignal } from "solid-js" import { LOCAL } from "../../webview-ui/agent-manager/navigate" -import { ambientDecision, createAmbientSetup, showTerminalStack } from "../../webview-ui/agent-manager/terminal/ambient" +import { + ambientDecision, + createAmbientSetup, + keepTerminalStack, + showTerminalStack, +} from "../../webview-ui/agent-manager/terminal/ambient" import { createTerminalState } from "../../webview-ui/agent-manager/terminal/state" describe("showTerminalStack", () => { @@ -33,6 +38,14 @@ describe("showTerminalStack", () => { }) }) +describe("keepTerminalStack", () => { + it("keeps live terminals mounted under history", () => { + expect(keepTerminalStack(true, "wt-1", false, 1)).toBe(true) + expect(keepTerminalStack(true, null, true, 1)).toBe(true) + expect(keepTerminalStack(true, "wt-1", false, 0)).toBe(false) + }) +}) + describe("ambientDecision", () => { it("waits while setup is still running", () => { expect(ambientDecision(undefined, "wt-1", "wt-1")).toBe("wait") 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 c9d0fdeebc..bbf0eee69e 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 @@ -15,6 +15,7 @@ const terminal = readFileSync( resolve(import.meta.dir, "../../webview-ui/agent-manager/terminal/TerminalTab.tsx"), "utf8", ) +const pkg = readFileSync(resolve(import.meta.dir, "../../package.json"), "utf8") test("xterm owns the padding used by FitAddon", () => { const host = css.match(/\.am-terminal-host\s*\{([^}]*)\}/)?.[1] @@ -74,6 +75,16 @@ test("does not refit hidden terminal buffers during resize", () => { expect(callback!.indexOf("if (!props.active) return")).toBeLessThan(callback!.indexOf("fit.fit()")) }) +test("uses the scalable DOM renderer for concurrent terminals", () => { + expect(terminal).not.toContain("WebglAddon") + expect(pkg).not.toContain("@xterm/addon-webgl") +}) + +test("orders local terminal status lines through the output batcher", () => { + expect(terminal).toContain("const writeLine =") + expect(terminal).not.toContain("term.writeln(") +}) + test("keeps raw PTY line endings and initializes Unicode widths before attaching", () => { expect(terminal).toContain("convertEol: false") expect(terminal).toContain('term.unicode.activeVersion = "15-graphemes"') @@ -85,7 +96,17 @@ test("keeps raw PTY line endings and initializes Unicode widths before attaching test("fits and forces the initial PTY dimensions before socket attach", () => { expect(terminal).toContain("const syncSize = (force = false)") expect(terminal).toContain("if (props.active) syncSize(true)") - expect(terminal.indexOf("fitNow()\n open(props.wsUrl)")).toBeGreaterThan(-1) + expect(terminal.indexOf("fitNow()\n if (!ws) open(props.wsUrl)")).toBeGreaterThan(-1) +}) + +test("keeps terminal sockets mounted while history is open", () => { + expect(css).toContain(".am-detail-stack-hidden") + expect(css).toMatch(/\.am-detail-stack-hidden[^}]*top: 36px/s) + expect(css).toMatch(/\.am-detail-stack-hidden[^}]*transform: translate\(-100vw, 0\)/s) +}) + +test("moves a closed side panel outside xterm's intersection area", () => { + expect(css).toMatch(/\.am-side-host-hidden[^}]*transform: translate\(-100vw, 0\)/s) }) test("re-sends dimensions when an optimistic terminal receives its PTY", () => { diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-replay.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-replay.test.ts index ef0babf93d..b2c66f99e9 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-replay.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-replay.test.ts @@ -51,6 +51,14 @@ describe("Agent Manager terminal write batcher", () => { expect(h.writes).toEqual(["abc"]) }) + it("keeps local status output after pending PTY output", () => { + const h = harness() + h.batcher.write("last output") + h.batcher.write("\r\n[terminal ended]\r\n") + h.run() + expect(h.writes).toEqual(["last output\r\n[terminal ended]\r\n"]) + }) + it("coalesces many frames into separate writes", () => { const h = harness() h.batcher.write("1") @@ -65,9 +73,9 @@ describe("Agent Manager terminal write batcher", () => { h.batcher.write("txt") h.batcher.write(new Uint8Array([1, 2])) h.run() - expect(h.writes).toHaveLength(1) - const merged = h.writes[0] as Uint8Array - expect(Array.from(merged)).toEqual([116, 120, 116, 1, 2]) + expect(h.writes).toHaveLength(2) + expect(h.writes[0]).toBe("txt") + expect(Array.from(h.writes[1] as Uint8Array)).toEqual([1, 2]) }) it("fires chunk callbacks after the batch write completes", () => { @@ -138,6 +146,22 @@ describe("Agent Manager terminal input buffer", () => { expect(input.take()).toBe("bcde2345") }) + + it("clears buffered input after a failed replay", () => { + const input = createInputBuffer() + input.add("command\r") + input.add("reply", true) + input.clear() + expect(input.take()).toBe("") + }) + + it("does not flush input when replay exceeds its limit", () => { + let flushed = 0 + const gate = createReplayGate({ write: () => undefined, flush: () => flushed++ }) + gate.attach(false) + expect(gate.output("x".repeat(8 * 1024 * 1024 + 1))).toBe(false) + expect(flushed).toBe(0) + }) }) describe("Agent Manager terminal replay gate", () => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 7fd749e149..113e28acd8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -154,7 +154,7 @@ import { createSideTerminal, createAmbientSetup, hasSetupTerminal, - showTerminalStack, + keepTerminalStack, readSavedDestination, resolveRunScriptRequest, resolveVscodeTerminalRequest, @@ -827,7 +827,9 @@ const AgentManagerContent: Component = () => { return false }) - const showDetailStack = createMemo(() => showTerminalStack(history(), selection(), contextEmpty())) + const showDetailStack = createMemo(() => + keepTerminalStack(history(), selection(), contextEmpty(), terms.all().length + terms.sides().length), + ) const overlay = createMemo((): SetupState | null => { const state = setup() @@ -2503,7 +2505,7 @@ const AgentManagerContent: Component = () => { {/* Terminal overlay is scoped to the main pane so it does not cover the tab bar or side panel. */} -
+
{/* Chat/terminal + side diff panel. Keep it mounted under the review tab so live xterm canvases never leave the paint tree. */}