diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0a6e4cdc3..7812d4846 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog ## [Unreleased] + ### Breaking Changes -- - Removed the `searchDb` field from session and extension tool contexts, so custom tools and extensions no longer receive a shared native search DB handle from `ToolSession`, `CustomToolContext`, `ExtensionContext`, and `CreateAgentSessionOptions` - Changed the `vim` tool API to require either `open: "path"` or `kbd: [...]` per call and removed direct `line`/`col` cursor parameters from `open`, so callers must position the cursor via key sequences after opening - Changed the `edit` schemas for patch, replace, hashline, and chunk modes from top-level request fields to `edits` array entries, requiring path/mode details on each edit and breaking callers that send legacy top-level `path`, `old_text`, `new_text`, `op`, `move`, or `delete` payloads @@ -47,13 +47,9 @@ - Updated chunk-mode `grep` output to include match lines under their containing chunk entries with consistent line-number alignment based on file length - Changed eager todo enforcement to only apply on the first user message of a conversation, skipping subsequent user turns that may correct, clarify, or redirect the prior task -### Deprecated - -- - ### Removed -- +- Removed live in-progress Vim tool previews during streaming call execution, so the TUI now shows only the last completed file viewport until the call finishes ### Fixed @@ -74,10 +70,6 @@ - Session accent border color now applied on session resume and after auto-title generation, not only after an explicit `/rename` - Fixed retained Python kernel ownership so `AgentSession.dispose()` only shuts down kernels owned by that session, including warmup-created kernels -### Security - -- - ## [14.1.0] - 2026-04-11 ### Added diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index d59ece2ec..9e33dc831 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -32,7 +32,6 @@ import { import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../tools/python"; import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils"; import { toolRenderers } from "../../tools/renderers"; -import { clearVimCallPreview, primeVimCallPreview } from "../../tools/vim"; import { renderStatusLine } from "../../tui"; import { convertToPng } from "../../utils/image-convert"; import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel"; @@ -96,7 +95,6 @@ export class ToolExecutionComponent extends Container { #tool?: AgentTool; #ui: TUI; #cwd: string; - #toolCallId?: string; #result?: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean; @@ -112,8 +110,6 @@ export class ToolExecutionComponent extends Container { #spinnerInterval?: NodeJS.Timeout; // Track if args are still being streamed (for edit/write spinner) #argsComplete = false; - #vimPreviewPrimeRunning = false; - #vimPreviewPrimeVersion = 0; #renderState: { spinnerFrame?: number; expanded: boolean; @@ -131,19 +127,18 @@ export class ToolExecutionComponent extends Container { tool: AgentTool | undefined, ui: TUI, cwd: string = getProjectDir(), - toolCallId?: string, + _toolCallId?: string, ) { super(); this.#toolName = toolName; this.#toolLabel = tool?.label ?? toolName; - this.#toolCallId = toolCallId; this.#showImages = options.showImages ?? true; this.#editFuzzyThreshold = options.editFuzzyThreshold; this.#editAllowFuzzy = options.editAllowFuzzy; this.#tool = tool; this.#ui = ui; this.#cwd = cwd; - this.#args = this.#prepareArgsForRender(args, toolCallId); + this.#args = cloneToolArgs(args); this.addChild(new Spacer(1)); @@ -160,16 +155,11 @@ export class ToolExecutionComponent extends Container { this.addChild(this.#contentText); } - this.#maybePrimeVimPreview(); this.#updateDisplay(); } - updateArgs(args: any, toolCallId?: string): void { - if (toolCallId) { - this.#toolCallId = toolCallId; - } - this.#args = this.#prepareArgsForRender(args, toolCallId ?? this.#toolCallId); - this.#maybePrimeVimPreview(); + updateArgs(args: any, _toolCallId?: string): void { + this.#args = cloneToolArgs(args); this.#updateSpinnerAnimation(); this.#updateDisplay(); } @@ -266,9 +256,6 @@ export class ToolExecutionComponent extends Container { ): void { this.#result = result; this.#isPartial = isPartial; - if (this.#toolName === "vim") { - clearVimCallPreview(this.#toolCallId); - } // When tool is complete, ensure args are marked complete so spinner stops if (!isPartial) { this.#argsComplete = true; @@ -279,46 +266,6 @@ export class ToolExecutionComponent extends Container { this.#maybeConvertImagesForKitty(); } - #prepareArgsForRender(args: any, toolCallId?: string): any { - const cloned = cloneToolArgs(args); - if (this.#toolName !== "vim" || !toolCallId || !cloned || typeof cloned !== "object" || Array.isArray(cloned)) { - return cloned; - } - return { ...cloned, __toolCallId: toolCallId, __cwd: this.#cwd }; - } - - #maybePrimeVimPreview(): void { - if (this.#toolName !== "vim" || this.#argsComplete || !this.#toolCallId) { - return; - } - - this.#vimPreviewPrimeVersion += 1; - if (this.#vimPreviewPrimeRunning) { - return; - } - - this.#vimPreviewPrimeRunning = true; - void (async () => { - try { - while (!this.#argsComplete && this.#toolCallId) { - const version = this.#vimPreviewPrimeVersion; - const toolCallId = this.#toolCallId; - const args = this.#args; - await primeVimCallPreview(toolCallId, args); - if (this.#toolCallId === toolCallId) { - this.#updateDisplay(); - this.#ui.requestRender(); - } - if (version === this.#vimPreviewPrimeVersion) { - break; - } - } - } finally { - this.#vimPreviewPrimeRunning = false; - } - })(); - } - /** * Get all image blocks from result content and details.images. * Some tools (like generate_image) store images in details to avoid bloating model context. diff --git a/packages/coding-agent/src/prompts/tools/vim.md b/packages/coding-agent/src/prompts/tools/vim.md index 69d8e43de..38db5817f 100644 --- a/packages/coding-agent/src/prompts/tools/vim.md +++ b/packages/coding-agent/src/prompts/tools/vim.md @@ -1,6 +1,6 @@ Stateful Vim editor. Every call requires `file`; the buffer loads automatically on first use. - `{"file": "path"}` - view file -- `{"file": "path", "steps": [{"kbd": ["..."], "insert": "..."}]}` - edit file +- `{"file": "path", "steps": [{"kbd": ["…"], "insert": "…"}]}` - edit file ## steps vs kbd vs insert @@ -51,11 +51,11 @@ Replace entire file. `ggdGi` = go to top, delete all, enter INSERT. Use that exa Multi-location edit — work bottom-up so earlier inserts don't shift later line numbers: ```json {"file": "f.py", "steps": [ - {"kbd": ["10Go"], "insert": "os.path.exists('...')"}, - {"kbd": ["3Go"], "insert": "import os"} + {"kbd": ["8Go"], "insert": " print(result)"}, + {"kbd": ["3Go"], "insert": "def helper(x):\n return x + 1"} ]} ``` -When inserting at multiple lines in one call, edit the **highest line number first** and work upward. Each insert shifts lines below it, so bottom-up order keeps all line targets stable. +When inserting at multiple lines in one call, edit the **highest line number first** and work upward. Each `o`/`O` insert shifts lines below it, so bottom-up order keeps all line targets stable. Use `\n` within `insert` for multi-line content (each `\n` creates a new line). Navigation or search step without insert: ```json @@ -74,7 +74,6 @@ Delete line range: Ex commands always start with `:` and end with ``. `3,5d` without `:` is NOT an ex command — it is interpreted as normal-mode keystrokes and will fail. ## Undo mistakes - - `{"file": "f.py", "steps": [{"kbd": ["u"]}]}` - undo last change - `{"file": "f.py", "steps": [{"kbd": ["3u"]}]}` - undo last 3 changes diff --git a/packages/coding-agent/src/tools/json-tree.ts b/packages/coding-agent/src/tools/json-tree.ts index 926c880fc..8476a8311 100644 --- a/packages/coding-agent/src/tools/json-tree.ts +++ b/packages/coding-agent/src/tools/json-tree.ts @@ -14,7 +14,7 @@ export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60; export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000; /** Keys injected by the harness that should not be displayed to users */ -const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD, "__partialJson", "__toolCallId", "__cwd"]); +const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD, "__partialJson"]); /** Strip harness-internal keys from tool args for display */ export function stripInternalArgs(args: Record): Record { diff --git a/packages/coding-agent/src/tools/vim.ts b/packages/coding-agent/src/tools/vim.ts index bf1fee635..11eef169b 100644 --- a/packages/coding-agent/src/tools/vim.ts +++ b/packages/coding-agent/src/tools/vim.ts @@ -11,7 +11,7 @@ import vimDescription from "../prompts/tools/vim.md" with { type: "text" }; import { CachedOutputBlock } from "../tui/output-block"; import { renderStatusLine } from "../tui/status-line"; import { VimBuffer } from "../vim/buffer"; -import { VimEngine, type VimEngineCallbacks, type VimSaveResult } from "../vim/engine"; +import { VimEngine, type VimSaveResult } from "../vim/engine"; import { parseKeySequences } from "../vim/parser"; import { buildDetails, @@ -77,25 +77,6 @@ export interface VimRenderArgs { steps?: VimRenderStep[]; pause?: boolean; __partialJson?: string; - __toolCallId?: string; - __cwd?: string; -} - -interface VimPreviewBuildResult { - details: VimToolDetails; - engine: VimEngine; - file?: string; - steps?: VimStep[]; - pause: boolean; -} - -interface VimCallPreviewState { - key: string; - details?: VimToolDetails; - engine?: VimEngine; - file?: string; - steps?: VimStep[]; - pause?: boolean; } function fingerprintEqual(left: VimFingerprint | null, right: VimFingerprint | null): boolean { @@ -201,73 +182,9 @@ async function executeKeySequences( } } -// Module-level cache of the last rendered VimToolDetails so that renderCall can -// show the current buffer viewport while the LLM is still streaming tool arguments. +// Module-level cache of the last real vim result so renderCall can reuse that +// viewport while the next tool call is still streaming. let lastVimDetails: VimToolDetails | undefined; -const previewEnginesByFile = new Map(); -const previewStatesByToolCall = new Map(); - -function clearStoredPreviewEngine(file: string): void { - previewEnginesByFile.delete(file); -} - -function storePreviewEngine(engine: VimEngine): void { - const snapshot = engine.clone(); - previewEnginesByFile.set(engine.buffer.displayPath, snapshot); - previewEnginesByFile.set(engine.buffer.filePath, snapshot); -} - -function buildLoadedFileFromBuffer(buffer: VimBuffer): VimLoadedFile { - return { - absolutePath: buffer.filePath, - displayPath: buffer.displayPath, - lines: [...buffer.lines], - trailingNewline: buffer.trailingNewline, - fingerprint: buffer.baseFingerprint ? { ...buffer.baseFingerprint } : null, - }; -} - -function createPreviewCallbacks(): VimEngineCallbacks { - return { - beforeMutate: async () => {}, - loadBuffer: async inputPath => ({ - absolutePath: inputPath, - displayPath: inputPath, - lines: [""], - trailingNewline: false, - fingerprint: null, - }), - saveBuffer: async buffer => ({ - loaded: buildLoadedFileFromBuffer(buffer), - }), - }; -} - -async function loadPreviewBaseEngine(args: VimRenderArgs): Promise { - if (!args.file || !args.__cwd) { - return undefined; - } - - try { - const { absolutePath, displayPath } = normalizeTargetPath(args.file, args.__cwd); - const loaded = await readTextFile(absolutePath); - const engine = new VimEngine( - new VimBuffer({ - absolutePath, - displayPath, - lines: loaded.lines, - trailingNewline: loaded.trailingNewline, - fingerprint: loaded.fingerprint, - }), - createPreviewCallbacks(), - ); - storePreviewEngine(engine); - return engine; - } catch { - return undefined; - } -} - function buildToolDetailsFromEngine( engine: VimEngine, viewportLines: number, @@ -298,60 +215,6 @@ function buildToolDetailsFromEngine( return details; } -function buildPreviewKey(args: VimRenderArgs): string { - return JSON.stringify({ - file: args.file, - steps: getStepsForDisplay(args) ?? [], - pause: args.pause === true, - }); -} - -function cloneSteps(steps: readonly VimStep[] | undefined): VimStep[] | undefined { - return steps?.map(step => ({ - kbd: [...step.kbd], - ...(step.insert !== undefined ? { insert: step.insert } : {}), - })); -} - -function arraysEqual(left: string[] | undefined, right: string[] | undefined): boolean { - if (left === right) { - return true; - } - if (!left || !right || left.length !== right.length) { - return false; - } - for (let index = 0; index < left.length; index += 1) { - if (left[index] !== right[index]) { - return false; - } - } - return true; -} - -function stepsEqualExceptLastInsert( - left: readonly VimStep[] | undefined, - right: readonly VimStep[] | undefined, -): boolean { - if (left === right) { - return true; - } - if (!left || !right || left.length !== right.length) { - return false; - } - for (let index = 0; index < left.length; index += 1) { - const leftStep = left[index]!; - const rightStep = right[index]!; - if (!arraysEqual(leftStep.kbd, rightStep.kbd)) { - return false; - } - const isLast = index === left.length - 1; - if (!isLast && leftStep.insert !== rightStep.insert) { - return false; - } - } - return true; -} - function getLastStepInsert(steps: readonly VimStep[] | undefined): string | undefined { if (!steps || steps.length === 0) { return undefined; @@ -387,20 +250,6 @@ function getStepsForDisplay(args: VimRenderArgs): VimStep[] | undefined { return steps; } -function buildPreviewState(key: string, preview: VimPreviewBuildResult | undefined): VimCallPreviewState { - if (!preview) { - return { key }; - } - return { - key, - details: preview.details, - engine: preview.engine, - file: preview.file, - steps: cloneSteps(preview.steps), - pause: preview.pause, - }; -} - function splitInsertIntoChunks(text: string): string[] { const maxChunkChars = 32; if (text.length <= maxChunkChars) { @@ -451,7 +300,6 @@ async function applyInsertWithStreaming( interface ExecuteVimStepsOptions { pauseLastStep?: boolean; - keepFinalInsertOpen?: boolean; onKbdStep?: () => Promise; onInsertStep?: () => Promise; } @@ -481,9 +329,11 @@ async function executeVimSteps( } if (!engine.closed && step.insert !== undefined && (step.insert.length > 0 || engine.inputMode === "insert")) { - const exitInsertMode = - options.keepFinalInsertOpen === true ? !isLast : !(isLast && options.pauseLastStep === true); - await applyInsertWithStreaming(engine, step.insert, exitInsertMode, options.onInsertStep); + // Strip trailing newline from insert text — `o`/`O` already create a line boundary, + // so a trailing \n would produce an unwanted blank line. + const normalizedInsert = step.insert.endsWith("\n") ? step.insert.slice(0, -1) : step.insert; + const exitInsertMode = !(isLast && options.pauseLastStep === true); + await applyInsertWithStreaming(engine, normalizedInsert, exitInsertMode, options.onInsertStep); } if (!isLast && engine.inputMode === "insert") { @@ -660,12 +510,6 @@ export class VimTool implements AgentTool { builder.diagnostics(engine.diagnostics.summary, engine.diagnostics.messages ?? []); } lastVimDetails = details; - if (closed) { - clearStoredPreviewEngine(engine.buffer.displayPath); - clearStoredPreviewEngine(engine.buffer.filePath); - } else { - storePreviewEngine(engine); - } return builder.done(); } @@ -899,136 +743,8 @@ function describeStepsForDisplay(args: VimRenderArgs): string { return description; } -async function buildPreviewDetails(args: VimRenderArgs): Promise { - if (!args.file) { - return undefined; - } - - const baseEngine = previewEnginesByFile.get(args.file) ?? (await loadPreviewBaseEngine(args)); - if (!baseEngine) { - return undefined; - } - - const preview = baseEngine.clone(createPreviewCallbacks()); - const steps = getStepsForDisplay(args) ?? []; - - try { - await executeVimSteps(preview, steps, { - pauseLastStep: args.pause === true, - keepFinalInsertOpen: true, - }); - } catch { - return undefined; - } - - const insertText = getLastStepInsert(steps); - if (!preview.closed && insertText !== undefined) { - preview.statusMessage = preview.statusMessage ?? "Streaming insert preview"; - } - - return { - details: buildToolDetailsFromEngine(preview, VIM_DEFAULT_VIEWPORT_LINES, preview.viewportStart, preview.closed), - engine: preview, - file: args.file, - steps: cloneSteps(steps), - pause: args.pause === true, - }; -} - -async function extendPreviewDetails( - state: VimCallPreviewState | undefined, - args: VimRenderArgs, -): Promise { - if (!state?.engine || !args.file || state.file !== args.file) { - return undefined; - } - - const steps = getStepsForDisplay(args) ?? []; - if (!stepsEqualExceptLastInsert(state.steps, steps)) { - return undefined; - } - - const pause = args.pause === true; - if ((state.pause ?? false) !== pause) { - return undefined; - } - - const nextInsert = getLastStepInsert(steps); - const previousInsert = getLastStepInsert(state.steps); - if (nextInsert === previousInsert) { - return { - details: - state.details ?? - buildToolDetailsFromEngine( - state.engine, - VIM_DEFAULT_VIEWPORT_LINES, - state.engine.viewportStart, - state.engine.closed, - ), - engine: state.engine, - file: args.file, - steps: cloneSteps(steps), - pause, - }; - } - - if (nextInsert === undefined || previousInsert === undefined || !nextInsert.startsWith(previousInsert)) { - return undefined; - } - - const preview = state.engine.clone(createPreviewCallbacks()); - const suffix = nextInsert.slice(previousInsert.length); - if (suffix.length > 0) { - try { - await preview.applyLiteralInsert(suffix, false); - } catch { - return undefined; - } - } - preview.statusMessage = preview.statusMessage ?? "Streaming insert preview"; - return { - details: buildToolDetailsFromEngine(preview, VIM_DEFAULT_VIEWPORT_LINES, preview.viewportStart, preview.closed), - engine: preview, - file: args.file, - steps: cloneSteps(steps), - pause, - }; -} - -export async function primeVimCallPreview(toolCallId: string | undefined, args: VimRenderArgs): Promise { - if (!toolCallId) { - return; - } - - const key = buildPreviewKey(args); - const existing = previewStatesByToolCall.get(toolCallId); - previewStatesByToolCall.set(toolCallId, { - key, - details: existing?.details, - engine: existing?.engine, - file: existing?.file, - steps: cloneSteps(existing?.steps), - pause: existing?.pause, - }); - const preview = (await extendPreviewDetails(existing, args)) ?? (await buildPreviewDetails(args)); - const current = previewStatesByToolCall.get(toolCallId); - if (!current || current.key !== key) { - return; - } - previewStatesByToolCall.set(toolCallId, buildPreviewState(key, preview)); -} - -export function clearVimCallPreview(toolCallId: string | undefined): void { - if (!toolCallId) { - return; - } - previewStatesByToolCall.delete(toolCallId); -} - export function resetVimRendererStateForTest(): void { lastVimDetails = undefined; - previewEnginesByFile.clear(); - previewStatesByToolCall.clear(); } export const vimToolRenderer = { @@ -1040,11 +756,8 @@ export const vimToolRenderer = { // Build a description of the streaming args for the header const argsDescription = describeStepsForDisplay(args); - // When a vim buffer is already open, show its viewport during LLM streaming - // instead of just a plain text line. This gives visual continuity. - const previewDetails = - args.__toolCallId !== undefined ? previewStatesByToolCall.get(args.__toolCallId)?.details : undefined; - const details = previewDetails ?? (lastVimDetails?.file === args.file ? lastVimDetails : undefined); + // Reuse the last real vim result for the same file while the next call is still streaming. + const details = lastVimDetails?.file === args.file ? lastVimDetails : undefined; if (details?.viewportLines && details.viewportLines.length > 0) { const lang = getLanguageFromPath(details.file); const langIcon = uiTheme.getLangIcon(lang); diff --git a/packages/coding-agent/test/tools/vim.test.ts b/packages/coding-agent/test/tools/vim.test.ts index 3936dac0b..7098ed236 100644 --- a/packages/coding-agent/test/tools/vim.test.ts +++ b/packages/coding-agent/test/tools/vim.test.ts @@ -6,12 +6,7 @@ import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/confi import { ToolExecutionComponent } from "@oh-my-pi/pi-coding-agent/modes/components/tool-execution"; import * as themeModule from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -import { - primeVimCallPreview, - resetVimRendererStateForTest, - VimTool, - vimToolRenderer, -} from "@oh-my-pi/pi-coding-agent/tools/vim"; +import { resetVimRendererStateForTest, VimTool, vimToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/vim"; import { VimBuffer } from "@oh-my-pi/pi-coding-agent/vim/buffer"; import { VimEngine } from "@oh-my-pi/pi-coding-agent/vim/engine"; import { parseKeySequences } from "@oh-my-pi/pi-coding-agent/vim/parser"; @@ -607,7 +602,7 @@ describe("vim tool", () => { }); describe("vim renderer", () => { - it("previews streamed cursor jumps using the live vim buffer snapshot", async () => { + it("reuses the last real vim snapshot while args stream", async () => { const previewDir = await fs.mkdtemp(path.join(os.tmpdir(), "vim-render-preview-")); const filePath = path.join(previewDir, "preview.ts"); await Bun.write(filePath, Array.from({ length: 900 }, (_, index) => `line ${index + 1};`).join("\n")); @@ -617,80 +612,28 @@ describe("vim renderer", () => { const uiTheme = theme!; await tool.execute("open", { file: "preview.ts" }); - await primeVimCallPreview("preview-call", { - file: "preview.ts", - steps: [step(["643G"])], - __toolCallId: "preview-call", - }); const component = vimToolRenderer.renderCall( - { file: "preview.ts", steps: [step(["643G"])], __toolCallId: "preview-call" }, + { file: "preview.ts", steps: [step(["643G"])] }, { expanded: false, isPartial: true, spinnerFrame: 0 }, uiTheme, ); const rendered = Bun.stripANSI(component.render(160).join("\n")); expect(rendered).toContain("643G"); - expect(rendered).toContain("line 643;"); + expect(rendered).toContain("line 1;"); + expect(rendered).not.toContain("line 643;"); }); - it("previews partial insert payloads before the tool call JSON closes", async () => { - const previewDir = await fs.mkdtemp(path.join(os.tmpdir(), "vim-render-insert-preview-")); - const filePath = path.join(previewDir, "preview.txt"); - await Bun.write(filePath, "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\n"); - const tool = new VimTool(createSession(previewDir)); + it("renders first-call streamed inserts as text instead of priming a preview buffer", async () => { const theme = await themeModule.getThemeByName("dark"); expect(theme).toBeDefined(); const uiTheme = theme!; - await tool.execute("open", { file: "preview.txt" }); - await primeVimCallPreview("insert-preview-call", { - file: "preview.txt", - steps: [step(["7Go"])], - __toolCallId: "insert-preview-call", - __partialJson: - '{"file":"preview.txt","steps":[{"kbd":["7Go"],"insert":"// long streamed comment still being typed by the model', - }); - - const component = vimToolRenderer.renderCall( - { - file: "preview.txt", - steps: [step(["7Go"])], - __toolCallId: "insert-preview-call", - __partialJson: - '{"file":"preview.txt","steps":[{"kbd":["7Go"],"insert":"// long streamed comment still being typed by the model', - }, - { expanded: false, isPartial: true, spinnerFrame: 0 }, - uiTheme, - ); - - const rendered = component.render(160).join("\n"); - expect(rendered).toContain("7Go"); - expect(rendered).toContain("// long streamed comment still being typed by the model"); - }); - - it("previews the target file on the first vim call without a prior open", async () => { - const previewDir = await fs.mkdtemp(path.join(os.tmpdir(), "vim-render-first-call-")); - const filePath = path.join(previewDir, "preview.txt"); - await Bun.write(filePath, "Line 1\nLine 2\nLine 3\n"); - const theme = await themeModule.getThemeByName("dark"); - expect(theme).toBeDefined(); - const uiTheme = theme!; - - await primeVimCallPreview("first-call-preview", { - file: "preview.txt", - steps: [step(["ggdGi"])], - __toolCallId: "first-call-preview", - __cwd: previewDir, - __partialJson: '{"file":"preview.txt","steps":[{"kbd":["ggdGi"],"insert":"replacement', - }); - const component = vimToolRenderer.renderCall( { file: "preview.txt", steps: [step(["ggdGi"])], - __toolCallId: "first-call-preview", - __cwd: previewDir, __partialJson: '{"file":"preview.txt","steps":[{"kbd":["ggdGi"],"insert":"replacement', }, { expanded: false, isPartial: true, spinnerFrame: 0 }, @@ -699,42 +642,12 @@ describe("vim renderer", () => { const rendered = Bun.stripANSI(component.render(140).join("\n")); expect(rendered).toContain("ggdGi"); - expect(rendered).toContain(">1│replacement"); + expect(rendered).toContain("insert: replacement"); + expect(rendered).not.toContain(">1│replacement"); }); - it("loads first-call vim previews through ToolExecutionComponent constructor state", async () => { - const previewDir = await fs.mkdtemp(path.join(os.tmpdir(), "vim-render-first-call-component-")); - const filePath = path.join(previewDir, "preview.txt"); - await Bun.write(filePath, "Line 1\nLine 2\nLine 3\n"); - const theme = await themeModule.getThemeByName("dark"); - expect(theme).toBeDefined(); - await themeModule.initTheme(false, undefined, undefined, "dark", "light"); - const uiStub = { requestRender() {} } as unknown as TUI; - - const component = new ToolExecutionComponent( - "vim", - { - file: "preview.txt", - steps: [step(["ggdGi"])], - __partialJson: '{"file":"preview.txt","steps":[{"kbd":["ggdGi"],"insert":"replacement', - }, - {}, - undefined, - uiStub, - previewDir, - "first-call-component", - ); - await Bun.sleep(50); - - const rendered = Bun.stripANSI(component.render(140).join("\n")); - expect(rendered).toContain("ggdGi"); - expect(rendered).toContain(">1│replacement"); - }); - - it("extends streamed first-call insert previews across ToolExecutionComponent arg updates", async () => { + it("updates streamed vim args without async preview priming", async () => { const previewDir = await fs.mkdtemp(path.join(os.tmpdir(), "vim-render-growing-first-call-component-")); - const filePath = path.join(previewDir, "preview.txt"); - await Bun.write(filePath, "Line 1\nLine 2\nLine 3\n"); const theme = await themeModule.getThemeByName("dark"); expect(theme).toBeDefined(); await themeModule.initTheme(false, undefined, undefined, "dark", "light"); @@ -753,10 +666,9 @@ describe("vim renderer", () => { previewDir, "growing-first-call-component", ); - await Bun.sleep(50); let rendered = Bun.stripANSI(component.render(140).join("\n")); - expect(rendered).toContain(">1│rep"); + expect(rendered).toContain("insert: rep"); component.updateArgs( { @@ -766,50 +678,9 @@ describe("vim renderer", () => { }, "growing-first-call-component", ); - await Bun.sleep(50); rendered = Bun.stripANSI(component.render(140).join("\n")); - expect(rendered).toContain(">1│replacement text"); - }); - - it("keeps vim preview state alive after args complete until a result arrives", async () => { - const previewDir = await fs.mkdtemp(path.join(os.tmpdir(), "vim-render-complete-preview-")); - const filePath = path.join(previewDir, "preview.ts"); - await Bun.write(filePath, Array.from({ length: 900 }, (_, index) => `line ${index + 1};`).join("\n")); - const tool = new VimTool(createSession(previewDir)); - const theme = await themeModule.getThemeByName("dark"); - expect(theme).toBeDefined(); - const uiTheme = theme!; - const uiStub = { requestRender() {} } as unknown as TUI; - await themeModule.initTheme(false, undefined, undefined, "dark", "light"); - - await tool.execute("open", { file: "preview.ts" }); - await primeVimCallPreview("complete-preview-call", { - file: "preview.ts", - steps: [step(["643G"])], - __toolCallId: "complete-preview-call", - }); - - const component = new ToolExecutionComponent( - "vim", - { file: "preview.ts", steps: [step(["643G"])] }, - {}, - undefined, - uiStub, - previewDir, - "complete-preview-call", - ); - component.setArgsComplete("complete-preview-call"); - - const rendered = vimToolRenderer - .renderCall( - { file: "preview.ts", steps: [step(["643G"])], __toolCallId: "complete-preview-call" }, - { expanded: false, isPartial: true, spinnerFrame: 0 }, - uiTheme, - ) - .render(160) - .join("\n"); - expect(Bun.stripANSI(rendered)).toContain("line 643;"); + expect(rendered).toContain("insert: replacement text"); }); it("caches repeated renders for the same viewport snapshot", async () => {