diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7e1205f88..4d55f17ed 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -10,7 +10,6 @@ ### Changed - Updated the tester subagent prompt to explicitly forbid testing default configuration values and allow skipping tests entirely for trivial changes - - Optimized session loading and rendering performance, including a 10x speedup for smooth streaming reveals on large messages, 35% faster session resumes for large files using native streaming JSONL parsing, and reduced overhead for edit-patch fallbacks. - Improved TUI responsiveness and reduced CPU usage during long-running tool sessions by throttling status-line redraws and optimizing subagent persistence checks. - Updated the advisor system prompt and documentation to accurately reflect WATCHDOG.yml tool grants. @@ -18,6 +17,10 @@ ### Fixed +- Fixed sequential patch/edit failures leaving dirty buffers in multi-file edits by flushing the LSP writethrough queue upfront on early abort paths +- Fixed the subagent task preview erroneously showing "ctrl+o: Expand" hints on output lines capped inside the already-expanded view + +- Fixed the cross-file write batching regression in `apply_patch` multi-file operations by reverting to flushing only on the last file write or explicitly on early failure paths, and refactored error counting to use clean booleans. - Fixed collab teardown (`/collab stop`, unrecoverable relay drop) cancelling a hook selector/editor the host user was actively typing in: teardown resolved pending guest asks the same way as a guest cancel, so the mirrored race dismissed the local dialog and dropped its input. Teardown now settles guest asks as `unavailable`, and the local dialog keeps running and wins with its eventual answer. - Fixed RPC mode deferred shutdown (`pi.shutdown()`) killing the process while a background-dispatched `bash` command was still running: the response frame is now written before exit, and a shutdown requested mid-bash fires once the command settles even when no further client frames arrive. - Fixed collab-guest transcript viewer rendering host-delivered errors raw: multi-line stacks broke the frame's row accounting and absolute host paths leaked to guests; errors are now collapsed to one sanitized, truncated row. diff --git a/packages/coding-agent/src/edit/index.ts b/packages/coding-agent/src/edit/index.ts index 8ba6f9461..6c004bb1b 100644 --- a/packages/coding-agent/src/edit/index.ts +++ b/packages/coding-agent/src/edit/index.ts @@ -246,7 +246,9 @@ async function executeSinglePathEntries( path: string, runs: ((batchRequest: LspBatchRequest | undefined) => Promise>)[], outerBatchRequest: LspBatchRequest | undefined, - onUpdate?: (partialResult: AgentToolResult) => void, + onUpdate: ((partialResult: AgentToolResult) => void) | undefined, + cwd: string, + signal: AbortSignal | undefined, ): Promise> { if (runs.length === 1) { return runs[0](outerBatchRequest); @@ -255,7 +257,7 @@ async function executeSinglePathEntries( const contentTexts: string[] = []; const diffTexts: string[] = []; let firstChangedLine: number | undefined; - let errorCount = 0; + let hasError = false; let metadataPath: string | undefined; let hasFirstOldText = false; let firstOldText: string | undefined; @@ -306,10 +308,13 @@ async function executeSinglePathEntries( `; re-read the file and re-issue only the failed and unapplied entries.`, ); } - errorCount++; + hasError = true; // Stop at the first failure: later entries were authored against // line numbers/content that assumed this entry succeeded, and // applying them after a failure compounds the damage. + if (outerBatchRequest?.flush) { + await flushLspWritethroughBatch(outerBatchRequest.id, cwd, signal); + } break; } @@ -320,7 +325,7 @@ async function executeSinglePathEntries( diff: diffTexts.join("\n"), firstChangedLine, }, - ...(errorCount > 0 ? { isError: true } : {}), + ...(hasError ? { isError: true } : {}), }); } } @@ -342,7 +347,7 @@ async function executeSinglePathEntries( // renderer takes the error branch instead of falling through to the // streaming-edit preview (which displays the *proposed* diff and looks // indistinguishable from success). - ...(errorCount > 0 ? { isError: true } : {}), + ...(hasError ? { isError: true } : {}), }; } @@ -547,7 +552,7 @@ export class EditTool implements AgentTool { beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p), }), ); - return executeSinglePathEntries(path, runs, batchRequest, onUpdate); + return executeSinglePathEntries(path, runs, batchRequest, onUpdate, tool.session.cwd, signal); }, }, apply_patch: { @@ -636,7 +641,7 @@ export class EditTool implements AgentTool { beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p), }), ); - return executeSinglePathEntries(path, runs, batchRequest, onUpdate); + return executeSinglePathEntries(path, runs, batchRequest, onUpdate, tool.session.cwd, signal); }, }, }[this.mode]; diff --git a/packages/coding-agent/src/task/render.ts b/packages/coding-agent/src/task/render.ts index 57a60d04e..bc64d4216 100644 --- a/packages/coding-agent/src/task/render.ts +++ b/packages/coding-agent/src/task/render.ts @@ -14,6 +14,7 @@ import { formatContextUsage } from "../modes/components/status-line/context-thre import { getMarkdownTheme, type Theme } from "../modes/theme/theme"; import { stripGeneratedOutputNotice, stripRawOutputArtifactNotice } from "../tools/output-meta"; import { + capPreviewLines, formatBadge, formatDuration, formatExpandHint, @@ -487,14 +488,6 @@ function sanitizeRecentOutput(output: string): string { return text; } -function capRecentOutputForPreview(output: string, maxRows: number): string { - const lines = output.split("\n"); - if (lines.length <= maxRows) return output; - const visible = maxRows <= 1 ? [] : lines.slice(lines.length - (maxRows - 1)); - const hidden = lines.length - visible.length; - return [`… ${hidden} earlier ${hidden === 1 ? "line" : "lines"}`, ...visible].join("\n"); -} - function renderOutputSection( output: string, continuePrefix: string, @@ -1062,10 +1055,14 @@ function renderAgentProgress( // Expanded view: recent output and tools if (expanded && progress.status === "running") { const previewRows = previewWindowRows(); - const output = capRecentOutputForPreview( - sanitizeRecentOutput([...progress.recentOutput].reverse().join("\n")), - previewRows, - ); + const output = capPreviewLines( + sanitizeRecentOutput([...progress.recentOutput].reverse().join("\n")).split("\n"), + theme, + { + max: previewRows, + expandHint: false, + }, + ).join("\n"); lines.push(...renderOutputSection(output, continuePrefix, expanded, theme, 2, previewRows)); } diff --git a/packages/coding-agent/src/tools/render-utils.ts b/packages/coding-agent/src/tools/render-utils.ts index e362b5767..a6427ee75 100644 --- a/packages/coding-agent/src/tools/render-utils.ts +++ b/packages/coding-agent/src/tools/render-utils.ts @@ -222,19 +222,21 @@ export function previewWindowRows(): number { * (ctrl+o) uncaps it. * * `prefix` (raw, e.g. a dim tree gutter) is prepended to the marker line so - * nested previews stay aligned. + * nested previews stay aligned. `expandHint: false` drops the "ctrl+o: Expand" + * suffix for callers that cap even inside the expanded view (task recent + * output), where the hint would point the wrong way. */ export function capPreviewLines( lines: string[], theme: Theme, - options: { max?: number; expanded?: boolean; prefix?: string } = {}, + options: { max?: number; expanded?: boolean; prefix?: string; expandHint?: boolean } = {}, ): string[] { if (options.expanded) return lines; const max = options.max ?? previewWindowRows(); if (lines.length <= max) return lines; const visible = max <= 1 ? [] : lines.slice(lines.length - (max - 1)); const hidden = lines.length - visible.length; - const hint = formatExpandHint(theme, false, true); + const hint = options.expandHint === false ? "" : formatExpandHint(theme, false, true); const marker = `… ${hidden} earlier ${pluralize("line", hidden)}${hint ? ` ${hint}` : ""}`; return [`${options.prefix ?? ""}${theme.fg("dim", marker)}`, ...visible]; }