Commit 0afb805b1 for llama.cpp

commit 0afb805b19e26c719a466145761faabad3af1a74
Author: Aleksander Grygier <aleksander.grygier@gmail.com>
Date:   Sun Sep 6 10:52:40 2026 +0200

    ui: Improve Chat Messages rendering performance (#28460)

    * ui : update active conversation fields in place

    updateCurrentNode, applyConversationUpdate, updateConversationTimestamp
    and the pin toggle replaced the whole activeConversation object, so its
    identity changed on every send, tool result and rename. ChatMessages
    tracks that identity to refresh sibling info, so each replacement
    triggered a full refetch of every message in the conversation. Write the
    changed fields instead, mirroring updateMessageAtIndex.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : reuse the conversation load read for sibling info

    Opening a conversation read every message from the database twice: once
    in loadConversation for the active path, once in ChatMessages for the
    sibling map. Hand the freshly read array over once so the chat screen
    builds sibling info from it, and set the conversation and its messages
    in one sync block so effects never see the new conversation paired with
    the previous one's messages.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : memoize leaf walks in sibling map build

    buildSiblingInfoMap resolves each sibling's leaf by walking the last-child
    chain, once per sibling per message, so the walk repeats along the same
    chains for every message in the conversation ( O(messages^2) on long
    chats ). Memoize leaf resolution per build with path compression so each
    edge is walked once.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : skip sibling refetch for in-place message edits

    refreshAllMessages refetches every message of the conversation just to
    rebuild sibling info, but preserve-responses and non-branching assistant
    edits never create branches, so the sibling map stays valid. Refresh only
    after actions that branch (editWithBranching kept) or delete.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : drop unused currentResponse reactive writes

    Nothing reads chatStore.currentResponse, but setChatStreaming reassigned
    it on every streamed chunk, so each token paid a reactive write and string
    assignment for nothing. Remove the field and the clearUIState wrapper
    that only reset it.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : reuse completed agentic turn sections during streaming

    deriveAgenticSections runs in a $derived invalidated per streamed chunk,
    but re-derived every turn of the session each time, so per-chunk cost grew
    with session length. Cache completed turns keyed by their assistant message
    plus reference checks on every field that feeds derivation; only the
    streaming turn recomputes. Cache hits return the same section objects, so
    tool block props stay stable and skip their per-chunk re-derive.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : share markdown block infrastructure

    Every markdown block duplicated shared work: a full copy of the hljs
    theme CSS per instance, and the remark/rehype plugin chain rebuilt on
    every processMarkdown call ( once per block at mount, again per coalesced
    chunk while streaming ). Use the single theme style element already
    maintained by SyntaxHighlightedCode, and build pipelines once - shared
    process-wide for attachment-less blocks, cached by attachments identity
    otherwise.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : measure assistant layout only for the last message

    Every assistant message ran getComputedStyle, getBoundingClientRect and
    a ResizeObserver over the previous user bubble at mount, even off-screen
    ones, forcing a layout pass per message while a long conversation
    renders. The measured vars only feed the :last-child min-height rule, so
    gate the effect on isLastAssistantMessage; one measurement and one
    observer remain, and the effect re-runs when the last message changes.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : trim whole-blob scans in tool block headers

    Tool block headers parsed their entire blobs at mount, even collapsed,
    and most tool results and args are large plain text or embedded file
    content: skip JSON.parse unless the blob starts with a JSON container,
    prefilter search-result extraction with a Title:/URL: substring check,
    and match the end-anchored exit-code marker against only the tail of exec
    outputs.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : parse write_file and edit_file titles without the content blob

    Both block headers parsed the full args JSON at mount, even collapsed, and
    write_file and edit_file args embed the whole file content or edit
    strings, so every block paid a full-blob JSON parse just to read the path.
    Split the meta into a title tier that extracts the path with a targeted
    key match (full parse only as fallback) and a body tier that keeps the
    full parse; Svelte deriveds are lazy, and the body snippet renders only
    while the block is expanded, so collapsed blocks no longer parse args.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : mount chat messages lazily near the viewport

    Every message row mounted its full component tree on load, so the cycle
    collector, GC and layout invalidation kept walking every live object and
    DOM node even for rows the user never scrolls to - which dominated the
    profile of long conversations. Wrap each row in a placeholder with an
    IntersectionObserver ( two viewport heights of runway ) that swaps in the
    real ChatMessage when the row approaches the viewport; the row shell
    keeps the content-visibility sizing, and rows stay mounted once
    realized. Rows targeted by the pending-edit flow mount eagerly.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : smooth the chat navigation animations

    Slide the centered new-chat form to the bottom edge with a transform
    instead of a bottom offset - layout-property transitions need the main
    thread every frame and stutter while a long conversation loads, while
    transform transitions run on the compositor. Fade the message list in
    with a CSS animation keyed to the conversation id, disabled under
    prefers-reduced-motion.

    Assisted-by: pi:zai-org/GLM-5.3

    * ui : follow the svelte runes guidance in chat message code

    Two effects detected changes with manual previous-value refs and reset
    flags. The permission request carries object identity, so its dismissal
    is now a derived comparing the dismissed request; the continue request
    is a bare boolean, so its dismissal only shrinks to a reset while no
    request is pending. Also drop a dead if (browser) guard in the markdown
    theme loader - effects never run on the server.

    Assisted-by: pi:zai-org/GLM-5.3

    * test : pin the chat perf invariants in the unit suite

    Cover the fixes whose silent regression would be stale or wrong UI rather
    than a crash: the turn-section cache must reuse unchanged turns yet
    recompute on every field it compares; the sibling map must resolve the
    same leaves after the leaf-walk memoization; the active conversation must
    keep its identity through field updates; and the blob gates ( exec tail
    window, plain-text result gate, search prefilter ) must keep accepting
    what they gate. Only the risky invariants are pinned - no coverage for
    coverage's sake.

    Assisted-by: pi:zai-org/GLM-5.3

    * refactor : address review remarks

    Name the tool-arg string-field pattern, move the file tools' path field
    aliases and the JSON container gates into lib/constants, and export the
    write_file / edit_file meta types from $lib/types instead of the parser
    modules.

    Assisted-by: pi:zai-org/GLM-5.3

diff --git a/tools/ui/src/app.d.ts b/tools/ui/src/app.d.ts
index 5309dce8f..639a16df2 100644
--- a/tools/ui/src/app.d.ts
+++ b/tools/ui/src/app.d.ts
@@ -137,7 +137,6 @@ declare global {

 declare global {
 	interface Window {
-		idxThemeStyle?: number;
 		idxCodeBlock?: number;

 		// File System Access API - not in the DOM lib and unavailable in some browsers
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
index fa2a50bc5..46d05338b 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
@@ -404,7 +404,7 @@
 	}
 </script>

-<div class:chat-message--synthetic={isSynthetic} class="chat-message">
+<div>
 	{#if message.role === MessageRole.SYSTEM}
 		<ChatMessageSystem bind:textareaElement class={className} {message} />
 	{:else if mcpPromptExtra}
@@ -425,25 +425,3 @@
 		/>
 	{/if}
 </div>
-
-<style>
-	/*
-	 * The browser skips layout and paint for messages outside the
-	 * viewport. contain-intrinsic-size reuses the last rendered size
-	 * once known; 500px sizes messages that have never been rendered.
-	 */
-	.chat-message {
-		--chat-message-intrinsic-size: 500px;
-		content-visibility: auto;
-		contain-intrinsic-size: auto var(--chat-message-intrinsic-size);
-	}
-
-	/*
-	 * Synthetic rows (e.g. the working-directory change) are small, so an
-	 * accurate placeholder keeps the injected row from inflating the
-	 * auto-scroll offset; the 500px default is for ordinary bubbles.
-	 */
-	.chat-message--synthetic {
-		--chat-message-intrinsic-size: 40px;
-	}
-</style>
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte
index a2c742f0f..dac55caff 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte
@@ -82,8 +82,11 @@
 	let lastUserMessageHeight = $state(0);
 	let assistantMarginTop = $state(0);

+	// The measured CSS vars feed the :last-child min-height rule only, so only
+	// the last assistant message needs them. Reading isLastAssistantMessage
+	// here also re-runs the effect when this message stops being the last.
 	$effect(() => {
-		if (!assistantEl) return;
+		if (!assistantEl || !isLastAssistantMessage) return;

 		assistantMarginTop = Math.round(parseFloat(getComputedStyle(assistantEl).marginTop));

diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte
index a604a97e3..cc2b4a562 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte
@@ -13,7 +13,12 @@
 	import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
 	import { BuiltInTool } from '$lib/enums';
 	import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
-	import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils';
+	import {
+		extractSearchQuery,
+		extractSearchResults,
+		isWebSearchToolName,
+		looksLikeSearchResult
+	} from '$lib/utils';

 	interface Props {
 		section: AgenticSection;
@@ -26,11 +31,16 @@

 	let { attachments, isExecuting, isStreaming, onToggle, open, section }: Props = $props();

-	const searchResults = $derived(extractSearchResults(section.toolResult));
-	const searchQuery = $derived(extractSearchQuery(section.toolArgs));
-	const isSearchCall = $derived(
-		searchResults.length > 0 || (searchQuery.length > 0 && isWebSearchToolName(section.toolName))
-	);
+	// Runs for every tool block on mount, before the body renders: the cheap
+	// content prefilter and the tool-name allow-list come first so blobs from
+	// exec/file tools are never line-split or JSON-parsed here
+	const isSearchCall = $derived.by(() => {
+		if (looksLikeSearchResult(section.toolResult)) {
+			return extractSearchResults(section.toolResult).length > 0;
+		}
+
+		return isWebSearchToolName(section.toolName) && extractSearchQuery(section.toolArgs).length > 0;
+	});
 </script>

 {#if isSearchCall}
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 2067e4268..22ffc256b 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 @@
 <script lang="ts">
-	import { parseEditFileMeta } from './parsers/edit-file';
+	import { parseEditFileMeta, parseEditFileTitleMeta } from './parsers/edit-file';
 	import ToolCallBlock from './ToolCallBlock.svelte';
 	import { XCircle } from '@lucide/svelte';
 	import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
@@ -16,10 +16,14 @@

 	let { isStreaming, onToggle, open, section }: Props = $props();

-	const editFileMeta = $derived(parseEditFileMeta(section));
+	const editFileMeta = $derived(parseEditFileTitleMeta(section));
+	// body-only: the full meta parses the embedded edit strings, and these
+	// deriveds are read solely from the children snippet, which renders only
+	// while the block is expanded
+	const editFileBody = $derived(parseEditFileMeta(section));
 	const home = $derived(toolsStore.serverHome);
 	const editDiffs = $derived(
-		(editFileMeta?.edits ?? []).map((edit) => computeLineDiff(edit.oldText, edit.newText))
+		(editFileBody?.edits ?? []).map((edit) => computeLineDiff(edit.oldText, edit.newText))
 	);
 </script>

@@ -45,11 +49,11 @@

 				<span>{meta.errorMessage}</span>
 			</div>
-		{:else if meta && meta.edits.length > 0}
+		{:else if meta && editFileBody && editFileBody.edits.length > 0}
 			{#each editDiffs as diffLines, ei (ei)}
 				<div class={ei === 0 ? '' : 'mt-3'}>
 					<div class="mb-1.5 text-xs text-muted-foreground/70 italic">
-						Edit {ei + 1}&nbsp;of&nbsp;{meta.edits.length}
+						Edit {ei + 1}&nbsp;of&nbsp;{editFileBody.edits.length}
 					</div>

 					<div style:max-height={MAX_HEIGHT_CODE_BLOCK} class="diff-block">
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 178c479d9..cafa5280b 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 @@
 <script lang="ts">
-	import { parseWriteFileMeta } from './parsers/write-file';
+	import { parseWriteFileMeta, parseWriteFileTitleMeta } from './parsers/write-file';
 	import ToolCallBlock from './ToolCallBlock.svelte';
 	import { XCircle } from '@lucide/svelte';
 	import { SyntaxHighlightedCode } from '$lib/components/app';
@@ -17,7 +17,11 @@

 	let { isStreaming, onToggle, open, section }: Props = $props();

-	const writeFileMeta = $derived(parseWriteFileMeta(section));
+	const writeFileMeta = $derived(parseWriteFileTitleMeta(section));
+	// body-only: the full meta parses the embedded file content, and this
+	// derived is read solely from the children snippet, which renders only
+	// while the block is expanded
+	const writeFileBody = $derived(parseWriteFileMeta(section));
 	const home = $derived(toolsStore.serverHome);
 </script>

@@ -45,7 +49,7 @@
 			</div>
 		{:else if meta}
 			<SyntaxHighlightedCode
-				code={meta.content}
+				code={writeFileBody?.content ?? ''}
 				language={meta.language}
 				maxHeight={MAX_HEIGHT_CODE_BLOCK}
 				streaming={ctx.isCodeStreaming}
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts
index 073e03de2..79d7c2e27 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts
@@ -4,6 +4,7 @@
 // args-present check, JSON parse) - keeping them here lets each parser
 // stay focused on its own format quirks.

+import { TOOL_ARG_STRING_FIELD_PATTERN_TEMPLATE } from '$lib/constants';
 import { BuiltInTool } from '$lib/enums';
 import type { AgenticSection } from '$lib/types/agentic';
 import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
@@ -28,6 +29,45 @@ function parseFinalToolArgs(blob: string): Record<string, unknown> | null {
 	}
 }

+// Compiled per key on first use; the key set is tiny and fixed.
+const toolArgStringRegexes = new Map<string, RegExp>();
+
+/**
+ * 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: readonly string[]
+): string | undefined {
+	for (const key of keys) {
+		let pattern = toolArgStringRegexes.get(key);
+
+		if (!pattern) {
+			pattern = new RegExp(TOOL_ARG_STRING_FIELD_PATTERN_TEMPLATE.replace('{key}', key));
+			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 9ed6f92bc..d711466cb 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,26 +3,12 @@
 // rendering), plus the result blob for `result` / `edits_applied` /
 // `error` fields.

-import { parseToolArgs } from './_shared';
-import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
+import { extractToolArgString, parseToolArgs } from './_shared';
+import { FILE_PATH_SEPARATOR_REGEX, TOOL_ARG_PATH_KEYS } from '$lib/constants';
 import { BuiltInTool } from '$lib/enums';
-import type { AgenticSection } from '$lib/types';
+import type { AgenticSection, EditFileEdit, EditFileMeta, EditFileTitleMeta } from '$lib/types';
 import { tryParseToolResultObject } from '$lib/utils';

-export type EditFileEdit = {
-	oldText: string;
-	newText: string;
-};
-
-export type EditFileMeta = {
-	fileName: string;
-	filePath: string;
-	edits: EditFileEdit[];
-	resultMessage?: string;
-	editsApplied?: number;
-	errorMessage?: string;
-};
-
 export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null {
 	const args = parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true });

@@ -79,3 +65,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, TOOL_ARG_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/run-javascript.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts
index 440a1f5d6..bd97cd2fe 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts
@@ -6,6 +6,7 @@
 // are handled.

 import { parseToolArgs } from './_shared';
+import { JSON_ARRAY_OPEN, JSON_OBJECT_OPEN } from '$lib/constants';
 import { BuiltInTool } from '$lib/enums';
 import type { AgenticSection } from '$lib/types';

@@ -38,14 +39,21 @@ export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMe
 		// do we scan raw lines for the `Error:` prefix.
 		let parsedObject: Record<string, unknown> | null = null;

-		try {
-			const parsed: unknown = JSON.parse(toolResultString);
+		// Successful sandbox output is a JSON array, errors are objects; plain
+		// text (huge console logs) fails the parse below anyway, so only try
+		// when the blob starts with a JSON container
+		const trimmedResult = toolResultString.trimStart();

-			if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
-				parsedObject = parsed as Record<string, unknown>;
+		if (trimmedResult[0] === JSON_OBJECT_OPEN || trimmedResult[0] === JSON_ARRAY_OPEN) {
+			try {
+				const parsed: unknown = JSON.parse(trimmedResult);
+
+				if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+					parsedObject = parsed as Record<string, unknown>;
+				}
+			} catch {
+				parsedObject = null;
 			}
-		} catch {
-			parsedObject = null;
 		}

 		if (typeof parsedObject?.error === 'string') {
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 5b9bf9f88..4a8e1a9c9 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,22 +3,12 @@
 // finishes) and surfaces `bytes`, `result`, and `error` from the
 // result blob.

-import { parseToolArgs } from './_shared';
-import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
+import { extractToolArgString, parseToolArgs } from './_shared';
+import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX, TOOL_ARG_PATH_KEYS } from '$lib/constants';
 import { BuiltInTool } from '$lib/enums';
-import type { AgenticSection } from '$lib/types';
+import type { AgenticSection, WriteFileMeta, WriteFileTitleMeta } from '$lib/types';
 import { getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils';

-export type WriteFileMeta = {
-	fileName: string;
-	filePath: string;
-	language: string;
-	content: string;
-	bytesWritten?: number;
-	resultMessage?: string;
-	errorMessage?: string;
-};
-
 export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
 	const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true });

@@ -51,3 +41,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, TOOL_ARG_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/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte
index 5137e261f..ea9428e07 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte
@@ -46,49 +46,44 @@
 		isLastAssistantMessage ? !!agenticStore.getLastError(message.convId) : false
 	);

-	let permissionDismissed = $state(false);
-
 	const pendingPermission = $derived(
 		isStreaming && isLastAssistantMessage
 			? agenticStore.getPendingPermissionRequest(message.convId)
 			: null
 	);

-	let prevPendingRef: typeof pendingPermission = null;
-	$effect(() => {
-		if (pendingPermission !== prevPendingRef) {
-			prevPendingRef = pendingPermission;
+	// dismissal applies to the request object, so the next request ( new
+	// identity ) shows the card again without any reset bookkeeping
+	let dismissedPermission: typeof pendingPermission = $state(null);

-			if (pendingPermission) {
-				permissionDismissed = false;
-			}
-		}
-	});
+	const visiblePermission = $derived(
+		pendingPermission && dismissedPermission !== pendingPermission ? pendingPermission : null
+	);

 	function handlePermission(decision: ToolPermissionDecision) {
-		permissionDismissed = true;
+		dismissedPermission = pendingPermission;
 		agenticStore.resolvePermission(message.convId, decision);
 	}

-	let continueDismissed = $state(false);
-
 	const pendingContinue = $derived(
 		isStreaming && isLastAssistantMessage
 			? agenticStore.getPendingContinueRequest(message.convId)
 			: false
 	);

-	let prevContinueRef = false;
-	$effect(() => {
-		if (pendingContinue !== prevContinueRef) {
-			prevContinueRef = pendingContinue;
+	let continueDismissed = $state(false);

-			if (pendingContinue) {
-				continueDismissed = false;
-			}
+	// the continue request is a plain boolean, so there is no identity to
+	// compare against; clear the dismissal whenever no request is pending so
+	// the next one starts from a clean state
+	$effect(() => {
+		if (!pendingContinue) {
+			continueDismissed = false;
 		}
 	});

+	const showContinue = $derived(Boolean(pendingContinue) && !continueDismissed);
+
 	function handleContinue(shouldContinue: boolean) {
 		continueDismissed = true;
 		agenticStore.resolveContinue(message.convId, shouldContinue);
@@ -238,15 +233,15 @@
 		{/each}
 	{/if}

-	{#if pendingPermission && !permissionDismissed}
+	{#if visiblePermission}
 		<ChatMessageActionCardPermissionRequest
 			onDecision={handlePermission}
-			serverLabel={pendingPermission.serverLabel}
-			toolName={pendingPermission.toolName}
+			serverLabel={visiblePermission.serverLabel}
+			toolName={visiblePermission.toolName}
 		/>
 	{/if}

-	{#if pendingContinue && !continueDismissed}
+	{#if showContinue}
 		<ChatMessageActionCardContinueRequest onDecision={handleContinue} />
 	{/if}
 </div>
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
index 4750a9f7c..0078225c0 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
@@ -1,5 +1,6 @@
 <script lang="ts">
-	import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
+	import LazyChatMessage from './LazyChatMessage.svelte';
+	import { ChatMessageUserPending } from '$lib/components/app';
 	import { MessageRole } from '$lib/enums';
 	import { agenticStore, chatStore, conversationsStore, settingsStore } from '$lib/stores';
 	import type { ChatMessageActions } from '$lib/types';
@@ -51,8 +52,9 @@
 			newExtras?: DatabaseMessageExtra[]
 		) => {
 			onUserAction?.();
+			// in-place edit: the store already updated activeMessages and no
+			// branch is created, so sibling info stays valid without a refetch
 			await chatStore.editUserMessagePreserveResponses(message.id, newContent, newExtras);
-			refreshAllMessages();
 		},

 		editWithBranching: async (
@@ -72,7 +74,10 @@
 		) => {
 			onUserAction?.();
 			await chatStore.editAssistantMessage(message.id, newContent, shouldBranch);
-			refreshAllMessages();
+
+			// only a branch changes sibling info; an in-place edit already
+			// landed in activeMessages
+			if (shouldBranch) refreshAllMessages();
 		},

 		forkConversation: async (
@@ -97,9 +102,17 @@
 		const conversation = conversationsStore.activeConversation;

 		if (conversation) {
-			conversationsStore.getConversationMessages(conversation.id).then((messages) => {
-				allConversationMessages = messages;
-			});
+			// reuse the array loadConversation just read, when present; branch
+			// actions fall through to a fresh fetch
+			const preloaded = conversationsStore.consumeLastLoadedMessages(conversation.id);
+
+			if (preloaded) {
+				allConversationMessages = preloaded;
+			} else {
+				conversationsStore.getConversationMessages(conversation.id).then((messages) => {
+					allConversationMessages = messages;
+				});
+			}
 		} else {
 			allConversationMessages = [];
 		}
@@ -224,48 +237,76 @@
 	});
 </script>

-<div>
-	{#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
-		<ChatMessage
-			{chatActions}
-			class="mx-auto mt-12 w-full max-w-3xl"
-			{isLastAssistantMessage}
-			{isLastUserMessage}
-			{message}
-			{nextAssistantMessage}
-			{siblingInfo}
-			{toolMessages}
-		/>
-	{/each}
-
-	{#if conversationsStore.activeConversation && agenticStore.getPendingSteeringMessageContent(conversationsStore.activeConversation!.id)}
-		{@const convId = conversationsStore.activeConversation!.id}
-		{@const pendingContent = agenticStore.getPendingSteeringMessageContent(convId)}
-
-		{#if pendingContent}
-			<ChatMessageUserPending
-				class="mx-auto mt-12 w-full max-w-[48rem]"
-				content={pendingContent}
-				extras={agenticStore.getPendingSteeringMessageExtras(convId)}
-				onDelete={() => agenticStore.clearSteeringMessage(convId)}
-				onEdit={(newContent, extras) =>
-					agenticStore.injectSteeringMessage(convId, newContent, extras)}
-				onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
-			/>
-		{/if}
-	{:else if conversationsStore.activeConversation && chatStore.getPendingMessageContent(conversationsStore.activeConversation!.id)}
-		{@const convId = conversationsStore.activeConversation!.id}
-		{@const pendingContent = chatStore.getPendingMessageContent(convId)}
-
-		{#if pendingContent}
-			<ChatMessageUserPending
-				class="mx-auto mt-12 w-full max-w-[48rem]"
-				content={pendingContent}
-				extras={chatStore.getPendingMessageExtras(convId)}
-				onDelete={() => chatStore.clearPendingMessage(convId)}
-				onEdit={(newContent, extras) => chatStore.injectPendingMessage(convId, newContent, extras)}
-				onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
+<!-- Re-created per conversation, so the CSS fade-in below plays on every
+     navigation into a chat route. -->
+{#key conversationsStore.activeConversation?.id ?? 'new'}
+	<div class="chat-messages">
+		{#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
+			<LazyChatMessage
+				{chatActions}
+				class="mx-auto mt-12 w-full max-w-3xl"
+				{isLastAssistantMessage}
+				{isLastUserMessage}
+				{message}
+				{nextAssistantMessage}
+				{siblingInfo}
+				{toolMessages}
 			/>
+		{/each}
+
+		{#if conversationsStore.activeConversation && agenticStore.getPendingSteeringMessageContent(conversationsStore.activeConversation!.id)}
+			{@const convId = conversationsStore.activeConversation!.id}
+			{@const pendingContent = agenticStore.getPendingSteeringMessageContent(convId)}
+
+			{#if pendingContent}
+				<ChatMessageUserPending
+					class="mx-auto mt-12 w-full max-w-[48rem]"
+					content={pendingContent}
+					extras={agenticStore.getPendingSteeringMessageExtras(convId)}
+					onDelete={() => agenticStore.clearSteeringMessage(convId)}
+					onEdit={(newContent, extras) =>
+						agenticStore.injectSteeringMessage(convId, newContent, extras)}
+					onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
+				/>
+			{/if}
+		{:else if conversationsStore.activeConversation && chatStore.getPendingMessageContent(conversationsStore.activeConversation!.id)}
+			{@const convId = conversationsStore.activeConversation!.id}
+			{@const pendingContent = chatStore.getPendingMessageContent(convId)}
+
+			{#if pendingContent}
+				<ChatMessageUserPending
+					class="mx-auto mt-12 w-full max-w-[48rem]"
+					content={pendingContent}
+					extras={chatStore.getPendingMessageExtras(convId)}
+					onDelete={() => chatStore.clearPendingMessage(convId)}
+					onEdit={(newContent, extras) =>
+						chatStore.injectPendingMessage(convId, newContent, extras)}
+					onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
+				/>
+			{/if}
 		{/if}
-	{/if}
-</div>
+	</div>
+{/key}
+
+<style>
+	/* Compositor-friendly opacity fade; the keyed block re-creates the list per
+	 * conversation, so the animation plays on every navigation into a chat. */
+	.chat-messages {
+		animation: chat-messages-fade-in 150ms ease-out;
+	}
+
+	@keyframes chat-messages-fade-in {
+		from {
+			opacity: 0;
+		}
+		to {
+			opacity: 1;
+		}
+	}
+
+	@media (prefers-reduced-motion: reduce) {
+		.chat-messages {
+			animation: none;
+		}
+	}
+</style>
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/LazyChatMessage.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/LazyChatMessage.svelte
new file mode 100644
index 000000000..f9667bbbb
--- /dev/null
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/LazyChatMessage.svelte
@@ -0,0 +1,105 @@
+<script lang="ts">
+	import ChatMessage from './ChatMessage/ChatMessage.svelte';
+	import { chatStore } from '$lib/stores';
+	import type { ChatMessageActions } from '$lib/types';
+
+	interface Props {
+		chatActions: ChatMessageActions;
+		class?: string;
+		isLastAssistantMessage?: boolean;
+		isLastUserMessage?: boolean;
+		message: DatabaseMessage;
+		nextAssistantMessage?: DatabaseMessage | null;
+		siblingInfo?: ChatMessageSiblingInfo | null;
+		toolMessages?: DatabaseMessage[];
+	}
+
+	let {
+		chatActions,
+		class: className = '',
+		isLastAssistantMessage = false,
+		isLastUserMessage = false,
+		message,
+		nextAssistantMessage = null,
+		siblingInfo = null,
+		toolMessages = []
+	}: Props = $props();
+
+	// A mounted message row is a whole component tree (contexts, effects,
+	// collapsibles, markdown blocks), and the cycle collector, GC and layout
+	// invalidation keep walking every live object and DOM node, even for
+	// rows the user never scrolls to. Mount the real tree only when the row
+	// approaches the viewport; until then the row is an empty placeholder
+	// that reserves its size through content-visibility.
+	let mounted = $state(false);
+	let wrapperEl: HTMLDivElement | undefined = $state();
+
+	$effect(() => {
+		if (mounted || !wrapperEl) return;
+
+		const observer = new IntersectionObserver(
+			(entries) => {
+				if (entries.some((entry) => entry.isIntersecting)) {
+					mounted = true;
+					observer.disconnect();
+				}
+			},
+			// pre-mount a couple of viewport heights ahead of the scroll
+			// position so a fast scroll never meets an empty row
+			{ rootMargin: '200% 0px' }
+		);
+
+		observer.observe(wrapperEl);
+
+		return () => observer.disconnect();
+	});
+
+	// Flows that target a row by id (pending edit) expect the message
+	// component and its effects to exist; mount the target row first
+	$effect(() => {
+		if (chatStore.pendingEditMessageId === message.id) {
+			mounted = true;
+		}
+	});
+</script>
+
+<div
+	bind:this={wrapperEl}
+	class:chat-message--synthetic={Boolean(message.isSynthetic)}
+	class="chat-message"
+>
+	{#if mounted}
+		<ChatMessage
+			{chatActions}
+			class={className}
+			{isLastAssistantMessage}
+			{isLastUserMessage}
+			{message}
+			{nextAssistantMessage}
+			{siblingInfo}
+			{toolMessages}
+		/>
+	{/if}
+</div>
+
+<style>
+	/*
+	 * The browser skips layout and paint for messages outside the
+	 * viewport. contain-intrinsic-size reuses the last rendered size
+	 * once known; 500px sizes messages that have never been rendered.
+	 */
+	.chat-message {
+		--chat-message-intrinsic-size: 500px;
+		content-visibility: auto;
+		contain-intrinsic-size: auto var(--chat-message-intrinsic-size);
+	}
+
+	/*
+	 * Synthetic rows (e.g. the working-directory change) are small, so an
+	 * accurate placeholder keeps the injected row from inflating the
+	 * auto-scroll offset; the 500px default is for ordinary bubbles.
+	 */
+	.chat-message--synthetic {
+		--chat-message-intrinsic-size: 40px;
+	}
+</style>
diff --git a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
index 3ad3f2468..6cea95d0d 100644
--- a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
@@ -315,13 +315,18 @@
 		<div
 			style:padding-top={!isEmpty ? 'var(--chat-form-padding-top)' : undefined}
 			class={[
-				'pointer-events-none md:sticky fixed  mt-auto transition-all duration-200',
+				// animate the centered->bottomed move with transform, not bottom:
+				// layout-property transitions need the main thread every frame and
+				// stutter while a long conversation loads; transform transitions
+				// run on the compositor and stay smooth
+				'pointer-events-none md:sticky fixed  mt-auto transition-transform duration-200',
 				deviceStore.isStandalone
 					? 'bottom-6 right-4 left-4'
 					: deviceStore.isIOSSafari
 						? 'bottom-1 left-2 right-2'
 						: 'bottom-2 right-2 left-2',
-				isEmpty ? 'md:bottom-[calc(50dvh-7rem)] 2xl:bottom-[calc(50dvh-4rem)]' : 'md:bottom-4'
+				'md:bottom-4',
+				isEmpty ? 'md:translate-y-[calc(-50dvh+8rem)] 2xl:translate-y-[calc(-50dvh+5rem)]' : ''
 			]}
 		>
 			<ChatScreenGreeting {isEmpty} />
diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte b/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte
index 87b41bd00..c217a769a 100644
--- a/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte
+++ b/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte
@@ -1,23 +1,12 @@
 <script lang="ts">
 	import '$lib/styles/katex-custom.scss';
+	import { getMarkdownProcessor, type MarkdownProcessor } from './markdown-processor';
 	import {
 		getCodeInfoFromTarget,
 		getHastNodeId,
 		getMdastNodeHash,
 		isAppendMode
 	} from './markdown-utils';
-	import { rehypeEnhanceCodeBlocks } from './plugins/rehype/enhance-code-blocks';
-	import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links';
-	import { rehypeEnhanceMermaidBlocks } from './plugins/rehype/enhance-mermaid-blocks';
-	import { rehypeEnhanceSvgBlocks } from './plugins/rehype/enhance-svg-blocks';
-	import { rehypeFileBadge } from './plugins/rehype/file-badge';
-	import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre';
-	import { rehypeRtlSupport } from './plugins/rehype/rehype-rtl-support';
-	import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images';
-	import { rehypeSvgPre } from './plugins/rehype/svg-pre';
-	import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer';
-	import { remarkLiteralHtml } from './plugins/remark/literal-html';
-	import { browser } from '$app/environment';
 	import {
 		ActionIconCopyToClipboard,
 		CodeBlockActions,
@@ -38,10 +27,10 @@
 		MERMAID_WRAPPER_CLASS,
 		SETTINGS_KEYS,
 		SVG,
-		TOGGLE_SOURCE_BTN_CLASS
+		TOGGLE_SOURCE_BTN_CLASS,
+		UI_DATA_ATTRS
 	} from '$lib/constants';
 	import { BooleanString, ColorMode, UrlProtocol } from '$lib/enums';
-	import { FileTypeText } from '$lib/enums/files.enums';
 	import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
 	import { settingsStore } from '$lib/stores';
 	import type { DatabaseMessageExtra } from '$lib/types/database';
@@ -58,17 +47,8 @@
 	import type { Root as HastRoot, RootContent as HastRootContent } from 'hast';
 	import githubLightCss from 'highlight.js/styles/github.css?inline';
 	import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
-	import { all as lowlightAll } from 'lowlight';
 	import type { Root as MdastRoot } from 'mdast';
 	import { mode } from 'mode-watcher';
-	import rehypeHighlight from 'rehype-highlight';
-	import rehypeKatex from 'rehype-katex';
-	import rehypeStringify from 'rehype-stringify';
-	import { remark } from 'remark';
-	import remarkBreaks from 'remark-breaks';
-	import remarkGfm from 'remark-gfm';
-	import remarkMath from 'remark-math';
-	import remarkRehype from 'remark-rehype';
 	import { onDestroy, tick } from 'svelte';
 	import { SvelteMap } from 'svelte/reactivity';

@@ -144,44 +124,6 @@
 	const transformCache = new SvelteMap<string, string>();
 	let previousContent = '';

-	const themeStyleId = `highlight-theme-${(window.idxThemeStyle = (window.idxThemeStyle ?? 0) + 1)}`;
-
-	let processor = $derived(() => {
-		void attachments;
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		let proc: any = remark().use(remarkGfm); // GitHub Flavored Markdown
-
-		if (!disableMath) {
-			proc = proc.use(remarkMath); // Parse $inline$ and $$block$$ math
-		}
-
-		proc = proc
-			.use(remarkBreaks) // Convert line breaks to <br>
-			.use(remarkLiteralHtml) // Treat raw HTML as literal text with preserved indentation
-			.use(remarkRehype); // Convert Markdown AST to rehype
-
-		if (!disableMath) {
-			proc = proc.use(rehypeKatex); // Render math using KaTeX
-		}
-
-		return proc
-			.use(rehypeHighlight, {
-				aliases: { [FileTypeText.XML]: [FileTypeText.SVELTE, FileTypeText.VUE] },
-				languages: lowlightAll
-			}) // Add syntax highlighting
-			.use(rehypeRestoreTableHtml) // Restore limited HTML (e.g., <br>, <ul>) inside Markdown tables
-			.use(rehypeEnhanceLinks) // Add target="_blank" to links
-			.use(rehypeFileBadge) // Render file:// anchors as inline badge chips
-			.use(rehypeMermaidPre) // Convert mermaid blocks to <pre class="mermaid">
-			.use(rehypeSvgPre) // Convert svg blocks to <pre class="svg-block">
-			.use(rehypeEnhanceCodeBlocks) // Wrap code blocks with header and actions
-			.use(rehypeEnhanceMermaidBlocks) // Wrap mermaid blocks with header and actions
-			.use(rehypeEnhanceSvgBlocks) // Wrap svg blocks with header and actions
-			.use(rehypeResolveAttachmentImages, { attachments })
-			.use(rehypeRtlSupport) // Add bidirectional text support
-			.use(rehypeStringify, { allowDangerousHtml: true }); // Convert to HTML string
-	});
-
 	/**
 	 * Removes click event listeners from copy and preview buttons.
 	 * Called on component destroy.
@@ -201,33 +143,22 @@
 		}
 	}

-	/**
-	 * Removes this component's highlight.js theme style from the document head.
-	 * Called on component destroy to clean up injected styles.
-	 */
-	function cleanupHighlightTheme() {
-		if (!browser) return;
-
-		const existingTheme = document.getElementById(themeStyleId);
-
-		existingTheme?.remove();
-	}
-
 	/**
 	 * Loads the appropriate highlight.js theme based on dark/light mode.
-	 * Injects a scoped style element into the document head.
+	 * One shared style element for every markdown block, mirroring
+	 * SyntaxHighlightedCode.svelte. The old per-instance copies duplicated the
+	 * full theme CSS once per rendered message, which grows without bound in
+	 * long conversations.
 	 * @param isDark - Whether to load the dark theme (true) or light theme (false)
 	 */
 	function loadHighlightTheme(isDark: boolean) {
-		if (!browser) return;
-
-		const existingTheme = document.getElementById(themeStyleId);
-
-		existingTheme?.remove();
+		document
+			.querySelectorAll(`style[${UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW}]`)
+			.forEach((style) => style.remove());

 		const style = document.createElement('style');

-		style.id = themeStyleId;
+		style.setAttribute(UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW, BooleanString.TRUE);
 		style.textContent = isDark ? githubDarkCss : githubLightCss;

 		document.head.appendChild(style);
@@ -247,7 +178,7 @@
 	 * @returns Object containing the HTML string and cache hash
 	 */
 	async function transformMdastNode(
-		processorInstance: ReturnType<typeof processor>,
+		processorInstance: MarkdownProcessor,
 		node: unknown,
 		index: number
 	): Promise<{ html: string; hash: string }> {
@@ -369,7 +300,7 @@

 			if (prefixMarkdown.trim()) {
 				const normalizedPrefix = preprocessLaTeX(prefixMarkdown);
-				const processorInstance = processor();
+				const processorInstance = getMarkdownProcessor({ attachments, disableMath });
 				const ast = processorInstance.parse(normalizedPrefix) as MdastRoot;
 				const mdastChildren = (ast as { children?: unknown[] }).children ?? [];
 				const nextBlocks: MarkdownBlock[] = [];
@@ -419,7 +350,7 @@
 		incompleteCodeBlock = null;

 		const normalized = preprocessLaTeX(markdown);
-		const processorInstance = processor();
+		const processorInstance = getMarkdownProcessor({ attachments, disableMath });
 		const ast = processorInstance.parse(normalized) as MdastRoot;
 		const mdastChildren = (ast as { children?: unknown[] }).children ?? [];
 		const stableCount = Math.max(mdastChildren.length - 1, 0);
@@ -858,7 +789,6 @@

 	onDestroy(() => {
 		cleanupEventListeners();
-		cleanupHighlightTheme();
 		streamingAutoScroll.destroy();
 	});
 </script>
diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-processor.ts b/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-processor.ts
new file mode 100644
index 000000000..e973a6a4b
--- /dev/null
+++ b/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-processor.ts
@@ -0,0 +1,112 @@
+// Shared remark/rehype pipeline factory for MarkdownContent.
+//
+// The frozen plugin chain is expensive to build ( ~15 plugin instances ),
+// and MarkdownContent used to rebuild it on every processMarkdown call:
+// once per block at mount, and again on every coalesced chunk while
+// streaming. Pipelines without attachments are shared process-wide per
+// math flag; attachment-bearing pipelines are cached by the attachments
+// array identity, which changes whenever extras are updated.
+
+import { rehypeEnhanceCodeBlocks } from './plugins/rehype/enhance-code-blocks';
+import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links';
+import { rehypeEnhanceMermaidBlocks } from './plugins/rehype/enhance-mermaid-blocks';
+import { rehypeEnhanceSvgBlocks } from './plugins/rehype/enhance-svg-blocks';
+import { rehypeFileBadge } from './plugins/rehype/file-badge';
+import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre';
+import { rehypeRtlSupport } from './plugins/rehype/rehype-rtl-support';
+import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images';
+import { rehypeSvgPre } from './plugins/rehype/svg-pre';
+import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer';
+import { remarkLiteralHtml } from './plugins/remark/literal-html';
+import { FileTypeText } from '$lib/enums/files.enums';
+import type { DatabaseMessageExtra } from '$lib/types/database';
+import type { Root as HastRoot } from 'hast';
+import { all as lowlightAll } from 'lowlight';
+import type { Root as MdastRoot } from 'mdast';
+import rehypeHighlight from 'rehype-highlight';
+import rehypeKatex from 'rehype-katex';
+import rehypeStringify from 'rehype-stringify';
+import { remark } from 'remark';
+import remarkBreaks from 'remark-breaks';
+import remarkGfm from 'remark-gfm';
+import remarkMath from 'remark-math';
+import remarkRehype from 'remark-rehype';
+
+export interface MarkdownProcessor {
+	parse(markdown: string): MdastRoot;
+	run(tree: MdastRoot): Promise<HastRoot>;
+	stringify(tree: HastRoot): string;
+}
+
+export interface MarkdownProcessorOptions {
+	attachments?: DatabaseMessageExtra[];
+	disableMath?: boolean;
+}
+
+const sharedPipelines = new Map<string, MarkdownProcessor>();
+const attachmentPipelines = new WeakMap<object, MarkdownProcessor>();
+
+function buildPipeline({
+	attachments,
+	disableMath = false
+}: MarkdownProcessorOptions): MarkdownProcessor {
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	let proc: any = remark().use(remarkGfm); // GitHub Flavored Markdown
+
+	if (!disableMath) {
+		proc = proc.use(remarkMath); // Parse $inline$ and $$block$$ math
+	}
+
+	proc = proc
+		.use(remarkBreaks) // Convert line breaks to <br>
+		// Treat raw HTML as literal text with preserved indentation
+		.use(remarkLiteralHtml)
+		.use(remarkRehype); // Convert Markdown AST to rehype
+
+	if (!disableMath) {
+		proc = proc.use(rehypeKatex); // Render math using KaTeX
+	}
+
+	const pipeline = proc
+		.use(rehypeHighlight, {
+			aliases: { [FileTypeText.XML]: [FileTypeText.SVELTE, FileTypeText.VUE] },
+			languages: lowlightAll
+		}) // Add syntax highlighting
+		.use(rehypeRestoreTableHtml) // Restore limited HTML (e.g. <br>, <ul>) inside Markdown tables
+		.use(rehypeEnhanceLinks) // Add target="_blank" to links
+		.use(rehypeFileBadge) // Render file:// anchors as inline badge chips
+		.use(rehypeMermaidPre) // Convert mermaid blocks to <pre class="mermaid">
+		.use(rehypeSvgPre) // Convert svg blocks to <pre class="svg-block">
+		.use(rehypeEnhanceCodeBlocks) // Wrap code blocks with header and actions
+		.use(rehypeEnhanceMermaidBlocks) // Wrap mermaid blocks with header and actions
+		.use(rehypeEnhanceSvgBlocks) // Wrap svg blocks with header and actions
+		.use(rehypeResolveAttachmentImages, { attachments })
+		.use(rehypeRtlSupport) // Add bidirectional text support
+		.use(rehypeStringify, { allowDangerousHtml: true }); // Convert to HTML string
+
+	return pipeline as MarkdownProcessor;
+}
+
+export function getMarkdownProcessor(options: MarkdownProcessorOptions): MarkdownProcessor {
+	if (options.attachments && options.attachments.length > 0) {
+		let cached = attachmentPipelines.get(options.attachments);
+
+		if (!cached) {
+			cached = buildPipeline(options);
+			attachmentPipelines.set(options.attachments, cached);
+		}
+
+		return cached;
+	}
+
+	const key = String(Boolean(options.disableMath));
+
+	let cached = sharedPipelines.get(key);
+
+	if (!cached) {
+		cached = buildPipeline(options);
+		sharedPipelines.set(key, cached);
+	}
+
+	return cached;
+}
diff --git a/tools/ui/src/lib/constants/index.ts b/tools/ui/src/lib/constants/index.ts
index e3241373e..d93ae6429 100644
--- a/tools/ui/src/lib/constants/index.ts
+++ b/tools/ui/src/lib/constants/index.ts
@@ -16,6 +16,7 @@ export * from './context-gauge-popup.constants';
 export * from './conversation-import.constants';
 export * from './binary-detection.constants';
 export * from './content-detection.constants';
+export * from './tool-call-args.constants';
 export * from './tool-ui.constants';
 export * from './cache.constants';
 export * from './chat-form.constants';
diff --git a/tools/ui/src/lib/constants/tool-call-args.constants.ts b/tools/ui/src/lib/constants/tool-call-args.constants.ts
new file mode 100644
index 000000000..e74260be2
--- /dev/null
+++ b/tools/ui/src/lib/constants/tool-call-args.constants.ts
@@ -0,0 +1,23 @@
+// Tool-args and tool-result parsing helpers: the file tools' path field
+// aliases, the JSON container gates for result blobs, and the targeted
+// string-field pattern used for cheap title-tier extraction.
+
+/**
+ * Field aliases the file tools accept for the path argument. Tool contracts
+ * drifted over time: some models emit `file_path` / `filePath`.
+ */
+export const TOOL_ARG_PATH_KEYS: readonly string[] = ['path', 'file_path', 'filePath'];
+
+/** Opening character of a JSON object; only an object root can carry fields. */
+export const JSON_OBJECT_OPEN = '{';
+
+/** Opening character of a JSON array; successful sandbox output is one. */
+export const JSON_ARRAY_OPEN = '[';
+
+/**
+ * Matches `"<key>": "<value>"` in a JSON args blob ( whitespace between
+ * tokens allowed ), capturing the raw string literal so only that literal
+ * gets decoded; escaped quotes stay inside the value group. `{key}` is
+ * replaced with the field name before use.
+ */
+export const TOOL_ARG_STRING_FIELD_PATTERN_TEMPLATE = '"{key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"';
diff --git a/tools/ui/src/lib/stores/chat/index.svelte.ts b/tools/ui/src/lib/stores/chat/index.svelte.ts
index 296c2cca5..4bdcc6845 100644
--- a/tools/ui/src/lib/stores/chat/index.svelte.ts
+++ b/tools/ui/src/lib/stores/chat/index.svelte.ts
@@ -55,7 +55,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
 		string,
 		{ response: string; messageId: string; model?: string | null }
 	>();
-	currentResponse = $state('');
 	errorDialogState = $state<ErrorDialogState | null>(null);
 	// true while the active conversation has a local pipe (send, attach or resume-wait)
 	isLoading = $derived(this.activity.isLocal(conversationsStore.activeConversation?.id ?? ''));
@@ -256,8 +255,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
 		}

 		this.chatStreamingStates.delete(convId);
-
-		if (convId === conversationsStore.activeConversation?.id) this.currentResponse = '';
 	}
 	clearEditMode(): void {
 		this.isEditModeActive = false;
@@ -272,11 +269,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
 		this.pendingMessages.delete(convId);
 	}

-	/** Reset per-view state when (re)mounting the empty chat screen. */
-	clearUIState(): void {
-		this.currentResponse = '';
-	}
-
 	consumePendingDraft(): { message: string; files: ChatUploadedFile[] } | null {
 		if (!this.pendingDraftMessage && this.pendingDraftFiles.length === 0) return null;

@@ -766,8 +758,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
 			model: model ?? this.chatStreamingStates.get(convId)?.model,
 			response
 		});
-
-		if (convId === conversationsStore.activeConversation?.id) this.currentResponse = response;
 	}

 	setEditModeActive(handler: (files: File[]) => void): void {
@@ -1244,7 +1234,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
 	syncLoadingStateForChat(convId: string): void {
 		const s = this.chatStreamingStates.get(convId);

-		this.currentResponse = s?.response || '';
 		this.processing.setActiveConversation(convId);

 		// Sync streaming content to activeMessages so UI displays current content
diff --git a/tools/ui/src/lib/stores/conversations/index.svelte.ts b/tools/ui/src/lib/stores/conversations/index.svelte.ts
index df5b1ecef..c4fea2e4e 100644
--- a/tools/ui/src/lib/stores/conversations/index.svelte.ts
+++ b/tools/ui/src/lib/stores/conversations/index.svelte.ts
@@ -52,6 +52,13 @@ class ConversationsStore implements ConversationsPreferencesHost {
 	/** In-flight init run; shared by concurrent callers, reset on failure to allow retry */
 	private initPromise: Promise<void> | null = null;

+	/**
+	 * Messages loadConversation just read, handed off once so the chat
+	 * screen can reuse them for sibling info instead of re-fetching the
+	 * whole conversation a second time.
+	 */
+	private lastLoadedMessages: { convId: string; messages: DatabaseMessage[] } | null = null;
+
 	/**
 	 * Memo of the last findMessageIndex() lookup. Streaming calls it once per
 	 * chunk for the same message, so a validated cache hit keeps that O(1)
@@ -88,7 +95,13 @@ class ConversationsStore implements ConversationsPreferencesHost {
 		}

 		if (this.activeConversation?.id === id) {
-			this.activeConversation = { ...this.activeConversation, ...updates };
+			// field-wise, not object replacement: effects that track the active
+			// conversation identity would otherwise refire on every rename or pin
+			const target = this.activeConversation as unknown as Record<string, unknown>;
+
+			for (const [key, value] of Object.entries(updates)) {
+				if (target[key] !== value) target[key] = value;
+			}
 		}
 	}

@@ -202,11 +215,8 @@ class ConversationsStore implements ConversationsPreferencesHost {
 			const updates = await DatabaseService.bulkToggleConversationPins(convIds);
 			const activeId = this.activeConversation?.id;

-			if (activeId && updates.has(activeId)) {
-				this.activeConversation = {
-					...this.activeConversation!,
-					pinned: updates.get(activeId)!
-				};
+			if (this.activeConversation && activeId && updates.has(activeId)) {
+				this.activeConversation.pinned = updates.get(activeId)!;
 			}

 			for (let i = 0; i < this.conversations.length; i++) {
@@ -236,6 +246,17 @@ class ConversationsStore implements ConversationsPreferencesHost {
 		this.preferences.resetPending();
 	}

+	/** One-shot handoff of the messages the last loadConversation read. */
+	consumeLastLoadedMessages(convId: string): DatabaseMessage[] | null {
+		if (this.lastLoadedMessages?.convId !== convId) return null;
+
+		const messages = this.lastLoadedMessages.messages;
+
+		this.lastLoadedMessages = null;
+
+		return messages;
+	}
+
 	/**
 	 * Creates a new conversation and navigates to it
 	 * @param name - Optional name for the conversation
@@ -509,22 +530,15 @@ class ConversationsStore implements ConversationsPreferencesHost {
 			// it doesn't belong to this conversation.
 			this.preferences.pendingCwd = null;

-			this.activeConversation = conversation;
-
-			if (conversation.currNode) {
-				const allMessages = await DatabaseService.getConversationMessages(convId);
-				const filteredMessages = filterByLeafNodeId(
-					allMessages,
-					conversation.currNode,
-					false
-				) as DatabaseMessage[];
+			const allMessages = await DatabaseService.getConversationMessages(convId);

-				this.activeMessages = filteredMessages;
-			} else {
-				const messages = await DatabaseService.getConversationMessages(convId);
-
-				this.activeMessages = messages;
-			}
+			// set conversation and messages in one sync block so effects never see
+			// the new conversation with the previous conversation's messages
+			this.lastLoadedMessages = { convId, messages: allMessages };
+			this.activeConversation = conversation;
+			this.activeMessages = conversation.currNode
+				? (filterByLeafNodeId(allMessages, conversation.currNode, false) as DatabaseMessage[])
+				: allMessages;

 			return true;
 		} catch (error) {
@@ -558,7 +572,7 @@ class ConversationsStore implements ConversationsPreferencesHost {
 		const currentLeafNodeId = findLeafNode(allMessages, siblingId);

 		await DatabaseService.updateCurrentNode(this.activeConversation.id, currentLeafNodeId);
-		this.activeConversation = { ...this.activeConversation, currNode: currentLeafNodeId };
+		this.activeConversation.currNode = currentLeafNodeId;
 		await this.refreshActiveMessages();

 		if (rootMessage && this.activeMessages.length > 0) {
@@ -694,7 +708,7 @@ class ConversationsStore implements ConversationsPreferencesHost {
 		}

 		if (this.activeConversation?.id === targetId) {
-			this.activeConversation = { ...this.activeConversation, lastModified: now };
+			this.activeConversation.lastModified = now;
 		}

 		DatabaseService.updateConversation(targetId, { lastModified: now }).catch((error) =>
@@ -710,7 +724,7 @@ class ConversationsStore implements ConversationsPreferencesHost {
 		if (!this.activeConversation) return;

 		await DatabaseService.updateCurrentNode(this.activeConversation.id, nodeId);
-		this.activeConversation = { ...this.activeConversation, currNode: nodeId };
+		this.activeConversation.currNode = nodeId;
 	}

 	/**
diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts
index d91c2811a..333c1bd3c 100644
--- a/tools/ui/src/lib/types/index.ts
+++ b/tools/ui/src/lib/types/index.ts
@@ -209,7 +209,16 @@ export type {
 export type { DesktopIconStripItem } from './navigation';

 // Tools types
-export type { ToolEntry, ToolGroup, ToolUiEntry } from './tools';
+export type {
+	EditFileEdit,
+	EditFileMeta,
+	EditFileTitleMeta,
+	ToolEntry,
+	ToolGroup,
+	ToolUiEntry,
+	WriteFileMeta,
+	WriteFileTitleMeta
+} from './tools';

 // Reasoning
 export type { ReasoningEffortLevel } from './reasoning';
diff --git a/tools/ui/src/lib/types/tools.d.ts b/tools/ui/src/lib/types/tools.d.ts
index edcec65c7..fa8963bd1 100644
--- a/tools/ui/src/lib/types/tools.d.ts
+++ b/tools/ui/src/lib/types/tools.d.ts
@@ -31,3 +31,50 @@ export interface ToolGroup {
 	serverId?: string;
 	tools: ToolEntry[];
 }
+
+export interface WriteFileMeta {
+	fileName: string;
+	filePath: string;
+	language: string;
+	content: string;
+	bytesWritten?: number;
+	resultMessage?: string;
+	errorMessage?: string;
+}
+
+/** Everything the write_file 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 interface WriteFileTitleMeta {
+	fileName: string;
+	filePath: string;
+	language: string;
+	bytesWritten?: number;
+	resultMessage?: string;
+	errorMessage?: string;
+}
+
+export interface EditFileEdit {
+	oldText: string;
+	newText: string;
+}
+
+export interface EditFileMeta {
+	fileName: string;
+	filePath: string;
+	edits: EditFileEdit[];
+	resultMessage?: string;
+	editsApplied?: number;
+	errorMessage?: string;
+}
+
+/** Everything the edit_file 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 interface EditFileTitleMeta {
+	fileName: string;
+	filePath: string;
+	resultMessage?: string;
+	editsApplied?: number;
+	errorMessage?: string;
+}
diff --git a/tools/ui/src/lib/utils/agentic.ts b/tools/ui/src/lib/utils/agentic.ts
index cd150c5ef..28b3f43ee 100644
--- a/tools/ui/src/lib/utils/agentic.ts
+++ b/tools/ui/src/lib/utils/agentic.ts
@@ -109,6 +109,89 @@ function deriveSingleTurnSections(
 	return sections;
 }

+interface TurnSectionsCacheEntry {
+	content: string | undefined;
+	extra: DatabaseMessageExtra[] | undefined;
+	reasoningContent: string | undefined;
+	toolCalls: string | undefined;
+	toolMessageContents: (string | undefined)[];
+	toolMessageExtras: (DatabaseMessageExtra[] | undefined)[];
+	toolMessages: DatabaseMessage[];
+	sections: AgenticSection[];
+}
+
+const turnSectionsCache = new WeakMap<DatabaseMessage, TurnSectionsCacheEntry>();
+
+function isTurnCacheValid(
+	entry: TurnSectionsCacheEntry,
+	message: DatabaseMessage,
+	toolMessages: DatabaseMessage[]
+): boolean {
+	if (
+		entry.content !== message.content ||
+		entry.reasoningContent !== message.reasoningContent ||
+		entry.toolCalls !== message.toolCalls ||
+		entry.extra !== message.extra
+	) {
+		return false;
+	}
+
+	if (entry.toolMessages.length !== toolMessages.length) return false;
+
+	for (let i = 0; i < toolMessages.length; i++) {
+		if (entry.toolMessages[i] !== toolMessages[i]) return false;
+
+		if (entry.toolMessageContents[i] !== toolMessages[i].content) return false;
+
+		if (entry.toolMessageExtras[i] !== toolMessages[i].extra) return false;
+	}
+
+	return true;
+}
+
+/**
+ * deriveSingleTurnSections with structural reuse for completed turns.
+ *
+ * deriveAgenticSections runs in a $derived invalidated per streamed chunk, but
+ * only the last turn actually changes. Messages mutate in place and are never
+ * replaced, so a WeakMap keyed by the turn's assistant message plus reference
+ * checks on every field deriveSingleTurnSections reads detects any change. A
+ * cache hit also returns the same section objects, keeping downstream props
+ * stable so tool blocks skip their per-chunk re-derive. The streaming turn
+ * recomputes uncached on every chunk.
+ */
+function deriveTurnSections(
+	message: DatabaseMessage,
+	toolMessages: DatabaseMessage[],
+	streamingToolCalls: ApiChatCompletionToolCall[],
+	isStreaming: boolean
+): AgenticSection[] {
+	if (isStreaming || streamingToolCalls.length > 0) {
+		return deriveSingleTurnSections(message, toolMessages, streamingToolCalls, isStreaming);
+	}
+
+	const cached = turnSectionsCache.get(message);
+
+	if (cached && isTurnCacheValid(cached, message, toolMessages)) {
+		return cached.sections;
+	}
+
+	const sections = deriveSingleTurnSections(message, toolMessages, [], false);
+
+	turnSectionsCache.set(message, {
+		content: message.content,
+		extra: message.extra,
+		reasoningContent: message.reasoningContent,
+		sections,
+		toolCalls: message.toolCalls,
+		toolMessageContents: toolMessages.map((tm) => tm.content),
+		toolMessageExtras: toolMessages.map((tm) => tm.extra),
+		toolMessages
+	});
+
+	return sections;
+}
+
 /**
  * Derives display sections from structured message data.
  *
@@ -132,13 +215,13 @@ export function deriveAgenticSections(
 	const hasAssistantContinuations = toolMessages.some((m) => m.role === MessageRole.ASSISTANT);

 	if (!hasAssistantContinuations) {
-		return deriveSingleTurnSections(message, toolMessages, streamingToolCalls, isStreaming);
+		return deriveTurnSections(message, toolMessages, streamingToolCalls, isStreaming);
 	}

 	const sections: AgenticSection[] = [];
 	const firstTurnToolMsgs = collectToolMessages(toolMessages, 0);

-	sections.push(...deriveSingleTurnSections(message, firstTurnToolMsgs));
+	sections.push(...deriveTurnSections(message, firstTurnToolMsgs, [], false));

 	let i = firstTurnToolMsgs.length;

@@ -150,7 +233,7 @@ export function deriveAgenticSections(
 			const isLastTurn = i + 1 + turnToolMsgs.length >= toolMessages.length;

 			sections.push(
-				...deriveSingleTurnSections(
+				...deriveTurnSections(
 					msg,
 					turnToolMsgs,
 					isLastTurn ? streamingToolCalls : [],
diff --git a/tools/ui/src/lib/utils/branching.ts b/tools/ui/src/lib/utils/branching.ts
index 6c2c895cb..43d33d424 100644
--- a/tools/ui/src/lib/utils/branching.ts
+++ b/tools/ui/src/lib/utils/branching.ts
@@ -105,18 +105,34 @@ export function filterByLeafNodeId(
  */
 function findLeafNodeInMap(
 	nodeMap: ReadonlyMap<string, DatabaseMessage>,
-	messageId: string
+	messageId: string,
+	leafCache?: Map<string, string>
 ): string {
+	const path: string[] = [];
+
 	let currentNode: DatabaseMessage | undefined = nodeMap.get(messageId);

 	while (currentNode && currentNode.children.length > 0) {
 		// Follow the last child (most recent branch)
+		const cached = leafCache?.get(currentNode.id);
+
+		if (cached !== undefined) {
+			for (const id of path) leafCache?.set(id, cached);
+
+			return cached;
+		}
+
+		path.push(currentNode.id);
 		const lastChildId = currentNode.children[currentNode.children.length - 1];

 		currentNode = nodeMap.get(lastChildId);
 	}

-	return currentNode?.id ?? messageId;
+	const leafId = currentNode?.id ?? messageId;
+
+	for (const id of path) leafCache?.set(id, leafId);
+
+	return leafId;
 }

 /**
@@ -176,7 +192,8 @@ export function findDescendantMessages(
  */
 export function getMessageSiblings(
 	nodeMap: ReadonlyMap<string, DatabaseMessage>,
-	messageId: string
+	messageId: string,
+	leafCache?: Map<string, string>
 ): ChatMessageSiblingInfo | null {
 	const message = nodeMap.get(messageId);

@@ -212,7 +229,7 @@ export function getMessageSiblings(
 	// Convert sibling message IDs to their corresponding leaf node IDs
 	// This allows navigation between different conversation branches
 	const siblingLeafIds = siblingIds.map((siblingId: string) =>
-		findLeafNodeInMap(nodeMap, siblingId)
+		findLeafNodeInMap(nodeMap, siblingId, leafCache)
 	);
 	// Find current message's position among siblings
 	const currentIndex = siblingIds.indexOf(messageId);
@@ -236,9 +253,12 @@ export function buildSiblingInfoMap(
 ): Map<string, ChatMessageSiblingInfo> {
 	const nodeMap = new Map(messages.map((msg) => [msg.id, msg] as const));
 	const siblingMap = new Map<string, ChatMessageSiblingInfo>();
+	// Leaf walks repeat along the same child chains for every message; memoize
+	// them per build so each edge is walked once instead of O(messages^2)
+	const leafCache = new Map<string, string>();

 	for (const msg of messages) {
-		const info = getMessageSiblings(nodeMap, msg.id);
+		const info = getMessageSiblings(nodeMap, msg.id, leafCache);

 		if (info) {
 			siblingMap.set(msg.id, info);
diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts
index 079cdc871..721618c48 100644
--- a/tools/ui/src/lib/utils/index.ts
+++ b/tools/ui/src/lib/utils/index.ts
@@ -285,7 +285,8 @@ export {
 	extractSearchResults,
 	extractSearchQuery,
 	faviconForUrl,
-	isWebSearchToolName
+	isWebSearchToolName,
+	looksLikeSearchResult
 } from './search-results';

 // Cache utilities
diff --git a/tools/ui/src/lib/utils/parse-exec-shell-error.ts b/tools/ui/src/lib/utils/parse-exec-shell-error.ts
index 42d2ee254..a7b2eb5c8 100644
--- a/tools/ui/src/lib/utils/parse-exec-shell-error.ts
+++ b/tools/ui/src/lib/utils/parse-exec-shell-error.ts
@@ -3,8 +3,14 @@ export function parseExecShellCommandError(
 ): string | undefined {
 	if (!toolResultString) return undefined;

+	// Exec results are usually large plain-text stdout; only a JSON object
+	// root can carry an error field, so skip the parse otherwise
+	const trimmed = toolResultString.trimStart();
+
+	if (trimmed[0] !== '{') return undefined;
+
 	try {
-		const parsed: unknown = JSON.parse(toolResultString);
+		const parsed: unknown = JSON.parse(trimmed);

 		if (
 			parsed &&
diff --git a/tools/ui/src/lib/utils/parse-exec-shell-status.ts b/tools/ui/src/lib/utils/parse-exec-shell-status.ts
index 1f7ec557e..71dd110bd 100644
--- a/tools/ui/src/lib/utils/parse-exec-shell-status.ts
+++ b/tools/ui/src/lib/utils/parse-exec-shell-status.ts
@@ -15,15 +15,18 @@ export interface ExecShellExitStatus {
 }

 // Anchor to the absolute end so intermediate "[exit code: N]" string content
-// (e.g. a shell echo) doesn't false-positive.
+// (e.g. a shell echo) doesn't false-positive. The marker is at most ~50 chars
+// with the timed-out suffix, so matching a tail slice keeps the cost constant
+// for megabyte exec outputs instead of scanning the whole blob.
 const EXIT_CODE_TAIL_REGEX = /\[exit code: (-?\d+)\](?: \[exit due to timed out\])?\s*$/;
+const EXIT_CODE_TAIL_SCAN = 128;

 export function parseExecShellCommandExitStatus(
 	toolResultString: string | undefined
 ): ExecShellExitStatus | undefined {
 	if (!toolResultString) return undefined;

-	const match = toolResultString.match(EXIT_CODE_TAIL_REGEX);
+	const match = toolResultString.slice(-EXIT_CODE_TAIL_SCAN).match(EXIT_CODE_TAIL_REGEX);

 	if (!match) return undefined;

diff --git a/tools/ui/src/lib/utils/search-results.ts b/tools/ui/src/lib/utils/search-results.ts
index facf7766d..0fe861d94 100644
--- a/tools/ui/src/lib/utils/search-results.ts
+++ b/tools/ui/src/lib/utils/search-results.ts
@@ -156,6 +156,20 @@ function parseChunk(chunk: string): SearchResult | null {
 	return result;
 }

+const EMPTY_SEARCH_RESULTS: SearchResult[] = [];
+
+/**
+ * Cheap prefilter for the wire format: a parseable result needs both a
+ * `Title:` and a `URL:` field line, so a blob missing either substring can
+ * never yield a result. Two substring scans cost far less than the
+ * line-split parse for the megabyte tool results exec and file tools emit.
+ */
+export function looksLikeSearchResult(text: string | undefined | null): boolean {
+	if (!text) return false;
+
+	return text.includes('Title:') && text.includes('URL:');
+}
+
 /** Bounded cache for extractSearchResults results. */
 const SEARCH_RESULTS_CACHE_MAX_SIZE = 32;
 const searchResultsCache = new Map<string, SearchResult[]>();
@@ -168,7 +182,7 @@ const searchResultsCache = new Map<string, SearchResult[]>();
  * tool result strings.
  */
 export function extractSearchResults(text: string | undefined | null): SearchResult[] {
-	if (!text) return [];
+	if (!text || !looksLikeSearchResult(text)) return EMPTY_SEARCH_RESULTS;

 	const cached = searchResultsCache.get(text);

diff --git a/tools/ui/src/lib/utils/tool-call-meta.ts b/tools/ui/src/lib/utils/tool-call-meta.ts
index b64bca786..2c035446d 100644
--- a/tools/ui/src/lib/utils/tool-call-meta.ts
+++ b/tools/ui/src/lib/utils/tool-call-meta.ts
@@ -4,6 +4,8 @@
 // Each tool needs to surface fields like `error`, `result`, `bytes`,
 // `edits_applied` without repeating the try/JSON.parse/object guard inline.

+import { JSON_OBJECT_OPEN } from '$lib/constants';
+
 /**
  * Parse a tool-result blob into a JSON object, or `null` if it isn't
  * one. Returns null for:
@@ -16,8 +18,14 @@ export function tryParseToolResultObject(
 ): Record<string, unknown> | null {
 	if (!toolResultString) return null;

+	// Tool results are usually large plain text (file contents, stdout); only
+	// a JSON object root can carry fields, so skip the parse otherwise
+	const trimmed = toolResultString.trimStart();
+
+	if (trimmed[0] !== JSON_OBJECT_OPEN) return null;
+
 	try {
-		const parsed: unknown = JSON.parse(toolResultString);
+		const parsed: unknown = JSON.parse(trimmed);

 		if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
 			return parsed as Record<string, unknown>;
diff --git a/tools/ui/src/routes/(chat)/+page.svelte b/tools/ui/src/routes/(chat)/+page.svelte
index 08a6b11ad..53975d7b3 100644
--- a/tools/ui/src/routes/(chat)/+page.svelte
+++ b/tools/ui/src/routes/(chat)/+page.svelte
@@ -3,7 +3,7 @@
 	import { page } from '$app/state';
 	import { DialogModelNotAvailable } from '$lib/components/app';
 	import { APP_NAME, URL_PARAMS } from '$lib/constants';
-	import { chatStore, conversationsStore, modelsStore, serverStore } from '$lib/stores';
+	import { conversationsStore, modelsStore, serverStore } from '$lib/stores';
 	import { onMount } from 'svelte';

 	let qParam = $derived(page.url.searchParams.get(URL_PARAMS.QUERY));
@@ -77,7 +77,6 @@
 		}

 		conversationsStore.clearActiveConversation();
-		chatStore.clearUIState();

 		await modelsStore.fetch();

diff --git a/tools/ui/tests/unit/agentic-sections.test.ts b/tools/ui/tests/unit/agentic-sections.test.ts
index 4096a1710..fdb3b2217 100644
--- a/tools/ui/tests/unit/agentic-sections.test.ts
+++ b/tools/ui/tests/unit/agentic-sections.test.ts
@@ -290,3 +290,114 @@ describe('hasAgenticContent', () => {
 		expect(hasAgenticContent(msg)).toBe(false);
 	});
 });
+
+// The turn-section cache: completed turns are immutable, so repeated
+// derivations return the same section objects - which is what keeps tool
+// block props stable while another turn streams. Every field the cache
+// compares must invalidate it; a miss here renders stale content.
+
+describe('completed turn section reuse', () => {
+	const toolCallsJson = JSON.stringify([
+		{ function: { arguments: '{"path":"/a"}', name: 'test' }, id: 'call_1', type: 'function' }
+	]);
+
+	function makeSession() {
+		return {
+			anchor: makeAssistant({
+				content: 'answer',
+				reasoningContent: 'thinking',
+				toolCalls: toolCallsJson
+			}),
+			tools: [makeToolMsg({ content: 'tool result', extra: [{ type: 'file' } as never] })]
+		};
+	}
+
+	it('returns the same section objects for unchanged inputs', () => {
+		const { anchor, tools } = makeSession();
+		const first = deriveAgenticSections(anchor, tools, [], false);
+		const second = deriveAgenticSections(anchor, tools, [], false);
+
+		expect(second[0]).toBe(first[0]);
+		expect(second[1]).toBe(first[1]);
+	});
+
+	it('recomputes when the assistant content changes', () => {
+		const { anchor, tools } = makeSession();
+		const first = deriveAgenticSections(anchor, tools, [], false);
+
+		anchor.content = 'edited';
+		const second = deriveAgenticSections(anchor, tools, [], false);
+
+		expect(second).not.toBe(first);
+		expect(second.some((s) => s.type === AgenticSectionType.TEXT && s.content === 'edited')).toBe(
+			true
+		);
+	});
+
+	it('recomputes when reasoning content changes', () => {
+		const { anchor, tools } = makeSession();
+		const first = deriveAgenticSections(anchor, tools, [], false);
+
+		anchor.reasoningContent = 'new thinking';
+		const second = deriveAgenticSections(anchor, tools, [], false);
+
+		expect(second).not.toBe(first);
+	});
+
+	it('recomputes when toolCalls change', () => {
+		const { anchor, tools } = makeSession();
+		const first = deriveAgenticSections(anchor, tools, [], false);
+
+		anchor.toolCalls = '[]';
+		const second = deriveAgenticSections(anchor, tools, [], false);
+
+		expect(second).not.toBe(first);
+	});
+
+	it('recomputes when a tool result or its extras change', () => {
+		const { anchor, tools } = makeSession();
+		const first = deriveAgenticSections(anchor, tools, [], false);
+
+		tools[0].content = 'new tool result';
+		expect(deriveAgenticSections(anchor, tools, [], false)).not.toBe(first);
+
+		const firstAfterContent = deriveAgenticSections(anchor, tools, [], false);
+
+		tools[0].extra = [{ type: 'image' } as never];
+		expect(deriveAgenticSections(anchor, tools, [], false)).not.toBe(firstAfterContent);
+	});
+
+	it('never reuses the streaming turn', () => {
+		const { anchor, tools } = makeSession();
+		const first = deriveAgenticSections(anchor, tools, [], true);
+		const second = deriveAgenticSections(anchor, tools, [], true);
+
+		expect(second).not.toBe(first);
+	});
+
+	it('keeps completed turns stable while the last turn streams', () => {
+		const anchor = makeAssistant({
+			content: 'turn one',
+			id: 'ast-1',
+			toolCalls: JSON.stringify([
+				{ function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
+			])
+		});
+		const continuation = makeAssistant({ content: 'turn two', id: 'ast-2' });
+		const tools = [
+			makeToolMsg({ content: 'r1', id: 'tool-1', toolCallId: 'call_1' }),
+			continuation,
+			makeToolMsg({ content: 'r2', id: 'tool-2', toolCallId: 'call_2' })
+		];
+		const first = deriveAgenticSections(anchor, tools, [], true);
+		const second = deriveAgenticSections(anchor, tools, [], true);
+
+		// turn one is complete: identical section objects across derivations
+		expect(second.slice(0, 2)).toEqual(first.slice(0, 2));
+		expect(second[0]).toBe(first[0]);
+		expect(second[1]).toBe(first[1]);
+
+		// the streaming last turn recomputed: fresh section objects
+		expect(second[second.length - 1]).not.toBe(first[first.length - 1]);
+	});
+});
diff --git a/tools/ui/tests/unit/branching.test.ts b/tools/ui/tests/unit/branching.test.ts
new file mode 100644
index 000000000..8a752ae2f
--- /dev/null
+++ b/tools/ui/tests/unit/branching.test.ts
@@ -0,0 +1,95 @@
+// Sibling-info correctness for buildSiblingInfoMap, including the memoized
+// leaf resolution. A wrong leaf id here breaks branch navigation, so the
+// deep-chain and multi-branch cases below pin the resolution down.
+
+import { MessageRole, MessageType } from '$lib/enums';
+import type { DatabaseMessage } from '$lib/types/database';
+import { buildSiblingInfoMap, findLeafNode } from '$lib/utils/branching';
+import { describe, expect, it } from 'vitest';
+
+function msg(id: string, parent: string | null, children: string[] = []): DatabaseMessage {
+	return {
+		children,
+		content: '',
+		convId: 'c1',
+		id,
+		parent,
+		role: MessageRole.USER,
+		timestamp: 0,
+		type: MessageType.TEXT
+	} as DatabaseMessage;
+}
+
+/** root -> m1 -> ... -> m depth, each node with a single child. */
+function linearChain(depth: number): DatabaseMessage[] {
+	const messages = [msg('m0', null, ['m1'])];
+
+	for (let i = 1; i <= depth; i++) {
+		messages.push(msg(`m${i}`, `m${i - 1}`, i < depth ? [`m${i + 1}`] : []));
+	}
+
+	return messages;
+}
+
+describe('buildSiblingInfoMap', () => {
+	it('resolves the deepest leaf for every node of a long single chain', () => {
+		const messages = linearChain(50);
+		const map = buildSiblingInfoMap(messages);
+		const leafId = messages[messages.length - 1].id;
+
+		// every non-root message of the chain is an only child, and its
+		// navigation target is the chain's deepest leaf
+		for (const m of messages.slice(1)) {
+			const info = map.get(m.id);
+
+			expect(info?.totalSiblings).toBe(1);
+			expect(info?.siblingIds).toEqual([leafId]);
+		}
+	});
+
+	it('reports sibling position and leaf targets on a branched tree', () => {
+		// m0 -> m1, m4 ; m1 -> m2 ; m2 -> m3, m6 ; m4 -> m5
+		const root = msg('m0', null, ['m1', 'm4']);
+		const m1 = msg('m1', 'm0', ['m2']);
+		const m2 = msg('m2', 'm1', ['m3', 'm6']);
+		const m3 = msg('m3', 'm2');
+		const m4 = msg('m4', 'm0', ['m5']);
+		const m5 = msg('m5', 'm4');
+		const m6 = msg('m6', 'm2');
+		const map = buildSiblingInfoMap([root, m1, m2, m3, m4, m5, m6]);
+
+		// m1 and m4 share the root as parent; their nav targets are the
+		// leaves of their subtrees ( m6 for the first branch, m5 for the second )
+		expect(map.get(m1.id)).toMatchObject({
+			currentIndex: 0,
+			siblingIds: [m6.id, m5.id],
+			totalSiblings: 2
+		});
+		expect(map.get(m4.id)).toMatchObject({
+			currentIndex: 1,
+			siblingIds: [m6.id, m5.id],
+			totalSiblings: 2
+		});
+
+		// m3 and m6 are siblings under m2; both are leaves
+		expect(map.get(m3.id)?.siblingIds).toEqual([m3.id, m6.id]);
+		expect(map.get(m6.id)?.currentIndex).toBe(1);
+
+		// the root has no parent and reports itself
+		expect(map.get(root.id)).toMatchObject({
+			currentIndex: 0,
+			siblingIds: [root.id],
+			totalSiblings: 1
+		});
+	});
+
+	it('agrees with findLeafNode for arbitrary nodes', () => {
+		const messages = linearChain(20);
+		const leafId = messages[messages.length - 1].id;
+
+		// every node of the chain resolves to the deepest leaf
+		for (const m of messages) {
+			expect(findLeafNode(messages, m.id), `leaf of ${m.id}`).toBe(leafId);
+		}
+	});
+});
diff --git a/tools/ui/tests/unit/conversations-store.test.ts b/tools/ui/tests/unit/conversations-store.test.ts
new file mode 100644
index 000000000..e06546597
--- /dev/null
+++ b/tools/ui/tests/unit/conversations-store.test.ts
@@ -0,0 +1,90 @@
+// Field updates to the active conversation must keep the object identity
+// stable: effects that track the identity ( the chat screen's sibling-info
+// refresh ) refire on every identity change, which used to trigger a full
+// message refetch on every send and tool result.
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('$lib/services/database.service', () => ({
+	DatabaseService: {
+		getConversation: vi.fn(),
+		getConversationMessages: vi.fn(),
+		updateConversation: vi.fn(),
+		updateCurrentNode: vi.fn()
+	}
+}));
+
+import { DatabaseService } from '$lib/services/database.service';
+import { conversationsStore } from '$lib/stores/conversations/index.svelte';
+import type { DatabaseConversation, DatabaseMessage } from '$lib/types/database';
+
+const getConversationMock = vi.mocked(DatabaseService.getConversation);
+const getMessagesMock = vi.mocked(DatabaseService.getConversationMessages);
+const updateCurrentNodeMock = vi.mocked(DatabaseService.updateCurrentNode);
+
+function makeConversation(overrides: Partial<DatabaseConversation> = {}): DatabaseConversation {
+	return {
+		currNode: 'node-1',
+		id: 'conv-1',
+		lastModified: 1000,
+		name: 'conversation',
+		...overrides
+	};
+}
+
+async function loadActive(conversation: DatabaseConversation, messages: DatabaseMessage[]) {
+	getConversationMock.mockResolvedValue(conversation);
+	getMessagesMock.mockResolvedValue(messages);
+
+	expect(await conversationsStore.loadConversation(conversation.id)).toBe(true);
+}
+
+beforeEach(() => {
+	getConversationMock.mockReset();
+	getMessagesMock.mockReset();
+	updateCurrentNodeMock.mockReset();
+	updateCurrentNodeMock.mockResolvedValue(undefined);
+	vi.mocked(DatabaseService.updateConversation).mockReset();
+	vi.mocked(DatabaseService.updateConversation).mockResolvedValue(undefined);
+});
+
+describe('active conversation identity', () => {
+	it('hands the load read off exactly once', async () => {
+		await loadActive(makeConversation(), []);
+
+		expect(conversationsStore.consumeLastLoadedMessages('conv-1')).toEqual([]);
+		// a second consume is a miss: branch actions must fall back to a refetch
+		expect(conversationsStore.consumeLastLoadedMessages('conv-1')).toBeNull();
+	});
+
+	it('writes currNode in place on updateCurrentNode', async () => {
+		await loadActive(makeConversation(), []);
+		const before = conversationsStore.activeConversation;
+
+		await conversationsStore.updateCurrentNode('node-2');
+
+		expect(conversationsStore.activeConversation).toBe(before);
+		expect(conversationsStore.activeConversation?.currNode).toBe('node-2');
+	});
+
+	it('writes renamed and pinned fields in place on applyConversationUpdate', async () => {
+		await loadActive(makeConversation(), []);
+		const before = conversationsStore.activeConversation;
+
+		conversationsStore.applyConversationUpdate('conv-1', { name: 'renamed', pinned: true });
+
+		expect(conversationsStore.activeConversation).toBe(before);
+		expect(conversationsStore.activeConversation?.name).toBe('renamed');
+		expect(conversationsStore.activeConversation?.pinned).toBe(true);
+	});
+
+	it('writes lastModified in place on updateConversationTimestamp', async () => {
+		await loadActive(makeConversation(), []);
+		const before = conversationsStore.activeConversation;
+
+		conversationsStore.updateConversationTimestamp('conv-1');
+
+		expect(conversationsStore.activeConversation).toBe(before);
+		expect(conversationsStore.activeConversation?.lastModified).toBeGreaterThan(1000);
+	});
+});
diff --git a/tools/ui/tests/unit/parse-exec-shell-status.test.ts b/tools/ui/tests/unit/parse-exec-shell-status.test.ts
index ed499d078..7e22bf9ee 100644
--- a/tools/ui/tests/unit/parse-exec-shell-status.test.ts
+++ b/tools/ui/tests/unit/parse-exec-shell-status.test.ts
@@ -71,3 +71,21 @@ describe('isExitCodeSummaryLine', () => {
 		expect(isExitCodeSummaryLine('[exit code: 7]', undefined)).toBe(false);
 	});
 });
+
+describe('parseExecShellCommandExitStatus tail scan', () => {
+	it('finds the marker at the end of a blob larger than the tail window', () => {
+		// the parser matches only the last ~128 chars; a marker past that
+		// window must still parse, and an earlier fake must not match
+		const blob = `${'the shell prints [exit code: 1] mid-stream\n'.repeat(2000)}[exit code: 0]`;
+		const status = parseExecShellCommandExitStatus(blob);
+
+		expect(status?.code).toBe(0);
+		expect(status?.timedOut).toBe(false);
+	});
+
+	it('keeps rejecting markers that are not at the absolute end', () => {
+		const blob = `${'stdout\n'.repeat(2000)}[exit code: 0]\nsome trailing log line`;
+
+		expect(parseExecShellCommandExitStatus(blob)).toBeUndefined();
+	});
+});
diff --git a/tools/ui/tests/unit/search-results.test.ts b/tools/ui/tests/unit/search-results.test.ts
index c168dec25..561ab935a 100644
--- a/tools/ui/tests/unit/search-results.test.ts
+++ b/tools/ui/tests/unit/search-results.test.ts
@@ -2,7 +2,8 @@ import {
 	extractSearchQuery,
 	extractSearchResults,
 	faviconForUrl,
-	isWebSearchToolName
+	isWebSearchToolName,
+	looksLikeSearchResult
 } from '$lib/utils/search-results';
 import { describe, expect, it } from 'vitest';

@@ -119,3 +120,27 @@ describe('isWebSearchToolName', () => {
 		expect(isWebSearchToolName('exec_shell_command')).toBe(false);
 	});
 });
+
+describe('extractSearchResults prefilter', () => {
+	it('returns the shared empty array for blobs without the wire format', () => {
+		// exec/file tool results never carry Title:/URL: field lines; the
+		// cheap prefilter must skip the line-split parse for them
+		const stdout = `${'make[1]: entering directory\n'.repeat(5000)}`;
+
+		expect(extractSearchResults(stdout)).toEqual([]);
+	});
+
+	it('returns an empty result when only one required field is present', () => {
+		expect(extractSearchResults('URL: https://example.com')).toEqual([]);
+		expect(extractSearchResults('Title: only a title')).toEqual([]);
+	});
+});
+
+describe('looksLikeSearchResult', () => {
+	it('requires both Title and URL field markers', () => {
+		expect(looksLikeSearchResult('Title: a\nURL: https://b')).toBe(true);
+		expect(looksLikeSearchResult('URL: https://b')).toBe(false);
+		expect(looksLikeSearchResult('plain stdout')).toBe(false);
+		expect(looksLikeSearchResult(undefined)).toBe(false);
+	});
+});
diff --git a/tools/ui/tests/unit/tool-call-meta.test.ts b/tools/ui/tests/unit/tool-call-meta.test.ts
index bb28e3830..f94d2279f 100644
--- a/tools/ui/tests/unit/tool-call-meta.test.ts
+++ b/tools/ui/tests/unit/tool-call-meta.test.ts
@@ -28,3 +28,15 @@ describe('tryParseToolResultObject', () => {
 		expect(tryParseToolResultObject('{bad')).toBeNull();
 	});
 });
+
+describe('tryParseToolResultObject gating', () => {
+	it('parses JSON objects that start after leading whitespace', () => {
+		expect(tryParseToolResultObject('\n  {"result":"ok"}')).toEqual({ result: 'ok' });
+	});
+
+	it('skips the parse for large plain-text results', () => {
+		// most tool results are file contents or stdout; the gate avoids a
+		// doomed JSON.parse over the whole blob
+		expect(tryParseToolResultObject(`${'stdout line\n'.repeat(2000)}`)).toBeNull();
+	});
+});
diff --git a/tools/ui/tests/unit/tool-calls.test.ts b/tools/ui/tests/unit/tool-calls.test.ts
index f84a2405e..a2274f9d2 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,10 +10,10 @@ import { parseReadFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMes
 import { parseRunJavascriptMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript';
 import {
 	parseWriteFileMeta,
-	type WriteFileMeta
+	parseWriteFileTitleMeta
 } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file';
 import { AgenticSectionType, BuiltInTool } from '$lib/enums';
-import type { AgenticSection } from '$lib/types';
+import type { AgenticSection, WriteFileMeta } from '$lib/types';
 import { abbreviateHome, formatCwdMessage, lastPathSegment, parseCwdMessage } from '$lib/utils';
 import { describe, expect, it } from 'vitest';

@@ -223,6 +226,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(