feat(coding-agent): added mode-aware streaming preview for edit outputs
- Added mode-aware streaming mode resolution and strategy registration in tool execution flow. - Changed edit rendering to propagate mode and file-scoped diff previews across edit/vim tool outputs. - Added chunk-mode streaming helpers (`loadChunkSource`, `computeChunkDiff`, `dropIncompleteLastEdit`) with abort-aware error fallbacks. - Added strategy-specific streaming preview support for replace, patch, hashline, apply_patch, and vim modes. - Fixed streaming previews by dropping incomplete trailing edits and handling invalid chunk or aborted inputs. - Expanded streaming and chunk-diff tests for partial JSON, open edits, empty paths, abort signals, and file loading checks.
This commit is contained in:
@@ -161,6 +161,57 @@ async function resolveChunkSourceContext(session: ToolSession, path: string): Pr
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview-safe loader: read raw source without plan-mode enforcement or
|
||||
* editable-file guards. Used by streaming diff previews that must not throw
|
||||
* side-effecting errors while args are still being streamed.
|
||||
*/
|
||||
export async function loadChunkSource(params: {
|
||||
cwd: string;
|
||||
path: string;
|
||||
}): Promise<{ resolvedPath: string; rawContent: string; language: string | undefined; exists: boolean }> {
|
||||
const resolvedPath = nodePath.isAbsolute(params.path) ? params.path : nodePath.resolve(params.cwd, params.path);
|
||||
const sourceFile = Bun.file(resolvedPath);
|
||||
const exists = await sourceFile.exists();
|
||||
const rawContent = exists ? await sourceFile.text() : "";
|
||||
return { resolvedPath, rawContent, language: getLanguageFromPath(resolvedPath), exists };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a unified diff preview for a chunk edit without applying it.
|
||||
* Used for streaming previews while args are still arriving. Returns
|
||||
* `{ error }` on any failure so callers can decide whether to surface it.
|
||||
*/
|
||||
export async function computeChunkDiff(
|
||||
input: { path: string; edits: ChunkToolEdit[] },
|
||||
cwd: string,
|
||||
options?: { anchorStyle?: ChunkAnchorStyle; signal?: AbortSignal },
|
||||
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
||||
try {
|
||||
options?.signal?.throwIfAborted?.();
|
||||
const { filePath } = parseChunkEditPath(input.path);
|
||||
if (!filePath) return { error: "chunk edit path is empty" };
|
||||
const { resolvedPath, rawContent, language } = await loadChunkSource({ cwd, path: filePath });
|
||||
options?.signal?.throwIfAborted?.();
|
||||
const { operations } = normalizeChunkEditOperations(input.edits);
|
||||
const result = applyChunkEdits({
|
||||
source: rawContent,
|
||||
language,
|
||||
cwd,
|
||||
filePath: resolvedPath,
|
||||
operations,
|
||||
anchorStyle: options?.anchorStyle,
|
||||
});
|
||||
options?.signal?.throwIfAborted?.();
|
||||
if (!result.changed) {
|
||||
return { diff: "", firstChangedLine: undefined };
|
||||
}
|
||||
return generateUnifiedDiffString(result.diffSourceBefore, result.diffSourceAfter);
|
||||
} catch (err) {
|
||||
return { error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeChunkRegionSyntax(text: string): string {
|
||||
return text.replaceAll("@body", "~").replaceAll("@head", "^");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user