diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte
index 2067e42688..22ffc256ba 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte
@@ -1,5 +1,5 @@
@@ -45,11 +49,11 @@
{meta.errorMessage}
- {:else if meta && meta.edits.length > 0}
+ {:else if meta && editFileBody && editFileBody.edits.length > 0}
{#each editDiffs as diffLines, ei (ei)}
- Edit {ei + 1} of {meta.edits.length}
+ Edit {ei + 1} of {editFileBody.edits.length}
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte
index 178c479d98..cafa5280bc 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte
@@ -1,5 +1,5 @@
@@ -45,7 +49,7 @@
{:else if meta}
| null {
}
}
+// Compiled per key on first use; the key set is tiny and fixed.
+const toolArgStringRegexes = new Map();
+
+/**
+ * Extract a string field from a JSON tool-args blob without parsing the
+ * whole document. write_file and edit_file args embed full file contents,
+ * yet the block title needs only the path; a targeted key match plus a
+ * JSON.parse of the captured string literal alone keeps title rendering
+ * O(path) instead of O(blob). Returns undefined when the key is missing
+ * or its value is not a string; callers fall back to the full parse.
+ */
+export function extractToolArgString(toolArgs: string, keys: string[]): string | undefined {
+ for (const key of keys) {
+ let pattern = toolArgStringRegexes.get(key);
+
+ if (!pattern) {
+ pattern = new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`);
+ toolArgStringRegexes.set(key, pattern);
+ }
+
+ const match = pattern.exec(toolArgs);
+
+ if (!match) continue;
+
+ try {
+ const value: unknown = JSON.parse(`"${match[1]}"`);
+
+ if (typeof value === 'string') return value;
+ } catch {
+ // fall through to the next key; the full parse is the fallback
+ }
+ }
+
+ return undefined;
+}
+
/**
* Parse a section's toolArgs against an expected tool name. Returns
* `null` when:
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts
index 9ed6f92bc0..d88186b581 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts
@@ -3,7 +3,7 @@
// rendering), plus the result blob for `result` / `edits_applied` /
// `error` fields.
-import { parseToolArgs } from './_shared';
+import { extractToolArgString, parseToolArgs } from './_shared';
import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
import { BuiltInTool } from '$lib/enums';
import type { AgenticSection } from '$lib/types';
@@ -23,6 +23,19 @@ export type EditFileMeta = {
errorMessage?: string;
};
+/** Everything the block title and status pill show; the full meta (with
+ * the embedded edit strings) stays body-only so collapsed blocks never
+ * parse the args blob. */
+export type EditFileTitleMeta = {
+ fileName: string;
+ filePath: string;
+ resultMessage?: string;
+ editsApplied?: number;
+ errorMessage?: string;
+};
+
+const PATH_KEYS = ['path', 'file_path', 'filePath'];
+
export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null {
const args = parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true });
@@ -79,3 +92,45 @@ export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null
resultMessage
};
}
+
+/**
+ * Title-tier meta for edit_file blocks: everything the header and status
+ * pill render, obtained without parsing the embedded edit strings. The path
+ * comes from a targeted key extraction; the full parse runs only as a
+ * fallback for arg shapes the extraction can't see.
+ */
+export function parseEditFileTitleMeta(section: AgenticSection): EditFileTitleMeta | null {
+ if (section.toolName !== BuiltInTool.SERVER_EDIT_FILE || !section.toolArgs) return null;
+
+ let rawPath: string | undefined = extractToolArgString(section.toolArgs, PATH_KEYS);
+
+ if (!rawPath) {
+ const args = parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true });
+ const fallbackPath = args?.path ?? args?.file_path ?? args?.filePath;
+
+ if (typeof fallbackPath === 'string' && fallbackPath) rawPath = fallbackPath;
+ }
+
+ if (!rawPath) return null;
+
+ const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
+ const resultObj = tryParseToolResultObject(section.toolResult);
+
+ let resultMessage: string | undefined;
+ let editsApplied: number | undefined;
+ let errorMessage: string | undefined;
+
+ if (typeof resultObj?.error === 'string') {
+ errorMessage = resultObj.error;
+ } else if (resultObj) {
+ if (typeof resultObj.result === 'string') {
+ resultMessage = resultObj.result;
+ }
+
+ if (Number.isFinite(Number(resultObj.edits_applied))) {
+ editsApplied = Number(resultObj.edits_applied);
+ }
+ }
+
+ return { editsApplied, errorMessage, fileName, filePath: rawPath, resultMessage };
+}
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts
index 5b9bf9f88c..de40bdc80b 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts
@@ -3,7 +3,7 @@
// finishes) and surfaces `bytes`, `result`, and `error` from the
// result blob.
-import { parseToolArgs } from './_shared';
+import { extractToolArgString, parseToolArgs } from './_shared';
import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
import { BuiltInTool } from '$lib/enums';
import type { AgenticSection } from '$lib/types';
@@ -19,6 +19,20 @@ export type WriteFileMeta = {
errorMessage?: string;
};
+/** Everything the block title and status pill show; the full meta (with
+ * the embedded file content) stays body-only so collapsed blocks never
+ * parse the content blob. */
+export type WriteFileTitleMeta = {
+ fileName: string;
+ filePath: string;
+ language: string;
+ bytesWritten?: number;
+ resultMessage?: string;
+ errorMessage?: string;
+};
+
+const PATH_KEYS = ['path', 'file_path', 'filePath'];
+
export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true });
@@ -51,3 +65,43 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul
resultMessage
};
}
+
+/**
+ * Title-tier meta for write_file blocks: everything the header and status
+ * pill render, obtained without parsing the embedded file content. The path
+ * comes from a targeted key extraction; the full parse runs only as a
+ * fallback for arg shapes the extraction can't see.
+ */
+export function parseWriteFileTitleMeta(section: AgenticSection): WriteFileTitleMeta | null {
+ if (section.toolName !== BuiltInTool.SERVER_WRITE_FILE || !section.toolArgs) return null;
+
+ let rawPath: string | undefined = extractToolArgString(section.toolArgs, PATH_KEYS);
+
+ if (!rawPath) {
+ const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true });
+ const fallbackPath = args?.path ?? args?.file_path ?? args?.filePath;
+
+ if (typeof fallbackPath === 'string' && fallbackPath) rawPath = fallbackPath;
+ }
+
+ if (!rawPath) return null;
+
+ const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
+ const language =
+ getFileTypeByExtension(rawPath)?.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '') ??
+ CODE_BLOCK.DEFAULT_LANGUAGE;
+ const resultObj = tryParseToolResultObject(section.toolResult);
+ const bytesWritten =
+ resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
+ const resultMessage = typeof resultObj?.result === 'string' ? resultObj.result : undefined;
+ const errorMessage = typeof resultObj?.error === 'string' ? resultObj.error : undefined;
+
+ return {
+ bytesWritten,
+ errorMessage,
+ fileName,
+ filePath: rawPath,
+ language,
+ resultMessage
+ };
+}
diff --git a/tools/ui/tests/unit/tool-calls.test.ts b/tools/ui/tests/unit/tool-calls.test.ts
index f84a2405ec..d1c5272932 100644
--- a/tools/ui/tests/unit/tool-calls.test.ts
+++ b/tools/ui/tests/unit/tool-calls.test.ts
@@ -1,5 +1,8 @@
import { parseToolArgs } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared';
-import { parseEditFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file';
+import {
+ parseEditFileMeta,
+ parseEditFileTitleMeta
+} from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file';
import { parseExecShellCommandMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command';
import { parseFileGlobSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search';
import { parseGrepSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search';
@@ -7,6 +10,7 @@ import { parseReadFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMes
import { parseRunJavascriptMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript';
import {
parseWriteFileMeta,
+ parseWriteFileTitleMeta,
type WriteFileMeta
} from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file';
import { AgenticSectionType, BuiltInTool } from '$lib/enums';
@@ -223,6 +227,113 @@ describe('parseWriteFileMeta', () => {
});
});
+describe('parseWriteFileTitleMeta', () => {
+ it('matches the full meta for path, language and result fields', () => {
+ const args = JSON.stringify({ content: 'x'.repeat(50_000), path: '/foo.ts' });
+ const toolResult = '{"result":"wrote","bytes":42}';
+ const section = makeSection(
+ { toolArgs: args, toolName: BuiltInTool.SERVER_WRITE_FILE, toolResult },
+ BuiltInTool.SERVER_WRITE_FILE
+ );
+ const full = parseWriteFileMeta(section);
+ const title = parseWriteFileTitleMeta(section);
+
+ expect(title?.filePath).toBe(full?.filePath);
+ expect(title?.fileName).toBe(full?.fileName);
+ expect(title?.language).toBe(full?.language);
+ expect(title?.bytesWritten).toBe(full?.bytesWritten);
+ expect(title?.resultMessage).toBe(full?.resultMessage);
+ expect(title?.errorMessage).toBe(full?.errorMessage);
+ });
+
+ it('extracts a path with escaped characters without parsing the content blob', () => {
+ const section = makeSection(
+ {
+ toolArgs: '{"path":"/a\\nb\\"c/d.ts","content":"x"}',
+ toolName: BuiltInTool.SERVER_WRITE_FILE
+ },
+ BuiltInTool.SERVER_WRITE_FILE
+ );
+
+ expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/a\nb"c/d.ts');
+ });
+
+ it('falls back to the full parse for args the extractor can not see', () => {
+ const section = makeSection(
+ {
+ // key written with an escaped unicode escape sequence in the name
+ toolArgs: '{"\\u0070ath":"/foo.ts","content":"x"}',
+ toolName: BuiltInTool.SERVER_WRITE_FILE
+ },
+ BuiltInTool.SERVER_WRITE_FILE
+ );
+
+ expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/foo.ts');
+ });
+
+ it('accepts partial args like the full parser', () => {
+ const section = makeSection(
+ { toolArgs: '{"path":"/foo.t', toolName: BuiltInTool.SERVER_WRITE_FILE },
+ BuiltInTool.SERVER_WRITE_FILE
+ );
+
+ expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/foo.t');
+ });
+
+ it('returns null for sections with a different tool name', () => {
+ expect(
+ parseWriteFileTitleMeta(
+ makeSection({
+ toolArgs: '{"path":"/x","content":"y"}',
+ toolName: BuiltInTool.SERVER_READ_FILE
+ })
+ )
+ ).toBeNull();
+ });
+});
+
+describe('parseEditFileTitleMeta', () => {
+ it('matches the full meta for path and result fields', () => {
+ const section = makeSection(
+ {
+ toolArgs: '{"path":"/foo.ts","edits":[{"old_text":"a","new_text":"b"}]}' + ' '.repeat(0),
+ toolName: BuiltInTool.SERVER_EDIT_FILE,
+ toolResult: '{"result":"ok","edits_applied":1}'
+ },
+ BuiltInTool.SERVER_EDIT_FILE
+ );
+ const full = parseEditFileMeta(section);
+ const title = parseEditFileTitleMeta(section);
+
+ expect(title?.filePath).toBe(full?.filePath);
+ expect(title?.fileName).toBe(full?.fileName);
+ expect(title?.editsApplied).toBe(full?.editsApplied);
+ expect(title?.resultMessage).toBe(full?.resultMessage);
+ expect(title?.errorMessage).toBe(full?.errorMessage);
+ });
+
+ it('surfaces errorMessage from the result blob without parsing args', () => {
+ const section = makeSection(
+ {
+ toolArgs: '{"path":"/foo.ts","edits":[]}',
+ toolName: BuiltInTool.SERVER_EDIT_FILE,
+ toolResult: '{"error":"permission denied"}'
+ },
+ BuiltInTool.SERVER_EDIT_FILE
+ );
+
+ expect(parseEditFileTitleMeta(section)?.errorMessage).toBe('permission denied');
+ });
+
+ it('returns null when args have no path-like field', () => {
+ expect(
+ parseEditFileTitleMeta(
+ makeSection({ toolArgs: '{"edits":[]}', toolName: BuiltInTool.SERVER_EDIT_FILE })
+ )
+ ).toBeNull();
+ });
+});
+
describe('parseEditFileMeta', () => {
it('parses edits array and applies editsApplied from the result', () => {
const section = makeSection(