// syCode native UI extension — vendored from pi-claude-code-ui 1.0.83
// (FammasMaz/pi-cc-tools, MIT — see NOTICE). Adapted: Symbol namespace
// `pi-claude-style-tools:` → `sycode-ui:`, user strings say syCode theme.
// Re-sync policy: AGENTS.md §10. Do not `pi install` upstream alongside this
// (shared Symbol keys would double-register renderers).
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { readFile as readFileAsync } from "node:fs/promises";
import { basename, dirname, extname, relative, resolve } from "node:path";

import type {
	BashToolDetails,
	EditToolDetails,
	ExtensionAPI,
	GrepToolDetails,
	ReadToolDetails,
	Theme,
} from "@earendil-works/pi-coding-agent";
import {
	AssistantMessageComponent,
	CustomMessageComponent,
	ToolExecutionComponent,
	UserMessageComponent,
	keyHint,
	keyText,
	rawKeyHint,
	createBashTool,
	createEditTool,
	createFindTool,
	createGrepTool,
	createLsTool,
	createReadTool,
	createWriteTool,
} from "@earendil-works/pi-coding-agent";
import {
	Box,
	Container,
	deleteAllKittyImages,
	getCapabilities,
	getImageDimensions,
	imageFallback,
	Markdown,
	ProcessTerminal,
	Spacer,
	Text,
	truncateToWidth,
	visibleWidth,
	wrapTextWithAnsi,
} from "@earendil-works/pi-tui";

import * as Diff from "diff";
import type { BundledLanguage, BundledTheme } from "shiki";

import {
	buildBashCommandPresentation,
	buildBashPreview,
	describeBashSource,
	formatBashDuration,
	getLastBashOutputLine,
} from "./bash-command.js";

const RESET = "\x1b[0m";
const TRANSPARENT_BG = "\x1b[49m";
const TRANSPARENT_RESET = `${RESET}${TRANSPARENT_BG}`;

// User/code box borders and thinking/thought text: branch color + OUTLINE_CHROME_BRIGHTEN.
// Branch ├└│ stay at `currentToolBranchAnsi` (see syncOutlineChromeFromBranch).
let BORDER_COLOR = "\x1b[38;5;238m";
let CODE_BLOCK_LANG_FG = "\x1b[38;2;95;95;95m";
const CHROME_ITALIC = "\x1b[3m";
/** Lift outline chrome above branch connectors so boxes and thought read brighter. */
const OUTLINE_CHROME_BRIGHTEN = 64;
const ANSI_RE = /\x1b\[[0-9;]*m/g;
const ANSI_PRESENT_RE = /\x1b\[[0-9;]*m/;
const PATCH_FLAG = Symbol.for("sycode-ui:patched-container-render");
const TOOL_RENDER_CACHE = Symbol.for("sycode-ui:tool-render-cache");
const COMPONENT_PARENT = Symbol.for("sycode-ui:component-parent");
const PARENT_TRACKING_PATCH_FLAG = Symbol.for("sycode-ui:patched-parent-tracking");
const TOOL_CACHE_PATCH_FLAG = Symbol.for("sycode-ui:patched-tool-cache-invalidation");
const TOOL_IMAGE_EXPAND_PATCH_FLAG = Symbol.for("sycode-ui:patched-read-image-expansion");
const CUSTOM_MESSAGE_PATCH_FLAG = Symbol.for("sycode-ui:patched-custom-message-render");
const USER_MESSAGE_PATCH_FLAG = Symbol.for("sycode-ui:patched-user-message-render");
const UI_NOTIFY_PATCH_FLAG = Symbol.for("sycode-ui:patched-ui-notifications-v2");
const WRAP_MARK = "\uE000";
const CLIP_MARK = "\uE001";
const TRAILING_MARK = "  · ";
const KITTY_IMAGE_PREFIX = "\x1b_G";
const ITERM2_IMAGE_PREFIX = "\x1b]1337;File=";

let toolBackgroundMode: "default" | "transparent" | "outlines" = "outlines";

interface SettingsFile {
	toolBackground?: "default" | "transparent" | "outlines" | "border";
	readOutputMode?: "hidden" | "summary" | "preview";
	searchOutputMode?: "hidden" | "count" | "preview";
	mcpOutputMode?: "hidden" | "summary" | "preview";
	previewLines?: number;
	expandedPreviewMaxLines?: number;
	extraExpandedPreviewMaxLines?: number;
	extraToolOutputExpanded?: boolean;
	groupToolCalls?: boolean;
	bashOutputMode?: "opencode" | "summary" | "preview";
	bashCollapsedLines?: number;
	/** Verbatim script lines shown while bash is running or after failure. Defaults to 8. */
	bashCommandPreviewLines?: number;
	/** Show a small live output preview while tools are still running. Defaults to true. */
	liveToolPreview?: boolean;
	/** Number of live output lines to show while collapsed. Defaults to 5. */
	liveToolPreviewLines?: number;
	showTruncationHints?: boolean;
	diffCollapsedLines?: number;
	diffTheme?: string;
	diffColors?: Record<string, string>;
	/**
	 * When true (default), derive borders, dim text, branch rules, and diff
	 * accents from the active syCode theme via `theme.getFgAnsi`/`getBgAnsi`.
	 * Explicit `diffTheme` / `diffColors` always win over theme-derived
	 * defaults so users keep full control.
	 */
	themeAdaptive?: boolean;
	/**
	 * Theme color key used for the spinner verb (e.g. "Cooking…"). Defaults
	 * to "accent". Useful when the active theme's accent is overloaded for
	 * borders, headings, or bash mode and the verb should pop differently.
	 * Valid keys are any of the syCode theme `ThemeColor` names (e.g. accent,
	 * borderAccent, success, warning, mdHeading, thinkingMedium, bashMode).
	 */
	spinnerVerbColor?: string;
	/**
	 * Theme color key used for the spinner status suffix (the parenthesized
	 * "(thinking · ↓ 10 tokens · 2s)" trailer). Defaults to "muted".
	 */
	spinnerStatusColor?: string;
	/**
	 * Thinking display mode. `live` (default): only the currently-streaming
	 * thinking is expanded; finished thinking collapses to a one-line
	 * `Thought for Xs` row (Ctrl+O still expands it). `full`: thinking always
	 * renders expanded, like stock pi.
	 */
	thinkingMode?: "live" | "full";
	/** Gray level 0–255 for ├ └ │ when branch color mode is `fixed`. */
	toolBranchRgbGray?: number;
	/** `fixed` (default): rgb gray 72, theme-independent. `theme`: dim → muted → borderMuted. */
	toolBranchColorMode?: "theme" | "fixed";
}

let _settingsCache: { value: SettingsFile; timestamp: number } | null = null;
const SETTINGS_CACHE_TTL_MS = 5_000;

function readSettings(): SettingsFile {
	const now = Date.now();
	if (_settingsCache && now - _settingsCache.timestamp < SETTINGS_CACHE_TTL_MS) {
		return _settingsCache.value;
	}
	const cwdPath = `${process.cwd()}/.pi/settings.json`;
	const homePath = `${process.env.HOME ?? ""}/.pi/settings.json`;
	const merged: SettingsFile = {};
	for (const path of [cwdPath, homePath]) {
		try {
			if (!path || !existsSync(path)) continue;
			const raw = JSON.parse(readFileSync(path, "utf8"));
			if (raw && typeof raw === "object") Object.assign(merged, raw as SettingsFile);
		} catch {
			// ignore invalid settings files
		}
	}
	_settingsCache = { value: merged, timestamp: now };
	return merged;
}

// Cross-extension bust signal for spinner.ts — it watches this counter on
// globalThis and invalidates its settings cache when it changes. Lets
// /cc-spinner edits take effect on the next 250ms spinner tick instead of
// waiting for the file-stat TTL.
const SPINNER_BUST_KEY = Symbol.for("sycode-ui:spinner-settings-bust");
function bustSpinnerSettingsCache(): void {
	const current = ((globalThis as any)[SPINNER_BUST_KEY] as number | undefined) ?? 0;
	(globalThis as any)[SPINNER_BUST_KEY] = current + 1;
}

function writeSettingsKey(key: string, value: unknown): void {
	_settingsCache = null; // invalidate cache on write
	const home = process.env.HOME ?? "";
	if (!home) return;
	const dir = `${home}/.pi`;
	const path = `${dir}/settings.json`;
	let settings: Record<string, unknown> = {};
	try {
		if (existsSync(path)) settings = JSON.parse(readFileSync(path, "utf8")) ?? {};
	} catch { /* start fresh */ }
	if (value === undefined) {
		delete settings[key];
	} else {
		settings[key] = value;
	}
	try {
		mkdirSync(dir, { recursive: true });
		writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
	} catch { /* best effort */ }
}

let toolBackgroundOverride: "default" | "transparent" | "outlines" | null = null;

function syncToolBackgroundMode(): void {
	if (toolBackgroundOverride) {
		toolBackgroundMode = toolBackgroundOverride;
		return;
	}
	const settings = readSettings();
	// Backward compat: "border" was renamed to "outlines"
	const raw = settings.toolBackground === "border" ? "outlines" : settings.toolBackground;
	toolBackgroundMode = raw ?? "outlines";
}

function setThemeBg(theme: unknown, key: string, value: string): void {
	const themeAny = theme as any;
	if (themeAny.bgColors instanceof Map) {
		themeAny.bgColors.set(key, value);
	} else if (themeAny.bgColors && typeof themeAny.bgColors === "object") {
		themeAny.bgColors[key] = value;
	}
}

const PI_GLOBAL_THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme");

function getGlobalPiTheme(): unknown {
	return (globalThis as any)[PI_GLOBAL_THEME_KEY];
}

/** Pi's ToolExecutionComponent reads `theme` from globalThis — keep it in sync with ctx.ui.theme. */
function applyToolBackgroundMode(theme: unknown): void {
	syncToolBackgroundMode();
	const targets = new Set<unknown>();
	if (theme) targets.add(theme);
	const globalTheme = getGlobalPiTheme();
	if (globalTheme) targets.add(globalTheme);
	for (const t of targets) {
		setThemeBg(t, "userMessageBg", TRANSPARENT_BG);
		if (toolBackgroundMode === "default") continue;
		setThemeBg(t, "toolPendingBg", TRANSPARENT_BG);
		setThemeBg(t, "toolSuccessBg", TRANSPARENT_BG);
		setThemeBg(t, "toolErrorBg", TRANSPARENT_BG);
	}
}

function stripAnsi(text: string): string {
	return text.replace(ANSI_RE, "");
}

function stripRenderedHeadingMarkers(line: string): string {
	return line.replace(/^((?:\x1b\[[0-9;]*m|[ \t])*)#{3,6}[ \t]*((?:\x1b\[[0-9;]*m)*)/, "$1$2");
}

const PLAIN_FENCE_LANGS = new Set(["text", "txt", "plain", "plaintext", ""]);

function parseRenderedFenceLine(line: string): { kind: "open" | "close"; language: string } | undefined {
	const plain = stripAnsi(line).trim();
	if (plain === "```") return { kind: "close", language: "" };
	if (!plain.startsWith("```")) return undefined;
	const rest = plain.slice(3).trim();
	if (rest.includes("`")) return undefined;
	return { kind: "open", language: rest };
}

function formatCodeBlockLanguageLabel(language: string): string {
	const raw = language.trim();
	if (!raw) return "";
	return raw.toLowerCase();
}

function mutedDotFill(count: number): string {
	if (count <= 0) return "";
	return `${BORDER_COLOR}${"·".repeat(count)}${TRANSPARENT_RESET}`;
}

function padRenderedLineToWidth(line: string, width: number): string {
	if (width <= 0) return "";
	const ceiling = Math.min(width, terminalColumnCeiling() || width);
	const gap = ceiling - visibleWidth(line);
	if (gap <= 0) return line;
	return line + " ".repeat(gap);
}

function isCodeBoxChromeLine(line: string): boolean {
	const plain = stripAnsi(line).trim();
	if (!plain) return false;
	if (/^[╭╮╰╯│·\s]+$/.test(plain) && /[╭╮╰╯│]/.test(plain)) return true;
	if (/^╭/.test(plain) && /╮$/.test(plain)) return true;
	if (/^╰/.test(plain) && /╯$/.test(plain)) return true;
	return false;
}

function isUserMessageChromeLine(line: string): boolean {
	const plain = stripAnsi(line).trim();
	if (/^╭/.test(plain) && /╮$/.test(plain)) return true;
	if (/^╰/.test(plain) && /╯$/.test(plain)) return true;
	return false;
}

function isBorderedContentLine(line: string): boolean {
	const plain = stripAnsi(line).trim();
	return plain.startsWith("│") && plain.endsWith("│") && plain.length > 2;
}

function extractBorderedInnerForCopy(line: string): string {
	const plain = stripAnsi(line);
	const start = plain.indexOf("│");
	const end = plain.lastIndexOf("│");
	if (start === -1 || end <= start) return stripAnsi(line).trim();
	return plain.slice(start + 1, end).replace(/^\s+/, "").replace(/\s+$/, "");
}

function applyTerminalCopyZones(lines: string[]): string[] {
	if (!Array.isArray(lines) || lines.length === 0) return lines;
	const out: string[] = [];
	let inZone = false;
	for (const line of lines) {
		if (isCopyExcludedChromeLine(line)) {
			if (inZone) {
				out[out.length - 1] += OSC133_ZONE_END;
				inZone = false;
			}
			out.push(line);
			continue;
		}
		const payload = copyPayloadForLine(line);
		if (!payload) {
			out.push(line);
			continue;
		}
		if (!inZone) {
			out.push(`${OSC133_ZONE_START}${line}`);
			inZone = true;
		} else {
			out.push(line);
		}
	}
	if (inZone && out.length > 0) {
		out[out.length - 1] += OSC133_ZONE_END + OSC133_ZONE_FINAL;
	}
	return out;
}

function isCopyExcludedChromeLine(line: string): boolean {
	return isCodeBoxChromeLine(line) || isUserMessageChromeLine(line);
}

function copyPayloadForLine(line: string): string | undefined {
	if (isCopyExcludedChromeLine(line)) return undefined;
	if (isBorderedContentLine(line)) return extractBorderedInnerForCopy(line);
	const plain = stripAnsi(line).trim();
	if (!plain) return undefined;
	return plain;
}

function roundedCodeBlockTop(width: number, language: string): string {
	if (width <= 1) return `${BORDER_COLOR}│${TRANSPARENT_RESET}`;
	const label = formatCodeBlockLanguageLabel(language);
	if (!label || width < 8) {
		const inner = Math.max(0, width - 2);
		return `${BORDER_COLOR}╭${TRANSPARENT_RESET}${mutedDotFill(inner)}${BORDER_COLOR}╮${TRANSPARENT_RESET}`;
	}
	const labelStyled = `${CODE_BLOCK_LANG_FG}${CHROME_ITALIC}${label}${RESET}${TRANSPARENT_RESET}`;
	const labelW = visibleWidth(labelStyled);
	const dotCount = Math.max(0, width - 6 - labelW);
	return `${BORDER_COLOR}╭· ${TRANSPARENT_RESET}${labelStyled} ${mutedDotFill(dotCount)}${BORDER_COLOR} ╮${TRANSPARENT_RESET}`;
}

function roundedCodeBlockBottom(width: number): string {
	if (width <= 1) return `${BORDER_COLOR}│${TRANSPARENT_RESET}`;
	const inner = Math.max(0, width - 2);
	return `${BORDER_COLOR}╰${TRANSPARENT_RESET}${mutedDotFill(inner)}${BORDER_COLOR}╯${TRANSPARENT_RESET}`;
}

function borderedCodeBlockLine(line: string, width: number): string {
	const innerWidth = Math.max(1, width - 4);
	let content = line;
	if (visibleWidth(content) > innerWidth) {
		content = truncateToWidth(content, innerWidth, "", false);
	}
	const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
	return `${BORDER_COLOR}│${TRANSPARENT_RESET} ${content}${padding} ${BORDER_COLOR}│${TRANSPARENT_RESET}`;
}

function boxRenderedCodeBlock(bodyLines: string[], language: string, width: number): string[] {
	const safeWidth = Math.max(4, Number.isFinite(width) ? Math.floor(width) : 0);
	const framed = [
		roundedCodeBlockTop(safeWidth, language),
		...bodyLines.map((line) => borderedCodeBlockLine(line, safeWidth)),
		roundedCodeBlockBottom(safeWidth),
	];
	return framed.map((line) => padRenderedLineToWidth(line, safeWidth));
}

function sanitizeRenderedTextBlockLines(lines: string[], width?: number): string[] {
	const result: string[] = [];
	let i = 0;
	const canBox = typeof width === "number" && width > 0;
	while (i < lines.length) {
		const fence = parseRenderedFenceLine(lines[i]);
		if (fence?.kind === "open") {
			const language = fence.language;
			const hideBox = PLAIN_FENCE_LANGS.has(language.trim().toLowerCase());
			const body: string[] = [];
			i++;
			while (i < lines.length) {
				const close = parseRenderedFenceLine(lines[i]);
				if (close?.kind === "close") {
					i++;
					break;
				}
				body.push(lines[i]);
				i++;
			}
			if (hideBox) {
				result.push(...body);
			} else if (canBox && (body.length > 0 || language.trim())) {
				result.push(...boxRenderedCodeBlock(body, language, width));
			} else {
				result.push(...body);
			}
			continue;
		}
		if (fence?.kind === "close") {
			i++;
			continue;
		}
		result.push(stripRenderedHeadingMarkers(lines[i]).replace(/###/g, ""));
		i++;
	}
	return result;
}

function isBlankLine(text: string): boolean {
	return stripAnsi(text).trim().length === 0;
}

function borderLine(width: number): string {
	return `${BORDER_COLOR}${"─".repeat(Math.max(1, width))}${TRANSPARENT_RESET}`;
}

function terminalColumnCeiling(): number {
	const cols = typeof process !== "undefined" ? process.stdout?.columns : undefined;
	return Number.isFinite(cols) && (cols as number) > 0 ? (cols as number) : 0;
}

function clampLineWidth(line: string, width: number): string {
	if (width <= 0) return "";
	// Hard ceiling: never emit a line wider than the real terminal. pi sometimes
	// hands renderers a width wider than stdout.columns (e.g. content later placed
	// in a narrower side panel), which trips pi's render width-assertion crash.
	const ceiling = Math.min(width, terminalColumnCeiling() || width);
	if (ceiling <= 0) return "";
	return visibleWidth(line) > ceiling ? truncateToWidth(line, ceiling) : line;
}

function isToolExecutionLike(value: unknown): value is { toolName: string; toolCallId: string } {
	if (!value || typeof value !== "object") return false;
	const candidate = value as Record<string, unknown>;
	return typeof candidate.toolName === "string" && typeof candidate.toolCallId === "string";
}

const AGENT_FAMILY_TOOL_NAMES = new Set(["Agent", "Agents", "get_subagent_result", "steer_subagent"]);

function isAgentFamilyToolName(name: unknown): boolean {
	return typeof name === "string" && AGENT_FAMILY_TOOL_NAMES.has(name);
}

function isTerminalImageLine(line: string): boolean {
	return line.includes(KITTY_IMAGE_PREFIX) || line.includes(ITERM2_IMAGE_PREFIX);
}

function normalizeLeadingCheckGlyph(line: string): string {
	return line.replace(/^((?:\x1b\[[0-9;]*m|[ \t]|[├└│─])*)[✓✔]((?:\x1b\[[0-9;]*m)*)(?=\s)/, "$1●$2");
}

function stripOuterBackgroundAnsi(line: string): string {
	return line
		.replace(/^\x1b\[(?:49|4[0-7]|10[0-7]|48;5;\d+|48;2;\d+;\d+;\d+)m/, "")
		.replace(/\x1b\[49m$/, "");
}

function firstImageBlockStart(lines: string[]): number {
	const imageLineIndex = lines.findIndex(isTerminalImageLine);
	if (imageLineIndex === -1) return -1;
	let start = imageLineIndex;
	while (start > 0 && isBlankLine(lines[start - 1])) start--;
	return start;
}

function splitRenderedImageBlock(lines: string[]): { textLines: string[]; imageLines: string[] } {
	const imageStart = firstImageBlockStart(lines);
	if (imageStart === -1) return { textLines: lines, imageLines: [] };
	const textLines = lines.slice(0, imageStart);
	while (textLines.length > 0 && isBlankLine(textLines[textLines.length - 1])) textLines.pop();
	return { textLines, imageLines: lines.slice(imageStart) };
}

function toolGroupingEnabled(): boolean {
	return readSettings().groupToolCalls !== false;
}

function setToolGroupingEnabled(enabled: boolean): void {
	writeSettingsKey("groupToolCalls", enabled);
}

type ThinkingMode = "live" | "full";

function getThinkingMode(): ThinkingMode {
	return getMode(readSettings().thinkingMode, ["live", "full"] as const, "live");
}

function isAssistantThinkingComplete(comp: any, message: any): boolean {
	if (!message || message.role !== "assistant") return false;
	if (typeof message[THINKING_DURATION_KEY] === "number") return true;
	if (message[THINKING_ACTIVE_KEY]) return false;
	// Providers keep stopReason "pending" for the whole stream ("deferred" while
	// a deferred call is unresolved); both are in-flight sentinels, never
	// completion signals. Without this, live-thinking detection would depend on
	// THINKING_ACTIVE_KEY being stamped before the UI renders the same event.
	if (message.stopReason === "pending" || message.stopReason === "deferred") return false;
	if (typeof message.stopReason === "string" && message.stopReason.length > 0) return true;
	if (Array.isArray(message.content)) {
		let sawThinking = false;
		for (const block of message.content) {
			if (block?.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim()) {
				sawThinking = true;
			} else if (sawThinking && (
				(block?.type === "text" && typeof block.text === "string" && block.text.trim()) ||
				block?.type === "toolCall"
			)) {
				return true;
			}
		}
		if (sawThinking) return false;
	}
	return true;
}

function isLiveThinkingMessage(comp: any, message: any): boolean {
	if (!message || message.role !== "assistant") return false;
	if (isAssistantThinkingComplete(comp, message)) return false;
	if ((message as any)[THINKING_ACTIVE_KEY]) return true;
	if (Array.isArray(message.content)) {
		return message.content.some((b: any) => b?.type === "thinking" && typeof b?.thinking === "string" && b.thinking.trim());
	}
	return false;
}

type ToolStatus = "pending" | "success" | "error";

function getToolStatusForGroup(tool: any): ToolStatus {
	if (tool?.result?.isError) return "error";
	if (tool?.result && tool?.isPartial !== true) return "success";
	// Only in-flight tools that actually started this agent run count as pending.
	// History rows reconstructed without a matching toolResult stay isPartial=true
	// forever; treating them as pending made interrupted tools blink again on resume.
	if (tool?.isPartial === true && tool?.executionStarted === true && currentAgentWorkStartMs !== undefined) {
		return "pending";
	}
	return "success";
}

let TOOL_STATUS_SUCCESS = "\x1b[32m";
let TOOL_STATUS_ERROR = "\x1b[31m";
let TOOL_STATUS_PENDING = "\x1b[90m";

function statusText(status: ToolStatus, count: number): string {
	const label = status === "success" ? "done" : status === "error" ? "failed" : "running";
	const color = status === "success" ? TOOL_STATUS_SUCCESS : status === "error" ? TOOL_STATUS_ERROR : TOOL_STATUS_PENDING;
	return `${color}${count}${TRANSPARENT_RESET} ${label}`;
}

function countToolStatuses(tools: any[]): Record<ToolStatus, number> {
	return tools.reduce((counts, tool) => {
		counts[getToolStatusForGroup(tool)]++;
		return counts;
	}, { pending: 0, success: 0, error: 0 } as Record<ToolStatus, number>);
}

function formatToolGroupCounts(tools: any[]): string {
	const counts = countToolStatuses(tools);
	const parts: string[] = [];
	if (counts.pending) parts.push(statusText("pending", counts.pending));
	if (counts.success) parts.push(statusText("success", counts.success));
	if (counts.error) parts.push(statusText("error", counts.error));
	return parts.join(`${TRANSPARENT_RESET} • `);
}

function getToolName(tool: any): string {
	return typeof tool?.toolName === "string" && tool.toolName ? tool.toolName : "tool";
}

function getGroupedToolName(tools: any[]): string | undefined {
	const first = getToolName(tools[0]);
	return tools.every((tool) => getToolName(tool) === first) ? first : undefined;
}

function getToolGroupLabel(tools: any[]): string {
	const sameName = getGroupedToolName(tools);
	return sameName ? humanizeToolName(sameName) : "Multiple Tools";
}

function getToolGroupOverallStatus(tools: any[]): ToolStatus {
	const counts = countToolStatuses(tools);
	if (counts.error > 0) return "error";
	if (counts.pending > 0) return "pending";
	return "success";
}

// Claude Code: solid filled circle that is either fully present or fully gone
// while pending — never a hollow outlined ○. Classic ● + bold is the sweet
// spot for ordinary tools. Agent-family tools use a breathing size cycle.
const STATUS_DOT_FILLED = "●";
const STATUS_DOT_BOLD = "\x1b[1m";
// Single-cell glyphs only (⬤ is often double-width and walks the baseline).
// Optical sizes share the same cell so the center stays put while breathing:
// big ● → medium • → small · → invisible → small · → medium •
const AGENT_BREATHE_GLYPHS = ["●", "•", "·", " ", "·", "•"] as const;
const AGENT_BREATHE_LEN = AGENT_BREATHE_GLYPHS.length;

function paintStatusDot(colorAnsi: string): string {
	return `${colorAnsi}${STATUS_DOT_BOLD}${STATUS_DOT_FILLED}${TRANSPARENT_RESET}`;
}

function themeStatusDot(theme: Theme, colorKey: "success" | "error" | "dim" | "muted"): string {
	// theme.fg may not preserve nested SGR cleanly — color the glyph string itself.
	return theme.fg(colorKey, `${STATUS_DOT_BOLD}${STATUS_DOT_FILLED}`);
}

function agentBreatheGlyphRaw(): string {
	// Always exactly one display cell — matches ordinary tool dots, keeps titles aligned.
	return AGENT_BREATHE_GLYPHS[_globalBlinkPhaseIndex % AGENT_BREATHE_LEN];
}

function paintAgentBreatheDot(colorAnsi: string = TOOL_STATUS_SUCCESS): string {
	const glyph = agentBreatheGlyphRaw();
	if (glyph === " ") return " ";
	// Bold only on the largest frame so weight changes without shifting the cell.
	const bold = glyph === "●" ? STATUS_DOT_BOLD : "";
	return `${colorAnsi}${bold}${glyph}${TRANSPARENT_RESET}`;
}

function agentBreatheDot(theme: Theme): string {
	const glyph = agentBreatheGlyphRaw();
	if (glyph === " ") return " ";
	const bold = glyph === "●" ? STATUS_DOT_BOLD : "";
	return theme.fg("success", `${bold}${glyph}`);
}

function groupStatusLight(status: ToolStatus, options?: { agentBreathe?: boolean }): string {
	const color = status === "success" ? TOOL_STATUS_SUCCESS : status === "error" ? TOOL_STATUS_ERROR : TOOL_STATUS_PENDING;
	if (status === "pending") {
		// Prefer the shared blink phase over wall-clock so group lights stay in sync
		// with the global timer (and Agent breathe). Space keeps column alignment.
		if (options?.agentBreathe) return paintAgentBreatheDot(TOOL_STATUS_SUCCESS);
		return _globalBlinkPhase ? paintStatusDot(TOOL_STATUS_SUCCESS) : " ";
	}
	return paintStatusDot(color);
}

function formatToolNameList(tools: any[]): string {
	const counts = new Map<string, number>();
	for (const tool of tools) {
		const name = getToolName(tool);
		counts.set(name, (counts.get(name) ?? 0) + 1);
	}
	return [...counts.entries()]
		.map(([name, count]) => `${humanizeToolName(name)}${count > 1 ? `×${count}` : ""}`)
		.join(", ");
}

function getRepeatedToolSubject(tools: any[], groupedName: string | undefined): string {
	if (!groupedName || tools.length === 0) return "";
	if (groupedName === "read") {
		const paths = tools.map((tool) => String(tool?.args?.path ?? ""));
		if (paths[0] && paths.every((path) => path === paths[0])) {
			return shortPath(process.cwd(), paths[0]);
		}
	}
	const summaries = tools.map(getToolArgSummary);
	return summaries[0] && summaries.every((summary) => summary === summaries[0])
		? summaries[0]
		: "";
}

function escapeRegex(text: string): string {
	return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function stripGroupedToolLabel(line: string, label: string | undefined): string {
	if (!label) return line;
	const ansi = "(?:\\x1b\\[[0-9;]*m)*";
	const pattern = new RegExp(`^(${ansi})${escapeRegex(label)}(${ansi})\\s+`);
	return line.replace(pattern, "$1$2");
}

function isChromeOnlyLine(line: string): boolean {
	const plain = stripAnsi(line).trim();
	return plain.length === 0 || /^[─━╭╮╰╯┌┐└┘│├┤┬┴┼\s]+$/.test(plain);
}

function stripToolChrome(lines: string[]): string[] {
	return trimRenderedBlankLines(lines).filter((line) => !isChromeOnlyLine(line));
}

function stripLeadingToolStatus(line: string): string {
	// Drop the single-cell status marker so group rows can re-prefix a fresh light.
	// Include Agent breathe glyphs (·) and the blank off-phase (space) so the title
	// never keeps a leftover marker that shifts when size changes.
	return line.replace(
		/^((?:\x1b\[[0-9;]*m|[ \t]|[├└│─])*)(?:\x1b\[[0-9;]*m)*(?:[●○✗■⬤•·]| )(?:\x1b\[[0-9;]*m)*\s+/,
		"$1",
	);
}

function trimAnsiLeft(text: string): string {
	let current = text;
	while (true) {
		const next = current.replace(/^((?:\x1b\[[0-9;]*m)*)[ \t]+/, "$1");
		if (next === current) return current;
		current = next;
	}
}

function removeGroupedToolPrefix(line: string, groupedLabel?: string): string {
	return trimAnsiLeft(stripGroupedToolLabel(trimAnsiLeft(stripLeadingToolStatus(line)), groupedLabel));
}

function tintGroupedToolLine(line: string, _groupedLabel?: string): string {
	return trimAnsiLeft(line);
}

function getToolArgSummary(tool: any): string {
	const args = tool?.args ?? {};
	const name = getToolName(tool);
	if (name === "read") {
		let value = shortPath(process.cwd(), args.path ?? "");
		const parts: string[] = [];
		if (args.offset) parts.push(`offset=${args.offset}`);
		if (args.limit) parts.push(`limit=${args.limit}`);
		if (parts.length > 0) value += ` (${parts.join(", ")})`;
		return value;
	}
	if (name === "bash") return buildBashCommandPresentation(args.command ?? "").headline;
	if (name === "grep") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
	if (name === "find") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
	if (name === "ls") return shortPath(process.cwd(), args.path ?? ".");
	return summarizeText(getStringArg(args, "path", "file_path", "url", "query", "name", "subject", "tool", "description", "prompt") || name, 72);
}

function getToolCallLine(tool: any): string {
	const value = (tool as any)?.callRendererComponent?.value;
	if (typeof value === "string" && value.trim()) {
		const line = value.split("\n").find((line) => stripAnsi(line).trim()) ?? value;
		return line.replaceAll(WRAP_MARK, "").replaceAll(CLIP_MARK, "");
	}
	const summary = getToolArgSummary(tool);
	const label = humanizeToolName(getToolName(tool));
	return `${label}${summary ? ` ${summary}` : ""}`;
}

function alignTrailingMarkedLine(line: string, width: number): string {
	const markerIndex = line.indexOf(TRAILING_MARK);
	if (markerIndex === -1) return clampLineWidth(line, width);
	const safeWidth = Math.max(1, width);
	const left = line.slice(0, markerIndex);
	const right = line.slice(markerIndex + TRAILING_MARK.length);
	const rightWidth = visibleWidth(right);
	if (rightWidth >= safeWidth) return truncateToWidth(right, safeWidth, "", false);
	const leftBudget = Math.max(0, safeWidth - rightWidth - 2);
	const clippedLeft = leftBudget > 0 ? truncateToWidth(left, leftBudget, "…", false) : "";
	const gap = Math.max(1, safeWidth - visibleWidth(clippedLeft) - rightWidth);
	return `${clippedLeft}${" ".repeat(gap)}${right}`;
}

function getCompactToolLine(tool: any, width: number, groupedLabel?: string, showTrailing = true): string {
	let content = removeGroupedToolPrefix(getToolCallLine(tool), groupedLabel);
	if (!showTrailing) content = content.split(TRAILING_MARK, 1)[0] ?? content;
	return alignTrailingMarkedLine(content || getToolName(tool), width);
}

interface CollapsedToolEntry {
	tools: any[];
	name: string;
	subject: string;
}

function collapseRepeatedToolEntries(tools: any[]): CollapsedToolEntry[] {
	const entries: CollapsedToolEntry[] = [];
	for (const tool of tools) {
		const name = getToolName(tool);
		const subject = getRepeatedToolSubject([tool], name);
		const previous = entries[entries.length - 1];
		if (subject && previous?.name === name && previous.subject === subject) {
			previous.tools.push(tool);
		} else {
			entries.push({ tools: [tool], name, subject });
		}
	}
	return entries;
}

function stripReadRangeFromToolLine(line: string): string {
	return line.replace(
		/\s+(?:\x1b\[[0-9;]*m)*\((?:offset|limit)=\d+(?:,\s*(?:offset|limit)=\d+)*\)(?=(?:\x1b\[[0-9;]*m)*$)/,
		"",
	);
}

function getCollapsedToolEntryLine(entry: CollapsedToolEntry, width: number, groupedLabel?: string): string {
	if (entry.tools.length === 1) return getCompactToolLine(entry.tools[0], width, groupedLabel);
	const counts = countToolStatuses(entry.tools);
	const attentionCounts = counts.pending > 0 || counts.error > 0
		? ` • ${formatToolGroupCounts(entry.tools)}`
		: "";
	const suffix = ` ${FG_DIM}×${entry.tools.length}${TRANSPARENT_RESET}${attentionCounts}`;
	const firstLine = getCompactToolLine(entry.tools[0], Math.max(1, width - visibleWidth(suffix)), groupedLabel, false);
	const sharedLine = entry.name === "read" ? stripReadRangeFromToolLine(firstLine) : firstLine;
	return clampLineWidth(`${sharedLine}${suffix}`, width);
}

function getCollapsedToolEntryLines(entry: CollapsedToolEntry, width: number, groupedLabel?: string): string[] {
	const lines = [getCollapsedToolEntryLine(entry, width, groupedLabel)];
	if (entry.name !== "bash") return lines;
	const running = [...entry.tools].reverse().find((tool) => getToolStatusForGroup(tool) === "pending");
	const latestOutput = running ? getLastBashOutputLine(getTextContent(running.result)) : undefined;
	if (latestOutput) lines.push(`${FG_DIM}${latestOutput}${TRANSPARENT_RESET}`);
	return lines;
}

function getExpandedToolGroupLines(tool: any, width: number, groupedLabel?: string): string[] {
	const lines = stripToolChrome(tool.render(Math.max(1, width)))
		.map((line) => removeGroupedToolPrefix(line, groupedLabel))
		.map((line) => tintGroupedToolLine(line, groupedLabel));
	return lines.length > 0 ? lines : [`${FG_DIM}${String(tool?.toolName ?? "tool")}${TRANSPARENT_RESET}`];
}

function branchPrefix(index: number, total: number, theme?: Theme): string {
	// Bare tee/corner only — no horizontal ─ arm.
	const branch = index === total - 1 ? "└" : "├";
	const rule = currentToolBranchAnsi(theme);
	return ` ${rule}${branch}${TRANSPARENT_RESET} `;
}

function branchContinuation(index: number, total: number, theme?: Theme): string {
	const rule = currentToolBranchAnsi(theme);
	// Match lead width of ` X ` (3 cols of structure + spaces handled outside).
	return index === total - 1 ? "   " : ` ${rule}│${TRANSPARENT_RESET} `;
}

function formatBranchedToolLines(
	lines: string[],
	index: number,
	total: number,
	width: number,
	status: ToolStatus,
	options?: { agentBreathe?: boolean },
): string[] {
	const output: string[] = [];
	const content = lines.filter((line) => isTerminalImageLine(line) || stripAnsi(line).trim().length > 0);
	const safeContent = content.length > 0 ? content : [""];
	const light = groupStatusLight(status, options);
	for (let lineIndex = 0; lineIndex < safeContent.length; lineIndex++) {
		const line = safeContent[lineIndex];
		if (isTerminalImageLine(line)) {
			output.push(line);
			continue;
		}
		// Always strip any leftover status marker from the child call line before
		// re-prefixing. Agent breathe used · which the old stripper missed, so the
		// title walked sideways as size changed inside groups.
		const body = lineIndex === 0 ? removeGroupedToolPrefix(line) : trimAnsiLeft(line);
		const prefix = lineIndex === 0 ? `${branchPrefix(index, total)}${light} ` : `${branchContinuation(index, total)}  `;
		output.push(clampLineWidth(`${prefix}${body}`, width));
	}
	return output;
}

const NON_GROUPABLE_TOOL_NAMES = new Set(["edit", "write", "apply_patch"]);
const ACTIVE_TOOL_GROUPS = new Set<any>();

function isGroupableTool(value: unknown): value is InstanceType<typeof ToolExecutionComponent> {
	return value instanceof ToolExecutionComponent && !NON_GROUPABLE_TOOL_NAMES.has(getToolName(value));
}

class ToolGroupComponent extends Container {
	private tools: any[] = [];
	private expanded = false;
	// Memoize full group output. Grouped history is the long-chat bottleneck:
	// each warm frame used to re-render every child tool, re-branch lines, and
	// re-clamp every row even when nothing changed.
	private dirty = true;
	private cachedWidth?: number;
	private cachedEpoch?: number;
	private cachedMode?: string;
	private cachedExpanded?: boolean;
	private cachedBlinkPhase?: boolean;
	private cachedStatusKey?: string;
	private cachedToolCount?: number;
	private cachedLines?: string[];

	private clearRenderCache(): void {
		this.dirty = true;
		this.cachedWidth = undefined;
		this.cachedEpoch = undefined;
		this.cachedMode = undefined;
		this.cachedExpanded = undefined;
		this.cachedBlinkPhase = undefined;
		this.cachedStatusKey = undefined;
		this.cachedToolCount = undefined;
		this.cachedLines = undefined;
	}

	private statusSnapshot(): { key: string; pending: number; success: number; error: number } {
		// Status counts + per-tool identity/expanded/partial bits detect membership
		// and completion changes without walking full child render output. Child
		// content changes still reach us via clearToolRenderCache → invalidate().
		const counts = countToolStatuses(this.tools);
		let idBits = "";
		for (let i = 0; i < this.tools.length; i++) {
			const tool = this.tools[i];
			const id = typeof tool?.toolCallId === "string" ? tool.toolCallId : getToolName(tool);
			const flags = (tool?.isPartial === true ? 1 : 0)
				| (tool?.result?.isError ? 2 : 0)
				| (tool?.expanded ? 4 : 0)
				| (tool?.argsComplete ? 8 : 0)
				| (tool?.executionStarted ? 16 : 0);
			idBits += `${id}:${flags},`;
		}
		return {
			key: `${this.tools.length}:${counts.pending}:${counts.success}:${counts.error}:${idBits}`,
			pending: counts.pending,
			success: counts.success,
			error: counts.error,
		};
	}

	addTool(tool: any): void {
		ACTIVE_TOOL_GROUPS.add(this);
		this.tools.push(tool);
		tool[COMPONENT_PARENT] = this;
		// Don't cascade invalidate into every child — only drop our own cache.
		// Child tools already rebuild via their own updateDisplay path.
		this.clearRenderCache();
	}

	releaseTools(): any[] {
		const tools = this.tools;
		this.tools = [];
		ACTIVE_TOOL_GROUPS.delete(this);
		this.clearRenderCache();
		return tools;
	}

	setExpanded(expanded: boolean): void {
		if (this.expanded === expanded) return;
		this.expanded = expanded;
		for (const tool of this.tools) tool.setExpanded?.(expanded);
		this.clearRenderCache();
	}

	invalidate(): void {
		// Parent/group invalidation should NOT force every child tool through
		// updateDisplay() (which re-runs call/result renderers). Drop our memo
		// only; children keep their own ToolText/TOOL_RENDER_CACHE entries and
		// recompute only when their content actually changes.
		this.clearRenderCache();
	}

	render(width: number): string[] {
		if (this.tools.length === 0) return [];
		const safeWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
		// Fast path: settled groups with a valid memo skip ALL child walks.
		// Child mutations mark dirty via clearToolRenderCache → invalidate().
		if (
			!this.dirty
			&& this.cachedLines
			&& this.cachedWidth === safeWidth
			&& this.cachedEpoch === _toolBranchVisualEpoch
			&& this.cachedMode === toolBackgroundMode
			&& this.cachedExpanded === this.expanded
		) {
			return this.cachedLines;
		}

		const status = this.statusSnapshot();
		// Only memoize fully-settled groups. Pending groups must recompute so
		// blink dots and live partial child content stay fresh. Long chats are
		// almost entirely settled history, which is the expensive warm path.
		const canCache = status.pending === 0;

		const groupedName = getGroupedToolName(this.tools);
		const label = getToolGroupLabel(this.tools);
		const names = groupedName ? "" : formatToolNameList(this.tools);
		const overall: ToolStatus = status.error > 0 ? "error" : status.pending > 0 ? "pending" : "success";
		// Group header breathes only when every pending member is Agent-family;
		// mixed groups keep the ordinary on/off light.
		const pendingTools = this.tools.filter((tool) => getToolStatusForGroup(tool) === "pending");
		const headerBreathe = pendingTools.length > 0 && pendingTools.every((tool) => isAgentFamilyToolName(getToolName(tool)));
		const light = groupStatusLight(overall, { agentBreathe: headerBreathe });
		const summaryLabel = `${label}:`;
		const countParts: string[] = [];
		if (status.pending) countParts.push(statusText("pending", status.pending));
		if (status.success) countParts.push(statusText("success", status.success));
		if (status.error) countParts.push(statusText("error", status.error));
		const countsText = countParts.join(`${TRANSPARENT_RESET} • `);
		const total = this.tools.length;
		const lines: string[] = [];
		const subject = this.expanded ? "" : getRepeatedToolSubject(this.tools, groupedName);
		const collapseToSingleRow = !this.expanded && !!groupedName && !!subject;
		if (collapseToSingleRow) {
			const entry = { tools: this.tools, name: groupedName, subject };
			const entryLine = getCollapsedToolEntryLine(entry, Math.max(1, safeWidth - 3));
			lines.push(clampLineWidth(
				` ${light} ${entryLine}${toolOutputDetailHint(undefined as any, false, true)}`,
				safeWidth,
			));
		} else {
			const summary = ` ${light} ${summaryLabel} ${countsText}${names ? ` ${TRANSPARENT_RESET}• ${names}` : ""}${toolOutputDetailHint(undefined as any, this.expanded, true)}`;
			lines.push(" ".repeat(safeWidth), clampLineWidth(summary, safeWidth));
			const childWidth = Math.max(1, safeWidth - 6);
			if (this.expanded) {
				for (let index = 0; index < total; index++) {
					const tool = this.tools[index];
					const branched = formatBranchedToolLines(
						getExpandedToolGroupLines(tool, childWidth, groupedName ? label : undefined),
						index,
						total,
						safeWidth,
						getToolStatusForGroup(tool),
						{ agentBreathe: isAgentFamilyToolName(getToolName(tool)) },
					);
					for (const line of branched) lines.push(clampLineWidth(line, safeWidth));
				}
			} else {
				const entries = collapseRepeatedToolEntries(this.tools);
				for (let index = 0; index < entries.length; index++) {
					const entry = entries[index];
					const branched = formatBranchedToolLines(
						getCollapsedToolEntryLines(entry, childWidth, groupedName ? label : undefined),
						index,
						entries.length,
						safeWidth,
						getToolGroupOverallStatus(entry.tools),
						{ agentBreathe: entry.tools.every((tool) => isAgentFamilyToolName(getToolName(tool))) },
					);
					for (const line of branched) lines.push(clampLineWidth(line, safeWidth));
				}
			}
		}

		// Final clamp already applied per-line above; avoid a second full pass.
		if (canCache) {
			this.dirty = false;
			this.cachedWidth = safeWidth;
			this.cachedEpoch = _toolBranchVisualEpoch;
			this.cachedMode = toolBackgroundMode;
			this.cachedExpanded = this.expanded;
			this.cachedBlinkPhase = true;
			this.cachedStatusKey = status.key;
			this.cachedToolCount = total;
			this.cachedLines = lines;
		} else {
			this.clearRenderCache();
		}
		return lines;
	}
}

function isToolGroupComponent(value: unknown): value is ToolGroupComponent {
	return value instanceof ToolGroupComponent;
}

function isSpacerComponent(value: unknown): value is InstanceType<typeof Spacer> {
	return value instanceof Spacer || (value as any)?.constructor?.name === "Spacer";
}

function isTextComponent(value: unknown): value is InstanceType<typeof Text> {
	return value instanceof Text || (value as any)?.constructor?.name === "Text";
}

function isMarkdownComponent(value: unknown): value is InstanceType<typeof Markdown> {
	return value instanceof Markdown || (value as any)?.constructor?.name === "Markdown";
}

function isIgnorableToolSeparator(value: unknown): boolean {
	if (isSpacerComponent(value)) return true;
	if (value instanceof AssistantMessageComponent || (value as any)?.constructor?.name === "AssistantMessageComponent") {
		// Empty assistant framing stays ignorable so it never splits tool groups.
		// A rendered thinking row ("Thought for Xs" / live thinking) is a visible
		// boundary: tool calls that follow it must start a new group instead of
		// silently joining the batch that ran before the thought.
		const contentChildren = (value as any).contentContainer?.children;
		if (!Array.isArray(contentChildren) || contentChildren.length === 0) return true;
		return contentChildren.every((child: any) => isSpacerComponent(child));
	}
	return false;
}

function findPreviousToolSibling(children: any[], startIndex: number): { child: any; index: number } | undefined {
	for (let index = startIndex; index >= 0; index--) {
		const child = children[index];
		if (isIgnorableToolSeparator(child)) continue;
		return { child, index };
	}
	return undefined;
}

function ungroupActiveToolGroups(): void {
	for (const group of [...ACTIVE_TOOL_GROUPS]) {
		const parent = group?.[COMPONENT_PARENT];
		const children = parent?.children;
		if (!Array.isArray(children)) {
			ACTIVE_TOOL_GROUPS.delete(group);
			continue;
		}
		const index = children.indexOf(group);
		if (index === -1) {
			ACTIVE_TOOL_GROUPS.delete(group);
			continue;
		}
		const tools = group.releaseTools();
		for (const tool of tools) tool[COMPONENT_PARENT] = parent;
		children.splice(index, 1, ...tools);
	}
}

function isThinkingOnlyAssistantComponent(comp: unknown): comp is InstanceType<typeof AssistantMessageComponent> {
	if (!comp || ((comp as any).constructor?.name !== "AssistantMessageComponent" && !(comp instanceof AssistantMessageComponent))) {
		return false;
	}
	const msg = (comp as any).lastMessage;
	if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) return false;
	const hasThinking = msg.content.some((c: any) => c?.type === "thinking" && typeof c?.thinking === "string" && c.thinking.trim());
	if (!hasThinking) return false;
	const hasText = msg.content.some((c: any) => c?.type === "text" && typeof c?.text === "string" && c.text.trim());
	if (hasText) return false;
	const hasToolCalls = msg.content.some((c: any) => c?.type === "toolCall");
	if (hasToolCalls) return false;
	if ((comp as any).isStreaming === true || msg[THINKING_ACTIVE_KEY]) return false;
	return true;
}

function maybeMergeConsecutiveThinkingMessages(parent: any): void {
	const children = parent?.children;
	if (!Array.isArray(children) || children.length < 2) return;

	for (let i = 0; i < children.length; i++) {
		const current = children[i];
		if (!isThinkingOnlyAssistantComponent(current)) continue;

		let nextIdx = i + 1;
		while (nextIdx < children.length && isSpacerComponent(children[nextIdx])) {
			nextIdx++;
		}
		if (nextIdx >= children.length) break;

		const nextComp = children[nextIdx];
		if (isThinkingOnlyAssistantComponent(nextComp)) {
			const curMsg = (current as any).lastMessage;
			const nextMsg = (nextComp as any).lastMessage;

			const durA = getMessageThinkingDurationMs(curMsg);
			const durB = getMessageThinkingDurationMs(nextMsg);
			const mergedDuration = durA + durB;

			const nextThinkingBlocks = nextMsg.content.filter((c: any) => c?.type === "thinking");
			curMsg.content.push(...nextThinkingBlocks);
			curMsg[THINKING_DURATION_KEY] = mergedDuration;

			(current as any).updateContent(curMsg);

			const removeCount = nextIdx - i;
			children.splice(i + 1, removeCount);
			i--;
		}
	}
}

function maybeGroupToolComponent(parent: any, component: any): void {
	if (!toolGroupingEnabled() || !isGroupableTool(component) || isToolGroupComponent(parent)) return;
	const children = parent?.children;
	if (!Array.isArray(children)) return;
	const index = children.indexOf(component);
	if (index <= 0) return;
	const previousEntry = findPreviousToolSibling(children, index - 1);
	if (!previousEntry) return;
	const previous = previousEntry.child;
	if (isToolGroupComponent(previous)) {
		children.splice(index, 1);
		previous.addTool(component);
		maybeMergeConsecutiveThinkingMessages(parent);
		return;
	}
	if (isGroupableTool(previous)) {
		const group = new ToolGroupComponent();
		group.setExpanded(Boolean((previous as any).expanded));
		group.addTool(previous);
		group.addTool(component);
		(group as any)[COMPONENT_PARENT] = parent;
		children[previousEntry.index] = group;
		children.splice(index, 1);
		maybeMergeConsecutiveThinkingMessages(parent);
	}
}

function patchContainerParentTracking(): void {
	const proto = Container.prototype as any;
	if (proto[PARENT_TRACKING_PATCH_FLAG]) return;
	const originalAddChild = proto.addChild;
	const originalRemoveChild = proto.removeChild;
	const originalClear = proto.clear;
	proto.addChild = function patchedAddChild(component: any) {
		const result = originalAddChild.call(this, component);
		if (component && typeof component === "object") component[COMPONENT_PARENT] = this;
		maybeGroupToolComponent(this, component);
		maybeMergeConsecutiveThinkingMessages(this);
		return result;
	};
	proto.removeChild = function patchedRemoveChild(component: any) {
		const result = originalRemoveChild.call(this, component);
		if (component && typeof component === "object" && component[COMPONENT_PARENT] === this) delete component[COMPONENT_PARENT];
		return result;
	};
	proto.clear = function patchedClear() {
		for (const child of this.children ?? []) {
			if (child && typeof child === "object" && child[COMPONENT_PARENT] === this) delete child[COMPONENT_PARENT];
		}
		return originalClear.call(this);
	};
	proto[PARENT_TRACKING_PATCH_FLAG] = true;
}

function formatTodoOverlayLines(lines: string[], width: number): string[] {
	// Hot path: nearly every Container.render hits this. Bail after the first
	// non-empty line unless it's actually the Magic Context todo overlay.
	let firstContent: string | undefined;
	for (let i = 0; i < lines.length; i++) {
		const plain = stripAnsi(lines[i]).trim();
		if (!plain) continue;
		firstContent = plain;
		break;
	}
	if (!firstContent || !/^[●○]\s+Todos\s+—/.test(firstContent)) return lines;
	return lines.map((line) => {
		const plain = stripAnsi(line);
		if (/^[●○]\s+Todos\s+—/.test(plain)) return clampLineWidth(` ${line}`, width);
		// Magic Context emits `├─` / `└─` or bare `├` / `└`; strip any arm to bare tee/corner.
		if (!/^[├└]─?\s+[✓○◐✗●⬤•]\s/.test(plain) && !/^[├└]─?\s+/.test(plain)) return line;
		const withoutTodoHash = line.replace(/#(?=[A-Za-z0-9_-]+)/, "");
		const bare = withoutTodoHash.replace(/([├└])─/, "$1");
		const colored = bare.replace(/[├└]/, (branch) => `${currentToolBranchAnsi()}${branch}${TRANSPARENT_RESET}`);
		return clampLineWidth(` ${colored}`, width);
	});
}

function patchGlobalToolBorders(): void {
	const proto = Container.prototype as any;
	if (proto[PATCH_FLAG]) return;

	const originalRender = proto.render;
	proto.render = function patchedContainerRender(width: number): string[] {
		maybeMergeConsecutiveThinkingMessages(this);
		if (isToolExecutionLike(this)) {
			const cached = (this as any)[TOOL_RENDER_CACHE];
			const branchKey = toolBranchRenderCacheKey();
			if (
				cached?.width === width
				&& cached?.mode === toolBackgroundMode
				&& cached?.branchKey === branchKey
				&& cached?.branchEpoch === _toolBranchVisualEpoch
			) {
				return cached.lines;
			}
		}

		const rendered = originalRender.call(this, width);
		if (!Array.isArray(rendered) || rendered.length === 0) return rendered;
		const todoOverlay = formatTodoOverlayLines(rendered, width);
		if (!isToolExecutionLike(this)) return todoOverlay;
		const branchCache = { branchKey: toolBranchRenderCacheKey(), branchEpoch: _toolBranchVisualEpoch };
		if (toolBackgroundMode === "default") {
			(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: rendered, ...branchCache };
			return rendered;
		}

		let start = 0;
		while (start < rendered.length && isBlankLine(rendered[start])) start++;
		let end = rendered.length - 1;
		while (end >= start && isBlankLine(rendered[end])) end--;
		if (start > end) return rendered;

		const { textLines, imageLines } = splitRenderedImageBlock(rendered.slice(start, end + 1));
		if (imageLines.length > 0) {
			(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: rendered, ...branchCache };
			return rendered;
		}
		// Agent-family tools stay column-aligned with every other tool row — no extra
		// leading indent (the old nested pad made Agent look offset from Read/Bash).
		const core = textLines.map((line) => {
			const normalized = normalizeLeadingCheckGlyph(line);
			return clampLineWidth(stripOuterBackgroundAnsi(normalized), width);
		});
		const spacerLine = " ".repeat(width);
		let result: string[];

		if (toolBackgroundMode === "outlines") {
			const ruleWidth = Math.max(1, width);
			const framed = core.length > 0 ? [borderLine(ruleWidth), ...core, borderLine(ruleWidth)] : [];
			result = [spacerLine, ...framed, ...imageLines];
		} else {
			result = [spacerLine, ...core, ...imageLines];
		}

		(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: result, ...branchCache };
		return result;
	};

	proto[PATCH_FLAG] = true;
}

function summarizeText(text: string, max = 60): string {
	const oneLine = text.replace(/\n/g, " ").trim();
	if (oneLine.length <= max) return oneLine;
	return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
}

function hashText(text: string): string {
	let hash = 2166136261;
	for (let i = 0; i < text.length; i++) {
		hash ^= text.charCodeAt(i);
		hash = Math.imul(hash, 16777619);
	}
	return (hash >>> 0).toString(36);
}

let extraToolOutputExpanded = false;

function syncExtraToolDetailMode(): void {
	extraToolOutputExpanded = readSettings().extraToolOutputExpanded === true;
}

function setExtraToolDetailMode(enabled: boolean): void {
	extraToolOutputExpanded = enabled;
	writeSettingsKey("extraToolOutputExpanded", enabled);
}

function configuredKeyHint(binding: Parameters<typeof keyText>[0], fallbackKey: string, description: string): string {
	try {
		if (keyText(binding).trim()) return keyHint(binding, description);
	} catch { /* fall back below */ }
	return rawKeyHint(fallbackKey, description);
}

function expandHint(_theme: Theme, action: "expand" | "collapse" | "toggle" = "toggle"): string {
	return ` • ${configuredKeyHint("app.tools.expand", "ctrl+o", `to ${action}`)}`;
}

function deepExpandHint(): string {
	return ` • ${rawKeyHint("ctrl+shift+o", extraToolOutputExpanded ? "less detail" : "more detail")}`;
}

function toolOutputDetailHint(theme: Theme, expanded: boolean, hasMore = false): string {
	if (!expanded) return expandHint(theme, "toggle");
	const parts = [expandHint(theme, "collapse")];
	if (hasMore || extraToolOutputExpanded) parts.push(deepExpandHint());
	return parts.join("");
}

function clearStateKeys(state: Record<string, unknown> | undefined, ...keys: string[]): void {
	if (!state) return;
	for (const key of keys) {
		delete state[key];
	}
}

function clearToolRenderCache(value: unknown): void {
	if (!value || typeof value !== "object") return;
	delete (value as any)[TOOL_RENDER_CACHE];
	// If this tool lives inside a ToolGroupComponent, drop the group's memo so
	// settled headers/counts/child lines can't go stale after a child update.
	// Only the parent group is touched — we do NOT cascade invalidate siblings.
	const parent = (value as any)[COMPONENT_PARENT];
	if (isToolGroupComponent(parent)) parent.invalidate();
}

function unrefTimer(timer: ReturnType<typeof setTimeout> | null | undefined): void {
	(timer as any)?.unref?.();
}

function safeInvalidate(ctx: any): void {
	try {
		if (typeof ctx?.invalidate === "function") ctx.invalidate();
	} catch {
		// Tool render contexts may outlive their row during reload/session switches.
	}
}

const ASSISTANT_PATCH_FLAG = Symbol.for("sycode-ui:patched-assistant-message");
const ASSISTANT_RENDER_PATCH_FLAG = Symbol.for("sycode-ui:patched-assistant-message-render");
const TOOL_EXECUTION_PATCH_FLAG = Symbol.for("sycode-ui:patched-tool-execution");

// Rendered-output cache for assistant/user/custom message components.
// Keyed by (width, branch visual epoch, tool background mode). The epoch changes on
// theme / /cc-tools branch / /cc-theme rebinds; the mode is included because subagent
// (custom-message) framing follows `toolBackgroundMode` via frameToolLikeLines. This avoids
// re-running the per-line ANSI stripping (applyTerminalCopyZones, normalizeLeadingCheckGlyph,
// border boxing) on every scroll/expand re-render — the dominant CPU cost on long chats,
// scaling linearly with chat length.
// Correctness:
//  - UserMessageComponent content is immutable after construction, so output is deterministic
//    given (width, epoch, mode).
//  - AssistantMessageComponent rebuilds children only via updateContent(), which clears the cache.
//  - CustomMessageComponent rebuilds children only via rebuild(), which clears the cache.
// The returned arrays are only ever spread-copied by Container.render (never mutated in place),
// so sharing the cached array reference across renders is safe.
const MESSAGE_RENDER_CACHE = Symbol.for("sycode-ui:message-render-cache");

function messageRenderCacheHit(thisArg: any, width: number): string[] | null {
	const cache = thisArg?.[MESSAGE_RENDER_CACHE];
	if (
		cache
		&& cache.width === width
		&& cache.epoch === _toolBranchVisualEpoch
		&& cache.mode === toolBackgroundMode
		&& Array.isArray(cache.lines)
	) {
		return cache.lines;
	}
	return null;
}

function storeMessageRenderCache(thisArg: any, width: number, lines: string[]): string[] {
	if (thisArg && typeof thisArg === "object") {
		thisArg[MESSAGE_RENDER_CACHE] = {
			width,
			epoch: _toolBranchVisualEpoch,
			mode: toolBackgroundMode,
			lines,
		};
	}
	return lines;
}

function clearMessageRenderCache(thisArg: any): void {
	if (thisArg && typeof thisArg === "object") thisArg[MESSAGE_RENDER_CACHE] = undefined;
}
const OSC133_ZONE_START = "\x1b]133;A\x07";
const OSC133_ZONE_END = "\x1b]133;B\x07";
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
const WORKED_DURATION_KEY = "_piClaudeStyleWorkedDurationMs";
const WORKED_START_KEY = "_piClaudeStyleWorkedStartMs";
const WORKED_SESSION_TOTAL_KEY = "_piClaudeStyleWorkedSessionTotalMs";
const WORKED_TURNS_KEY = "_piClaudeStyleWorkedTurns";
const WORKED_DURATION_MARKER = "Turn took";
const THINKING_DURATION_KEY = "_piClaudeStyleThinkingDurationMs";
const THINKING_ACTIVE_KEY = "_piClaudeStyleThinkingActive";
const MIN_THINKING_SUMMARY_MS = 100;

let lastThinkingBlockDurationMs: number | undefined;
let thinkingBlockStartMs = 0;
/** True from thinking_start until thinking_end on the current assistant stream. */
let thinkingBlockInFlight = false;
// WORKED_LINE_FG is theme-derived (from "muted") when themeAdaptive is on.
let WORKED_LINE_FG = "\x1b[38;2;140;140;140m";
let currentAgentWorkStartMs: number | undefined;
let currentAssistantMessageStartMs: number | undefined;
// Session-wide accumulators for the "Turn took … (Total time … · N turns)" line.
// Seeded from the `context` event (which carries the full message history,
// including resumed sessions) so totals reflect the whole session, not just the
// current process. `userTurnCount` counts role==="user" messages (= prompts sent).
let sessionStartMs: number | undefined;
let userTurnCount = 0;

function formatWorkedDuration(ms: number): string {
	const safeMs = Math.max(0, Number.isFinite(ms) ? ms : 0);
	if (safeMs < 60_000) {
		return `${Math.max(0, Math.floor(safeMs / 1000))}s`;
	}
	let days = Math.floor(safeMs / 86_400_000);
	let hours = Math.floor((safeMs % 86_400_000) / 3_600_000);
	let minutes = Math.floor((safeMs % 3_600_000) / 60_000);
	let seconds = Math.round((safeMs % 60_000) / 1000);
	if (seconds === 60) {
		seconds = 0;
		minutes++;
	}
	if (minutes === 60) {
		minutes = 0;
		hours++;
	}
	if (hours === 24) {
		hours = 0;
		days++;
	}
	if (days > 0) return `${days}d ${hours}h ${minutes}m`;
	if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
	return `${minutes}m ${seconds}s`;
}

function formatThoughtDuration(ms: number): string {
	const safeMs = Math.max(0, Number.isFinite(ms) ? ms : 0);
	if (safeMs < 60_000) return `${Math.max(1, Math.round(safeMs / 1000))}s`;
	return formatWorkedDuration(safeMs);
}

/** Session-total duration: seconds are always shown; minutes and hours are
 *  added only once the session has actually lasted that long.
 *  e.g. 45s, 12m 30s, 1h 12m 30s. */
function formatSessionTotal(ms: number): string {
	const safeMs = Math.max(0, Number.isFinite(ms) ? ms : 0);
	const totalSeconds = Math.floor(safeMs / 1000);
	const seconds = totalSeconds % 60;
	const totalMinutes = Math.floor(totalSeconds / 60);
	const minutes = totalMinutes % 60;
	const hours = Math.floor(totalMinutes / 60);
	if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
	if (totalMinutes > 0) return `${minutes}m ${seconds}s`;
	return `${seconds}s`;
}

function pluralizeTurns(n: number): string {
	return `${n} turn${n === 1 ? "" : "s"}`;
}

function thinkingSummaryStyledText(body: string): string {
	// Preserve the visible thinking text column while omitting ∴ when collapsed.
	return `   ${WORKED_LINE_FG}${body}${RESET}`;
}

function thinkingActiveSummaryText(): string {
	return thinkingSummaryStyledText("Thinking…");
}

function thoughtDurationSummaryText(ms: number): string {
	return thinkingSummaryStyledText(`Thought for ${formatThoughtDuration(ms)}`);
}

/** Single-line hidden thinking row — no Text paddingX or thinking symbol. */
class HiddenThinkingSummary {
	private summaryText: string;
	private cachedWidth?: number;
	private cachedLines?: string[];

	constructor(summaryText: string) {
		this.summaryText = summaryText;
	}

	setSummary(summaryText: string): void {
		this.summaryText = summaryText;
		this.invalidate();
	}

	invalidate(): void {
		this.cachedWidth = undefined;
		this.cachedLines = undefined;
	}

	render(width: number): string[] {
		if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
		const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
		if (safeWidth <= 0) {
			this.cachedWidth = width;
			this.cachedLines = [""];
			return this.cachedLines;
		}
		const line = padRenderedLineToWidth(this.summaryText, safeWidth);
		this.cachedWidth = width;
		this.cachedLines = [line];
		return this.cachedLines;
	}
}

function getMessageThinkingDurationMs(message: any): number {
	const stored = (message as any)?.[THINKING_DURATION_KEY];
	if (typeof stored === "number" && stored > 0) return stored;
	if (typeof lastThinkingBlockDurationMs === "number" && lastThinkingBlockDurationMs > 0) {
		return lastThinkingBlockDurationMs;
	}
	if (typeof (message as any)?.[WORKED_DURATION_KEY] === "number" && (message as any)[WORKED_DURATION_KEY] > 0) {
		return (message as any)[WORKED_DURATION_KEY];
	}
	let totalChars = 0;
	if (Array.isArray(message?.content)) {
		for (const block of message.content) {
			if (block?.type === "thinking" && typeof block.thinking === "string") {
				totalChars += block.thinking.length;
			}
		}
	}
	return Math.max(1000, Math.round((totalChars / 150) * 1000));
}

function assistantMessageThinkingComplete(this: any, message: any): boolean {
	return isAssistantThinkingComplete(this, message);
}

function hiddenThinkingSummaryForMessage(message: any, comp?: any): string {
	if (message && !isAssistantThinkingComplete(comp, message) && isLiveThinkingMessage(comp, message)) {
		return thinkingActiveSummaryText();
	}
	const durationMs = getMessageThinkingDurationMs(message);
	if (message && typeof message === "object") {
		(message as any)[THINKING_DURATION_KEY] = durationMs;
	}
	return thoughtDurationSummaryText(durationMs);
}

function isHiddenThinkingPlaceholderText(child: unknown): child is InstanceType<typeof Text> {
	if (!isTextComponent(child)) return false;
	const plain = stripAnsi(String((child as any).text ?? "")).trim();
	if (/^[✻∴]\s*Thinking/i.test(plain)) return true;
	if (/^[✻∴]\s*Thought for/i.test(plain)) return true;
	if (/^Thought for\b/i.test(plain)) return true;
	if (/^Thinking\.\.\.$/i.test(plain)) return true;
	if (/^Thinking…$/i.test(plain)) return true;
	return /^Thinking:?\s*$/i.test(plain);
}

function messageHasThinkingContent(message: any): boolean {
	return Array.isArray(message?.content)
		&& message.content.some((block: any) => block?.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim());
}

function workedDurationText(ms: number, sessionTotalMs?: number, turns?: number): string {
	let text = `${WORKED_LINE_FG}✻ Turn took ${formatWorkedDuration(ms)}`;
	if (typeof sessionTotalMs === "number" && typeof turns === "number" && turns > 0) {
		text += ` (Total time ${formatSessionTotal(sessionTotalMs)} · ${pluralizeTurns(turns)})`;
	}
	return `${text}${RESET}`;
}

function isWorkedDurationLine(line: string): boolean {
	return line.includes(WORKED_DURATION_MARKER) && /^✻ Turn took [^\r\n]+$/.test(stripAnsi(line).trim());
}

function stripWorkedDurationLine(text: string): string {
	if (!text.includes(WORKED_DURATION_MARKER)) return text;
	return text
		.split(/\r?\n/)
		.filter((line) => !isWorkedDurationLine(line))
		.join("\n")
		.replace(/\n{3,}/g, "\n\n");
}

function hasWorkedDurationLine(message: any): boolean {
	if (!Array.isArray(message?.content)) return false;
	return message.content.some((block: any) => {
		if (block?.type !== "text" || typeof block.text !== "string" || !block.text.includes(WORKED_DURATION_MARKER)) return false;
		return block.text.split(/\r?\n/).some(isWorkedDurationLine);
	});
}

type MarkdownThemeLike = ConstructorParameters<typeof Markdown>[3];

type ParagraphSegment =
	| { kind: "markdown"; md: InstanceType<typeof Markdown> }
	| { kind: "math"; raw: string };

interface MathDelimiter {
	open: string;
	close: string;
}

const DISPLAY_MATH_DELIMITERS: MathDelimiter[] = [
	{ open: "\\[", close: "\\]" },
	{ open: "$$", close: "$$" },
	{ open: "\\begin{equation}", close: "\\end{equation}" },
	{ open: "\\begin{equation*}", close: "\\end{equation*}" },
	{ open: "\\begin{align}", close: "\\end{align}" },
	{ open: "\\begin{align*}", close: "\\end{align*}" },
	{ open: "\\begin{aligned}", close: "\\end{aligned}" },
];

const MATH_COMMANDS: Record<string, string> = {
	alpha: "α", beta: "β", gamma: "γ", Gamma: "Γ", delta: "δ", Delta: "Δ",
	epsilon: "ε", varepsilon: "ε", zeta: "ζ", eta: "η", theta: "θ", Theta: "Θ",
	vartheta: "ϑ", iota: "ι", kappa: "κ", lambda: "λ", Lambda: "Λ", mu: "μ",
	nu: "ν", xi: "ξ", Xi: "Ξ", pi: "π", Pi: "Π", rho: "ρ", varrho: "ϱ",
	sigma: "σ", Sigma: "Σ", tau: "τ", upsilon: "υ", Upsilon: "Υ", phi: "φ",
	varphi: "φ", Phi: "Φ", chi: "χ", psi: "ψ", Psi: "Ψ", omega: "ω", Omega: "Ω",
	pm: "±", mp: "∓", times: "×", cdot: "·", div: "÷", ast: "*", le: "≤", leq: "≤",
	ge: "≥", geq: "≥", neq: "≠", ne: "≠", approx: "≈", sim: "∼", propto: "∝",
	infty: "∞", partial: "∂", nabla: "∇", sum: "Σ", prod: "Π", int: "∫", sqrt: "√",
	to: "→", rightarrow: "→", leftarrow: "←", leftrightarrow: "↔", in: "∈", notin: "∉",
	cup: "∪", cap: "∩", subset: "⊂", subseteq: "⊆", superset: "⊃", superseteq: "⊇",
	wedge: "∧", vee: "∨", forall: "∀", exists: "∃", emptyset: "∅", degree: "°",
};

const COPY_SAFE_MARKDOWN_LINKS_FLAG = Symbol.for("sycode-ui:copy-safe-markdown-links");

/** Unordered list marker: monochrome ◉ (fisheye) instead of "- " (thinking blocks skip this). */
function assistantListBulletMarker(marker: string): string {
	if (marker.startsWith("- ")) return `◉ ${marker.slice(2)}`;
	return marker;
}

function copySafeMarkdownTheme(theme: MarkdownThemeLike): MarkdownThemeLike {
	const listBullet = theme.listBullet;
	return {
		...theme,
		link: (text: string) => stripAnsi(text),
		linkUrl: (text: string) => stripAnsi(text),
		listBullet: listBullet
			? (marker: string) => listBullet(assistantListBulletMarker(marker))
			: (marker: string) => assistantListBulletMarker(marker),
	};
}

function makeMarkdownLinksCopySafe(markdown: InstanceType<typeof Markdown>): void {
	const markdownAny = markdown as any;
	if (markdownAny[COPY_SAFE_MARKDOWN_LINKS_FLAG] || !markdownAny.theme) return;
	markdownAny.theme = copySafeMarkdownTheme(markdownAny.theme);
	markdownAny[COPY_SAFE_MARKDOWN_LINKS_FLAG] = true;
	markdown.invalidate?.();
}

function codeSpan(text: string): string {
	const safe = text.replace(/`/g, "′");
	return `\`${safe}\``;
}

function looksLikeInlineMath(text: string): boolean {
	return /\\[A-Za-z]+|[_^=<>+*/-]/.test(text) && /[A-Za-z0-9}]/.test(text);
}

function hasInlineMathMarkers(text: string): boolean {
	return text.includes("\\(") || text.includes("$");
}

// Magic Context prefixes live assistant text with §N§ while the response is
// streaming and removes that metadata on message_end. Keep the transient tag
// out of the display without mutating the message used by context management.
const MAGIC_CONTEXT_TAG_LINE_PREFIX = /(^|\r?\n)[ \t]*(?:§\d+§[ \t]*)+/g;

function stripTransientMagicContextTags(text: string): string {
	return text.replace(MAGIC_CONTEXT_TAG_LINE_PREFIX, "$1");
}

// Tool results can carry transient Magic Context tags too (live-prefixed output
// chunks). Renderers must see sanitized text WITHOUT mutating this.result — the
// result object is the stored message used by context management. Clone blocks
// only when a tag is actually present so the common path stays zero-cost.
function sanitizeToolResultForDisplay(result: any): any {
	if (!result || !Array.isArray(result.content)) return result;
	let changed = false;
	const content = result.content.map((block: any) => {
		if (block && typeof block.text === "string") {
			const stripped = stripTransientMagicContextTags(block.text);
			if (stripped !== block.text) {
				changed = true;
				return { ...block, text: stripped };
			}
		}
		return block;
	});
	return changed ? { ...result, content } : result;
}

// Last-resort display scrubber at the terminal writer choke point. Every
// rendered frame — every component, overlay, preview, and search hit — exits
// through ProcessTerminal.write, so stripping complete §N§ tokens there covers
// any surface the targeted strips above can't reach, including mid-sentence
// tag references replayed from old tool output on resume. Display only:
// storage, LLM context, copy/paste sources, and ANSI sequences are untouched
// (tags are plain characters; escape sequences never contain them).
const MAGIC_CONTEXT_TAG_TOKEN = /§\d+§/g;
const TERMINAL_SCRUB_PATCH_FLAG = Symbol.for("sycode-ui:terminal-write-tag-scrub");

function patchTerminalWriteTagScrubber(): void {
	const proto = (ProcessTerminal as any)?.prototype;
	if (!proto || proto[TERMINAL_SCRUB_PATCH_FLAG]) return;
	const originalWrite = proto.write;
	if (typeof originalWrite !== "function") return;
	proto.write = function patchedTerminalWrite(this: any, data: any, ...rest: any[]) {
		if (typeof data === "string" && data.includes("§")) {
			data = data.replace(MAGIC_CONTEXT_TAG_TOKEN, "");
		}
		return originalWrite.call(this, data, ...rest);
	};
	proto[TERMINAL_SCRUB_PATCH_FLAG] = true;
}

function replaceInlineMath(text: string): string {
	if (!hasInlineMathMarkers(text)) return text;
	const withParens = text.replace(/\\\(([\s\S]*?)\\\)/g, (_match, body: string) => {
		return codeSpan(formatMathForDisplay(body, false));
	});
	return withParens.replace(/(^|[^\\])\$([^\n$]{1,200})\$/g, (match, prefix: string, body: string) => {
		if (!looksLikeInlineMath(body)) return match;
		return `${prefix}${codeSpan(formatMathForDisplay(body, false))}`;
	});
}

interface MathBlock {
	index: number;
	contentStart: number;
	contentEnd: number;
	endIndex: number;
}

function findNextDelimitedMathBlock(text: string, start: number): MathBlock | undefined {
	let best: MathBlock | undefined;
	for (const delimiter of DISPLAY_MATH_DELIMITERS) {
		const index = text.indexOf(delimiter.open, start);
		if (index === -1) continue;
		const contentStart = index + delimiter.open.length;
		const contentEnd = text.indexOf(delimiter.close, contentStart);
		if (contentEnd === -1) continue;
		if (!best || index < best.index) {
			best = { index, contentStart, contentEnd, endIndex: contentEnd + delimiter.close.length };
		}
	}
	return best;
}

function looksLikeDisplayMath(text: string): boolean {
	return /\\[A-Za-z]+|[_^=<>+*/|]/.test(text) && /[A-Za-z0-9}]/.test(text);
}

function findNextLooseBracketMathBlock(text: string, start: number): MathBlock | undefined {
	const openRe = /(^|\r?\n)[ \t]*\[[ \t]*(?:\r?\n)/g;
	openRe.lastIndex = start;
	let openMatch: RegExpExecArray | null;
	while ((openMatch = openRe.exec(text))) {
		const index = openMatch.index + openMatch[1].length;
		const contentStart = openMatch.index + openMatch[0].length;
		const closeRe = /(^|\r?\n)[ \t]*\][ \t]*(?=\r?\n|$)/g;
		closeRe.lastIndex = contentStart;
		const closeMatch = closeRe.exec(text);
		if (!closeMatch) return undefined;
		const contentEnd = closeMatch.index + closeMatch[1].length;
		const raw = text.slice(contentStart, contentEnd).trim();
		if (looksLikeDisplayMath(raw)) {
			return { index, contentStart, contentEnd, endIndex: closeMatch.index + closeMatch[0].length };
		}
		openRe.lastIndex = contentStart;
	}
	return undefined;
}

function hasDisplayMathMarkers(text: string): boolean {
	return text.includes("\\[") || text.includes("$$") || text.includes("\\begin{") || /(^|\n)[ \t]*\[[ \t]*(?:\r?\n)/.test(text);
}

function shouldScanLooseBracketMath(text: string): boolean {
	return text.length < 20_000 && /(^|\n)[ \t]*\[[ \t]*(?:\r?\n)/.test(text);
}

function findNextDisplayMathBlock(text: string, start: number, scanLoose: boolean): MathBlock | undefined {
	const delimited = findNextDelimitedMathBlock(text, start);
	const loose = scanLoose ? findNextLooseBracketMathBlock(text, start) : undefined;
	if (!delimited) return loose;
	if (!loose) return delimited;
	return loose.index < delimited.index ? loose : delimited;
}

function looksLikeMarkdownDocument(text: string): boolean {
	if (/\r?\n\s*\r?\n/.test(text)) return true;
	if (/^#{1,6}\s/m.test(text) || /```/.test(text)) return true;
	if (/^\s*[-*+]\s/m.test(text) || /^\s*\d+\.\s/m.test(text)) return true;
	if (/\|[^|\n]+\|/.test(text)) return true;
	if (/https?:\/\//.test(text)) return true;
	return false;
}

function shouldFormatStandaloneMath(text: string): boolean {
	const plain = text.trim();
	if (!plain || !plain.includes("\\")) return false;
	// Whole assistant paragraphs must stay markdown; misclassifying them collapses newlines.
	if (looksLikeMarkdownDocument(text)) return false;
	if (plain.length > 600 || plain.split(/\r?\n/).length > 3) return false;
	if (/\\(?:frac|dfrac|tfrac|sqrt|left|right|begin|end|partial|boldsymbol|bm|mathrm|mathbf|mathit|mathsf|mathtt|mathbb|sigma|epsilon|delta|gamma|Gamma|Delta|theta|Theta|pi|Pi|rho|varrho|tau|phi|varphi|Psi|psi|omega|Omega|alpha|beta|mu|nu|xi|chi|sum|prod|int|to|rightarrow|leftarrow|leftrightarrow|infty)/.test(plain)) {
		return true;
	}
	// Paths like \opencode and prose with "=" are not math; require tight expression shape.
	if (!/[_^]/.test(plain) || !/\\[A-Za-z]+/.test(plain)) return false;
	return !/[.!?]\s/.test(plain) && plain.length <= 240;
}

function appendMarkdownSegment(segments: ParagraphSegment[], text: string, theme: MarkdownThemeLike): void {
	if (!text.trim()) return;
	const normalized = shouldFormatStandaloneMath(text) ? formatMathForDisplay(text, false) : replaceInlineMath(text);
	segments.push({ kind: "markdown", md: new Markdown(normalized, 0, 0, theme) });
}

function buildParagraphSegments(text: string, theme: MarkdownThemeLike): ParagraphSegment[] {
	const segments: ParagraphSegment[] = [];
	if (!hasDisplayMathMarkers(text)) {
		appendMarkdownSegment(segments, text, theme);
		return segments;
	}
	const scanLoose = shouldScanLooseBracketMath(text);
	let cursor = 0;
	while (cursor < text.length) {
		const next = findNextDisplayMathBlock(text, cursor, scanLoose);
		if (!next) break;
		appendMarkdownSegment(segments, text.slice(cursor, next.index), theme);
		const raw = text.slice(next.contentStart, next.contentEnd).trim();
		if (raw) segments.push({ kind: "math", raw });
		cursor = next.endIndex;
	}
	appendMarkdownSegment(segments, text.slice(cursor), theme);
	return segments;
}

function replaceSimpleCommandGroups(text: string): string {
	return text
		.replace(/\\(?:text|mathrm|operatorname|mathbf|boldsymbol|bm|mathit|mathsf|mathtt)\{([^{}]*)\}/g, "$1")
		.replace(/\\(?:boldsymbol|bm)\s+([A-Za-z])/g, "$1")
		.replace(/\\mathbb\{R\}/g, "ℝ")
		.replace(/\\mathbb\{N\}/g, "ℕ")
		.replace(/\\mathbb\{Z\}/g, "ℤ")
		.replace(/\\mathbb\{Q\}/g, "ℚ")
		.replace(/\\mathbb\{C\}/g, "ℂ");
}

function readLatexGroup(text: string, start: number): { content: string; end: number } | undefined {
	let open = start;
	while (open < text.length && /\s/.test(text[open])) open++;
	if (text[open] !== "{") return undefined;
	let depth = 1;
	for (let index = open + 1; index < text.length; index++) {
		const char = text[index];
		if (char === "{") depth++;
		else if (char === "}") {
			depth--;
			if (depth === 0) return { content: text.slice(open + 1, index), end: index + 1 };
		}
	}
	return undefined;
}

function replaceFractions(text: string): string {
	let output = "";
	let index = 0;
	while (index < text.length) {
		const command = ["\\frac", "\\dfrac", "\\tfrac"].find((candidate) => text.startsWith(candidate, index));
		if (!command) {
			output += text[index];
			index++;
			continue;
		}
		const numerator = readLatexGroup(text, index + command.length);
		const denominator = numerator ? readLatexGroup(text, numerator.end) : undefined;
		if (!numerator || !denominator) {
			output += text[index];
			index++;
			continue;
		}
		output += `(${replaceFractions(numerator.content)})/(${replaceFractions(denominator.content)})`;
		index = denominator.end;
	}
	return output;
}

function replaceMathCommands(text: string): string {
	return text.replace(/\\([A-Za-z]+)/g, (match, name: string) => MATH_COMMANDS[name] ?? match);
}

function formatMathForDisplay(raw: string, multiline = true): string {
	let text = raw.replace(/\\\\/g, "\n");
	text = text.replace(/\\(?:begin|end)\{[^{}]+\}/g, "").replace(/&/g, "");
	text = replaceSimpleCommandGroups(text);
	text = replaceFractions(text);
	text = text.replace(/\\sqrt\s*\{([^{}]+)\}/g, "√($1)");
	text = replaceMathCommands(text);
	text = text.replace(/\\(?:left|right|big|Big|bigg|Bigg)/g, "");
	text = text.replace(/_\{([^{}]+)\}/g, "_$1").replace(/\^\{([^{}]+)\}/g, "^$1");
	text = text.replace(/[{}]/g, "").replace(/\\[,;!]/g, " ").replace(/\\ /g, " ");
	text = text.replace(/[ \t]+/g, " ").replace(/\s*([=+\-×·÷<>≤≥≈≠|])\s*/g, " $1 ");
	const lines = text.split(/\r?\n/).map((line) => line.replace(/[ \t]{2,}/g, " ").trim()).filter(Boolean);
	return multiline ? lines.join("\n") : lines.join(" ");
}

function renderMathBlock(raw: string, width: number, theme: MarkdownThemeLike): string[] {
	const safeWidth = Math.max(12, width);
	const formatted = formatMathForDisplay(raw, true) || raw;
	return formatted
		.split("\n")
		.flatMap((line) => wrapTextWithAnsi(theme.bold(line), safeWidth));
}

class DottedParagraph {
	private segments: ParagraphSegment[];
	private markdownTheme: MarkdownThemeLike;
	private cachedWidth?: number;
	private cachedLines?: string[];

	constructor(text: string, markdownTheme: MarkdownThemeLike) {
		this.markdownTheme = copySafeMarkdownTheme(markdownTheme);
		this.segments = buildParagraphSegments(stripTransientMagicContextTags(text), this.markdownTheme);
	}

	invalidate(): void {
		this.cachedWidth = undefined;
		this.cachedLines = undefined;
		for (const segment of this.segments) {
			if (segment.kind === "markdown") segment.md.invalidate();
		}
	}

	render(width: number): string[] {
		if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
		const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
		if (safeWidth <= 0) {
			this.cachedWidth = width;
			this.cachedLines = [""];
			return this.cachedLines;
		}
		// " ● " = 1 margin + dot + space = 3 visible chars
		const PREFIX_W = 3;
		if (safeWidth <= PREFIX_W) {
			this.cachedWidth = width;
			this.cachedLines = [clampLineWidth(" ● ", safeWidth)];
			return this.cachedLines;
		}
		const contentWidth = safeWidth - PREFIX_W;
		const lines = this.segments.flatMap((segment) => {
			return segment.kind === "math"
				? renderMathBlock(segment.raw, contentWidth, this.markdownTheme)
				: sanitizeRenderedTextBlockLines(segment.md.render(contentWidth), contentWidth);
		});
		const looksLikeTaskStatus = lines.some((line) => /\b(?:transcript:|No output\.|Wrapped up)/.test(stripAnsi(line)));
		const displayLines = looksLikeTaskStatus ? lines.map(normalizeLeadingCheckGlyph) : lines;
		let dotPlaced = false;
		const rendered = displayLines.map((line: string) => {
			if (!stripAnsi(line).trim()) return `   ${line}`;
			if (isCodeBoxChromeLine(line)) return `   ${line}`;
			if (!dotPlaced) {
				dotPlaced = true;
				return ` ● ${line}`;
			}
			return `   ${line}`;
		}).map((line) => {
			const gap = safeWidth - visibleWidth(line);
			return gap > 0 ? line + " ".repeat(gap) : gap < 0 ? truncateToWidth(line, safeWidth, "", false) : line;
		});
		this.cachedWidth = width;
		this.cachedLines = rendered;
		return rendered;
	}
}

function replaceHiddenThinkingPlaceholders(container: { children?: any[] }, message: any): void {
	if (!container?.children) return;
	const summary = hiddenThinkingSummaryForMessage(message);
	let firstReplaced = false;
	for (let i = 0; i < container.children.length; i++) {
		const child = container.children[i];
		const inner = (child as any)?.child ?? child;
		if (inner instanceof HiddenThinkingSummary || (inner as any)?.constructor?.name === "HiddenThinkingSummary") {
			if (!firstReplaced) {
				inner.setSummary(summary);
				firstReplaced = true;
			} else {
				container.children.splice(i, 1);
				i--;
			}
			continue;
		}
		if (isHiddenThinkingPlaceholderText(inner)) {
			if (!firstReplaced) {
				const summaryComp = new HiddenThinkingSummary(summary);
				if ((child as any)?.child !== undefined) {
					(child as any).child = summaryComp;
				} else {
					container.children[i] = summaryComp;
				}
				firstReplaced = true;
			} else {
				container.children.splice(i, 1);
				i--;
			}
		}
	}
}

class ThinkingParagraph {
	private text: string;
	private cachedWidth?: number;
	private cachedLines?: string[];
	private chromeEpoch = -1;

	constructor(
		text: string,
		_markdownTheme: ConstructorParameters<typeof Markdown>[3],
		_defaultTextStyle?: ConstructorParameters<typeof Markdown>[4],
	) {
		this.text = stripTransientMagicContextTags(text);
	}

	private thinkingMarkdown(): InstanceType<typeof Markdown> {
		const DIM_FG = WORKED_LINE_FG;
		const wrap = (s: string) => `${DIM_FG}${s}`;
		const wrapPlain = (s: string) => wrap(stripAnsi(s));
		const plainTheme: ConstructorParameters<typeof Markdown>[3] = {
			heading: wrap,
			link: wrapPlain,
			linkUrl: wrapPlain,
			code: wrap,
			codeBlock: wrap,
			codeBlockBorder: wrap,
			quote: wrap,
			quoteBorder: wrap,
			hr: wrap,
			listBullet: (marker: string) => wrap(marker),
			bold: wrap,
			italic: wrap,
			strikethrough: wrap,
			underline: wrap,
			highlightCode: (code: string, _lang?: string) => code.split("\n").map((line) => `${DIM_FG}${line}`),
		};
		const plainStyle: ConstructorParameters<typeof Markdown>[4] = {
			italic: false,
			color: (s: string) => `${DIM_FG}${s}`,
		};
		return new Markdown(this.text, 0, 0, plainTheme, plainStyle);
	}

	invalidate(): void {
		this.cachedWidth = undefined;
		this.cachedLines = undefined;
		this.chromeEpoch = -1;
	}

	render(width: number): string[] {
		if (
			this.cachedLines
			&& this.cachedWidth === width
			&& this.chromeEpoch === _toolBranchVisualEpoch
		) {
			return this.cachedLines;
		}
		const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
		if (safeWidth <= 0) {
			this.cachedWidth = width;
			this.cachedLines = [""];
			this.chromeEpoch = _toolBranchVisualEpoch;
			return this.cachedLines;
		}
		const md = this.thinkingMarkdown();
		// " ∴ " = 1 margin + symbol + space = 3 visible chars
		const PREFIX_W = 3;
		const prefix = `${WORKED_LINE_FG}∴${RESET}`;
		if (safeWidth <= PREFIX_W) {
			this.cachedWidth = width;
			this.cachedLines = [clampLineWidth(` ${prefix} `, safeWidth)];
			return this.cachedLines;
		}
		const lines = sanitizeRenderedTextBlockLines(md.render(safeWidth - PREFIX_W), safeWidth - PREFIX_W);
		let symbolPlaced = false;
		const rendered = lines.map((line: string) => {
			if (!symbolPlaced && stripAnsi(line).trim()) {
				symbolPlaced = true;
				return ` ${prefix} ${line}`;
			}
			return `   ${line}`;
		}).map((line) => clampLineWidth(line, safeWidth));
		this.cachedWidth = width;
		this.cachedLines = rendered;
		this.chromeEpoch = _toolBranchVisualEpoch;
		return rendered;
	}
}

function trimRenderedBlankLines(lines: string[]): string[] {
	let start = 0;
	while (start < lines.length && isBlankLine(lines[start])) start++;
	let end = lines.length - 1;
	while (end >= start && isBlankLine(lines[end])) end--;
	return start <= end ? lines.slice(start, end + 1) : [];
}

function isSubagentNotificationMessage(message: unknown): boolean {
	const candidate = message as Record<string, unknown> | undefined;
	return candidate?.customType === "subagent-notification";
}

function isSubagentHeaderLine(line: string): boolean {
	return /^[✓✔✗■●]\s+/.test(stripAnsi(line).trimStart());
}

function isSubagentDetailLine(line: string): boolean {
	const plain = stripAnsi(line).trimStart();
	return plain.startsWith("⎿")
		|| plain.startsWith("transcript:")
		|| plain === "No output."
		|| /^(?:Done|Wrapped up|Stopped|Error:|Aborted)\b/.test(plain);
}

function cleanSubagentDetailLine(line: string): string {
	const markerIndex = line.indexOf("⎿");
	if (markerIndex !== -1) {
		const prefixAnsi = (line.slice(0, markerIndex).match(ANSI_RE) ?? []).join("");
		return `${prefixAnsi}${line.slice(markerIndex + 1).replace(/^\s+/, "")}`;
	}
	return line
		.replace(/^((?:\x1b\[[0-9;]*m)*)\s{2}/, "$1")
		.replace(/^\s{2}/, "");
}

function formatSubagentNotificationGroup(lines: string[]): string[] {
	if (lines.length === 0) return [];
	const header = normalizeLeadingCheckGlyph(lines[0]);
	const rest = lines.slice(1);
	const detailStart = rest.findIndex(isSubagentDetailLine);
	if (detailStart === -1) {
		return [header, ...rest];
	}

	const metadata = rest.slice(0, detailStart);
	const detailLines = rest.slice(detailStart).map(cleanSubagentDetailLine).filter((line) => stripAnsi(line).trim().length > 0);
	const formattedDetails = withFinalBranchBlock(detailLines.join("\n"), undefined as any).split("\n").filter((line) => line.length > 0);
	return [header, ...metadata, ...formattedDetails];
}

function splitSubagentNotificationGroups(lines: string[]): string[][] {
	const groups: string[][] = [];
	let current: string[] = [];
	for (const line of lines) {
		if (isSubagentHeaderLine(line) && current.length > 0) {
			groups.push(current);
			current = [line];
		} else {
			current.push(line);
		}
	}
	if (current.length > 0) groups.push(current);
	return groups;
}

function frameToolLikeLines(lines: string[], width: number): string[] {
	syncToolBackgroundMode();
	const safeWidth = Math.max(1, width);
	const core = trimRenderedBlankLines(lines).map((line) => clampLineWidth(line, safeWidth));
	if (core.length === 0 || toolBackgroundMode === "default") return core;
	const spacerLine = " ".repeat(safeWidth);
	if (toolBackgroundMode === "outlines") {
		return [spacerLine, borderLine(safeWidth), ...core, borderLine(safeWidth)];
	}
	return [spacerLine, ...core];
}

function formatSubagentNotification(lines: string[], width: number): string[] {
	const core = trimRenderedBlankLines(lines).map(normalizeLeadingCheckGlyph);
	if (core.length === 0) return lines;
	const formatted = splitSubagentNotificationGroups(core).flatMap((group, index) => {
		const groupLines = formatSubagentNotificationGroup(group);
		return index === 0 ? groupLines : ["", ...groupLines];
	});
	const indented = formatted.map((line) => (line ? ` ${line}` : line));
	return frameToolLikeLines(indented, width);
}

function patchCustomMessageRender(): void {
	const proto = CustomMessageComponent.prototype as any;
	if (proto[CUSTOM_MESSAGE_PATCH_FLAG]) return;
	const originalRender = proto.render;
	if (typeof originalRender !== "function") return;
	proto.render = function patchedCustomMessageRender(width: number) {
		// Subagent framing follows `toolBackgroundMode` (via frameToolLikeLines), which
		// can change via /cc-tools or by editing settings.json. Re-sync before the
		// cache check so the mode key reflects the current setting on warm renders too.
		syncToolBackgroundMode();
		const cached = messageRenderCacheHit(this, width);
		if (cached) return cached;
		visitMarkdownDescendants(this, (child) => {
			const markdownAny = child as any;
			if (typeof markdownAny.text === "string") {
				const stripped = stripTransientMagicContextTags(markdownAny.text);
				if (stripped !== markdownAny.text) {
					markdownAny.text = stripped;
					child.invalidate?.();
				}
			}
		});
		const lines = originalRender.call(this, width);
		if (!Array.isArray(lines)) return lines;
		const result = isSubagentNotificationMessage(this?.message)
			? formatSubagentNotification(lines, width)
			: lines.map(normalizeLeadingCheckGlyph);
		return storeMessageRenderCache(this, width, result);
	};
	// CustomMessageComponent rebuilds its children via rebuild() (called from
	// invalidate() and setExpanded()); drop the cached render so the next render
	// reflects the rebuilt content.
	const originalRebuild = proto.rebuild;
	if (typeof originalRebuild === "function") {
		proto.rebuild = function patchedCustomMessageRebuild(...args: any[]) {
			clearMessageRenderCache(this);
			return originalRebuild.apply(this, args);
		};
	}
	proto[CUSTOM_MESSAGE_PATCH_FLAG] = true;
}

function stripOsc133Zones(line: string): string {
	return line
		.replace(OSC133_ZONE_START, "")
		.replace(OSC133_ZONE_END, "")
		.replace(OSC133_ZONE_FINAL, "");
}

function stripBackgroundAnsi(text: string): string {
	return text.replace(/\x1b\[([0-9;]*)m/g, (match, paramsText: string) => {
		const params = paramsText === "" ? ["0"] : paramsText.split(";");
		const kept: string[] = [];
		for (let i = 0; i < params.length; i++) {
			const code = Number(params[i] || "0");
			if (code === 48) {
				const mode = Number(params[i + 1] || "0");
				i += mode === 2 ? 4 : mode === 5 ? 2 : 0;
				continue;
			}
			if (code === 49 || (code >= 40 && code <= 47) || (code >= 100 && code <= 107)) continue;
			kept.push(params[i]);
		}
		return kept.length === 0 ? "" : `\x1b[${kept.join(";")}m`;
	});
}

function roundedUserBorder(width: number, top: boolean): string {
	if (width <= 1) return `${BORDER_COLOR}│${TRANSPARENT_RESET}`;
	const left = top ? "╭" : "╰";
	const right = top ? "╮" : "╯";
	if (!top || width < 10) {
		return `${BORDER_COLOR}${left}${"─".repeat(Math.max(0, width - 2))}${right}${TRANSPARENT_RESET}`;
	}
	const label = `${WORKED_LINE_FG} User ${TRANSPARENT_RESET}`;
	const prefix = "─";
	const suffixWidth = Math.max(0, width - 2 - visibleWidth(prefix) - visibleWidth(label));
	return `${BORDER_COLOR}${left}${prefix}${TRANSPARENT_RESET}${label}${BORDER_COLOR}${"─".repeat(suffixWidth)}${right}${TRANSPARENT_RESET}`;
}

function trimAnsiRight(text: string): string {
	let trimmed = text;
	while (true) {
		const next = trimmed.replace(/[ \t]+((?:\x1b\[[0-9;]*m)*)$/g, "$1");
		if (next === trimmed) return trimmed;
		trimmed = next;
	}
}

function cleanUserMessageLine(line: string): string {
	return `${TRANSPARENT_BG}${trimAnsiRight(stripBackgroundAnsi(stripOsc133Zones(line)))}${TRANSPARENT_BG}`;
}

function borderedUserMessageLine(line: string, width: number): string {
	const innerWidth = Math.max(1, width - 4);
	const content = clampLineWidth(cleanUserMessageLine(line), innerWidth);
	const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
	return `${BORDER_COLOR}│${TRANSPARENT_RESET} ${content}${padding} ${BORDER_COLOR}│${TRANSPARENT_RESET}`;
}

function visitMarkdownDescendants(root: unknown, visit: (md: InstanceType<typeof Markdown>) => void): void {
	if (!root || typeof root !== "object") return;
	const node = root as { children?: unknown[] };
	for (const child of node.children ?? []) {
		if (isMarkdownComponent(child)) visit(child);
		else visitMarkdownDescendants(child, visit);
	}
}

function patchUserMessageRender(): void {
	const proto = UserMessageComponent.prototype as any;
	if (proto[USER_MESSAGE_PATCH_FLAG]) return;
	const originalRender = proto.render;
	if (typeof originalRender !== "function") return;
	proto.render = function patchedUserMessageRender(width: number) {
		const cached = messageRenderCacheHit(this, width);
		if (cached) return cached;
		visitMarkdownDescendants(this, (child) => {
			const markdownAny = child as any;
			if (typeof markdownAny.text === "string") {
				const stripped = stripTransientMagicContextTags(markdownAny.text);
				if (stripped !== markdownAny.text) {
					markdownAny.text = stripped;
					child.invalidate?.();
				}
			}
			makeMarkdownLinksCopySafe(child);
			if (markdownAny.defaultTextStyle?.bgColor) {
				markdownAny.defaultTextStyle.bgColor = undefined;
				child.invalidate?.();
			}
		});
		const borderWidth = Math.max(1, width);
		const contentWidth = Math.max(1, borderWidth - 4);
		const lines = originalRender.call(this, contentWidth);
		if (!Array.isArray(lines) || lines.length === 0) return lines;
		const rendered = [
			roundedUserBorder(borderWidth, true),
			...lines.slice(1, -1).map((line: string) => borderedUserMessageLine(line, borderWidth)),
			roundedUserBorder(borderWidth, false),
		];
		const clamped = rendered.map((line) => clampLineWidth(line, borderWidth));
		return storeMessageRenderCache(this, width, applyTerminalCopyZones(clamped));
	};
	proto[USER_MESSAGE_PATCH_FLAG] = true;
}

function patchAssistantMessages(): void {
	const proto = AssistantMessageComponent.prototype as any;
	if (proto[ASSISTANT_PATCH_FLAG]) return;
	const originalRender = proto.render;
	if (typeof originalRender === "function" && !proto[ASSISTANT_RENDER_PATCH_FLAG]) {
		proto.render = function patchedAssistantMessageRender(width: number) {
			const cached = messageRenderCacheHit(this, width);
			if (cached) return cached;
			visitMarkdownDescendants(this, (child) => {
				const markdownAny = child as any;
				if (typeof markdownAny.text === "string") {
					const stripped = stripTransientMagicContextTags(markdownAny.text);
					if (stripped !== markdownAny.text) {
						markdownAny.text = stripped;
						child.invalidate?.();
					}
				}
			});
			const lines = originalRender.call(this, width);
			if (!Array.isArray(lines) || lines.length === 0) return lines;
			if ((this as any).hasToolCalls) {
				// Tool-call messages skip copy-zone processing, but still benefit from
				// caching the rendered output to avoid re-rendering stable children.
				return storeMessageRenderCache(this, width, lines);
			}
			return storeMessageRenderCache(this, width, applyTerminalCopyZones(lines));
		};
		proto[ASSISTANT_RENDER_PATCH_FLAG] = true;
	}
	const originalUpdateContent = proto.updateContent;
	proto.updateContent = function patchedUpdateContent(message: any, isStreaming?: boolean) {
		// Content changed (also reached via invalidate() → updateContent): drop the
		// cached rendered output so the next render rebuilds with the new children.
		clearMessageRenderCache(this);
		if (!(this as any)[WORKED_START_KEY]) {
			(this as any)[WORKED_START_KEY] = Date.now();
		}
		if (!message || !Array.isArray(message.content)) {
			return originalUpdateContent.call(this, message, isStreaming);
		}
		// Thinking display:
		// When thinking blocks are expanded via Ctrl+T (`hideThinkingBlock === false`),
		// all thinking blocks (old and new) render in full markdown.
		// When thinking blocks are collapsed (`hideThinkingBlock === true`):
		// - "live" mode (default): the actively-streaming thinking block renders
		//   expanded while streaming, and collapses to `Thought for Xs` once done.
		// - "full" mode: behaves like stock pi (stays collapsed).
		const liveMode = getThinkingMode() === "live";
		const thinkingCollapsed = !!(this as any).hideThinkingBlock;
		const showLiveThinking = liveMode && thinkingCollapsed && isLiveThinkingMessage(this, message);
		if (thinkingCollapsed && messageHasThinkingContent(message)) {
			// Pi wraps this in theme.italic/fg again — keep plain label for the placeholder pass.
			(this as any).hiddenThinkingLabel = "Thinking…";
		}
		if (showLiveThinking) (this as any).hideThinkingBlock = false;
		try {
			// Call original to build all children (text, thinking, spacers, errors)
			originalUpdateContent.call(this, message, isStreaming);
		} finally {
			if (showLiveThinking) (this as any).hideThinkingBlock = true;
		}
		// Replace text-block Markdown children with DottedParagraph wrappers
		const container = (this as any).contentContainer;
		if (!container?.children) return;
		if (thinkingCollapsed && !showLiveThinking && messageHasThinkingContent(message)) {
			replaceHiddenThinkingPlaceholders(container, message);
		}
		const mdTheme = (this as any).markdownTheme;
		for (let i = container.children.length - 1; i >= 0; i--) {
			const child = container.children[i];
			const inner = (child as any)?.child ?? child;
			if (isMarkdownComponent(inner)) {
				const text = (inner as any).text;
				if (!text) continue;
				const isThinking = !!(inner as any).defaultTextStyle?.italic;
				if (isThinking) {
					const style = (inner as any).defaultTextStyle;
					const replacement = new ThinkingParagraph(text, mdTheme, style);
					if ((child as any)?.child !== undefined) {
						(child as any).child = replacement;
					} else {
						container.children[i] = replacement;
					}
				} else {
					const replacement = new DottedParagraph(text, mdTheme);
					if ((child as any)?.child !== undefined) {
						(child as any).child = replacement;
					} else {
						container.children[i] = replacement;
					}
				}
			}
		}
		const explicitDuration = (message as any)[WORKED_DURATION_KEY];
		const explicitSessionTotal = (message as any)[WORKED_SESSION_TOTAL_KEY];
		const explicitTurns = (message as any)[WORKED_TURNS_KEY];
		// The "Turn took" line must only appear once the stream has truly closed.
		// `message.stopReason === "stop"` is not a safe "finished" signal here because
		// providers may initialize a live message with that value. The `message_end`
		// handler stamps `explicitDuration` after the final stream event. Render the
		// styled line as a TUI child so ANSI presentation never enters message content
		// or persisted session transcripts.
		const isFinalAssistantMessage = message.stopReason === "stop";
		const workedDuration = typeof explicitDuration === "number" ? explicitDuration : undefined;
		const workedSessionTotal = typeof explicitSessionTotal === "number"
			? explicitSessionTotal
			: typeof sessionStartMs === "number"
				? Date.now() - sessionStartMs
				: undefined;
		const workedTurns = typeof explicitTurns === "number" ? explicitTurns : userTurnCount;
		const hasAssistantText = message.content.some((block: any) => block?.type === "text" && typeof block.text === "string" && block.text.trim());
		if (typeof workedDuration === "number" && isFinalAssistantMessage && hasAssistantText && !hasWorkedDurationLine(message)) {
			container.children.push(new Spacer(1), new Text(workedDurationText(workedDuration, workedSessionTotal, workedTurns), 1, 0));
		}
	};
	proto[ASSISTANT_PATCH_FLAG] = true;
}

const TOOL_BG_PATCH_FLAG = Symbol.for("sycode-ui:patched-tool-bg-sync");

function patchToolExecutionBackgroundSync(): void {
	const proto = ToolExecutionComponent.prototype as any;
	if (proto[TOOL_BG_PATCH_FLAG]) return;
	const originalUpdateDisplay = proto.updateDisplay;
	if (typeof originalUpdateDisplay !== "function") return;
	proto.updateDisplay = function patchedToolBackgroundSync(this: any) {
		syncToolBackgroundMode();
		applyToolBackgroundMode(getGlobalPiTheme());
		return originalUpdateDisplay.apply(this, arguments as any);
	};
	proto[TOOL_BG_PATCH_FLAG] = true;
}

function syncLiveToolRenderState(component: any): void {
	// updateDisplay paints call header BEFORE result. Pre-seed status + live line
	// count so the header's blinking ● and `(N lines)` trail stay in sync with
	// the partial result that is about to render underneath.
	const state = component?.rendererState;
	if (!state || typeof state !== "object") return;
	const ctxLike = {
		state,
		isPartial: component?.isPartial === true,
		executionStarted: component?.executionStarted === true,
		isError: component?.result?.isError === true,
	};
	syncToolCallStatus(ctxLike);
	if (component?.isPartial === true && component?.result) {
		const raw = getTextContent(component.result).replace(/\r\n/g, "\n").trimEnd();
		// tailLimit=0 → count-only (no line materialization) so huge bash tails stay cheap.
		state._liveLineCount = collectNonEmptyLines(raw, 0).total;
	} else if (component?.isPartial !== true) {
		delete state._liveLineCount;
	}
}

function patchToolRenderCacheInvalidation(): void {
	const proto = ToolExecutionComponent.prototype as any;
	if (proto[TOOL_CACHE_PATCH_FLAG]) return;

	const methods = [
		"updateDisplay",
		"updateArgs",
		"markExecutionStarted",
		"setArgsComplete",
		"updateResult",
		"setExpanded",
		"setShowImages",
		"setImageWidthCells",
		"invalidate",
	];

	for (const method of methods) {
		const original = proto[method];
		if (typeof original !== "function") continue;
		proto[method] = function patchedToolMutation(...args: any[]) {
			clearToolRenderCache(this);
			if (method === "updateDisplay" || method === "updateResult" || method === "invalidate") {
				syncLiveToolRenderState(this);
			}
			const result = original.apply(this, args);
			clearToolRenderCache(this);
			return result;
		};
	}

	proto[TOOL_CACHE_PATCH_FLAG] = true;
}

function deleteRenderedKittyImages(component: any): void {
	if (!process.stdout.isTTY || getCapabilities().images !== "kitty" || !Array.isArray(component.imageComponents) || component.imageComponents.length === 0) return;
	try { process.stdout.write(deleteAllKittyImages()); } catch { /* noop */ }
}

function removeImageChildren(component: any): void {
	deleteRenderedKittyImages(component);
	const children = [
		...(Array.isArray(component.imageComponents) ? component.imageComponents : []),
		...(Array.isArray(component.imageSpacers) ? component.imageSpacers : []),
	];
	for (const child of children) {
		try { component.removeChild?.(child); } catch { /* noop */ }
	}
	component.imageComponents = [];
	component.imageSpacers = [];
}

function patchReadImageExpansion(): void {
	const proto = ToolExecutionComponent.prototype as any;
	if (proto[TOOL_IMAGE_EXPAND_PATCH_FLAG]) return;
	const originalUpdateDisplay = proto.updateDisplay;
	if (typeof originalUpdateDisplay !== "function") return;
	proto.updateDisplay = function patchedReadImageUpdateDisplay(...args: any[]) {
		const result = originalUpdateDisplay.apply(this, args);
		const hasImage = Array.isArray(this.result?.content) && this.result.content.some((block: any) => block?.type === "image");
		if (this.toolName === "read" && hasImage && this.expanded !== true) {
			removeImageChildren(this);
			clearToolRenderCache(this);
		}
		return result;
	};
	proto[TOOL_IMAGE_EXPAND_PATCH_FLAG] = true;
}

function patchToolExecutionRenderers(): void {
	const proto = ToolExecutionComponent.prototype as any;
	if (proto[TOOL_EXECUTION_PATCH_FLAG]) return;

	const originalHasRendererDefinition = proto.hasRendererDefinition;
	const originalGetCallRenderer = proto.getCallRenderer;
	const originalGetResultRenderer = proto.getResultRenderer;

	if (typeof originalHasRendererDefinition === "function") {
		proto.hasRendererDefinition = function patchedHasRendererDefinition() {
			return originalHasRendererDefinition.call(this) || shouldUseGenericToolRenderer(this?.toolName);
		};
	}

	proto.getCallRenderer = function patchedGetCallRenderer() {
		const toolName = typeof this?.toolName === "string" ? this.toolName : "";
		if (toolName === "apply_patch") {
			return (args: any, theme: Theme, ctx: any) =>
				renderApplyPatchCall(args, theme, ctx, (path: string) => shortPath(ctx.cwd ?? process.cwd(), path));
		}
		if (shouldUseGenericToolRenderer(toolName)) {
			return (args: any, theme: Theme, ctx: any) => renderGenericToolCall(toolName, args, theme, ctx);
		}
		return typeof originalGetCallRenderer === "function" ? originalGetCallRenderer.call(this) : undefined;
	};

	proto.getResultRenderer = function patchedGetResultRenderer() {
		const toolName = typeof this?.toolName === "string" ? this.toolName : "";
		let renderer: any;
		if (toolName === "apply_patch") {
			renderer = (result: any, options: any, theme: Theme, ctx: any) =>
				renderApplyPatchResult({ content: result.content, details: result.details }, options.isPartial, theme, ctx);
		} else if (shouldUseGenericToolRenderer(toolName)) {
			renderer = (result: any, options: any, theme: Theme, ctx: any) =>
				renderGenericToolResult(toolName, result, options, theme, ctx);
		} else {
			renderer = typeof originalGetResultRenderer === "function" ? originalGetResultRenderer.call(this) : undefined;
		}
		if (typeof renderer !== "function") return renderer;
		// Strip transient Magic Context tags from the text the renderer sees,
		// without touching the stored result message.
		return (result: any, options: any, theme: Theme, ctx: any) => renderer(sanitizeToolResultForDisplay(result), options, theme, ctx);
	};

	// Fallback path for tools without a renderer definition formats raw text.
	const originalFormatToolExecution = proto.formatToolExecution;
	if (typeof originalFormatToolExecution === "function") {
		proto.formatToolExecution = function patchedFormatToolExecution(this: any, ...args: any[]) {
			const formatted = originalFormatToolExecution.apply(this, args);
			return typeof formatted === "string" ? stripTransientMagicContextTags(formatted) : formatted;
		};
	}

	proto[TOOL_EXECUTION_PATCH_FLAG] = true;
}

function shortPath(cwd: string, filePath: string): string {
	if (!filePath) return "";
	const rel = relative(cwd, filePath);
	if (!rel.startsWith("..") && !rel.startsWith("/")) return rel || ".";
	const home = process.env.HOME ?? "";
	return home ? filePath.replace(home, "~") : filePath;
}

// ---------------------------------------------------------------------------
// Status dot — flickers green/gray while pending
// ---------------------------------------------------------------------------

function isBlinkOn(): boolean {
	return Math.floor(Date.now() / 500) % 2 === 0;
}

function toolHeader(tool: string, summary: string, theme: Theme, prefix = "", trailing = ""): string {
	applyThemePaletteIfNeeded(theme);
	const label = theme.fg("toolTitle", theme.bold(tool));
	const body = summary
		? `${label} ${WRAP_MARK}${theme.fg("accent", summary)}`
		: label;
	return trailing ? `${prefix}${body}${trailing}` : `${prefix}${body}`;
}

function liveLineCountTrailing(ctx: any, theme: Theme): string {
	if (ctx?.isPartial !== true) return "";
	const count = ctx?.state?._liveLineCount;
	if (typeof count !== "number" || !Number.isFinite(count) || count <= 0) return "";
	return ` ${theme.fg("muted", `(${lineCountLabel(count)})`)}`;
}

const BASH_STARTED_AT_KEY = "_bashStartedAtMs";
const BASH_ENDED_AT_KEY = "_bashEndedAtMs";

type BashDurationEntry = { invalidate: () => void };

const BASH_DURATION_CONTEXTS = new Map<any, BashDurationEntry>();
let bashDurationTimer: ReturnType<typeof setTimeout> | null = null;

function scheduleBashDurationTick(): void {
	if (bashDurationTimer || BASH_DURATION_CONTEXTS.size === 0) return;
	bashDurationTimer = setTimeout(() => {
		bashDurationTimer = null;
		for (const entry of BASH_DURATION_CONTEXTS.values()) {
			try { entry.invalidate(); } catch { /* noop */ }
		}
		scheduleBashDurationTick();
	}, 1_000);
	unrefTimer(bashDurationTimer);
}

function registerBashDurationContext(ctx: any): void {
	const key = ctx?.state ?? ctx;
	if (!key) return;
	const invalidate = typeof ctx?.invalidate === "function" ? () => safeInvalidate(ctx) : () => {};
	BASH_DURATION_CONTEXTS.set(key, { invalidate });
	scheduleBashDurationTick();
}

function clearBashDurationContext(ctx: any): void {
	const key = ctx?.state ?? ctx;
	if (key) BASH_DURATION_CONTEXTS.delete(key);
	if (bashDurationTimer && BASH_DURATION_CONTEXTS.size === 0) {
		clearTimeout(bashDurationTimer);
		bashDurationTimer = null;
	}
}

function clearAllBashDurationContexts(): void {
	BASH_DURATION_CONTEXTS.clear();
	if (bashDurationTimer) {
		clearTimeout(bashDurationTimer);
		bashDurationTimer = null;
	}
}

function syncBashDuration(ctx: any, isPartial = true): void {
	const state = ctx?.state;
	if (!state) return;
	if (state._toolStatus === "pending" && typeof state[BASH_STARTED_AT_KEY] !== "number") {
		state[BASH_STARTED_AT_KEY] = Date.now();
		delete state[BASH_ENDED_AT_KEY];
	}
	const startedAt = state[BASH_STARTED_AT_KEY];
	if (typeof startedAt !== "number") return;
	if (!isPartial || ctx?.isError) {
		if (typeof state[BASH_ENDED_AT_KEY] !== "number") state[BASH_ENDED_AT_KEY] = Date.now();
		clearBashDurationContext(ctx);
		return;
	}
	registerBashDurationContext(ctx);
}

function bashHeaderTrailing(ctx: any, theme: Theme): string {
	const parts: string[] = [];
	if (ctx?.isPartial === true) {
		const count = ctx?.state?._liveLineCount;
		if (typeof count === "number" && Number.isFinite(count) && count > 0) parts.push(lineCountLabel(count));
	}
	const startedAt = ctx?.state?.[BASH_STARTED_AT_KEY];
	if (typeof startedAt === "number") {
		const endedAt = ctx?.state?.[BASH_ENDED_AT_KEY];
		parts.push(formatBashDuration((typeof endedAt === "number" ? endedAt : Date.now()) - startedAt));
	}
	return parts.length > 0 ? `${TRAILING_MARK}${theme.fg("muted", parts.join(" · "))}` : "";
}

function setToolStatus(ctx: any, status: "pending" | "success" | "error" | "idle"): void {
	if (ctx?.state) ctx.state._toolStatus = status;
}

function syncToolCallStatus(ctx: any): void {
	if (ctx?.isPartial) {
		// Blink only for tools that actually started in the current agent run.
		// History rebuilds (resume, compaction, /tree) leave unmatched tool calls
		// with isPartial=true forever and never set executionStarted — those must
		// render settled, not keep a pending blink alive across sessions.
		const agentLive = currentAgentWorkStartMs !== undefined;
		const started = ctx?.executionStarted === true;
		if (agentLive && started) {
			setToolStatus(ctx, "pending");
			return;
		}
		if (agentLive && !started) {
			// Args still streaming before tool_execution_start — static, no blink.
			setToolStatus(ctx, "idle");
			clearBlinkTimer(ctx);
			return;
		}
		setToolStatus(ctx, "success");
		clearBlinkTimer(ctx);
		return;
	}
	setToolStatus(ctx, ctx.isError ? "error" : "success");
	clearBlinkTimer(ctx);
}

function shouldRevealCallArgs(ctx: any): boolean {
	if (ctx?.argsComplete === true || ctx?.executionStarted === true) return true;
	const args = ctx?.args;
	if (!args || typeof args !== "object") return false;
	return Object.keys(args).some((key) => args[key] !== undefined && args[key] !== null && args[key] !== "");
}

function stableCallSummary(ctx: any, key: string, build: () => string, reveal = shouldRevealCallArgs(ctx)): string {
	const state = ctx?.state;
	const cached = state?.[key];
	const completeKey = `${key}Complete`;
	if (!reveal) return typeof cached === "string" ? cached : "";
	if (ctx?.argsComplete === true && state?.[completeKey] === true && typeof cached === "string") return cached;
	if (!shouldRevealCallArgs(ctx) && typeof cached === "string" && cached) return cached;
	const summary = build();
	if (state) {
		state[key] = summary;
		if (ctx?.argsComplete === true) state[completeKey] = true;
		else delete state[completeKey];
	}
	return summary;
}

function hasOwnArg(args: any, key: string): boolean {
	return !!args && Object.prototype.hasOwnProperty.call(args, key);
}

function fileExistsForTool(cwd: string, filePath: string): boolean {
	if (!filePath) return false;
	try {
		return existsSync(resolve(cwd, filePath));
	} catch {
		return false;
	}
}

const WRITE_EXISTED_BEFORE = new Map<string, boolean>();

interface RtkRewriteRecord {
	original: string;
	rewritten: string;
	notice: string;
}

const RTK_ORIGINAL_BASH_COMMANDS = new Map<string, string>();
const RTK_REWRITES_BY_TOOL_ID = new Map<string, RtkRewriteRecord>();
const RTK_PENDING_REWRITES: RtkRewriteRecord[] = [];
const RTK_PENDING_REWRITE_LIMIT = 20;
const PRESERVED_BASH_PREVIEWS = new Set<string>();
const BASH_PREVIEW_INVALIDATORS = new Map<string, () => void>();

function preserveBashPreview(ctx: any): void {
	const toolCallId = typeof ctx?.toolCallId === "string" ? ctx.toolCallId : undefined;
	if (!toolCallId) return;
	PRESERVED_BASH_PREVIEWS.add(toolCallId);
	if (typeof ctx?.invalidate === "function") {
		BASH_PREVIEW_INVALIDATORS.set(toolCallId, () => safeInvalidate(ctx));
	}
}

function clearPreservedBashPreviews(): void {
	if (PRESERVED_BASH_PREVIEWS.size === 0) return;
	const invalidators = [...PRESERVED_BASH_PREVIEWS]
		.map((toolCallId) => BASH_PREVIEW_INVALIDATORS.get(toolCallId))
		.filter((invalidate): invalidate is () => void => typeof invalidate === "function");
	PRESERVED_BASH_PREVIEWS.clear();
	BASH_PREVIEW_INVALIDATORS.clear();
	for (const invalidate of invalidators) {
		try { invalidate(); } catch { /* noop */ }
	}
}

function shouldPreserveBashPreview(ctx: any): boolean {
	return typeof ctx?.toolCallId === "string" && PRESERVED_BASH_PREVIEWS.has(ctx.toolCallId);
}

function normalizeRtkCommandPreview(command: string): string {
	return command.replace(/\s+/g, " ").trim();
}

function rtkPreviewMatches(command: string, preview: string): boolean {
	const normalized = normalizeRtkCommandPreview(command);
	const normalizedPreview = normalizeRtkCommandPreview(preview);
	if (!normalized || !normalizedPreview) return false;
	if (normalized === normalizedPreview) return true;
	if (normalizedPreview.endsWith("…")) {
		return normalized.startsWith(normalizedPreview.slice(0, -1));
	}
	return normalized.startsWith(normalizedPreview) || normalizedPreview.startsWith(normalized);
}

function parseRtkRewriteNotice(message: string): RtkRewriteRecord | undefined {
	const match = message.match(/^RTK rewrite:\s*(.*?)\s*->\s*(.+)$/s);
	if (!match) return undefined;
	const original = match[1]?.trim() ?? "";
	const rewritten = match[2]?.trim() ?? "";
	if (!original || !rewritten) return undefined;
	return { original, rewritten, notice: message };
}

function rememberPendingRtkRewrite(record: RtkRewriteRecord): void {
	RTK_PENDING_REWRITES.push(record);
	while (RTK_PENDING_REWRITES.length > RTK_PENDING_REWRITE_LIMIT) RTK_PENDING_REWRITES.shift();
}

function findRtkRewriteToolId(record: RtkRewriteRecord): string | undefined {
	const entries = [...RTK_ORIGINAL_BASH_COMMANDS.entries()].reverse();
	return entries.find(([, command]) => rtkPreviewMatches(command, record.original))?.[0];
}

function rememberRtkRewrite(record: RtkRewriteRecord): void {
	const toolCallId = findRtkRewriteToolId(record);
	if (toolCallId) {
		RTK_REWRITES_BY_TOOL_ID.set(toolCallId, record);
		return;
	}
	rememberPendingRtkRewrite(record);
}

function takePendingRtkRewrite(originalCommand: string | undefined, currentCommand: string | undefined): RtkRewriteRecord | undefined {
	const index = RTK_PENDING_REWRITES.findIndex((record) => {
		return (!!originalCommand && rtkPreviewMatches(originalCommand, record.original))
			|| (!!currentCommand && (rtkPreviewMatches(currentCommand, record.rewritten) || rtkPreviewMatches(currentCommand, record.original)));
	});
	if (index === -1) return undefined;
	const [record] = RTK_PENDING_REWRITES.splice(index, 1);
	return record;
}

function ensureRtkRewriteForContext(ctx: any, args: any): RtkRewriteRecord | undefined {
	if (ctx?.state?._rtkRewriteRecord) return ctx.state._rtkRewriteRecord as RtkRewriteRecord;
	const toolCallId = typeof ctx?.toolCallId === "string" ? ctx.toolCallId : undefined;
	const currentCommand = typeof args?.command === "string" ? args.command : undefined;
	const originalCommand = toolCallId ? RTK_ORIGINAL_BASH_COMMANDS.get(toolCallId) : undefined;
	if (!toolCallId) return undefined;

	let record = RTK_REWRITES_BY_TOOL_ID.get(toolCallId);
	if (!record) {
		record = takePendingRtkRewrite(originalCommand, currentCommand);
		if (record) RTK_REWRITES_BY_TOOL_ID.set(toolCallId, record);
	}
	if (!record && originalCommand && currentCommand && normalizeRtkCommandPreview(originalCommand) !== normalizeRtkCommandPreview(currentCommand)) {
		record = {
			original: originalCommand,
			rewritten: currentCommand,
			notice: `RTK rewrite: ${originalCommand} -> ${currentCommand}`,
		};
		RTK_REWRITES_BY_TOOL_ID.set(toolCallId, record);
	}
	if (record && ctx?.state) ctx.state._rtkRewriteRecord = record;
	return record;
}

function formatRtkRewriteDetails(record: RtkRewriteRecord, theme: Theme): string {
	return [
		theme.fg("muted", "RTK rewrite"),
		`${theme.fg("muted", "original :")} ${theme.fg("dim", record.original)}`,
		`${theme.fg("muted", "rewritten:")} ${theme.fg("dim", record.rewritten)}`,
	].join("\n");
}

function patchUiNotifications(ui: any): void {
	if (!ui || ui[UI_NOTIFY_PATCH_FLAG]) return;
	const originalNotify = ui.notify;
	if (typeof originalNotify !== "function") return;
	ui.notify = function patchedUiNotify(message: string, type?: "info" | "warning" | "error") {
		if (typeof message === "string") {
			const rewrite = parseRtkRewriteNotice(message);
			if (rewrite) {
				rememberRtkRewrite(rewrite);
				return;
			}
			if (message === "💾 Memory auto-reviewed and updated") {
				applyThemePaletteIfNeeded(ui.theme);
				message = `${BORDER_COLOR}✻ Memory auto-reviewed and updated${TRANSPARENT_RESET}`;
			}
		}
		return originalNotify.call(this, message, type);
	};
	ui[UI_NOTIFY_PATCH_FLAG] = true;
}

function trackRtkOriginalBashCommand(toolCallId: unknown, args: unknown): void {
	if (typeof toolCallId !== "string") return;
	const command = (args as any)?.command;
	if (typeof command === "string" && command.trim()) {
		RTK_ORIGINAL_BASH_COMMANDS.set(toolCallId, command);
	}
}

function forgetRtkBashCommand(toolCallId: unknown): void {
	if (typeof toolCallId !== "string") return;
	RTK_ORIGINAL_BASH_COMMANDS.delete(toolCallId);
	RTK_REWRITES_BY_TOOL_ID.delete(toolCallId);
}

function clearRtkRewriteState(): void {
	RTK_ORIGINAL_BASH_COMMANDS.clear();
	RTK_REWRITES_BY_TOOL_ID.clear();
	RTK_PENDING_REWRITES.length = 0;
	clearPreservedBashPreviews();
}

function getWriteWasNewFile(ctx: any, cwd: string, filePath: string, reveal = shouldRevealCallArgs(ctx)): boolean | undefined {
	if (typeof ctx?.state?._writeWasNewFile === "boolean") return ctx.state._writeWasNewFile;
	if (!filePath || !reveal) return undefined;
	const existedBefore = typeof ctx?.toolCallId === "string" ? WRITE_EXISTED_BEFORE.get(ctx.toolCallId) : undefined;
	const wasNew = existedBefore === undefined ? !fileExistsForTool(cwd, filePath) : !existedBefore;
	if (ctx?.state) ctx.state._writeWasNewFile = wasNew;
	return wasNew;
}

function toolStatusDot(ctx: any, theme: Theme): string {
	const status = ctx.state?._toolStatus as "pending" | "success" | "error" | "idle" | undefined;
	if (status === "success") return `${themeStatusDot(theme, "success")} `;
	if (status === "error") return `${themeStatusDot(theme, "error")} `;
	if (status === "idle") return `${themeStatusDot(theme, "dim")} `;
	return `${blinkDot(ctx, theme)} `;
}

// ---------------------------------------------------------------------------
// Branch connector — visual tree from header to output
// ---------------------------------------------------------------------------

function branchIndent(text: string, continued = false, theme?: Theme): string {
	const rule = currentToolBranchAnsi(theme);
	// Align under bare `├ `/`└ ` (│ + one space, or two spaces when closed).
	const prefix = continued ? `${rule}│${TRANSPARENT_RESET} ` : "  ";
	return `${prefix}${WRAP_MARK}${text}`;
}

function branchLead(text: string, continued = false, theme?: Theme): string {
	const rule = currentToolBranchAnsi(theme);
	// Bare tee/corner only — no horizontal ─ arm.
	return `${rule}${continued ? "├" : "└"}${TRANSPARENT_RESET} ${WRAP_MARK}${text}`;
}

function withBranch(content: string, theme: Theme, _isError = false, continued = false): string {
	if (!content || !content.trim()) return "";
	const lines = content.split("\n");
	const first = lines[0] ?? "";
	if (lines.length === 1) return branchLead(first, continued, theme);
	const rest = lines.slice(1).map((line) => branchIndent(line, continued, theme));
	return `${branchLead(first, continued, theme)}\n${rest.join("\n")}`;
}

function withClippedBranch(content: string, theme: Theme, continued = false): string {
	return withBranch(content, theme, false, continued).replaceAll(WRAP_MARK, CLIP_MARK);
}

function withFinalBranchBlock(content: string, theme: Theme, isError = false): string {
	if (!content || !content.trim()) return "";
	const lines = content.split("\n");
	const first = lines[0] ?? "";
	if (lines.length === 1) return branchLead(first, false, theme);
	const middle = lines.slice(1, -1).map((line) => branchIndent(line, true, theme));
	const last = lines[lines.length - 1] ?? "";
	return [branchLead(first, true, theme), ...middle, branchLead(last, false, theme)].join("\n");
}

function indentBranchBlock(block: string): string {
	return block
		.split("\n")
		.map((line) => (line ? ` ${line}` : line))
		.join("\n");
}

// ---------------------------------------------------------------------------
// Blink timer for partial (running) states
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Global blink timer — single timer invalidates all active contexts
// ---------------------------------------------------------------------------

const MAX_BLINKING_TOOLS = 5;
const BLINK_INTERVAL_MS = 500;
// Safety net ONLY for leaked entries after the agent has stopped. Quiet
// long-running tools (sleep, sparse builds, waiting on network) legitimately
// emit no tool_execution_update for minutes — treating that silence as "stale"
// is what made their ● freeze mid-run.
const BLINK_STALE_TIMEOUT_MS = 15000;
let _lastBlinkActivity = 0;

function markBlinkActivity(): void {
	_lastBlinkActivity = Date.now();
}

type BlinkEntry = { key: any; order: number; invalidate: () => void };

const _blinkContexts = new Map<any, BlinkEntry>();
let _globalBlinkTimer: ReturnType<typeof setTimeout> | null = null;
let _blinkOrder = 0;
// Shared phase for all blinkers. Ordinary tools use even/odd (on/off ●).
// Agent-family tools map the index onto a 6-step size breath cycle.
let _globalBlinkPhaseIndex = 0;
let _globalBlinkPhase = true;

function getBlinkIntervalMs(): number {
	return BLINK_INTERVAL_MS;
}

function getBlinkKey(ctx: any): any {
	return ctx?.state ?? ctx;
}

function getBlinkingEntries(): BlinkEntry[] {
	return [..._blinkContexts.values()]
		.sort((a, b) => b.order - a.order)
		.slice(0, MAX_BLINKING_TOOLS);
}

function updateBlinkActiveStates(): void {
	const activeSet = new Set(getBlinkingEntries().map((entry) => entry.key));
	for (const entry of _blinkContexts.values()) {
		const active = activeSet.has(entry.key);
		if (entry.key?._blinkActive !== active) {
			entry.key._blinkActive = active;
			try { entry.invalidate(); } catch { /* noop */ }
		}
	}
}

function _clearAllBlinkContexts(): void {
	for (const entry of _blinkContexts.values()) {
		try { entry.key._blinkActive = false; } catch { /* noop */ }
	}
	_blinkContexts.clear();
	if (_globalBlinkTimer) {
		clearTimeout(_globalBlinkTimer);
		_globalBlinkTimer = null;
	}
	updateBlinkActiveStates();
}

function _scheduleGlobalBlinkTimer(): void {
	if (_globalBlinkTimer) return;
	const intervalMs = getBlinkIntervalMs();
	if (_blinkContexts.size === 0) return;
	_globalBlinkTimer = setTimeout(() => {
		_globalBlinkTimer = null;
		if (_blinkContexts.size === 0) {
			updateBlinkActiveStates();
			return;
		}
		// While an agent run is live, quiet tools are still in flight — keep blinking.
		// Heartbeat here so sparse/no-output commands never look "stale".
		if (currentAgentWorkStartMs !== undefined) {
			markBlinkActivity();
		} else if (_lastBlinkActivity && Date.now() - _lastBlinkActivity > BLINK_STALE_TIMEOUT_MS) {
			// Agent already finished; leftover entries are leaks. Stop the re-render storm.
			_clearAllBlinkContexts();
			return;
		}
		_globalBlinkPhaseIndex = (_globalBlinkPhaseIndex + 1) % AGENT_BREATHE_LEN;
		_globalBlinkPhase = _globalBlinkPhaseIndex % 2 === 0;
		for (const entry of getBlinkingEntries()) {
			try { entry.invalidate(); } catch { /* noop */ }
		}
		_scheduleGlobalBlinkTimer();
	}, intervalMs);
	unrefTimer(_globalBlinkTimer);
}

function _stopGlobalBlinkTimerIfEmpty(): void {
	if (_globalBlinkTimer && _blinkContexts.size === 0) {
		clearTimeout(_globalBlinkTimer);
		_globalBlinkTimer = null;
	}
}

function setupBlinkTimer(ctx: any): void {
	const key = getBlinkKey(ctx);
	if (!key) return;
	const invalidate = typeof ctx?.invalidate === "function" ? () => safeInvalidate(ctx) : () => {};
	const existing = _blinkContexts.get(key);
	if (existing) {
		// Already tracked — refresh invalidate + ensure the global timer is alive.
		// If a prior watchdog/pass stopped the timer without removing this entry
		// (or the timer simply died), a quiet long-running tool would otherwise
		// stay registered forever with a frozen ●.
		existing.invalidate = invalidate;
		markBlinkActivity();
		_scheduleGlobalBlinkTimer();
		return;
	}
	_blinkContexts.set(key, { key, order: ++_blinkOrder, invalidate });
	key._blinkActive = false;
	markBlinkActivity();
	updateBlinkActiveStates();
	_stopGlobalBlinkTimerIfEmpty();
	_scheduleGlobalBlinkTimer();
}

function clearBlinkTimer(ctx: any): void {
	const key = getBlinkKey(ctx);
	if (!key) return;
	_blinkContexts.delete(key);
	key._blinkActive = false;
	updateBlinkActiveStates();
	_stopGlobalBlinkTimerIfEmpty();
	_scheduleGlobalBlinkTimer();
}

function pendingToolChromeColor(theme: Theme): "dim" | "muted" | "thinkingText" {
	if (!themeAdaptiveEnabled()) return "muted";
	return "dim";
}

function blinkDot(ctx: any, theme: Theme): string {
	// Only true in-flight tools arm the blink timer. Idle partials (args still
	// streaming, or history rows left isPartial without a result) stay static.
	if (ctx?.state?._toolStatus !== "pending") {
		return themeStatusDot(theme, "dim");
	}
	setupBlinkTimer(ctx);
	const key = getBlinkKey(ctx);
	if (key?._blinkActive !== true) return " ";
	// Agent-family tools breathe through sizes; ordinary tools still on/off ●.
	if (ctx?.state?._agentBreathe === true) {
		return agentBreatheDot(theme);
	}
	// Claude Code: solid filled circle that either shows or fully disappears —
	// never a hollow outlined ○ in the off phase.
	return _globalBlinkPhase ? themeStatusDot(theme, "success") : " ";
}

// ---------------------------------------------------------------------------
// File icons — Nerd Font glyphs (requires Nerd Font terminal)
// ---------------------------------------------------------------------------

const NF_DIR = `\x1b[38;2;100;140;220m\ue5ff\x1b[0m`;
const NF_DEFAULT = `\x1b[38;2;80;80;80m\uf15b\x1b[0m`;

const EXT_ICON: Record<string, string> = {
	ts: `\x1b[38;2;49;120;198m\ue628\x1b[0m`,
	tsx: `\x1b[38;2;49;120;198m\ue7ba\x1b[0m`,
	js: `\x1b[38;2;241;224;90m\ue74e\x1b[0m`,
	jsx: `\x1b[38;2;97;218;251m\ue7ba\x1b[0m`,
	py: `\x1b[38;2;55;118;171m\ue73c\x1b[0m`,
	rs: `\x1b[38;2;222;165;132m\ue7a8\x1b[0m`,
	go: `\x1b[38;2;0;173;216m\ue724\x1b[0m`,
	java: `\x1b[38;2;204;62;68m\ue738\x1b[0m`,
	rb: `\x1b[38;2;204;52;45m\ue739\x1b[0m`,
	swift: `\x1b[38;2;255;172;77m\ue755\x1b[0m`,
	c: `\x1b[38;2;85;154;211m\ue61e\x1b[0m`,
	cpp: `\x1b[38;2;85;154;211m\ue61d\x1b[0m`,
	html: `\x1b[38;2;228;77;38m\ue736\x1b[0m`,
	css: `\x1b[38;2;66;165;245m\ue749\x1b[0m`,
	scss: `\x1b[38;2;207;100;154m\ue749\x1b[0m`,
	vue: `\x1b[38;2;65;184;131m\ue6a0\x1b[0m`,
	svelte: `\x1b[38;2;255;62;0m\ue697\x1b[0m`,
	json: `\x1b[38;2;241;224;90m\ue60b\x1b[0m`,
	yaml: `\x1b[38;2;160;116;196m\ue6a8\x1b[0m`,
	yml: `\x1b[38;2;160;116;196m\ue6a8\x1b[0m`,
	toml: `\x1b[38;2;160;116;196m\ue6b2\x1b[0m`,
	md: `\x1b[38;2;66;165;245m\ue73e\x1b[0m`,
	sh: `\x1b[38;2;137;180;130m\ue795\x1b[0m`,
	bash: `\x1b[38;2;137;180;130m\ue795\x1b[0m`,
	zsh: `\x1b[38;2;137;180;130m\ue795\x1b[0m`,
	lua: `\x1b[38;2;81;160;207m\ue620\x1b[0m`,
	php: `\x1b[38;2;137;147;186m\ue73d\x1b[0m`,
	sql: `\x1b[38;2;218;218;218m\ue706\x1b[0m`,
	xml: `\x1b[38;2;228;77;38m\ue619\x1b[0m`,
	graphql: `\x1b[38;2;224;51;144m\ue662\x1b[0m`,
	dockerfile: `\x1b[38;2;56;152;236m\ue7b0\x1b[0m`,
	lock: `\x1b[38;2;130;130;130m\uf023\x1b[0m`,
	png: `\x1b[38;2;160;116;196m\uf1c5\x1b[0m`,
	jpg: `\x1b[38;2;160;116;196m\uf1c5\x1b[0m`,
	svg: `\x1b[38;2;255;180;50m\uf1c5\x1b[0m`,
	gif: `\x1b[38;2;160;116;196m\uf1c5\x1b[0m`,
};

const NAME_ICON: Record<string, string> = {
	"package.json": `\x1b[38;2;137;180;130m\ue71e\x1b[0m`,
	"tsconfig.json": `\x1b[38;2;49;120;198m\ue628\x1b[0m`,
	".gitignore": `\x1b[38;2;222;165;132m\ue702\x1b[0m`,
	"dockerfile": `\x1b[38;2;56;152;236m\ue7b0\x1b[0m`,
	"makefile": `\x1b[38;2;130;130;130m\ue615\x1b[0m`,
	"readme.md": `\x1b[38;2;66;165;245m\ue73e\x1b[0m`,
	"license": `\x1b[38;2;218;218;218m\ue60a\x1b[0m`,
};

function fileIcon(fp: string): string {
	const base = fp.split('/').pop()?.toLowerCase() ?? '';
	if (NAME_ICON[base]) return `${NAME_ICON[base]} `;
	const ext = base.includes('.') ? base.split('.').pop() ?? '' : '';
	return EXT_ICON[ext] ? `${EXT_ICON[ext]} ` : `${NF_DEFAULT} `;
}

function dirIcon(): string {
	return `${NF_DIR} `;
}

function lineCount(text: string): number {
	if (!text) return 0;
	return text.split("\n").length;
}

function padToWidth(line: string, width: number): string {
	const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
	const clipped = clampLineWidth(line, safeWidth);
	const padding = Math.max(0, safeWidth - visibleWidth(clipped));
	return `${clipped}${" ".repeat(padding)}`;
}

function markedContinuationPrefix(prefix: string): string {
	const plain = stripAnsi(prefix);
	// Match bare leads (`├ `/`└ `/`│ `) and legacy armed forms (`├─ `/`└─ `/`│  `).
	const branchMatch = /^(\s*)(│  |│ |├─ |└─ |├ |└ )/.exec(plain);
	if (branchMatch) {
		const indent = branchMatch[1];
		// Keep the same structure width as the lead glyph so wraps stay aligned.
		const pad = Math.max(0, visibleWidth(branchMatch[2]) - 1);
		return `${indent}${currentToolBranchAnsi()}│${TRANSPARENT_RESET}${" ".repeat(pad)}`;
	}
	return " ".repeat(visibleWidth(prefix));
}

function wrapMarkedLine(line: string, width: number): string[] {
	const clipIndex = line.indexOf(CLIP_MARK);
	if (clipIndex !== -1) {
		const prefix = line.slice(0, clipIndex);
		const body = line.slice(clipIndex + CLIP_MARK.length);
		if (body.includes(TRAILING_MARK)) return [alignTrailingMarkedLine(`${prefix}${body}`, width)];
		const bodyWidth = Math.max(1, width - visibleWidth(prefix));
		if (visibleWidth(body) <= bodyWidth) return [`${prefix}${body}`];
		const hint = "…";
		return [`${prefix}${truncateToWidth(body, Math.max(0, bodyWidth - visibleWidth(hint)), "", false)}${hint}`];
	}
	const markerIndex = line.indexOf(WRAP_MARK);
	if (markerIndex === -1) return wrapTextWithAnsi(line, width);
	const prefix = line.slice(0, markerIndex);
	const body = line.slice(markerIndex + WRAP_MARK.length);
	const prefixWidth = visibleWidth(prefix);
	const bodyWidth = Math.max(1, width - prefixWidth);
	const wrapped = wrapTextWithAnsi(body, bodyWidth);
	const continuation = markedContinuationPrefix(prefix);
	return wrapped.map((part, index) => (index === 0 ? `${prefix}${part}` : `${continuation}${part}`));
}

class ToolText extends Text {
	private value = "";
	private toolCachedValue?: string;
	private toolCachedWidth?: number;
	private toolCachedLines?: string[];
	private observedWidth?: number;
	private pendingObservedWidth?: number;
	private widthObserver?: (width: number) => void;
	private widthObserverScheduled = false;

	constructor(text = "") {
		super("", 0, 0);
		this.value = text;
	}

	setText(text: string): void {
		if (this.value === text) return;
		this.value = text;
		this.invalidate();
	}

	setWidthObserver(observer?: (width: number) => void): void {
		this.widthObserver = observer;
		if (!observer) this.pendingObservedWidth = undefined;
	}

	private observeWidth(width: number): void {
		if (this.observedWidth === width) return;
		this.observedWidth = width;
		if (!this.widthObserver) return;
		this.pendingObservedWidth = width;
		if (this.widthObserverScheduled) return;
		this.widthObserverScheduled = true;
		queueMicrotask(() => {
			this.widthObserverScheduled = false;
			const observed = this.pendingObservedWidth;
			this.pendingObservedWidth = undefined;
			if (observed !== undefined) this.widthObserver?.(observed);
		});
	}

	invalidate(): void {
		this.toolCachedValue = undefined;
		this.toolCachedWidth = undefined;
		this.toolCachedLines = undefined;
	}

	render(width: number): string[] {
		this.observeWidth(width);
		const branchKey = toolBranchRenderCacheKey();
		if (
			this.toolCachedLines
			&& this.toolCachedValue === this.value
			&& this.toolCachedWidth === width
			&& (this as any)._toolBranchCacheKey === branchKey
			&& (this as any)._toolBranchCacheEpoch === _toolBranchVisualEpoch
		) return this.toolCachedLines;
		if (!this.value || this.value.trim() === "") {
			this.toolCachedValue = this.value;
			this.toolCachedWidth = width;
			this.toolCachedLines = [];
			return this.toolCachedLines;
		}
		const contentWidth = Math.max(1, width);
		const lines = this.value.replace(/\t/g, "   ").split("\n");
		const rendered = lines
			.flatMap((line) => wrapMarkedLine(line, contentWidth))
			.map((line) => padToWidth(line, width));
		this.toolCachedValue = this.value;
		this.toolCachedWidth = width;
		this.toolCachedLines = rendered;
		(this as any)._toolBranchCacheKey = branchKey;
		(this as any)._toolBranchCacheEpoch = _toolBranchVisualEpoch;
		return rendered;
	}
}

function makeText(last: unknown, text: string): Text {
	const component = last instanceof ToolText ? last : new ToolText();
	component.setWidthObserver();
	component.setText(text);
	return component;
}

function makeResponsiveDiffText(ctx: any, last: unknown, text: string): Text {
	const component = makeText(last, text) as ToolText;
	component.setWidthObserver((width) => {
		if (ctx.state?._diffComponentWidth === width) return;
		if (ctx.state) ctx.state._diffComponentWidth = width;
		safeInvalidate(ctx);
	});
	return component;
}

function previewLimit(): number {
	const value = readSettings().previewLines;
	return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 8;
}

function expandedPreviewLimit(): number {
	const settings = readSettings();
	const key = extraToolOutputExpanded ? "extraExpandedPreviewMaxLines" : "expandedPreviewMaxLines";
	const value = settings[key];
	const fallback = extraToolOutputExpanded ? 12000 : 4000;
	return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
}

function bashCollapsedLimit(): number {
	const value = readSettings().bashCollapsedLines;
	return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 10;
}

function bashCommandPreviewLimit(): number {
	const value = readSettings().bashCommandPreviewLines;
	return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 8;
}

function renderBashCommandBlock(
	command: string,
	expanded: boolean,
	theme: Theme,
): string {
	const presentation = buildBashCommandPresentation(command);
	const limit = bashCommandPreviewLimit();
	if (!expanded && (limit === 0 || presentation.sourceLineCount < 2)) return "";
	const sourceLimit = expandedPreviewLimit();
	const lines = expanded ? presentation.sourceLines.slice(0, sourceLimit) : buildBashPreview(presentation.sourceLines, limit);
	if (lines.length === 0) return "";
	if (expanded && presentation.sourceLines.length > sourceLimit) {
		lines.push(`... ${presentation.sourceLines.length - sourceLimit} more command lines`);
	}
	const body = lines.map((line: string) => theme.fg("accent", line || " ")).join("\n");
	return expanded ? withBranch(body, theme, false, true) : withClippedBranch(body, theme, true);
}

function liveToolPreviewEnabled(): boolean {
	return readSettings().liveToolPreview !== false;
}

function liveToolPreviewLimit(): number {
	const value = readSettings().liveToolPreviewLines;
	return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 5;
}

function diffCollapsedLimit(): number {
	const value = readSettings().diffCollapsedLines;
	return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 24;
}

function collapsedPreviewCount(expanded: boolean, fallback: number): number {
	return expanded ? expandedPreviewLimit() : fallback;
}

function buildPreviewText(
	lines: string[],
	expanded: boolean,
	theme: Theme,
	fallbackCollapsed = 8,
	totalLineCount = lines.length,
	styleLine?: (line: string) => string,
): string {
	if (lines.length === 0 && totalLineCount === 0) return theme.fg("muted", "(no output)");
	const maxLines = collapsedPreviewCount(expanded, fallbackCollapsed);
	// Only style/join the lines we will actually display. Callers used to map
	// theme.fg over the entire output array first, which scaled with full tool
	// output even when only 8–10 lines were shown.
	const limit = Math.min(lines.length, maxLines);
	let text = "";
	for (let i = 0; i < limit; i++) {
		const line = styleLine ? styleLine(lines[i]) : lines[i];
		text += i === 0 ? line : `\n${line}`;
	}
	const remaining = Math.max(0, totalLineCount - limit);
	if (remaining > 0) {
		text += `${text ? "\n" : ""}${theme.fg("muted", `... (${remaining} more lines${toolOutputDetailHint(theme, expanded, true)})`)}`;
	}
	if (expanded && totalLineCount > maxLines) {
		text += `\n${theme.fg("warning", `(display capped at ${maxLines} lines${deepExpandHint()})`)}`;
	}
	return text;
}

// ===========================================================================
// Diff rendering — adapted from /tmp/pi-diff
// ===========================================================================

interface DiffPreset {
	name: string;
	description: string;
	shikiTheme?: string;
	bgAdd?: string;
	bgDel?: string;
	bgAddHighlight?: string;
	bgDelHighlight?: string;
	bgGutterAdd?: string;
	bgGutterDel?: string;
	bgEmpty?: string;
	fgAdd?: string;
	fgDel?: string;
	fgDim?: string;
	fgLnum?: string;
	fgRule?: string;
	fgStripe?: string;
	fgSafeMuted?: string;
}

interface DiffUserConfig {
	diffTheme?: string;
	diffColors?: Record<string, string>;
}

const DIFF_PRESETS: Record<string, DiffPreset> = {
	default: {
		name: "default",
		description: "Original diff colors",
		bgAdd: "#162620",
		bgDel: "#2d1919",
		bgAddHighlight: "#234b32",
		bgDelHighlight: "#502323",
		bgGutterAdd: "#12201a",
		bgGutterDel: "#261616",
		bgEmpty: "#121212",
		fgDim: "#505050",
		fgLnum: "#646464",
		fgRule: "#323232",
		fgStripe: "#282828",
		fgSafeMuted: "#8b949e",
	},
	midnight: {
		name: "midnight",
		description: "Subtle tints for black backgrounds",
		bgAdd: "#0d1a12",
		bgDel: "#1a0d0d",
		bgAddHighlight: "#1a3825",
		bgDelHighlight: "#381a1a",
		bgGutterAdd: "#091208",
		bgGutterDel: "#120908",
		bgEmpty: "#080808",
		fgDim: "#404040",
		fgLnum: "#505050",
		fgRule: "#282828",
		fgStripe: "#1e1e1e",
		fgSafeMuted: "#8b949e",
	},
	neon: {
		name: "neon",
		description: "Higher contrast backgrounds",
		bgAdd: "#1a3320",
		bgDel: "#331a16",
		bgAddHighlight: "#2d5c3a",
		bgDelHighlight: "#5c2d2d",
		bgGutterAdd: "#142818",
		bgGutterDel: "#28120e",
		bgEmpty: "#141414",
		fgDim: "#606060",
		fgLnum: "#787878",
		fgRule: "#404040",
		fgStripe: "#303030",
		fgSafeMuted: "#9da5ae",
	},
};

function loadDiffConfig(): DiffUserConfig {
	const settings = readSettings();
	return { diffTheme: settings.diffTheme, diffColors: settings.diffColors };
}

// 6x6x6 color cube channel values used by pi's 256color fallback.
const CUBE_VALUES = [0, 95, 135, 175, 215, 255];

function xterm256ToRgb(index: number): { r: number; g: number; b: number } | null {
	if (!Number.isInteger(index) || index < 0 || index > 255) return null;
	if (index < 16) {
		// Standard 16 ANSI colors — terminal-defined, approximate with VS Code defaults.
		const basic: Array<[number, number, number]> = [
			[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
			[0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],
			[128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0],
			[0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255],
		];
		const [r, g, b] = basic[index];
		return { r, g, b };
	}
	if (index < 232) {
		const i = index - 16;
		return {
			r: CUBE_VALUES[Math.floor(i / 36) % 6],
			g: CUBE_VALUES[Math.floor(i / 6) % 6],
			b: CUBE_VALUES[i % 6],
		};
	}
	const level = 8 + (index - 232) * 10;
	return { r: level, g: level, b: level };
}

function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
	if (!ansi) return null;
	const esc = "\u001b";
	// Truecolor: \e[38;2;R;G;Bm or \e[48;2;R;G;Bm
	const tc = ansi.match(new RegExp(`${esc}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`));
	if (tc) return { r: +tc[1], g: +tc[2], b: +tc[3] };
	// 256-color: \e[38;5;Nm or \e[48;5;Nm — happens on Apple Terminal, screen, etc.
	const idx = ansi.match(new RegExp(`${esc}\\[(?:38|48);5;(\\d+)m`));
	if (idx) return xterm256ToRgb(+idx[1]);
	return null;
}

function hexToBgAnsi(hex: string): string {
	if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return "";
	const r = Number.parseInt(hex.slice(1, 3), 16);
	const g = Number.parseInt(hex.slice(3, 5), 16);
	const b = Number.parseInt(hex.slice(5, 7), 16);
	return `\x1b[48;2;${r};${g};${b}m`;
}

function hexToFgAnsi(hex: string): string {
	if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return "";
	const r = Number.parseInt(hex.slice(1, 3), 16);
	const g = Number.parseInt(hex.slice(3, 5), 16);
	const b = Number.parseInt(hex.slice(5, 7), 16);
	return `\x1b[38;2;${r};${g};${b}m`;
}

// ---------------------------------------------------------------------------
// Theme palette extraction — pull RGB from the active syCode theme so our
// hardcoded greys and accent colors track the user's selected theme.
//
// `theme.getFgAnsi(name)` / `theme.getBgAnsi(name)` return raw ANSI escapes
// (either truecolor or 256color depending on the terminal). We parse those
// back into RGB so we can mix tints for diff backgrounds.
// ---------------------------------------------------------------------------

type Rgb = { r: number; g: number; b: number };

function safeFgAnsi(theme: any, key: string): string | null {
	try {
		const ansi = theme?.getFgAnsi?.(key);
		return typeof ansi === "string" && ansi.length > 0 ? ansi : null;
	} catch {
		return null;
	}
}

function safeBgAnsi(theme: any, key: string): string | null {
	try {
		const ansi = theme?.getBgAnsi?.(key);
		return typeof ansi === "string" && ansi.length > 0 ? ansi : null;
	} catch {
		return null;
	}
}

function themeFgRgb(theme: any, key: string): Rgb | null {
	const ansi = safeFgAnsi(theme, key);
	return ansi ? parseAnsiRgb(ansi) : null;
}

function themeBgRgb(theme: any, key: string): Rgb | null {
	const ansi = safeBgAnsi(theme, key);
	return ansi ? parseAnsiRgb(ansi) : null;
}

// Cache theme identity so we only recompute on theme change. The Theme
// object is reused across renders within a single session unless the user
// switches themes via the picker.
let _themePaletteCacheTheme: unknown = null;
let _themePaletteCacheName: string | null = null;
let _themePaletteCacheFingerprint: string | null = null;

/** Resolved-color fingerprint so palette re-derives when the active theme file changes under the same name/object. */
function themePaletteFingerprint(theme: any): string {
	const keys = ["success", "error", "borderMuted", "accent", "muted", "toolDiffAdded", "toolDiffRemoved"] as const;
	return keys.map((k) => safeFgAnsi(theme, k) ?? "").join("\u001f");
}

function invalidateThemePaletteCache(): void {
	_themePaletteCacheTheme = null;
	_themePaletteCacheName = null;
	_themePaletteCacheFingerprint = null;
}

function themeAdaptiveEnabled(): boolean {
	const settings = readSettings();
	return settings.themeAdaptive !== false;
}

let DIFF_THEME: BundledTheme = (process.env.DIFF_THEME as BundledTheme | undefined) ?? "github-dark";
/** True when the active syCode theme has a light panel background (edit/write diff chrome). */
let _diffOnLightBg = false;
let codeToAnsiLoader: Promise<any> | null = null;

const SPLIT_MIN_WIDTH = 150;
const SPLIT_MIN_CODE_WIDTH = 60;
const SPLIT_MAX_WRAP_RATIO = 0.2;
const SPLIT_MAX_WRAP_LINES = 8;
const MAX_TERM_WIDTH = 210;
const DEFAULT_TERM_WIDTH = 200;
const MAX_PREVIEW_LINES = 60;
const MAX_RENDER_LINES = 150;
const MAX_HL_CHARS = 32_000;
const CACHE_LIMIT = 48;
const DIFF_RENDER_CONCURRENCY = 2;
const WORD_DIFF_MIN_SIM = 0.15;
const MAX_WRAP_ROWS_WIDE = 3;
const MAX_WRAP_ROWS_MED = 2;
const MAX_WRAP_ROWS_NARROW = 1;

let D_RST = "\x1b[0m";
const D_BOLD = "\x1b[1m";
const D_DIM = "\x1b[2m";

// Diff backgrounds — defaults are transparent; autoDeriveBgFromTheme fills them
// using pi-tool-display's mix ratios against the theme's toolSuccessBg.
let BG_ADD = "\x1b[49m";
let BG_DEL = "\x1b[49m";
let BG_ADD_W = "\x1b[49m";
let BG_DEL_W = "\x1b[49m";
let BG_GUTTER_ADD = "\x1b[49m";
let BG_GUTTER_DEL = "\x1b[49m";
let BG_EMPTY = "\x1b[49m";
let BG_BASE = "\x1b[49m";

let FG_ADD = "\x1b[38;2;100;180;120m";
let FG_DEL = "\x1b[38;2;200;100;100m";
let FG_DIM = "\x1b[38;2;80;80;80m";
let FG_LNUM = "\x1b[38;2;100;100;100m";
let FG_RULE = "\x1b[38;2;50;50;50m";
// Tool branch connectors (├ └ │). Default fixed gray 72 — independent of syCode theme.
const DEFAULT_TOOL_BRANCH_GRAY = 72;

function toolBranchRgbAnsi(gray: number): string {
	const g = Math.max(0, Math.min(255, Math.round(gray)));
	return `\x1b[38;2;${g};${g};${g}m`;
}

function ansiRgbBrightenedBy(ansi: string, delta: number): string | null {
	const rgb = parseAnsiRgb(ansi);
	if (!rgb) return null;
	const bump = (c: number) => Math.max(0, Math.min(255, Math.round(c + delta)));
	return `\x1b[38;2;${bump(rgb.r)};${bump(rgb.g)};${bump(rgb.b)}m`;
}

/** Outline chrome always brighter than branch; never falls back to identical branch ANSI. */
function outlineChromeAnsiFromBranch(theme?: any): string {
	const t = theme ?? _toolBranchThemeHint;
	const branch = currentToolBranchAnsi(t);
	const fromBranch = ansiRgbBrightenedBy(branch, OUTLINE_CHROME_BRIGHTEN);
	if (fromBranch) return fromBranch;
	let gray = DEFAULT_TOOL_BRANCH_GRAY;
	if (toolBranchColorModeFixed()) {
		gray = getConfiguredToolBranchGray();
	} else if (t) {
		const hint = safeFgAnsi(t, "dim") ?? safeFgAnsi(t, "muted") ?? safeFgAnsi(t, "borderMuted");
		const rgb = hint ? parseAnsiRgb(hint) : null;
		if (rgb) gray = Math.round((rgb.r + rgb.g + rgb.b) / 3);
	}
	return toolBranchRgbAnsi(Math.min(255, gray + OUTLINE_CHROME_BRIGHTEN));
}

function getConfiguredToolBranchGray(): number {
	const raw = readSettings().toolBranchRgbGray;
	return typeof raw === "number" && Number.isFinite(raw) ? Math.max(0, Math.min(255, Math.round(raw))) : DEFAULT_TOOL_BRANCH_GRAY;
}

function toolBranchColorModeFixed(): boolean {
	return readSettings().toolBranchColorMode !== "theme";
}

function toolBranchRenderCacheKey(): string {
	if (toolBranchColorModeFixed()) return `fixed:${getConfiguredToolBranchGray()}`;
	return `theme:${stripAnsi(TOOL_RULE)}`;
}

let _toolBranchVisualEpoch = 0;

function bumpToolBranchVisualEpoch(): void {
	_toolBranchVisualEpoch++;
}

/** On light panels, theme dim/muted can be nearly white — pull chrome toward mid-gray. */
function attenuateChromeAnsi(ansi: string, theme: any): string {
	const rgb = parseAnsiRgb(ansi);
	if (!rgb) return ansi;
	if (!isLightThemeBackground(theme)) return ansi;
	const lum = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
	// Already quiet enough on light backgrounds.
	if (lum <= 145) return ansi;
	const target = 118;
	const t = Math.min(1, (lum - 130) / 110);
	const mix = (c: number) => Math.round(c + (target - c) * t);
	return `\x1b[38;2;${mix(rgb.r)};${mix(rgb.g)};${mix(rgb.b)}m`;
}

/** Shared outline chrome: user box, tool rules, code fences, branch connectors. */
function resolveThemeChromeFg(theme: any): string | null {
	if (!theme || !themeAdaptiveEnabled()) return null;
	const dim = safeFgAnsi(theme, "dim");
	const muted = safeFgAnsi(theme, "muted");
	const borderMuted = safeFgAnsi(theme, "borderMuted");
	const thinking = safeFgAnsi(theme, "thinkingText");
	const raw = dim ?? muted ?? borderMuted ?? thinking;
	return raw ? attenuateChromeAnsi(raw, theme) : null;
}

/** Resolve ├ └ │ color from settings + theme on every use (not a stale global). */
let _toolBranchThemeHint: any;

function currentToolBranchAnsi(theme?: any): string {
	const t = theme ?? _toolBranchThemeHint;
	if (toolBranchColorModeFixed()) {
		return toolBranchRgbAnsi(getConfiguredToolBranchGray());
	}
	const chrome = t ? resolveThemeChromeFg(t) : null;
	if (chrome) return chrome;
	return toolBranchRgbAnsi(getConfiguredToolBranchGray());
}

/** User box, code fences, thinking/thought: branch + OUTLINE_CHROME_BRIGHTEN (never same as branch). */
function syncOutlineChromeFromBranch(theme?: any): void {
	const outline = outlineChromeAnsiFromBranch(theme);
	const prevBorder = BORDER_COLOR;
	BORDER_COLOR = outline;
	WORKED_LINE_FG = outline;
	CODE_BLOCK_LANG_FG = outline;
	if (outline !== prevBorder) bumpToolBranchVisualEpoch();
}

function applyToolBranchColor(theme?: any): void {
	if (theme) _toolBranchThemeHint = theme;
	const prev = TOOL_RULE;
	TOOL_RULE = currentToolBranchAnsi(theme);
	if (TOOL_RULE !== prev) bumpToolBranchVisualEpoch();
	syncOutlineChromeFromBranch(theme);
}

/** Strip baked ├/└/│ prefixes (short or long arm) so branch color can be reapplied. */
function stripBranchMarkupLine(line: string): string {
	let plain = stripAnsi(line);
	plain = plain.replace(/^\s*[├└]─?\s*/, "");
	plain = plain.replace(/^\s*│\s{0,2}/, "");
	return plain;
}

function stripBranchMarkupBlock(text: string): string {
	return text
		.split("\n")
		.map((line) => (stripAnsi(line).trim() ? stripBranchMarkupLine(line) : line))
		.join("\n");
}

function liveBranchDisplay(state: Record<string, unknown> | undefined, theme: Theme): string | undefined {
	if (!state || typeof state !== "object") return undefined;
	const body = state._ptBody;
	if (typeof body === "string" && body.trim() && !body.includes("(rendering")) {
		return indentBranchBlock(withBranch(body, theme, false, true));
	}
	const display = state._ptDisplay;
	if (typeof display === "string" && display.trim()) {
		return indentBranchBlock(withBranch(stripBranchMarkupBlock(display), theme, false, true));
	}
	return undefined;
}

function refreshToolBranchDisplaysInState(state: Record<string, unknown> | undefined, theme: Theme): void {
	if (!state || typeof state !== "object") return;
	const body = state._ptBody;
	if (typeof body === "string" && body.trim() && !body.includes("(rendering")) {
		state._ptDisplay = indentBranchBlock(withBranch(body, theme, false, true));
		return;
	}
	const display = state._ptDisplay;
	if (typeof display === "string" && display.trim()) {
		const stripped = stripBranchMarkupBlock(display);
		state._ptDisplay = indentBranchBlock(withBranch(stripped, theme, false, true));
	}
}

function refreshAllToolBranchVisuals(ctx: any): void {
	_settingsCache = null;
	syncToolBackgroundMode();
	invalidateThemePaletteCache();
	applyToolBackgroundMode(ctx?.ui?.theme);
	applyToolBranchColor(ctx?.ui?.theme);
	bumpToolBranchVisualEpoch(); // always bust ToolText + container caches after /cc-tools branch
	// Tool rows recompute branch markup on next render (liveBranchDisplay + cache bust).
	if (ctx?.hasUI) {
		try {
			ctx.ui.setToolsExpanded(ctx.ui.getToolsExpanded());
			ctx.ui.invalidate?.();
			ctx.ui.requestRender?.();
		} catch { /* noop */ }
	}
}

/** Re-derive borders, branches, diffs, and spinner keys from the active syCode theme (no cross-extension deps). */
function rebindUiChromeToTheme(ctx: any): void {
	if (!ctx?.hasUI) return;
	_settingsCache = null;
	syncToolBackgroundMode();
	const theme = ctx.ui?.theme;
	invalidateThemePaletteCache();
	clearHighlightCache();
	applyDiffPalette();
	bustSpinnerSettingsCache();
	applyToolBackgroundMode(theme);
	applyThemePaletteIfNeeded(theme);
	syncDiffShikiTheme(theme);
	if (themeAdaptiveEnabled() && theme?.getFgAnsi && !hasExplicitBgConfig) {
		autoDeriveBgFromTheme(theme);
		autoDerivePending = false;
	}
	bumpToolBranchVisualEpoch();
	refreshAllToolBranchVisuals(ctx);
}

function scheduleDeferredChromeRebind(ctx: any, delayMs = 0): void {
	const timer = setTimeout(() => {
		try {
			rebindUiChromeToTheme(ctx);
		} catch { /* noop */ }
	}, delayMs);
	unrefTimer(timer);
}

let TOOL_RULE = toolBranchRgbAnsi(DEFAULT_TOOL_BRANCH_GRAY);
let FG_SAFE_MUTED = "\x1b[38;2;139;148;158m";
let FG_STRIPE = "\x1b[38;2;40;40;40m";

let DIVIDER = `${FG_RULE}│${D_RST}`;

interface DiffColors {
	fgAdd: string;
	fgDel: string;
	fgCtx: string;
}

let DEFAULT_DIFF_COLORS: DiffColors = { fgAdd: FG_ADD, fgDel: FG_DEL, fgCtx: FG_DIM };
let autoDerivePending = true;
let hasExplicitBgConfig = false;

function mixBg(
	base: { r: number; g: number; b: number },
	accent: { r: number; g: number; b: number },
	intensity: number,
): string {
	const r = Math.round(base.r + (accent.r - base.r) * intensity);
	const g = Math.round(base.g + (accent.g - base.g) * intensity);
	const b = Math.round(base.b + (accent.b - base.b) * intensity);
	return `\x1b[48;2;${r};${g};${b}m`;
}

// pi-tool-display tint targets for diff palette derivation
const ADDITION_TINT_TARGET = { r: 84, g: 190, b: 118 };
const DELETION_TINT_TARGET = { r: 232, g: 95, b: 122 };
// Fallback panel bases when theme bg vars are unavailable
const FALLBACK_BASE_BG_DARK = { r: 32, g: 35, b: 42 };
const FALLBACK_BASE_BG_LIGHT = { r: 232, g: 233, b: 236 };

function isLightThemeBackground(theme: any): boolean {
	const panel =
		themeBgRgb(theme, "toolSuccessBg") ||
		themeBgRgb(theme, "userMessageBg") ||
		themeBgRgb(theme, "selectedBg");
	if (panel) {
		const lum = 0.2126 * panel.r + 0.7152 * panel.g + 0.0722 * panel.b;
		return lum > 165;
	}
	const fg = themeFgRgb(theme, "text") || themeFgRgb(theme, "fg");
	if (fg) {
		const lum = 0.2126 * fg.r + 0.7152 * fg.g + 0.0722 * fg.b;
		return lum < 95;
	}
	return false;
}

function syncDiffShikiTheme(theme: any): void {
	if (process.env.DIFF_THEME) return;
	const config = loadDiffConfig();
	if (config.diffTheme) return;
	_diffOnLightBg = isLightThemeBackground(theme);
	DIFF_THEME = (_diffOnLightBg ? "github-light" : "github-dark") as BundledTheme;
	clearHighlightCache();
}
const UNIVERSAL_DIFF_ADD_FG = { r: 110, g: 210, b: 130 };
const UNIVERSAL_DIFF_DEL_FG = { r: 225, g: 110, b: 110 };

function mixRgb(
	a: { r: number; g: number; b: number },
	b: { r: number; g: number; b: number },
	ratio: number,
): { r: number; g: number; b: number } {
	return {
		r: a.r + (b.r - a.r) * ratio,
		g: a.g + (b.g - a.g) * ratio,
		b: a.b + (b.b - a.b) * ratio,
	};
}

function rgbToBgAnsi(c: { r: number; g: number; b: number }): string {
	return `\x1b[48;2;${Math.round(c.r)};${Math.round(c.g)};${Math.round(c.b)}m`;
}

function autoDeriveBgFromTheme(theme: any): void {
	// Diff palette derivation.
	//
	// `toolDiffAdded` / `toolDiffRemoved` from the active syCode theme give us the
	// fg accents. The base background is taken from `toolSuccessBg` (close to
	// the panel color the row will sit on) so the tinted backgrounds blend in
	// instead of forcing a hardcoded dark hue. Falls back to the universal
	// dark palette when the theme is unavailable or themeAdaptive=false.
	const useTheme = themeAdaptiveEnabled() && theme;
	const onLight = useTheme && isLightThemeBackground(theme);
	_diffOnLightBg = !!onLight;
	const addFgRgb = (useTheme && themeFgRgb(theme, "toolDiffAdded")) || UNIVERSAL_DIFF_ADD_FG;
	const delFgRgb = (useTheme && themeFgRgb(theme, "toolDiffRemoved")) || UNIVERSAL_DIFF_DEL_FG;
	const base =
		(useTheme && themeBgRgb(theme, "toolSuccessBg")) ||
		(useTheme && themeBgRgb(theme, "userMessageBg")) ||
		(onLight ? FALLBACK_BASE_BG_LIGHT : FALLBACK_BASE_BG_DARK);

	const addTint = mixRgb(addFgRgb, ADDITION_TINT_TARGET, 0.35);
	const delTint = mixRgb(delFgRgb, DELETION_TINT_TARGET, 0.65);

	FG_ADD = `\x1b[38;2;${Math.round(addFgRgb.r)};${Math.round(addFgRgb.g)};${Math.round(addFgRgb.b)}m`;
	FG_DEL = `\x1b[38;2;${Math.round(delFgRgb.r)};${Math.round(delFgRgb.g)};${Math.round(delFgRgb.b)}m`;
	BG_ADD = rgbToBgAnsi(mixRgb(base, addTint, 0.24));
	BG_DEL = rgbToBgAnsi(mixRgb(base, delTint, 0.12));
	BG_ADD_W = rgbToBgAnsi(mixRgb(base, addTint, 0.44));
	BG_DEL_W = rgbToBgAnsi(mixRgb(base, delTint, 0.26));
	BG_GUTTER_ADD = rgbToBgAnsi(mixRgb(base, addTint, 0.14));
	BG_GUTTER_DEL = rgbToBgAnsi(mixRgb(base, delTint, 0.08));
	BG_EMPTY = TRANSPARENT_BG;
	BG_BASE = TRANSPARENT_BG;
	D_RST = TRANSPARENT_RESET;
	DIVIDER = `${FG_RULE}│${D_RST}`;
	DEFAULT_DIFF_COLORS = { fgAdd: FG_ADD, fgDel: FG_DEL, fgCtx: FG_DIM };
}

// Track which palette fields the user explicitly set so theme-derived
// updates don't clobber their config.
const _explicitFgFields = new Set<"fgAdd" | "fgDel" | "fgDim" | "fgLnum" | "fgRule" | "fgStripe" | "fgSafeMuted">();

// Original Claude-Code-style palette captured at module-load so we can
// restore it when the user toggles themeAdaptive off at runtime.
const _claudeStyleDefaults = {
	BORDER_COLOR: "\x1b[38;5;238m",
	WORKED_LINE_FG: "\x1b[38;2;140;140;140m",
	CODE_BLOCK_LANG_FG: "\x1b[38;2;95;95;95m",
	TOOL_RULE: toolBranchRgbAnsi(DEFAULT_TOOL_BRANCH_GRAY),
	FG_DIM: "\x1b[38;2;80;80;80m",
	FG_LNUM: "\x1b[38;2;100;100;100m",
	FG_RULE: "\x1b[38;2;50;50;50m",
	FG_STRIPE: "\x1b[38;2;40;40;40m",
	FG_SAFE_MUTED: "\x1b[38;2;139;148;158m",
	FG_ADD: "\x1b[38;2;100;180;120m",
	FG_DEL: "\x1b[38;2;200;100;100m",
	TOOL_STATUS_SUCCESS: "\x1b[32m",
	TOOL_STATUS_ERROR: "\x1b[31m",
	TOOL_STATUS_PENDING: "\x1b[90m",
};

function resetThemePalette(): void {
	BORDER_COLOR = _claudeStyleDefaults.BORDER_COLOR;
	WORKED_LINE_FG = _claudeStyleDefaults.WORKED_LINE_FG;
	CODE_BLOCK_LANG_FG = _claudeStyleDefaults.CODE_BLOCK_LANG_FG;
	applyToolBranchColor();
	TOOL_STATUS_SUCCESS = _claudeStyleDefaults.TOOL_STATUS_SUCCESS;
	TOOL_STATUS_ERROR = _claudeStyleDefaults.TOOL_STATUS_ERROR;
	TOOL_STATUS_PENDING = _claudeStyleDefaults.TOOL_STATUS_PENDING;
	if (!_explicitFgFields.has("fgDim")) FG_DIM = _claudeStyleDefaults.FG_DIM;
	if (!_explicitFgFields.has("fgLnum")) FG_LNUM = _claudeStyleDefaults.FG_LNUM;
	if (!_explicitFgFields.has("fgRule")) FG_RULE = _claudeStyleDefaults.FG_RULE;
	if (!_explicitFgFields.has("fgStripe")) FG_STRIPE = _claudeStyleDefaults.FG_STRIPE;
	if (!_explicitFgFields.has("fgSafeMuted")) FG_SAFE_MUTED = _claudeStyleDefaults.FG_SAFE_MUTED;
	if (!_explicitFgFields.has("fgAdd")) FG_ADD = _claudeStyleDefaults.FG_ADD;
	if (!_explicitFgFields.has("fgDel")) FG_DEL = _claudeStyleDefaults.FG_DEL;
	DIVIDER = `${FG_RULE}│${D_RST}`;
	DEFAULT_DIFF_COLORS = { fgAdd: FG_ADD, fgDel: FG_DEL, fgCtx: FG_DIM };
}

function applyThemePaletteIfNeeded(theme: any): void {
	if (!theme) return;
	if (!themeAdaptiveEnabled()) {
		applyToolBranchColor(theme);
		syncOutlineChromeFromBranch(theme);
		return;
	}
	const themeName = typeof theme?.name === "string" ? theme.name : "";
	const fingerprint = themePaletteFingerprint(theme);
	if (
		_themePaletteCacheTheme === theme &&
		_themePaletteCacheName === themeName &&
		_themePaletteCacheFingerprint === fingerprint
	) {
		applyToolBranchColor(theme);
		syncOutlineChromeFromBranch(theme);
		return;
	}
	const paletteChanged =
		_themePaletteCacheName !== themeName || _themePaletteCacheFingerprint !== fingerprint;
	if (paletteChanged) bumpToolBranchVisualEpoch();
	_themePaletteCacheTheme = theme;
	_themePaletteCacheName = themeName;
	_themePaletteCacheFingerprint = fingerprint;

	const borderMuted = safeFgAnsi(theme, "borderMuted");
	const muted = safeFgAnsi(theme, "muted");
	const dim = safeFgAnsi(theme, "dim") ?? muted;

	// User box, code fences, thinking/thought text, and ├ └ │ all follow branch chrome.
	applyToolBranchColor(theme);

	const chromeFg = BORDER_COLOR;

	// Grouped-tool status counts follow the same semantic theme colors as regular tool dots.
	TOOL_STATUS_SUCCESS = safeFgAnsi(theme, "success") ?? TOOL_STATUS_SUCCESS;
	TOOL_STATUS_ERROR = safeFgAnsi(theme, "error") ?? TOOL_STATUS_ERROR;
	const thinking = safeFgAnsi(theme, "thinkingText");
	TOOL_STATUS_PENDING = dim ?? muted ?? thinking ?? TOOL_STATUS_PENDING;

	// Diff support text colors. These are user-overridable via diffColors.* so
	// we only touch the ones not explicitly set.
	if (!_explicitFgFields.has("fgDim") && muted) FG_DIM = muted;
	if (!_explicitFgFields.has("fgLnum") && muted) FG_LNUM = muted;
	const ruleChrome = chromeFg ?? borderMuted;
	if (!_explicitFgFields.has("fgRule") && ruleChrome) FG_RULE = ruleChrome;
	if (!_explicitFgFields.has("fgStripe") && ruleChrome) FG_STRIPE = ruleChrome;
	if (!_explicitFgFields.has("fgSafeMuted") && muted) FG_SAFE_MUTED = muted;

	DIVIDER = `${FG_RULE}│${D_RST}`;

	// Re-trigger background derivation against the new theme unless the user
	// set explicit bg overrides via diffTheme/diffColors.
	if (!hasExplicitBgConfig) {
		autoDeriveBgFromTheme(theme);
		autoDerivePending = false;
	} else if (themeAdaptiveEnabled()) {
		_diffOnLightBg = isLightThemeBackground(theme);
	}
	syncDiffShikiTheme(theme);
}

function applyDiffPalette(): void {
	const config = loadDiffConfig();
	hasExplicitBgConfig = false;
	const preset = config.diffTheme ? DIFF_PRESETS[config.diffTheme] : null;
	if (preset) hasExplicitBgConfig = true;
	const overrides = config.diffColors ?? {};
	if (Object.keys(overrides).length > 0) hasExplicitBgConfig = true;
	_explicitFgFields.clear();

	const applyBg = (key: string, presetValue: string | undefined, set: (value: string) => void) => {
		const hex = overrides[key] ?? presetValue;
		if (!hex) return;
		const ansi = hexToBgAnsi(hex);
		if (ansi) set(ansi);
	};
	const applyFg = (
		key: "fgAdd" | "fgDel" | "fgDim" | "fgLnum" | "fgRule" | "fgStripe" | "fgSafeMuted",
		presetValue: string | undefined,
		set: (value: string) => void,
	) => {
		const hex = overrides[key] ?? presetValue;
		if (!hex) return;
		const ansi = hexToFgAnsi(hex);
		if (!ansi) return;
		set(ansi);
		_explicitFgFields.add(key);
	};

	applyBg("bgAdd", preset?.bgAdd, (v) => {
		BG_ADD = v;
	});
	applyBg("bgDel", preset?.bgDel, (v) => {
		BG_DEL = v;
	});
	applyBg("bgAddHighlight", preset?.bgAddHighlight, (v) => {
		BG_ADD_W = v;
	});
	applyBg("bgDelHighlight", preset?.bgDelHighlight, (v) => {
		BG_DEL_W = v;
	});
	applyBg("bgGutterAdd", preset?.bgGutterAdd, (v) => {
		BG_GUTTER_ADD = v;
	});
	applyBg("bgGutterDel", preset?.bgGutterDel, (v) => {
		BG_GUTTER_DEL = v;
	});
	applyBg("bgEmpty", preset?.bgEmpty, (v) => {
		BG_EMPTY = v;
	});

	applyFg("fgAdd", preset?.fgAdd, (v) => {
		FG_ADD = v;
	});
	applyFg("fgDel", preset?.fgDel, (v) => {
		FG_DEL = v;
	});
	applyFg("fgDim", preset?.fgDim, (v) => {
		FG_DIM = v;
	});
	applyFg("fgLnum", preset?.fgLnum, (v) => {
		FG_LNUM = v;
	});
	applyFg("fgRule", preset?.fgRule, (v) => {
		FG_RULE = v;
	});
	applyFg("fgStripe", preset?.fgStripe, (v) => {
		FG_STRIPE = v;
	});
	applyFg("fgSafeMuted", preset?.fgSafeMuted, (v) => {
		FG_SAFE_MUTED = v;
	});

	const shiki = overrides.shikiTheme ?? preset?.shikiTheme;
	if (shiki) DIFF_THEME = shiki as BundledTheme;

	DIVIDER = `${FG_RULE}│${D_RST}`;
	DEFAULT_DIFF_COLORS = { fgAdd: FG_ADD, fgDel: FG_DEL, fgCtx: FG_DIM };
	// Only trigger auto-derive when the user did NOT supply an explicit
	// preset or per-color override; otherwise we would overwrite their config
	// with the hardcoded dark palette on first render.
	autoDerivePending = !hasExplicitBgConfig;
}

function resolveDiffColors(theme?: any): DiffColors {
	applyThemePaletteIfNeeded(theme);
	if (autoDerivePending && theme?.getFgAnsi) {
		autoDeriveBgFromTheme(theme);
		autoDerivePending = false;
	}
	return DEFAULT_DIFF_COLORS;
}

interface DiffLine {
	type: "add" | "del" | "ctx" | "sep";
	oldNum: number | null;
	newNum: number | null;
	content: string;
}

interface ParsedDiff {
	lines: DiffLine[];
	added: number;
	removed: number;
	chars: number;
}

function diffStrip(value: string): string {
	return value.replace(ANSI_RE, "");
}

function tabs(text: string): string {
	return text.replace(/\t/g, "  ");
}

function termW(): number {
	const raw =
		process.stdout.columns ||
		(process.stderr as any).columns ||
		Number.parseInt(process.env.COLUMNS ?? "", 10) ||
		DEFAULT_TERM_WIDTH;
	return Math.max(40, Math.min(raw - 4, MAX_TERM_WIDTH));
}

function branchDiffWidth(componentWidth?: number, chromeWidth = 2): number {
	const width = typeof componentWidth === "number" && Number.isFinite(componentWidth)
		? Math.floor(componentWidth)
		: termW();
	return Math.max(20, Math.min(width - chromeWidth, MAX_TERM_WIDTH));
}

function contextDiffWidth(ctx: any, chromeWidth = 2): number {
	return branchDiffWidth(ctx.state?._diffComponentWidth, chromeWidth);
}

function adaptiveWrapRows(tw?: number): number {
	const width = tw ?? termW();
	if (width >= 180) return MAX_WRAP_ROWS_WIDE;
	if (width >= 120) return MAX_WRAP_ROWS_MED;
	return MAX_WRAP_ROWS_NARROW;
}

function fit(value: string, width: number): string {
	if (width <= 0) return "";
	const plain = diffStrip(value);
	if (plain.length <= width) return value + " ".repeat(width - plain.length);
	const showWidth = width > 2 ? width - 1 : width;
	let vis = 0;
	let i = 0;
	while (i < value.length && vis < showWidth) {
		if (value[i] === "\x1b") {
			const end = value.indexOf("m", i);
			if (end !== -1) {
				i = end + 1;
				continue;
			}
		}
		vis++;
		i++;
	}
	return width > 2 ? `${value.slice(0, i)}${D_RST}${FG_DIM}›${D_RST}` : `${value.slice(0, i)}${D_RST}`;
}

function ansiState(text: string): string {
	const matches = text.match(/\x1b\[[0-9;]*m/g) ?? [];
	let fg = "";
	let bg = "";
	for (const seq of matches) {
		const params = seq.slice(2, -1);
		if (params === "0") {
			fg = "";
			bg = "";
		} else if (params === "39") {
			fg = "";
		} else if (params.startsWith("38;")) {
			fg = seq;
		} else if (params.startsWith("48;")) {
			bg = seq;
		}
	}
	return bg + fg;
}

function normalizeShikiContrast(ansi: string): string {
	const darkFgThreshold = _diffOnLightBg ? 140 : 72;
	return ansi.replace(/\x1b\[([0-9;]*)m/g, (seq, params: string) => {
		if (params === "30" || params === "90" || params === "38;5;0" || params === "38;5;8") return FG_SAFE_MUTED;
		if (!params.startsWith("38;2;")) return seq;
		const parts = params.split(";").map(Number);
		if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n))) return seq;
		const [, , r, g, b] = parts;
		const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
		if (_diffOnLightBg) {
			return luminance < darkFgThreshold ? seq : FG_SAFE_MUTED;
		}
		return luminance < darkFgThreshold ? FG_SAFE_MUTED : seq;
	});
}

function wrapAnsi(text: string, width: number, maxRows = adaptiveWrapRows(), fillBg = ""): string[] {
	if (width <= 0) return [""];
	const plain = diffStrip(text);
	if (plain.length <= width) {
		const pad = width - plain.length;
		return pad > 0 ? [text + fillBg + " ".repeat(pad) + (fillBg ? D_RST : "")] : [text];
	}

	const rows: string[] = [];
	let row = "";
	let vis = 0;
	let i = 0;
	let onLastRow = false;
	let effectiveWidth = width;

	while (i < text.length) {
		if (!onLastRow && rows.length >= maxRows - 1) {
			onLastRow = true;
			effectiveWidth = width > 2 ? width - 1 : width;
		}
		if (text[i] === "\x1b") {
			const end = text.indexOf("m", i);
			if (end !== -1) {
				row += text.slice(i, end + 1);
				i = end + 1;
				continue;
			}
		}
		if (vis >= effectiveWidth) {
			if (onLastRow) {
				let hasMore = false;
				for (let j = i; j < text.length; j++) {
					if (text[j] === "\x1b") {
						const e2 = text.indexOf("m", j);
						if (e2 !== -1) {
							j = e2;
							continue;
						}
					}
					hasMore = true;
					break;
				}
				if (hasMore && width > 2) row += `${D_RST}${FG_DIM}›${D_RST}`;
				else row += fillBg + " ".repeat(Math.max(0, width - vis)) + D_RST;
				rows.push(row);
				return rows;
			}
			const state = ansiState(row);
			rows.push(row + D_RST);
			row = state + fillBg;
			vis = 0;
			if (rows.length >= maxRows - 1) {
				onLastRow = true;
				effectiveWidth = width > 2 ? width - 1 : width;
			}
		}
		row += text[i];
		vis++;
		i++;
	}

	if (row.length > 0 || rows.length === 0) {
		rows.push(row + fillBg + " ".repeat(Math.max(0, width - vis)) + D_RST);
	}
	return rows;
}

function lnum(n: number | null, width: number, fg = FG_LNUM): string {
	if (n === null) return " ".repeat(width);
	const value = String(n);
	// Callers reset after the whole gutter cell so wrapped rows keep one
	// continuous add/remove background through the line-number/sign columns.
	return `${fg}${" ".repeat(Math.max(0, width - value.length))}${value}`;
}

function stripes(width: number): string {
	return BG_BASE + FG_STRIPE + "╱".repeat(width) + D_RST;
}

function renderDiffStatBar(added: number, removed: number, width = termW()): string {
	const total = added + removed;
	if (total === 0 || width < 20) return "";
	const slots = Math.max(8, Math.min(20, Math.floor(width / 14)));
	let addSlots = Math.max(0, Math.min(slots, Math.round((added / total) * slots)));
	if (added > 0 && addSlots === 0) addSlots = 1;
	if (removed > 0 && addSlots >= slots) addSlots = slots - 1;
	const removeSlots = Math.max(0, slots - addSlots);
	const addBar = addSlots > 0 ? `${FG_ADD}${"━".repeat(addSlots)}${D_RST}` : "";
	const removeBar = removeSlots > 0 ? `${FG_DEL}${"━".repeat(removeSlots)}${D_RST}` : "";
	return `${FG_DIM}[${D_RST}${addBar}${removeBar}${FG_DIM}]${D_RST}`;
}

function summarizeDiff(added: number, removed: number): string {
	const parts: string[] = [];
	if (added > 0) parts.push(`${FG_ADD}+${added}${D_RST}`);
	if (removed > 0) parts.push(`${FG_DEL}-${removed}${D_RST}`);
	if (!parts.length) return `${FG_DIM}no changes${D_RST}`;
	const bar = renderDiffStatBar(added, removed);
	return bar ? `${parts.join(" ")} ${bar}` : parts.join(" ");
}

function diffSummaryWithMeta(added: number, removed: number, hunks: number, mode: string): string {
	const base = summarizeDiff(added, removed);
	const extras: string[] = [];
	if (hunks > 0) extras.push(`${FG_DIM}${hunks} hunk${hunks === 1 ? "" : "s"}${D_RST}`);
	if (mode) extras.push(`${FG_DIM}${mode}${D_RST}`);
	return extras.length ? `${base} ${FG_DIM}•${D_RST} ${extras.join(` ${FG_DIM}•${D_RST} `)}` : base;
}

function collapsedDiffHint(remainingLines: number, hiddenHunks: number): string {
	const width = termW();
	const candidates = [
		`… (${remainingLines} more diff lines${hiddenHunks > 0 ? ` • ${hiddenHunks} more hunks` : ""} • ${keyHint("app.tools.expand", "to toggle")})`,
		`… (${remainingLines} more lines${hiddenHunks > 0 ? ` • ${hiddenHunks} hunks` : ""})`,
		`… (+${remainingLines}${hiddenHunks > 0 ? ` • +${hiddenHunks}h` : ""})`,
		"…",
	];
	for (const candidate of candidates) {
		if (visibleWidth(candidate) <= width) return candidate;
	}
	return truncateToWidth("…", width, "");
}

function diffRule(width: number): string {
	return `${BG_BASE}${FG_RULE}${"─".repeat(width)}${D_RST}`;
}

/** Max line number across diff lines. Loop-based (not Math.max(...spread)) so huge
 *  diffs don't blow the call stack with a RangeError. Returns identical results. */
function maxLineNumber(lines: DiffLine[]): number {
	let max = 0;
	for (let i = 0; i < lines.length; i++) {
		const n = lines[i].oldNum ?? lines[i].newNum ?? 0;
		if (n > max) max = n;
	}
	return max;
}

function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVIEW_LINES): boolean {
	if (!diff.lines.length) return false;
	if (tw < SPLIT_MIN_WIDTH) return false;
	const nw = Math.max(2, String(maxLineNumber(diff.lines)).length);
	const half = Math.floor((tw - 1) / 2);
	const gw = nw + 5;
	const cw = Math.max(12, half - gw);
	if (cw < SPLIT_MIN_CODE_WIDTH) return false;
	const vis = diff.lines.slice(0, maxRows);
	let contentLines = 0;
	let wrapCandidates = 0;
	for (const line of vis) {
		if (line.type === "sep") continue;
		contentLines++;
		if (tabs(line.content).length > cw) wrapCandidates++;
	}
	if (contentLines === 0) return true;
	const wrapRatio = wrapCandidates / contentLines;
	if (wrapCandidates >= SPLIT_MAX_WRAP_LINES) return false;
	if (wrapRatio >= SPLIT_MAX_WRAP_RATIO) return false;
	return true;
}

const EXT_LANG: Record<string, BundledLanguage> = {
	ts: "typescript",
	tsx: "tsx",
	js: "javascript",
	jsx: "jsx",
	mjs: "javascript",
	cjs: "javascript",
	py: "python",
	rb: "ruby",
	rs: "rust",
	go: "go",
	java: "java",
	c: "c",
	cpp: "cpp",
	h: "c",
	hpp: "cpp",
	cs: "csharp",
	swift: "swift",
	kt: "kotlin",
	html: "html",
	css: "css",
	scss: "scss",
	json: "json",
	yaml: "yaml",
	yml: "yaml",
	toml: "toml",
	md: "markdown",
	sql: "sql",
	sh: "bash",
	bash: "bash",
	zsh: "bash",
	lua: "lua",
	php: "php",
	dart: "dart",
	xml: "xml",
	graphql: "graphql",
	svelte: "svelte",
	vue: "vue",
};

function lang(filePath: string): BundledLanguage | undefined {
	return EXT_LANG[extname(filePath).slice(1).toLowerCase()];
}

async function codeToAnsiLazy(code: string, language: BundledLanguage, theme: BundledTheme): Promise<string> {
	if (!codeToAnsiLoader) {
		// Self-healing: a failed import (missing dep, transient error) must not leave a
		// permanently-rejected promise that later becomes an unhandled rejection. Reset
		// on failure so the next call retries.
		codeToAnsiLoader = import("@shikijs/cli").then(
			(mod) => mod.codeToANSI,
			(err) => {
				codeToAnsiLoader = null;
				throw err;
			},
		);
	}
	const codeToAnsi = await codeToAnsiLoader;
	return codeToAnsi(code, language, theme);
}

const hlCache = new Map<string, string[]>();

function clearHighlightCache(): void {
	hlCache.clear();
}

function touchCache(key: string, value: string[]): string[] {
	hlCache.delete(key);
	hlCache.set(key, value);
	while (hlCache.size > CACHE_LIMIT) {
		const first = hlCache.keys().next().value;
		if (first === undefined) break;
		hlCache.delete(first);
	}
	return value;
}

async function hlBlock(code: string, language: BundledLanguage | undefined): Promise<string[]> {
	if (!code) return [""];
	if (!language || code.length > MAX_HL_CHARS) return code.split("\n");
	const key = `${DIFF_THEME}\0${language}\0${code}`;
	const hit = hlCache.get(key);
	if (hit) return touchCache(key, hit);
	try {
		const ansi = normalizeShikiContrast(await codeToAnsiLazy(code, language, DIFF_THEME));
		const out = (ansi.endsWith("\n") ? ansi.slice(0, -1) : ansi).split("\n");
		return touchCache(key, out);
	} catch {
		return code.split("\n");
	}
}

async function mapWithConcurrency<T, R>(items: T[], limit: number, mapItem: (item: T, index: number) => Promise<R>): Promise<R[]> {
	const safeLimit = Math.max(1, Math.min(items.length || 1, Math.floor(limit)));
	const results = new Array<R>(items.length);
	let nextIndex = 0;
	await Promise.all(Array.from({ length: safeLimit }, async () => {
		while (true) {
			const index = nextIndex++;
			if (index >= items.length) return;
			results[index] = await mapItem(items[index], index);
		}
	}));
	return results;
}

function parseDiff(oldContent: string, newContent: string, ctxLines = 3): ParsedDiff {
	const patch = Diff.structuredPatch("", "", oldContent, newContent, "", "", { context: ctxLines });
	const lines: DiffLine[] = [];
	let added = 0;
	let removed = 0;
	for (let hi = 0; hi < patch.hunks.length; hi++) {
		if (hi > 0) {
			const prev = patch.hunks[hi - 1];
			const gap = patch.hunks[hi].oldStart - (prev.oldStart + prev.oldLines);
			lines.push({ type: "sep", oldNum: null, newNum: gap > 0 ? gap : null, content: "" });
		}
		const hunk = patch.hunks[hi];
		let oldLine = hunk.oldStart;
		let newLine = hunk.newStart;
		for (const raw of hunk.lines) {
			if (raw === "\\ No newline at end of file") continue;
			const ch = raw[0];
			const text = raw.slice(1);
			if (ch === "+") {
				lines.push({ type: "add", oldNum: null, newNum: newLine++, content: text });
				added++;
			} else if (ch === "-") {
				lines.push({ type: "del", oldNum: oldLine++, newNum: null, content: text });
				removed++;
			} else {
				lines.push({ type: "ctx", oldNum: oldLine++, newNum: newLine++, content: text });
			}
		}
	}
	return { lines, added, removed, chars: oldContent.length + newContent.length };
}

function getCachedParsedDiff(ctx: any, key: string, oldContent: string, newContent: string): ParsedDiff {
	if (ctx.state?._parsedDiffKey === key && ctx.state._parsedDiff) {
		return ctx.state._parsedDiff as ParsedDiff;
	}
	const diff = parseDiff(oldContent, newContent);
	if (ctx.state) {
		ctx.state._parsedDiffKey = key;
		ctx.state._parsedDiff = diff;
	}
	return diff;
}

function wordDiffAnalysis(
	oldText: string,
	newText: string,
): { similarity: number; oldRanges: Array<[number, number]>; newRanges: Array<[number, number]> } {
	if (!oldText && !newText) return { similarity: 1, oldRanges: [], newRanges: [] };
	const parts = Diff.diffWords(oldText, newText);
	const oldRanges: Array<[number, number]> = [];
	const newRanges: Array<[number, number]> = [];
	let oldPos = 0;
	let newPos = 0;
	let same = 0;
	for (const part of parts) {
		if (part.removed) {
			oldRanges.push([oldPos, oldPos + part.value.length]);
			oldPos += part.value.length;
		} else if (part.added) {
			newRanges.push([newPos, newPos + part.value.length]);
			newPos += part.value.length;
		} else {
			const len = part.value.length;
			same += len;
			oldPos += len;
			newPos += len;
		}
	}
	const maxLen = Math.max(oldText.length, newText.length);
	return { similarity: maxLen > 0 ? same / maxLen : 1, oldRanges, newRanges };
}

function injectBg(ansiLine: string, ranges: Array<[number, number]>, baseBg: string, hlBg: string): string {
	if (!ranges.length) return baseBg + ansiLine + D_RST;
	let out = baseBg;
	let vis = 0;
	let inHL = false;
	let rangeIndex = 0;
	let i = 0;
	while (i < ansiLine.length) {
		if (ansiLine[i] === "\x1b") {
			const end = ansiLine.indexOf("m", i);
			if (end !== -1) {
				const seq = ansiLine.slice(i, end + 1);
				out += seq;
				if (seq === "\x1b[0m") out += inHL ? hlBg : baseBg;
				i = end + 1;
				continue;
			}
		}
		while (rangeIndex < ranges.length && vis >= ranges[rangeIndex][1]) rangeIndex++;
		const want = rangeIndex < ranges.length && vis >= ranges[rangeIndex][0] && vis < ranges[rangeIndex][1];
		if (want !== inHL) {
			inHL = want;
			out += inHL ? hlBg : baseBg;
		}
		out += ansiLine[i];
		vis++;
		i++;
	}
	return out + D_RST;
}

function plainWordDiff(oldText: string, newText: string): { old: string; new: string } {
	const parts = Diff.diffWords(oldText, newText);
	let oldOut = "";
	let newOut = "";
	for (const part of parts) {
		if (part.removed) oldOut += `${BG_DEL_W}${part.value}${D_RST}${BG_DEL}`;
		else if (part.added) newOut += `${BG_ADD_W}${part.value}${D_RST}${BG_ADD}`;
		else {
			oldOut += part.value;
			newOut += part.value;
		}
	}
	return { old: oldOut, new: newOut };
}

async function renderUnified(
	diff: ParsedDiff,
	language: BundledLanguage | undefined,
	max = MAX_RENDER_LINES,
	dc: DiffColors = DEFAULT_DIFF_COLORS,
	width = termW(),
): Promise<string> {
	if (!diff.lines.length) return "";
	const vis = diff.lines.slice(0, max);
	const tw = width;
	const nw = Math.max(2, String(maxLineNumber(vis)).length);
	const gw = nw + 5;
	const cw = Math.max(20, tw - gw);
	const canHL = diff.chars <= MAX_HL_CHARS && vis.length <= MAX_RENDER_LINES;

	const oldSrc: string[] = [];
	const newSrc: string[] = [];
	for (const line of vis) {
		if (line.type === "ctx" || line.type === "del") oldSrc.push(line.content);
		if (line.type === "ctx" || line.type === "add") newSrc.push(line.content);
	}
	const [oldHL, newHL] = canHL
		? await Promise.all([hlBlock(oldSrc.join("\n"), language), hlBlock(newSrc.join("\n"), language)])
		: [oldSrc, newSrc];

	let oldIndex = 0;
	let newIndex = 0;
	let index = 0;
	const out: string[] = [diffRule(tw)];

	function emitRow(num: number | null, sign: string, gutterBg: string, signFg: string, body: string, bodyBg = ""): void {
		const borderFg = sign === "-" ? dc.fgDel : sign === "+" ? dc.fgAdd : "";
		const border = borderFg ? `${borderFg}▌${D_RST}` : `${BG_BASE} `;
		const numFg = borderFg || FG_LNUM;
		const gutter = `${border}${gutterBg}${lnum(num, nw, numFg)}${signFg}${sign} ${D_RST}${DIVIDER} `;
		const cont = `${border}${gutterBg}${" ".repeat(nw + 2)}${D_RST}${DIVIDER} `;
		const rows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), bodyBg);
		out.push(`${gutter}${rows[0]}${D_RST}`);
		for (let r = 1; r < rows.length; r++) out.push(`${cont}${rows[r]}${D_RST}`);
	}

	while (index < vis.length) {
		const line = vis[index];
		if (line.type === "sep") {
			const gap = line.newNum;
			const label = gap && gap > 0 ? ` ${gap} unmodified lines ` : "···";
			const totalW = Math.min(tw, 72);
			const pad = Math.max(0, totalW - label.length - 2);
			const half1 = Math.floor(pad / 2);
			const half2 = pad - half1;
			out.push(`${BG_BASE}${FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${D_RST}`);
			index++;
			continue;
		}
		if (line.type === "ctx") {
			const hl = oldHL[oldIndex] ?? line.content;
			emitRow(line.newNum, " ", BG_BASE, dc.fgCtx, `${BG_BASE}${D_DIM}${hl}`, BG_BASE);
			oldIndex++;
			newIndex++;
			index++;
			continue;
		}

		const dels: Array<{ l: DiffLine; hl: string }> = [];
		while (index < vis.length && vis[index].type === "del") {
			dels.push({ l: vis[index], hl: oldHL[oldIndex] ?? vis[index].content });
			oldIndex++;
			index++;
		}
		const adds: Array<{ l: DiffLine; hl: string }> = [];
		while (index < vis.length && vis[index].type === "add") {
			adds.push({ l: vis[index], hl: newHL[newIndex] ?? vis[index].content });
			newIndex++;
			index++;
		}

		const isPaired = dels.length === 1 && adds.length === 1;
		const wd = isPaired ? wordDiffAnalysis(dels[0].l.content, adds[0].l.content) : null;
		if (isPaired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
			emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${D_BOLD}`, injectBg(dels[0].hl, wd.oldRanges, BG_DEL, BG_DEL_W), BG_DEL);
			emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${D_BOLD}`, injectBg(adds[0].hl, wd.newRanges, BG_ADD, BG_ADD_W), BG_ADD);
			continue;
		}
		if (isPaired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
			const pwd = plainWordDiff(dels[0].l.content, adds[0].l.content);
			emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${D_BOLD}`, `${BG_DEL}${pwd.old}`, BG_DEL);
			emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${D_BOLD}`, `${BG_ADD}${pwd.new}`, BG_ADD);
			continue;
		}
		for (const d of dels) emitRow(d.l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${D_BOLD}`, `${BG_DEL}${canHL ? d.hl : d.l.content}`, BG_DEL);
		for (const a of adds) emitRow(a.l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${D_BOLD}`, `${BG_ADD}${canHL ? a.hl : a.l.content}`, BG_ADD);
	}

	out.push(diffRule(tw));
	if (diff.lines.length > vis.length) out.push(`${BG_BASE}${FG_DIM}  ${collapsedDiffHint(diff.lines.length - vis.length, 0)}${D_RST}`);
	return out.join("\n");
}

async function renderSplit(
	diff: ParsedDiff,
	language: BundledLanguage | undefined,
	max = MAX_PREVIEW_LINES,
	dc: DiffColors = DEFAULT_DIFF_COLORS,
	width = termW(),
): Promise<string> {
	const tw = width;
	if (!shouldUseSplit(diff, tw, max)) return renderUnified(diff, language, max, dc, width);
	if (!diff.lines.length) return "";

	type Row = { left: DiffLine | null; right: DiffLine | null };
	const rows: Row[] = [];
	let i = 0;
	while (i < diff.lines.length) {
		const line = diff.lines[i];
		if (line.type === "sep" || line.type === "ctx") {
			rows.push({ left: line, right: line });
			i++;
			continue;
		}
		const dels: DiffLine[] = [];
		const adds: DiffLine[] = [];
		while (i < diff.lines.length && diff.lines[i].type === "del") dels.push(diff.lines[i++]);
		while (i < diff.lines.length && diff.lines[i].type === "add") adds.push(diff.lines[i++]);
		const n = Math.max(dels.length, adds.length);
		for (let j = 0; j < n; j++) rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
	}

	const vis = rows.slice(0, max);
	const half = Math.floor((tw - 1) / 2);
	const nw = Math.max(2, String(maxLineNumber(diff.lines)).length);
	const gw = nw + 5;
	const cw = Math.max(12, half - gw);
	const canHL = diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;

	const leftSrc: string[] = [];
	const rightSrc: string[] = [];
	for (const row of vis) {
		if (row.left && row.left.type !== "sep") leftSrc.push(row.left.content);
		if (row.right && row.right.type !== "sep") rightSrc.push(row.right.content);
	}
	const [leftHL, rightHL] = canHL
		? await Promise.all([hlBlock(leftSrc.join("\n"), language), hlBlock(rightSrc.join("\n"), language)])
		: [leftSrc, rightSrc];

	let leftIndex = 0;
	let rightIndex = 0;

	type HalfResult = { gutter: string; contGutter: string; bodyRows: string[] };
	function halfBuild(
		line: DiffLine | null,
		hl: string,
		ranges: Array<[number, number]> | null,
		side: "left" | "right",
	): HalfResult {
		if (!line) {
			const gPat = FG_STRIPE + "╱".repeat(nw + 2) + D_RST;
			const gutter = ` ${gPat}${FG_RULE}│${D_RST} `;
			return { gutter, contGutter: gutter, bodyRows: [stripes(cw)] };
		}
		if (line.type === "sep") {
			const gap = line.newNum;
			const label = gap && gap > 0 ? `··· ${gap} lines ···` : "···";
			const gutter = `${BG_BASE} ${FG_DIM}${fit("", nw + 2)}${D_RST}${FG_RULE}│${D_RST} `;
			return { gutter, contGutter: gutter, bodyRows: [`${BG_BASE}${FG_DIM}${fit(label, cw)}${D_RST}`] };
		}
		const isDel = line.type === "del";
		const isAdd = line.type === "add";
		const gBg = isDel ? BG_GUTTER_DEL : isAdd ? BG_GUTTER_ADD : BG_BASE;
		const cBg = isDel ? BG_DEL : isAdd ? BG_ADD : BG_BASE;
		const sFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : dc.fgCtx;
		const sign = isDel ? "-" : isAdd ? "+" : " ";
		const num = isDel ? line.oldNum : isAdd ? line.newNum : side === "left" ? line.oldNum : line.newNum;
		const borderFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : "";
		const border = borderFg ? `${borderFg}▌${D_RST}` : ` ${BG_BASE}`;
		const numFg = borderFg || FG_LNUM;
		let body: string;
		if (ranges && ranges.length > 0) body = injectBg(hl, ranges, cBg, isDel ? BG_DEL_W : BG_ADD_W);
		else if (isDel || isAdd) body = `${cBg}${hl}`;
		else body = `${BG_BASE}${D_DIM}${hl}`;
		const gutter = `${border}${gBg}${lnum(num, nw, numFg)}${sFg}${D_BOLD}${sign} ${D_RST}${FG_RULE}│${D_RST} `;
		const contGutter = `${border}${gBg}${" ".repeat(nw + 2)}${D_RST}${FG_RULE}│${D_RST} `;
		return { gutter, contGutter, bodyRows: wrapAnsi(tabs(body), cw, adaptiveWrapRows(), cBg) };
	}

	const out: string[] = [];
	const hdrOld = `${BG_BASE}${" ".repeat(Math.max(0, nw - 2))}${dc.fgDel}${D_DIM}old${D_RST}`;
	const hdrNew = `${BG_BASE}${" ".repeat(Math.max(0, nw - 2))}${dc.fgAdd}${D_DIM}new${D_RST}`;
	out.push(`${BG_BASE}${hdrOld}${" ".repeat(Math.max(0, half - nw - 1))}${FG_RULE}┊${D_RST}${hdrNew}`);
	out.push(`${diffRule(half)}${FG_RULE}┊${D_RST}${diffRule(half)}`);

	for (const row of vis) {
		const leftLine = row.left;
		const rightLine = row.right;
		const paired = Boolean(leftLine && rightLine && leftLine.type === "del" && rightLine.type === "add");
		const wd = paired && leftLine && rightLine ? wordDiffAnalysis(leftLine.content, rightLine.content) : null;
		let leftResult: HalfResult;
		let rightResult: HalfResult;
		if (paired && wd && leftLine && rightLine && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
			leftResult = halfBuild(leftLine, leftHL[leftIndex++] ?? leftLine.content, wd.oldRanges, "left");
			rightResult = halfBuild(rightLine, rightHL[rightIndex++] ?? rightLine.content, wd.newRanges, "right");
		} else if (paired && wd && leftLine && rightLine && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
			const pwd = plainWordDiff(leftLine.content, rightLine.content);
			leftIndex++;
			rightIndex++;
			leftResult = halfBuild(leftLine, pwd.old, null, "left");
			rightResult = halfBuild(rightLine, pwd.new, null, "right");
		} else {
			leftResult = halfBuild(
				row.left,
				row.left && row.left.type !== "sep" ? (leftHL[leftIndex++] ?? row.left.content) : "",
				null,
				"left",
			);
			rightResult = halfBuild(
				row.right,
				row.right && row.right.type !== "sep" ? (rightHL[rightIndex++] ?? row.right.content) : "",
				null,
				"right",
			);
		}
		const maxRows = Math.max(leftResult.bodyRows.length, rightResult.bodyRows.length);
		for (let rowIndex = 0; rowIndex < maxRows; rowIndex++) {
			const lg = rowIndex === 0 ? leftResult.gutter : leftResult.contGutter;
			const rg = rowIndex === 0 ? rightResult.gutter : rightResult.contGutter;
			const lb = leftResult.bodyRows[rowIndex] ?? (!row.left ? stripes(cw) : `${BG_EMPTY}${" ".repeat(cw)}${D_RST}`);
			const rb = rightResult.bodyRows[rowIndex] ?? (!row.right ? stripes(cw) : `${BG_EMPTY}${" ".repeat(cw)}${D_RST}`);
			out.push(`${lg}${lb}${DIVIDER}${rg}${rb}`);
		}
	}

	out.push(`${diffRule(half)}${FG_RULE}┊${D_RST}${diffRule(half)}`);
	if (rows.length > vis.length) out.push(`${BG_BASE}${FG_DIM}  ${collapsedDiffHint(rows.length - vis.length, 0)}${D_RST}`);
	return out.join("\n");
}

function getEditOperations(input: any): Array<{ oldText: string; newText: string }> {
	if (Array.isArray(input?.edits)) {
		return input.edits
			.map((edit: any) => ({
				oldText: typeof edit?.oldText === "string" ? edit.oldText : typeof edit?.old_text === "string" ? edit.old_text : "",
				newText: typeof edit?.newText === "string" ? edit.newText : typeof edit?.new_text === "string" ? edit.new_text : "",
			}))
			.filter((edit: { oldText: string; newText: string }) => edit.oldText && edit.oldText !== edit.newText);
	}
	const oldText = typeof input?.oldText === "string" ? input.oldText : typeof input?.old_text === "string" ? input.old_text : "";
	const newText = typeof input?.newText === "string" ? input.newText : typeof input?.new_text === "string" ? input.new_text : "";
	return oldText && oldText !== newText ? [{ oldText, newText }] : [];
}

function summarizeEditOperations(operations: Array<{ oldText: string; newText: string }>) {
	const diffs = operations.map((edit) => parseDiff(edit.oldText, edit.newText));
	const totalAdded = diffs.reduce((sum, diff) => sum + diff.added, 0);
	const totalRemoved = diffs.reduce((sum, diff) => sum + diff.removed, 0);
	const totalLines = diffs.reduce((sum, diff) => sum + diff.lines.length, 0);
	const totalHunks = diffs.reduce((sum, diff) => sum + diff.lines.filter((l) => l.type === "sep").length + (diff.lines.length ? 1 : 0), 0);
	return { diffs, totalAdded, totalRemoved, totalLines, totalHunks, summary: summarizeDiff(totalAdded, totalRemoved) };
}

type EditOperationSummary = ReturnType<typeof summarizeEditOperations>;

function getCachedEditOperationSummary(ctx: any, key: string, operations: Array<{ oldText: string; newText: string }>): EditOperationSummary {
	if (ctx.state?._editSummaryKey === key && ctx.state._editSummary) {
		return ctx.state._editSummary as EditOperationSummary;
	}
	const summary = summarizeEditOperations(operations);
	if (ctx.state) {
		ctx.state._editSummaryKey = key;
		ctx.state._editSummary = summary;
	}
	return summary;
}

function normalizeToLf(text: string): string {
	return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}

function stripBomText(text: string): string {
	return text.startsWith("\uFEFF") ? text.slice(1) : text;
}

function normalizeTextForFuzzyMatch(text: string): string {
	return text
		.normalize("NFKC")
		.split("\n")
		.map((line) => line.trimEnd())
		.join("\n")
		.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
		.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
		.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
		.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
}

function findEditMatch(content: string, oldText: string): { found: boolean; index: number; matchLength: number; usedFuzzyMatch: boolean } {
	const exactIndex = content.indexOf(oldText);
	if (exactIndex !== -1) return { found: true, index: exactIndex, matchLength: oldText.length, usedFuzzyMatch: false };
	const fuzzyContent = normalizeTextForFuzzyMatch(content);
	const fuzzyOldText = normalizeTextForFuzzyMatch(oldText);
	const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
	return fuzzyIndex === -1
		? { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false }
		: { found: true, index: fuzzyIndex, matchLength: fuzzyOldText.length, usedFuzzyMatch: true };
}

function countFuzzyOccurrences(content: string, oldText: string): number {
	const fuzzyContent = normalizeTextForFuzzyMatch(content);
	const fuzzyOldText = normalizeTextForFuzzyMatch(oldText);
	return fuzzyContent.split(fuzzyOldText).length - 1;
}

function lineNumberAtIndex(text: string, index: number): number {
	return text.slice(0, Math.max(0, index)).split("\n").length;
}

function countLineBreaks(text: string): number {
	return (text.match(/\n/g) ?? []).length;
}

function offsetParsedDiff(diff: ParsedDiff, oldOffset: number, newOffset = oldOffset): ParsedDiff {
	return {
		...diff,
		lines: diff.lines.map((line) =>
			line.type === "sep"
				? line
				: {
					...line,
					oldNum: line.oldNum === null ? null : line.oldNum + oldOffset,
					newNum: line.newNum === null ? null : line.newNum + newOffset,
				},
		),
	};
}

function getFirstChangedNewLine(diff: ParsedDiff): number {
	let currentNewLine = 0;
	for (let i = 0; i < diff.lines.length; i++) {
		const line = diff.lines[i];
		if (line.type === "sep") {
			currentNewLine = 0;
			continue;
		}
		if (line.type === "ctx") {
			currentNewLine = (line.newNum ?? currentNewLine) + 1;
			continue;
		}
		if (line.type === "add") return line.newNum ?? currentNewLine;
		if (currentNewLine > 0) return currentNewLine;
		const next = diff.lines.slice(i + 1).find((entry) => entry.type !== "sep" && entry.newNum !== null);
		if (next && next.newNum !== null) return next.newNum;
		return line.oldNum ?? 0;
	}
	return 0;
}

interface LocalizedEditDiff {
	diff: ParsedDiff;
	line: number;
}

async function computeLocalizedEditDiffs(filePath: string, operations: Array<{ oldText: string; newText: string }>, cwd: string): Promise<LocalizedEditDiff[] | null> {
	if (!filePath || operations.length === 0) return null;
	try {
		const rawContent = await readFileAsync(resolve(cwd, filePath), "utf8");
		const normalizedContent = normalizeToLf(stripBomText(rawContent));
		const normalizedOps = operations.map((edit) => ({ oldText: normalizeToLf(edit.oldText), newText: normalizeToLf(edit.newText) }));
		const baseContent = normalizedOps.some((edit) => findEditMatch(normalizedContent, edit.oldText).usedFuzzyMatch)
			? normalizeTextForFuzzyMatch(normalizedContent)
			: normalizedContent;
		const matches = normalizedOps.map((edit, editIndex) => {
			const match = findEditMatch(baseContent, edit.oldText);
			if (!match.found || countFuzzyOccurrences(baseContent, edit.oldText) !== 1) return null;
			return { editIndex, matchIndex: match.index, matchLength: match.matchLength, newText: edit.newText };
		});
		if (matches.some((match) => match === null)) return null;
		const ordered = [...(matches as Array<{ editIndex: number; matchIndex: number; matchLength: number; newText: string }>)].sort((a, b) => a.matchIndex - b.matchIndex);
		for (let i = 1; i < ordered.length; i++) {
			const prev = ordered[i - 1];
			const current = ordered[i];
			if (prev.matchIndex + prev.matchLength > current.matchIndex) return null;
		}
		const localized: Array<LocalizedEditDiff | null> = Array(operations.length).fill(null);
		let lineDelta = 0;
		for (const match of ordered) {
			const oldChunk = baseContent.slice(match.matchIndex, match.matchIndex + match.matchLength);
			const oldStartLine = lineNumberAtIndex(baseContent, match.matchIndex);
			const newStartLine = oldStartLine + lineDelta;
			const diff = offsetParsedDiff(parseDiff(oldChunk, match.newText), oldStartLine - 1, newStartLine - 1);
			localized[match.editIndex] = { diff, line: getFirstChangedNewLine(diff) };
			lineDelta += countLineBreaks(match.newText) - countLineBreaks(oldChunk);
		}
		return localized.every(Boolean) ? (localized as LocalizedEditDiff[]) : null;
	} catch {
		return null;
	}
}

function renderEditPreviewBody(
	ctx: any,
	key: string,
	theme: Theme,
	language: BundledLanguage | undefined,
	operations: Array<{ oldText: string; newText: string }>,
	diffs: ParsedDiff[],
	lines: number[],
	summary: string,
): void {
	const dc = resolveDiffColors(theme);
	const branchWidth = contextDiffWidth(ctx, 3);
	if (operations.length === 1) {
		const [diff] = diffs;
		const line = lines[0] ?? getFirstChangedNewLine(diff);
		renderSplit(diff, language, ctx.expanded ? MAX_PREVIEW_LINES : 32, dc, branchWidth)
			.then((rendered) => {
				if (ctx.state._pk !== key) return;
				ctx.state._ptBody = `${summarizeDiff(diff.added, diff.removed)}${formatLineMeta(line, theme)}\n${rendered}`;
				ctx.state._ptDisplay = indentBranchBlock(withBranch(ctx.state._ptBody, theme, false, true));
				safeInvalidate(ctx);
			})
			.catch(() => {
				if (ctx.state._pk !== key) return;
				ctx.state._ptBody = `${summarizeDiff(diff.added, diff.removed)}${formatLineMeta(line, theme)}`;
				ctx.state._ptDisplay = indentBranchBlock(withBranch(ctx.state._ptBody, theme, false, true));
				safeInvalidate(ctx);
			});
		return;
	}
	const maxShown = ctx.expanded ? operations.length : Math.min(operations.length, 3);
	const previewLines = ctx.expanded
		? Math.max(6, Math.floor(MAX_RENDER_LINES / Math.max(1, maxShown)))
		: Math.max(8, Math.floor(MAX_PREVIEW_LINES / Math.max(1, maxShown)));
	mapWithConcurrency(diffs.slice(0, maxShown), DIFF_RENDER_CONCURRENCY, async (diff, index) => {
		const line = lines[index] ?? getFirstChangedNewLine(diff);
		return renderSplit(diff, language, previewLines, dc, branchWidth)
			.then((rendered) => `Edit ${index + 1}/${operations.length}${formatLineMeta(line, theme)}\n${rendered}`)
			.catch(() => `Edit ${index + 1}/${operations.length}${formatLineMeta(line, theme)} ${summarizeDiff(diff.added, diff.removed)}`);
	})
		.then((sections) => {
			if (ctx.state._pk !== key) return;
			const remainder = operations.length - maxShown;
			const suffix = remainder > 0
				? `\n${theme.fg("muted", `… ${remainder} more edit blocks${toolOutputDetailHint(theme, ctx.expanded, true)}`)}`
				: "";
			ctx.state._ptBody = `${operations.length} edits ${summary}\n\n${sections.join("\n\n")}${suffix}`;
			ctx.state._ptDisplay = indentBranchBlock(withBranch(ctx.state._ptBody, theme, false, true));
			safeInvalidate(ctx);
		})
		.catch(() => {
			if (ctx.state._pk !== key) return;
			ctx.state._ptBody = `${operations.length} edits ${summary}`;
			ctx.state._ptDisplay = indentBranchBlock(withBranch(ctx.state._ptBody, theme, false, true));
			safeInvalidate(ctx);
		});
}

function stripThinkingPresentationArtifacts(text: string): string {
	if (!ANSI_PRESENT_RE.test(text) && !/^\s*thinking:\s*/i.test(text)) return text;
	let current = ANSI_PRESENT_RE.test(text) ? text.replace(ANSI_RE, "") : text;
	while (true) {
		const next = current.replace(/^(?:thinking:\s*)+/i, "").trimStart();
		if (next === current) return current;
		current = next;
	}
}

function prefixThinkingLine(text: string, _theme: Theme | undefined): string {
	if (!ANSI_PRESENT_RE.test(text) && text.startsWith("Thinking: ") && !/^Thinking:\s*thinking:\s*/i.test(text)) {
		return text;
	}
	const normalized = stripThinkingPresentationArtifacts(text).trim();
	if (!normalized) return text;
	return `Thinking: ${normalized}`;
}

function trackThinkingBlockEvents(event: any, ctx?: any): void {
	const evt = event?.assistantMessageEvent;
	const message = event?.message;
	if (!evt || typeof evt.type !== "string") return;
	function refreshThinkingChrome(): void {
		try {
			ctx?.ui?.invalidate?.();
			ctx?.ui?.requestRender?.();
		} catch { /* noop */ }
		// Pi may call AssistantMessageComponent.updateContent before extension
		// handlers run on the same thinking_end event — nudge one more frame.
		setTimeout(() => {
			try {
				ctx?.ui?.invalidate?.();
				ctx?.ui?.requestRender?.();
			} catch { /* noop */ }
		}, 0);
	}

	if (evt.type === "thinking_start") {
		thinkingBlockInFlight = true;
		thinkingBlockStartMs = Date.now();
		lastThinkingBlockDurationMs = undefined;
		if (message?.role === "assistant") {
			(message as any)[THINKING_ACTIVE_KEY] = true;
			delete (message as any)[THINKING_DURATION_KEY];
		}
		refreshThinkingChrome();
		return;
	}
	if (evt.type === "thinking_end") {
		thinkingBlockInFlight = false;
		const duration = Math.max(0, Date.now() - thinkingBlockStartMs);
		if (message?.role === "assistant") delete (message as any)[THINKING_ACTIVE_KEY];
		lastThinkingBlockDurationMs = duration;
		if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
		refreshThinkingChrome();
		return;
	}
	// Fallback: some providers/models never emit thinking_end (a second
	// thinking_start can overwrite the first, or the turn can end with toolUse).
	// The live "Thinking..." row would otherwise stick forever while later calls
	// run normally. Any non-thinking stream event on the same assistant message
	// means thinking is no longer the live activity: freeze the elapsed time into
	// a "Thought for Xs" duration so the row always resolves.
	if ((message as any)?.[THINKING_ACTIVE_KEY] || thinkingBlockInFlight) {
		if (evt.type === "text_start" || evt.type === "text_delta" || evt.type === "toolcall_start" || evt.type === "toolcall_end") {
			thinkingBlockInFlight = false;
			const duration = thinkingBlockStartMs > 0 ? Math.max(0, Date.now() - thinkingBlockStartMs) : undefined;
			if (message?.role === "assistant") delete (message as any)[THINKING_ACTIVE_KEY];
			if (typeof duration === "number") {
				lastThinkingBlockDurationMs = duration;
				if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
			}
			refreshThinkingChrome();
		}
	}
}

function registerThinkingLabels(pi: ExtensionAPI): void {
	const patchMessage = (event: any, theme?: Theme) => {
		// Keep theme-derived border / dim text colors in sync with the
		// active syCode theme. Cheap when the theme hasn't changed (identity check).
		if (theme) applyThemePaletteIfNeeded(theme);
		const message = event?.message;
		if (!message || message.role !== "assistant" || !Array.isArray(message.content)) return;
		for (const block of message.content) {
			if (block && block.type === "thinking" && typeof block.thinking === "string") {
				block.thinking = prefixThinkingLine(block.thinking, theme);
			}
		}
	};
	pi.on("before_agent_start", async () => {
		// Start once per top-level request. Steering/follow-up messages can be
		// injected while the agent is already active; those must not reset the
		// request timer.
		if (currentAgentWorkStartMs === undefined) {
			currentAgentWorkStartMs = Date.now();
		}
		if (sessionStartMs === undefined) sessionStartMs = Date.now();
		currentAssistantMessageStartMs = undefined;
	});
	pi.on("agent_start", async () => {
		if (currentAgentWorkStartMs === undefined) {
			currentAgentWorkStartMs = Date.now();
		}
		if (sessionStartMs === undefined) sessionStartMs = Date.now();
		currentAssistantMessageStartMs = undefined;
	});
	pi.on("message_start", async (event: any) => {
		const message = event?.message;
		if (message?.role === "user" && currentAgentWorkStartMs === undefined) {
			currentAgentWorkStartMs = Date.now();
		}
		if (message?.role === "assistant") {
			currentAssistantMessageStartMs = Date.now();
			(message as any)[WORKED_START_KEY] = currentAssistantMessageStartMs;
			// A new assistant message starts a fresh thinking lifecycle. Without
			// this, a missing thinking_end on the previous message leaves the
			// global in-flight flag set and the next message renders a stale
			// "Thinking..." row until its own thinking events arrive.
			thinkingBlockInFlight = false;
			delete (message as any)[THINKING_ACTIVE_KEY];
		}
	});
	pi.on("message_update", async (event, ctx) => {
		trackThinkingBlockEvents(event, ctx);
		patchMessage(event, ctx.ui?.theme);
	});
	pi.on("message_end", async (event, ctx) => {
		const message = (event as any)?.message;
		if (message?.role === "assistant") {
			// Belt-and-suspenders: if thinking_end never fired (provider skipped
			// it, or the turn ended on toolUse/text), freeze any live "Thinking..."
			// into its "Thought for Xs" duration here so the row always resolves
			// at the end of the message instead of sticking into later calls.
			thinkingBlockInFlight = false;
			delete (message as any)[THINKING_ACTIVE_KEY];
			if (typeof (message as any)[THINKING_DURATION_KEY] !== "number") {
				const duration = thinkingBlockStartMs > 0 ? Math.max(0, Date.now() - thinkingBlockStartMs) : undefined;
				if (typeof duration === "number" && duration > 0) {
					lastThinkingBlockDurationMs = duration;
					(message as any)[THINKING_DURATION_KEY] = duration;
				} else if (typeof lastThinkingBlockDurationMs === "number" && lastThinkingBlockDurationMs > 0) {
					(message as any)[THINKING_DURATION_KEY] = lastThinkingBlockDurationMs;
				}
			}
			thinkingBlockStartMs = 0;
			const started = typeof currentAgentWorkStartMs === "number"
				? currentAgentWorkStartMs
				: typeof (message as any)[WORKED_START_KEY] === "number"
					? (message as any)[WORKED_START_KEY]
					: currentAssistantMessageStartMs;
			const isFinalAssistantMessage = message.stopReason === "stop";
			if (started !== undefined && isFinalAssistantMessage) {
				const durationMs = Date.now() - started;
				const sessionTotalMs = typeof sessionStartMs === "number" ? Date.now() - sessionStartMs : undefined;
				const turns = userTurnCount > 0 ? userTurnCount : undefined;
				(message as any)[WORKED_DURATION_KEY] = durationMs;
				if (typeof sessionTotalMs === "number") (message as any)[WORKED_SESSION_TOTAL_KEY] = sessionTotalMs;
				if (typeof turns === "number") (message as any)[WORKED_TURNS_KEY] = turns;
				// Duration metadata drives the assistant component's TUI-only status line.
				// Message content stays presentation-neutral for persistence and consumers.
			}
			currentAssistantMessageStartMs = undefined;
		}
		patchMessage(event, ctx.ui?.theme);
		try {
			(ctx as any)?.ui?.invalidate?.();
			(ctx as any)?.ui?.requestRender?.();
		} catch { /* noop */ }
	});
	pi.on("agent_end", async () => {
		currentAgentWorkStartMs = undefined;
		currentAssistantMessageStartMs = undefined;
	});
	pi.on("session_start", async () => {
		// Reset session-wide accumulators on every session transition (new / resume /
		// fork / reload). The `context` event re-seeds them from the new session's
		// message history, so /new starts fresh while /resume picks up past prompts
		// and the original session start time. Resetting here is what lets /new
		// clear the totals (Math.min / Math.max seeding alone could never lower them).
		// Also drop the live-agent marker so history partials rebuilt during resume
		// never look "in flight" and re-arm blink timers.
		currentAgentWorkStartMs = undefined;
		currentAssistantMessageStartMs = undefined;
		sessionStartMs = undefined;
		userTurnCount = 0;
	});
	pi.on("context", async (event) => {
		const messages = (event as any)?.messages;
		if (!Array.isArray(messages)) return;
		// Seed session-wide accumulators from the full message history (covers
		// /resume — past prompts and the original session start time are included).
		// Values are monotonic, so recomputing on every fire stays stable.
		let earliest: number | undefined;
		let userCount = 0;
		for (const msg of messages) {
			if (!msg) continue;
			if (msg.role === "user") userCount++;
			if (typeof msg.timestamp === "number" && Number.isFinite(msg.timestamp)) {
				if (earliest === undefined || msg.timestamp < earliest) earliest = msg.timestamp;
			}
		}
		if (earliest !== undefined) {
			sessionStartMs = sessionStartMs === undefined ? earliest : Math.min(sessionStartMs, earliest);
		}
		if (userCount > userTurnCount) userTurnCount = userCount;
		for (const msg of messages) {
			if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
			for (const block of msg.content) {
				if (block && block.type === "thinking" && typeof block.thinking === "string") {
					block.thinking = stripThinkingPresentationArtifacts(block.thinking);
				}
				if (block && block.type === "text" && typeof block.text === "string") {
					block.text = stripWorkedDurationLine(block.text);
				}
			}
		}
	});
}

function getMode<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
	return typeof value === "string" && (allowed as readonly string[]).includes(value) ? (value as T) : fallback;
}

const CORE_TOOL_OVERRIDES = new Set(["read", "bash", "grep", "find", "ls", "write", "edit"]);

const OPENAI_STYLE_TOOL_NAMES = new Set([
	"apply_patch",
	"webfetch",
	"question",
	"questionnaire",
	"context_tag",
	"context_log",
	"context_checkout",
	"annotate",
	"web_search",
	"code_search",
	"fetch_content",
	"get_search_content",
	"alpha_search",
	"alpha_get_paper",
	"alpha_ask_paper",
	"alpha_annotate_paper",
	"alpha_list_annotations",
	"alpha_read_code",
	"Skill",
	"EnterPlanMode",
	"ExitPlanMode",
	"Agent",
	"get_subagent_result",
	"steer_subagent",
	"TaskCreate",
	"TaskList",
	"TaskGet",
	"TaskUpdate",
	"TaskOutput",
	"TaskStop",
	"TaskExecute",
	// Magic Context registers specialized renderers of its own. Re-register its
	// tools through the public API so they use the same Claude-style rows as
	// every other external tool handled by this extension.
	"ctx_search",
	"ctx_memory",
	"ctx_note",
	"ctx_expand",
	"ctx_reduce",
	"todowrite",
]);

function isMcpToolCandidate(tool: unknown): boolean {
	const rec = tool as Record<string, unknown> | undefined;
	const name = typeof rec?.name === "string" ? rec.name : "";
	const description = typeof rec?.description === "string" ? rec.description : "";
	return name === "mcp" || /\bmcp\b/i.test(description);
}

function isOpenAiToolCandidate(tool: unknown): boolean {
	const rec = tool as Record<string, unknown> | undefined;
	const name = typeof rec?.name === "string" ? rec.name : "";
	if (!name || CORE_TOOL_OVERRIDES.has(name) || isMcpToolCandidate(tool)) return false;
	return OPENAI_STYLE_TOOL_NAMES.has(name);
}

function humanizeToolName(name: string): string {
	return name
		.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
		.replace(/[_-]+/g, " ")
		.replace(/\b\w/g, (char) => char.toUpperCase());
}

function isMcpToolName(name: string): boolean {
	return name === "mcp" || /^mcp[_:-]/i.test(name) || /[_:-]mcp[_:-]/i.test(name);
}

function shouldUseGenericToolRenderer(name: unknown): boolean {
	return typeof name === "string" && name.length > 0 && !CORE_TOOL_OVERRIDES.has(name);
}

function genericToolLabel(name: string): string {
	return isMcpToolName(name) ? "MCP" : humanizeToolName(name);
}

function renderGenericToolCall(name: string, args: any, theme: Theme, ctx: any): Text {
	syncToolCallStatus(ctx);
	ctx.state._openAiPatchFiles = [];
	// Agent / subagent tools get a size-breathing pending marker, not on/off ●.
	if (isAgentFamilyToolName(name)) ctx.state._agentBreathe = true;
	const sp = (path: string) => shortPath(ctx.cwd ?? process.cwd(), path);
	const summary = stableCallSummary(ctx, "_callSummary", () => summarizeGenericToolCall(name, args, theme, sp));
	return makeText(
		ctx.lastComponent,
		toolHeader(genericToolLabel(name), summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
	);
}

function renderGenericToolResult(name: string, result: any, options: any, theme: Theme, ctx: any): Text {
	if (isMcpToolName(name)) {
		return renderMcpToolResult(result, !!options?.expanded, !!options?.isPartial, theme, ctx);
	}
	return renderOpenAiToolResult(
		name,
		{ content: result.content, details: result.details },
		!!options?.expanded,
		!!options?.isPartial,
		theme,
		ctx,
	);
}

function getTextContent(result: any): string {
	if (!Array.isArray(result?.content)) return "";
	return result.content
		.filter((block: any) => block?.type === "text" && typeof block.text === "string")
		.map((block: any) => block.text)
		.join("\n");
}

function collectNonEmptyLines(text: string, tailLimit?: number): { lines: string[]; total: number } {
	const keepTail = typeof tailLimit === "number" && Number.isFinite(tailLimit);
	const limit = keepTail ? Math.max(0, Math.floor(tailLimit)) : 0;
	const lines: string[] = [];
	let total = 0;
	let start = 0;
	while (start <= text.length) {
		const newline = text.indexOf("\n", start);
		const end = newline === -1 ? text.length : newline;
		const line = text.slice(start, end);
		if (line.trim().length > 0) {
			total++;
			if (!keepTail) {
				lines.push(line);
			} else if (limit > 0) {
				if (lines.length === limit) lines.shift();
				lines.push(line);
			}
		}
		if (newline === -1) break;
		start = newline + 1;
	}
	return { lines, total };
}

function lineCountLabel(count: number): string {
	return `${count} line${count === 1 ? "" : "s"}`;
}

function runningPreviewBlock(
	result: any,
	_statusText: string,
	expanded: boolean,
	theme: Theme,
	ctx: any,
	options: { lines?: string[]; totalLineCount?: number; styleLine?: (line: string) => string; tail?: boolean } = {},
): string {
	// Keep the header status dot blinking while partial output streams. Call/result
	// renderers share rendererState, so setupBlinkTimer here re-arms the same key
	// the call header uses — but only when this tool actually started executing.
	syncToolCallStatus(ctx);
	if (ctx?.state?._toolStatus === "pending") setupBlinkTimer(ctx);
	else clearBlinkTimer(ctx);

	const limit = liveToolPreviewLimit();
	let lines: string[];
	let totalLineCount: number;
	if (options.lines) {
		lines = options.lines;
		totalLineCount = options.totalLineCount ?? lines.length;
	} else {
		// Single-pass collect; when collapsed only keep the preview window.
		const raw = getTextContent(result).replace(/\r\n/g, "\n").trimEnd();
		const collected = collectNonEmptyLines(raw, expanded ? undefined : limit);
		lines = collected.lines;
		totalLineCount = collected.total;
	}
	// Line count lives on the tool heading (via liveLineCountTrailing); keep it in
	// renderer state so the next renderCall pass can pick it up.
	if (ctx?.state) ctx.state._liveLineCount = totalLineCount;

	if (!liveToolPreviewEnabled() || limit <= 0 || totalLineCount === 0) {
		// No status row — the blinking ● on the header is the only running indicator.
		return "";
	}

	const styleLine = options.styleLine ?? ((line: string) => theme.fg("dim", line || " "));
	// Prefer pre-collected tail lines; otherwise only take what the preview needs.
	const previewSource = options.tail && !expanded
		? (lines.length > limit ? lines.slice(-limit) : lines)
		: lines;
	// For tail previews the "earlier lines" prefix owns the remaining count — pass
	// previewSource.length so buildPreviewText doesn't also append "more lines".
	const previewTotal = options.tail && !expanded ? previewSource.length : totalLineCount;
	let preview = buildPreviewText(previewSource, expanded, theme, limit, previewTotal, styleLine);
	if (options.tail && !expanded && totalLineCount > previewSource.length) {
		preview = `${theme.fg("muted", `... (${totalLineCount - previewSource.length} earlier lines${toolOutputDetailHint(theme, expanded, true)})`)}\n${preview}`;
	}
	return withBranch(preview, theme);
}

function buildPersistentBashPreview(lines: string[], theme: Theme): string {
	const limit = liveToolPreviewLimit();
	if (!liveToolPreviewEnabled() || limit <= 0 || lines.length === 0) return "";
	const start = Math.max(0, lines.length - limit);
	let preview = "";
	for (let i = start; i < lines.length; i++) {
		const styled = theme.fg("dim", lines[i]);
		preview += i === start ? styled : `\n${styled}`;
	}
	const earlier = start;
	if (earlier > 0) {
		preview = `${theme.fg("muted", `... (${earlier} earlier lines)`)}\n${preview}`;
	}
	return preview;
}

function getStringArg(args: any, ...keys: string[]): string {
	for (const key of keys) {
		const value = args?.[key];
		if (typeof value === "string" && value.trim()) return value.trim();
	}
	return "";
}

function getStringArrayArg(args: any, ...keys: string[]): string[] {
	for (const key of keys) {
		const value = args?.[key];
		if (!Array.isArray(value)) continue;
		const items = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
		if (items.length > 0) return items;
	}
	return [];
}

function extractApplyPatchFiles(patchText: string): string[] {
	if (!patchText) return [];
	const files = new Set<string>();
	for (const match of patchText.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)) {
		const filePath = match[1]?.trim();
		if (filePath) files.add(filePath);
	}
	return [...files];
}

interface ApplyPatchChangePreview {
	kind: "add" | "update" | "delete";
	path: string;
	displayPath: string;
	moveTo?: string;
	diff: ParsedDiff;
	language: BundledLanguage | undefined;
	hunks: number;
	summary: string;
	line: number;
}

interface ApplyPatchPreview {
	changes: ApplyPatchChangePreview[];
	totalAdded: number;
	totalRemoved: number;
	totalHunks: number;
	totalLines: number;
	summary: string;
}

interface ApplyPatchResultMeta {
	changeCount: number;
	totalAdded: number;
	totalRemoved: number;
	totalHunks: number;
	totalLines: number;
	firstChange?: {
		displayPath: string;
		kind: ApplyPatchChangePreview["kind"];
		hunks: number;
		line: number;
		added: number;
		removed: number;
	};
}

function buildApplyPatchResultMeta(preview: ApplyPatchPreview): ApplyPatchResultMeta {
	const firstChange = preview.changes[0];
	return {
		changeCount: preview.changes.length,
		totalAdded: preview.totalAdded,
		totalRemoved: preview.totalRemoved,
		totalHunks: preview.totalHunks,
		totalLines: preview.totalLines,
		firstChange: firstChange
			? {
				displayPath: firstChange.displayPath,
				kind: firstChange.kind,
				hunks: firstChange.hunks,
				line: firstChange.line,
				added: firstChange.diff.added,
				removed: firstChange.diff.removed,
			}
			: undefined,
	};
}

function countDiffHunks(diff: ParsedDiff): number {
	return diff.lines.length === 0 ? 0 : diff.lines.filter((line) => line.type === "sep").length + 1;
}

function getApplyPatchLine(diff: ParsedDiff, kind: ApplyPatchChangePreview["kind"]): number {
	if (kind === "add") {
		return diff.lines.find((line) => line.type === "add" && line.newNum !== null)?.newNum ?? 1;
	}
	if (kind === "delete") {
		return diff.lines.find((line) => line.type === "del" && line.oldNum !== null)?.oldNum ?? 1;
	}
	for (const line of diff.lines) {
		if (line.type === "add" && line.newNum !== null) return line.newNum;
		if (line.type === "del" && line.oldNum !== null) return line.oldNum;
	}
	return 0;
}

function parsePatchBodyLine(rawLine: string): { marker: "+" | "-" | " "; content: string } {
	const marker = rawLine[0];
	if (marker === "+" || marker === "-" || marker === " ") return { marker, content: rawLine.slice(1) };
	return { marker: " ", content: rawLine };
}

function findLineSequence(haystack: string[], needle: string[], fromIndex = 0): number {
	if (needle.length === 0) return Math.max(0, fromIndex);
	outer: for (let i = Math.max(0, fromIndex); i <= haystack.length - needle.length; i++) {
		for (let j = 0; j < needle.length; j++) {
			if (haystack[i + j] !== needle[j]) continue outer;
		}
		return i;
	}
	return -1;
}

function inferApplyPatchHunkStarts(lines: string[], sourceContent: string): Array<{ oldStart: number | null; newStart: number | null }> {
	const sourceLines = normalizeToLf(sourceContent).split("\n");
	const hunks: string[][] = [];
	let currentHunk: string[] | null = null;
	for (const rawLine of lines) {
		if (rawLine.startsWith("*** Move to: ")) continue;
		if (rawLine.startsWith("@@")) {
			if (currentHunk) hunks.push(currentHunk);
			currentHunk = [];
			continue;
		}
		if (!currentHunk) currentHunk = [];
		currentHunk.push(rawLine);
	}
	if (currentHunk) hunks.push(currentHunk);

	const starts: Array<{ oldStart: number | null; newStart: number | null }> = [];
	let searchFrom = 0;
	let lineDelta = 0;
	for (const hunk of hunks) {
		const oldLines = hunk
			.map((rawLine) => parsePatchBodyLine(rawLine))
			.filter((line) => line.marker !== "+")
			.map((line) => line.content);
		let matchIndex = findLineSequence(sourceLines, oldLines, searchFrom);
		if (matchIndex === -1) matchIndex = findLineSequence(sourceLines, oldLines, 0);
		const oldStart = matchIndex === -1 ? null : matchIndex + 1;
		const newStart = oldStart === null ? null : oldStart + lineDelta;
		starts.push({ oldStart, newStart });
		if (matchIndex === -1) continue;
		searchFrom = matchIndex + oldLines.length;
		const added = hunk.filter((rawLine) => parsePatchBodyLine(rawLine).marker === "+").length;
		const removed = hunk.filter((rawLine) => parsePatchBodyLine(rawLine).marker === "-").length;
		lineDelta += added - removed;
	}
	return starts;
}

function stripPatchLinePrefix(line: string, prefix: "+" | "-"): string {
	return line.startsWith(prefix) ? line.slice(1) : line;
}

function trimDiffSeparators(lines: DiffLine[]): DiffLine[] {
	const trimmed = [...lines];
	while (trimmed[0]?.type === "sep") trimmed.shift();
	while (trimmed[trimmed.length - 1]?.type === "sep") trimmed.pop();
	return trimmed;
}

function parseApplyPatchUpdateDiff(lines: string[], sourceContent?: string): ParsedDiff {
	const diffLines: DiffLine[] = [];
	let added = 0;
	let removed = 0;
	let chars = 0;
	let oldLine: number | null = null;
	let newLine: number | null = null;
	let inHunk = false;
	const inferredStarts = sourceContent ? inferApplyPatchHunkStarts(lines, sourceContent) : [];
	let hunkIndex = 0;

	for (const rawLine of lines) {
		if (rawLine.startsWith("*** Move to: ")) continue;
		if (rawLine.startsWith("@@")) {
			if (diffLines.length > 0 && diffLines[diffLines.length - 1]?.type !== "sep") {
				diffLines.push({ type: "sep", oldNum: null, newNum: null, content: "" });
			}
			const match = rawLine.match(/^@@\s*-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s*@@/);
			const inferred = inferredStarts[hunkIndex] ?? { oldStart: null, newStart: null };
			oldLine = match ? Number.parseInt(match[1], 10) : inferred.oldStart;
			newLine = match ? Number.parseInt(match[2], 10) : inferred.newStart;
			hunkIndex++;
			inHunk = true;
			continue;
		}
		if (rawLine === "\\ No newline at end of file") continue;
		if (!inHunk) {
			const inferred = inferredStarts[hunkIndex] ?? { oldStart: null, newStart: null };
			oldLine = inferred.oldStart;
			newLine = inferred.newStart;
			hunkIndex++;
			inHunk = true;
		}

		const { marker, content } = parsePatchBodyLine(rawLine);

		chars += content.length;
		if (marker === "+") {
			diffLines.push({ type: "add", oldNum: null, newNum: newLine, content });
			added++;
			if (newLine !== null) newLine++;
			continue;
		}
		if (marker === "-") {
			diffLines.push({ type: "del", oldNum: oldLine, newNum: null, content });
			removed++;
			if (oldLine !== null) oldLine++;
			continue;
		}
		diffLines.push({ type: "ctx", oldNum: oldLine, newNum: newLine, content });
		if (oldLine !== null) oldLine++;
		if (newLine !== null) newLine++;
	}

	return {
		lines: trimDiffSeparators(diffLines),
		added,
		removed,
		chars,
	};
}

function parseApplyPatchPreview(patchText: string, sp: (path: string) => string, cwd = process.cwd()): ApplyPatchPreview {
	const normalized = patchText.replace(/\r\n/g, "\n");
	const lines = normalized.split("\n");
	const changes: ApplyPatchChangePreview[] = [];
	let index = 0;

	const fileHeader = /^\*\*\* (Add|Update|Delete) File: (.+)$/;
	const endHeader = /^\*\*\* End Patch$/;

	while (index < lines.length) {
		const line = lines[index];
		if (!line || line === "*** Begin Patch") {
			index++;
			continue;
		}
		if (endHeader.test(line)) break;
		const header = line.match(fileHeader);
		if (!header) {
			index++;
			continue;
		}

		const kind = header[1].toLowerCase() as ApplyPatchChangePreview["kind"];
		const path = header[2].trim();
		index++;

		let moveTo: string | undefined;
		const body: string[] = [];
		while (index < lines.length && !fileHeader.test(lines[index]) && !endHeader.test(lines[index])) {
			if (lines[index].startsWith("*** Move to: ")) {
				moveTo = lines[index].slice("*** Move to: ".length).trim();
				index++;
				continue;
			}
			body.push(lines[index]);
			index++;
		}

		const displayPath = moveTo ? `${sp(path)} ${BORDER_COLOR}→${TRANSPARENT_RESET} ${sp(moveTo)}` : sp(path);
		let sourceContent: string | undefined;
		if (kind === "update") {
			try {
				sourceContent = readFileSync(resolve(cwd, path), "utf8");
			} catch {
				sourceContent = undefined;
			}
		}
		const diff = kind === "add"
			? parseDiff("", body.map((entry) => stripPatchLinePrefix(entry, "+")).join("\n"))
			: kind === "delete"
				? parseDiff(body.map((entry) => stripPatchLinePrefix(entry, "-")).join("\n"), "")
				: parseApplyPatchUpdateDiff(body, sourceContent);
		changes.push({
			kind,
			path,
			displayPath,
			moveTo,
			diff,
			language: lang(moveTo || path),
			hunks: countDiffHunks(diff),
			summary: summarizeDiff(diff.added, diff.removed),
			line: getApplyPatchLine(diff, kind),
		});
	}

	const totalAdded = changes.reduce((sum, change) => sum + change.diff.added, 0);
	const totalRemoved = changes.reduce((sum, change) => sum + change.diff.removed, 0);
	const totalHunks = changes.reduce((sum, change) => sum + change.hunks, 0);
	const totalLines = changes.reduce((sum, change) => sum + change.diff.lines.length, 0);
	return {
		changes,
		totalAdded,
		totalRemoved,
		totalHunks,
		totalLines,
		summary: summarizeDiff(totalAdded, totalRemoved),
	};
}

function describeApplyPatchChange(change: ApplyPatchChangePreview): string {
	if (change.moveTo) return `Rename ${change.displayPath}`;
	if (change.kind === "add") return `Create ${change.displayPath}`;
	if (change.kind === "delete") return `Delete ${change.displayPath}`;
	return `Update ${change.displayPath}`;
}

function formatLineMeta(line: number, theme: Theme): string {
	return line > 0 ? ` ${theme.fg("muted", `at line ${line}`)}` : "";
}

function formatApplyPatchLine(change: ApplyPatchChangePreview, theme: Theme): string {
	return formatLineMeta(change.line, theme);
}

function getCachedApplyPatchPreview(patchText: string, sp: (path: string) => string, ctx: any): ApplyPatchPreview | null {
	if (!patchText) return null;
	const key = `apply-meta:${ctx.cwd ?? process.cwd()}:${hashText(patchText)}`;
	if (ctx.state?._applyPatchMetaKey === key && ctx.state._applyPatchPreview) {
		return ctx.state._applyPatchPreview as ApplyPatchPreview;
	}
	try {
		const preview = parseApplyPatchPreview(patchText, sp, ctx.cwd ?? process.cwd());
		if (ctx.state) {
			ctx.state._applyPatchMetaKey = key;
			ctx.state._applyPatchPreview = preview;
			ctx.state._applyPatchMeta = buildApplyPatchResultMeta(preview);
		}
		return preview;
	} catch {
		return null;
	}
}

function getApplyPatchResultMeta(args: any, ctx: any, sp: (path: string) => string): ApplyPatchResultMeta | null {
	const patchText = getStringArg(args ?? ctx?.args, "patchText", "patch_text");
	if (!patchText) return null;
	const preview = getCachedApplyPatchPreview(patchText, sp, ctx);
	return preview && ctx.state?._applyPatchMeta ? (ctx.state._applyPatchMeta as ApplyPatchResultMeta) : null;
}

function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: string) => string): Text {
	syncToolCallStatus(ctx);
	const patchText = getStringArg(args, "patchText", "patch_text");
	const summary = stableCallSummary(ctx, "_callSummary", () => summarizeOpenAiToolCall("apply_patch", args, theme, sp));
	const hdr = toolHeader("Apply Patch", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme));

	if (!ctx.argsComplete) return makeText(ctx.lastComponent, hdr);
	const preview = getCachedApplyPatchPreview(patchText, sp, ctx);
	if (!preview || preview.changes.length === 0) {
		ctx.state._openAiPatchFiles = [];
		return makeText(ctx.lastComponent, hdr);
	}
	ctx.state._openAiPatchFiles = preview.changes.map((change) => change.displayPath);

	const diffWidth = contextDiffWidth(ctx);
	const key = `apply-preview:${ctx.state._applyPatchMetaKey ?? hashText(patchText)}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
	if (ctx.state._applyPatchPreviewKey !== key) {
		ctx.state._applyPatchPreviewKey = key;
		ctx.state._applyPatchPreviewBody = theme.fg("muted", "(rendering…)");
		ctx.state._applyPatchPreviewDisplay = withBranch(ctx.state._applyPatchPreviewBody, theme, false, true);
		const dc = resolveDiffColors(theme);
		if (preview.changes.length === 1) {
			const [change] = preview.changes;
			renderSplit(change.diff, change.language, ctx.expanded ? MAX_PREVIEW_LINES : 32, dc, diffWidth)
				.then((rendered) => {
					if (ctx.state._applyPatchPreviewKey !== key) return;
					ctx.state._applyPatchPreviewBody = `${describeApplyPatchChange(change)} ${change.summary}${formatApplyPatchLine(change, theme)}\n${rendered}`;
					ctx.state._applyPatchPreviewDisplay = withBranch(ctx.state._applyPatchPreviewBody, theme, false, true);
					safeInvalidate(ctx);
				})
				.catch(() => {
					if (ctx.state._applyPatchPreviewKey !== key) return;
					ctx.state._applyPatchPreviewBody = `${describeApplyPatchChange(change)} ${change.summary}${formatApplyPatchLine(change, theme)}`;
					ctx.state._applyPatchPreviewDisplay = withBranch(ctx.state._applyPatchPreviewBody, theme, false, true);
					safeInvalidate(ctx);
				});
		} else {
			const maxShown = ctx.expanded ? preview.changes.length : Math.min(preview.changes.length, 3);
			const previewLines = ctx.expanded
				? Math.max(6, Math.floor(MAX_RENDER_LINES / Math.max(1, maxShown)))
				: Math.max(8, Math.floor(MAX_PREVIEW_LINES / Math.max(1, maxShown)));
			mapWithConcurrency(preview.changes.slice(0, maxShown), DIFF_RENDER_CONCURRENCY, async (change, index) =>
				renderSplit(change.diff, change.language, previewLines, dc, diffWidth)
					.then((rendered) => `${describeApplyPatchChange(change)} ${change.summary}${formatApplyPatchLine(change, theme)}\n${rendered}`)
					.catch(() => `${index + 1}. ${describeApplyPatchChange(change)} ${change.summary}${formatApplyPatchLine(change, theme)}`),
			)
				.then((sections) => {
					if (ctx.state._applyPatchPreviewKey !== key) return;
					const remainder = preview.changes.length - maxShown;
					const suffix = remainder > 0
						? `\n${theme.fg("muted", `… ${remainder} more file patches${toolOutputDetailHint(theme, ctx.expanded, true)}`)}`
						: "";
					const summary = `${preview.changes.length} files ${preview.summary}`;
					ctx.state._applyPatchPreviewBody = `${summary}\n\n${sections.join("\n\n")}${suffix}`;
					ctx.state._applyPatchPreviewDisplay = withBranch(ctx.state._applyPatchPreviewBody, theme, false, true);
					safeInvalidate(ctx);
				})
				.catch(() => {
					if (ctx.state._applyPatchPreviewKey !== key) return;
					ctx.state._applyPatchPreviewBody = `${preview.changes.length} files ${preview.summary}`;
					ctx.state._applyPatchPreviewDisplay = withBranch(ctx.state._applyPatchPreviewBody, theme, false, true);
					safeInvalidate(ctx);
				});
		}
	}

	const body = ctx.state._applyPatchPreviewDisplay as string | undefined;
	return makeResponsiveDiffText(ctx, ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
}

function renderApplyPatchResult(result: any, isPartial: boolean, theme: Theme, ctx: any): Text {
	if (isPartial) {
		return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "Applying Patch..."), !!ctx?.expanded, theme, ctx));
	}
	clearBlinkTimer(ctx);
	setToolStatus(ctx, ctx.isError ? "error" : "success");

	if (ctx.isError) {
		const raw = getTextContent(result).trim();
		const firstLine = raw ? raw.split("\n")[0] : "Apply patch failed";
		return makeText(ctx.lastComponent, withBranch(theme.fg("error", firstLine), theme));
	}

	const meta = getApplyPatchResultMeta(ctx.args, ctx, (path: string) => shortPath(ctx.cwd ?? process.cwd(), path));
	if (!meta || meta.changeCount === 0) {
		return makeText(ctx.lastComponent, withBranch(theme.fg("success", "Applied"), theme));
	}

	if (meta.changeCount === 1 && meta.firstChange) {
		const change = meta.firstChange;
		const summary = diffSummaryWithMeta(change.added, change.removed, change.hunks, change.kind === "add" ? "new file" : change.kind === "delete" ? "delete" : "");
		return makeText(ctx.lastComponent, withBranch(`${theme.fg("success", "Applied")} ${theme.fg("muted", change.displayPath)} ${summary}${formatLineMeta(change.line, theme)}`, theme));
	}

	const summary = diffSummaryWithMeta(meta.totalAdded, meta.totalRemoved, meta.totalHunks, "");
	return makeText(ctx.lastComponent, withBranch(`${theme.fg("success", "Applied")} ${meta.changeCount} files ${summary}${meta.totalLines ? ` ${theme.fg("muted", `(${meta.totalLines} diff lines)`)}` : ""}`, theme));
}

function summarizeMcpToolCall(args: any, theme: Theme): string {
	const tool = getStringArg(args, "tool");
	if (tool) return args?.server ? `${args.server}:${tool}` : tool;
	const connect = getStringArg(args, "connect");
	if (connect) return `connect ${connect}`;
	const search = getStringArg(args, "search", "describe", "server", "action");
	if (search) return summarizeText(search, 72);
	return theme.fg("muted", "status");
}

function summarizeGenericToolCall(name: string, args: any, theme: Theme, sp: (path: string) => string): string {
	if (isMcpToolName(name)) return summarizeMcpToolCall(args, theme);
	return summarizeOpenAiToolCall(name, args, theme, sp);
}

function renderMcpToolResult(result: any, expanded: boolean, isPartial: boolean, theme: Theme, ctx: any): Text {
	if (isPartial) {
		return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "MCP running..."), expanded, theme, ctx, {
			styleLine: (line) => theme.fg("toolOutput", line || " "),
		}));
	}
	clearBlinkTimer(ctx);
	setToolStatus(ctx, ctx.isError ? "error" : "success");

	const mode = getMode(readSettings().mcpOutputMode, ["hidden", "summary", "preview"] as const, "preview");
	if (mode === "hidden") return makeText(ctx.lastComponent, "");

	const raw = getTextContent(result).trim();
	const lines = raw ? raw.split("\n") : [];
	if (lines.length === 0) {
		return makeText(ctx.lastComponent, withBranch(theme.fg(ctx.isError ? "error" : "success", ctx.isError ? "Failed" : "Done"), theme));
	}

	const statusText = ctx.isError ? theme.fg("error", lines[0]) : theme.fg("muted", `${lines.length} line${lines.length === 1 ? "" : "s"} returned`);
	if (mode === "summary") return makeText(ctx.lastComponent, withBranch(statusText, theme));
	if (!expanded) return makeText(ctx.lastComponent, withBranch(`${statusText}${toolOutputDetailHint(theme, expanded)}`, theme));
	const preview = buildPreviewText(
		lines,
		true,
		theme,
		previewLimit(),
		lines.length,
		(line) => theme.fg(ctx.isError ? "error" : "toolOutput", line || " "),
	);
	return makeText(ctx.lastComponent, withBranch(`${statusText}\n${preview}`, theme));
}

function summarizeOpenAiToolCall(name: string, args: any, theme: Theme, sp: (path: string) => string): string {
	switch (name) {
		case "apply_patch": {
			const patchText = getStringArg(args, "patchText", "patch_text");
			const files = extractApplyPatchFiles(patchText);
			if (files.length === 0) return theme.fg("muted", "patch");
			if (files.length === 1) return sp(files[0]);
			return `${sp(files[0])} ${theme.fg("muted", `(+${files.length - 1} files)`)}`;
		}
		case "webfetch":
			return getStringArg(args, "url") || theme.fg("muted", "fetch page");
		case "fetch_content": {
			const url = getStringArg(args, "url");
			if (url) return url;
			const urls = getStringArrayArg(args, "urls");
			if (urls.length === 0) return theme.fg("muted", "fetch content");
			if (urls.length === 1) return urls[0];
			return `${urls[0]} ${theme.fg("muted", `(+${urls.length - 1} urls)`)}`;
		}
		case "get_search_content":
			return getStringArg(args, "responseId", "response_id") || theme.fg("muted", "load cached content");
		case "web_search": {
			const query = getStringArg(args, "query");
			if (query) return summarizeText(query, 72);
			const queries = getStringArrayArg(args, "queries");
			if (queries.length === 0) return theme.fg("muted", "search web");
			if (queries.length === 1) return summarizeText(queries[0], 72);
			return `${summarizeText(queries[0], 48)} ${theme.fg("muted", `(+${queries.length - 1} queries)`)}`;
		}
		case "code_search":
			return summarizeText(getStringArg(args, "query") || "search code", 72);
		case "question":
			return summarizeText(getStringArg(args, "question") || "ask user", 72);
		case "questionnaire": {
			const questions = Array.isArray(args?.questions) ? args.questions.length : 0;
			return questions > 0 ? `${questions} questions` : theme.fg("muted", "questionnaire");
		}
		case "context_tag":
			return getStringArg(args, "name") || theme.fg("muted", "save point");
		case "context_log":
			return theme.fg("muted", "history");
		case "context_checkout":
			return getStringArg(args, "target") || theme.fg("muted", "checkout context");
		case "annotate":
			return getStringArg(args, "url") || theme.fg("muted", "current tab");
		case "alpha_search":
			return summarizeText(getStringArg(args, "query") || "search papers", 72);
		case "alpha_get_paper":
		case "alpha_ask_paper":
		case "alpha_annotate_paper":
			return getStringArg(args, "paper") || theme.fg("muted", "paper");
		case "alpha_read_code":
			return getStringArg(args, "githubUrl", "github_url") || theme.fg("muted", "repository");
		case "Skill":
			return getStringArg(args, "name") || theme.fg("muted", "run skill");
		case "EnterPlanMode":
			return theme.fg("muted", "enable read-only planning");
		case "ExitPlanMode":
			return theme.fg("muted", "present plan");
		case "Agent":
			return summarizeText(getStringArg(args, "description", "prompt") || "launch agent", 72);
		case "get_subagent_result":
			return getStringArg(args, "agent_id") || theme.fg("muted", "agent result");
		case "steer_subagent":
			return getStringArg(args, "agent_id") || theme.fg("muted", "steer agent");
		case "TaskCreate":
			return summarizeText(getStringArg(args, "subject") || "create task", 72);
		case "TaskList":
			return theme.fg("muted", "task list");
		case "TaskGet":
		case "TaskUpdate":
			return getStringArg(args, "taskId", "task_id") || theme.fg("muted", "task");
		case "TaskOutput":
		case "TaskStop":
			return getStringArg(args, "task_id", "taskId") || theme.fg("muted", "background task");
		case "TaskExecute": {
			const taskIds = getStringArrayArg(args, "task_ids", "taskIds");
			if (taskIds.length === 0) return theme.fg("muted", "start tasks");
			return taskIds.length === 1 ? taskIds[0] : `${taskIds[0]} ${theme.fg("muted", `(+${taskIds.length - 1} tasks)`)}`;
		}
		default:
			return summarizeText(
				getStringArg(args, "path", "file_path", "url", "query", "name", "subject", "tool", "description", "prompt") || humanizeToolName(name),
				72,
			);
	}
}

interface ParsedTaskListLine {
	id: string;
	status: string;
	subject: string;
}

function parseTaskListLine(line: string): ParsedTaskListLine | null {
	const match = line.match(/^#(\d+) \[([^\]]+)\] (.+)$/);
	if (!match) return null;
	return {
		id: match[1],
		status: match[2],
		subject: match[3],
	};
}

function formatTaskStatus(status: string, theme: Theme): string {
	if (status === "completed") return theme.fg("success", status);
	if (status === "in_progress") return theme.fg("warning", status);
	return theme.fg("muted", status);
}

function formatOpenAiSuccessLine(name: string, line: string, theme: Theme): string {
	const trimmed = line.trim();
	if (!trimmed) return theme.fg("success", "Done");

	if (name === "TaskCreate") {
		const match = trimmed.match(/^Task #(\d+) created successfully: (.+)$/);
		if (match) {
			return `${theme.fg("success", "Created task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
		}
	}

	if (name === "TaskUpdate") {
		const match = trimmed.match(/^Updated task #(\d+) (.+)$/);
		if (match) {
			return `${theme.fg("success", "Updated task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
		}
	}

	if (name === "TaskExecute") {
		return `${theme.fg("success", "Started")} ${theme.fg("muted", trimmed)}`;
	}

	if (name === "context_tag") {
		const match = trimmed.match(/^Created tag '([^']+)' at (.+)$/);
		if (match) {
			return `${theme.fg("success", "Created tag")} ${theme.fg("accent", match[1])} ${theme.fg("muted", match[2])}`;
		}
	}

	if (name === "context_checkout") {
		return `${theme.fg("success", "Checked out")} ${theme.fg("muted", trimmed.replace(/^Checked out\s*/i, ""))}`;
	}

	if (name === "TaskStop") {
		return `${theme.fg("success", "Stopped")} ${theme.fg("muted", trimmed)}`;
	}

	return theme.fg("muted", trimmed);
}

function renderTaskListResult(lines: string[], expanded: boolean, theme: Theme, ctx: any): Text {
	const tasks = lines.map(parseTaskListLine).filter((task): task is ParsedTaskListLine => task !== null);
	if (tasks.length === 0) {
		const text = lines.length === 0
			? theme.fg("muted", "no tasks")
			: buildPreviewText(lines, expanded, theme, previewLimit(), lines.length, (line) => theme.fg("dim", line));
		return makeText(ctx.lastComponent, withBranch(text, theme));
	}

	const pending = tasks.filter((task) => task.status === "pending").length;
	const inProgress = tasks.filter((task) => task.status === "in_progress").length;
	const completed = tasks.filter((task) => task.status === "completed").length;
	let summary = theme.fg("muted", `${tasks.length} tasks`);
	const parts: string[] = [];
	if (inProgress > 0) parts.push(`${theme.fg("warning", String(inProgress))} in progress`);
	if (pending > 0) parts.push(`${theme.fg("muted", String(pending))} pending`);
	if (completed > 0) parts.push(`${theme.fg("success", String(completed))} completed`);
	if (parts.length > 0) summary += ` ${theme.fg("muted", "•")} ${parts.join(` ${theme.fg("muted", "•")} `)}`;

	if (!expanded) {
		return makeText(ctx.lastComponent, withBranch(`${summary}${toolOutputDetailHint(theme, expanded)}`, theme));
	}

	const shown = tasks.slice(0, previewLimit());
	const preview = shown.map((task) => `${theme.fg("accent", `#${task.id}`)} ${formatTaskStatus(task.status, theme)} ${theme.fg("dim", task.subject)}`);
	const remaining = tasks.length - shown.length;
	if (remaining > 0) preview.push(theme.fg("muted", `… ${remaining} more tasks`));
	return makeText(ctx.lastComponent, withBranch(`${summary}\n${preview.join("\n")}`, theme));
}

function getFirstImageBlock(result: any): { data: string; mimeType: string } | undefined {
	if (!Array.isArray(result?.content)) return undefined;
	return result.content.find((block: any) => block?.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string");
}

function getReadImageFallback(result: any, ctx: any): string {
	const image = getFirstImageBlock(result);
	if (!image) return "";
	let dimensions;
	try {
		dimensions = getImageDimensions(image.data, image.mimeType) ?? undefined;
	} catch {
		dimensions = undefined;
	}
	const path = getStringArg(ctx.args, "path", "file_path");
	const filename = path ? shortPath(ctx.cwd ?? process.cwd(), path) : undefined;
	return imageFallback(image.mimeType, dimensions, filename);
}

function renderReadImageResult(result: any, expanded: boolean, theme: Theme, ctx: any): Text {
	const image = getFirstImageBlock(result);
	const mimeType = image?.mimeType ?? "image";
	const summary = `${theme.fg("success", "Image loaded")} ${theme.fg("muted", `[${mimeType}]`)}`;
	if (!expanded) {
		return makeText(ctx.lastComponent, withBranch(`${summary}${toolOutputDetailHint(theme, expanded)}`, theme));
	}

	const noteLines = getTextContent(result)
		.split("\n")
		.map((line) => line.trim())
		.filter((line) => line && !/^Read image file\b/i.test(line));
	const lines = [summary, ...noteLines.map((line) => theme.fg("dim", line))];
	if (!getCapabilities().images || !ctx.showImages) {
		const fallback = getReadImageFallback(result, ctx);
		if (fallback) lines.push(theme.fg("toolOutput", fallback));
	}
	return makeText(ctx.lastComponent, withBranch(lines.join("\n"), theme));
}

function renderOpenAiToolResult(name: string, result: any, expanded: boolean, isPartial: boolean, theme: Theme, ctx: any): Text {
	if (isPartial) {
		return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", `${humanizeToolName(name)}...`), expanded, theme, ctx));
	}
	clearBlinkTimer(ctx);
	setToolStatus(ctx, ctx.isError ? "error" : "success");

	const raw = getTextContent(result).trim();
	const lines = raw ? raw.split("\n") : [];
	const patchFiles = Array.isArray(ctx.state?._openAiPatchFiles) ? ctx.state._openAiPatchFiles : [];

	if (lines.length === 0) {
		if (patchFiles.length > 0) {
			const suffix = patchFiles.length === 1 ? patchFiles[0] : `${patchFiles.length} files`;
			return makeText(ctx.lastComponent, withBranch(`${theme.fg(ctx.isError ? "error" : "success", ctx.isError ? "Failed" : "Applied")} ${theme.fg("muted", suffix)}`, theme));
		}
		return makeText(ctx.lastComponent, withBranch(theme.fg(ctx.isError ? "error" : "success", ctx.isError ? "Failed" : "Done"), theme));
	}

	if (!ctx.isError && name === "TaskList") {
		return renderTaskListResult(lines, expanded, theme, ctx);
	}

	const statusText = ctx.isError
		? theme.fg("error", lines[0])
		: theme.fg("muted", `${lines.length} line${lines.length === 1 ? "" : "s"} returned`);
	if (!expanded) {
		return makeText(ctx.lastComponent, withBranch(`${statusText}${toolOutputDetailHint(theme, expanded)}`, theme));
	}

	if (!ctx.isError && lines.length === 1) {
		return makeText(ctx.lastComponent, withBranch(formatOpenAiSuccessLine(name, lines[0], theme), theme));
	}

	const preview = lines.length === 1
		? theme.fg(ctx.isError ? "error" : "dim", lines[0])
		: buildPreviewText(
			lines,
			true,
			theme,
			previewLimit(),
			lines.length,
			(line) => theme.fg(ctx.isError ? "error" : "dim", line || " "),
		);
	return makeText(ctx.lastComponent, withBranch(`${statusText}\n${preview}`, theme));
}

// ===========================================================================
// Extension
// ===========================================================================

export default function (pi: ExtensionAPI) {
	patchTerminalWriteTagScrubber();
	patchToolExecutionBackgroundSync();
	patchToolRenderCacheInvalidation();
	patchReadImageExpansion();
	patchContainerParentTracking();
	patchGlobalToolBorders();
	patchCustomMessageRender();
	patchUserMessageRender();
	patchAssistantMessages();
	patchToolExecutionRenderers();
	applyDiffPalette();
	registerThinkingLabels(pi);
	syncExtraToolDetailMode();

	pi.registerShortcut("ctrl+shift+o", {
		description: "Toggle extra tool output detail",
		handler: async (ctx) => {
			setExtraToolDetailMode(!extraToolOutputExpanded);
			if (ctx.hasUI) {
				ctx.ui.setToolsExpanded(ctx.ui.getToolsExpanded());
				ctx.ui.notify(`Extra tool detail: ${extraToolOutputExpanded ? "on" : "off"}`, "info");
			}
		},
	});

	// /cc-tools command — control tool chrome, grouping, and detail level.
	const TOOL_MODES = ["outlines", "transparent", "default"] as const;
	const TOOL_BOOL_MODES = ["on", "off", "toggle", "status"] as const;
	const TOOL_SUBCOMMANDS = [...TOOL_MODES, "group", "detail", "thinking", "branch", "status"] as const;
	const booleanMode = (raw: string | undefined, current: boolean): boolean | "status" | undefined => {
		const mode = raw || "toggle";
		if (mode === "on") return true;
		if (mode === "off") return false;
		if (mode === "toggle") return !current;
		if (mode === "status") return "status";
		return undefined;
	};
	const notifyToolStatus = (ctx: any): void => {
		if (!ctx.hasUI) return;
		const branchMode = toolBranchColorModeFixed() ? "fixed" : "theme";
		const branchGray = getConfiguredToolBranchGray();
		const theme = ctx.ui?.theme;
		const chromeHint = branchMode === "theme" && theme
			? (resolveThemeChromeFg(theme) ? " (attenuated on light themes)" : " (fallback gray if theme keys missing)")
			: "";
		const branchLine = branchMode === "fixed"
			? `Branch color: fixed rgb(${branchGray})`
			: `Branch color: theme${chromeHint}`;
		ctx.ui.notify([
			`Tool style: ${toolBackgroundMode}`,
			`Tool grouping: ${toolGroupingEnabled() ? "on" : "off"}`,
			`Thinking: ${getThinkingMode()}`,
			`Extra detail: ${extraToolOutputExpanded ? "on" : "off"} (${rawKeyHint("ctrl+shift+o", "toggle")})`,
			branchLine,
			`  /cc-tools branch <0-255> | theme | fixed | reset`,
		].join("\n"), "info");
	};
	pi.registerCommand("cc-tools", {
		description: "Control tool UI: style, grouped rows, and Ctrl+Shift+O extra-detail mode",
		getArgumentCompletions(prefix) {
			const parts = prefix.trimStart().split(/\s+/);
			const first = parts[0] ?? "";
			if (parts.length <= 1) {
				return TOOL_SUBCOMMANDS
					.filter((m) => m.startsWith(first))
					.map((m) => ({
						value: m,
						label: m,
						description:
							m === "group" ? "Toggle grouped adjacent/concurrent tool rows"
							: m === "thinking" ? "Thinking display: live (default) or full"
							: m === "detail" ? "Toggle Ctrl+Shift+O extra-detail mode"
							: m === "branch" ? "├ └ │ gray (0-255), theme, fixed, or reset"
							: m === "status" ? "Show tool UI settings"
							: m === "outlines" ? "Horizontal rules around each tool (default)"
							: m === "transparent" ? "No borders or backgrounds"
							: "Pi built-in tool backgrounds",
					}));
			}
			if (first === "branch") {
				const second = parts[1] ?? "";
				const opts = ["theme", "fixed", "reset", "status"];
				return opts
					.filter((o) => o.startsWith(second))
					.map((o) => ({ value: `branch ${o}`, label: o, description: "Branch connector color" }));
			}
			if (first === "thinking") {
				const second = parts[1] ?? "";
				return ["live", "full", "status"]
					.filter((m) => m.startsWith(second))
					.map((m) => ({ value: `thinking ${m}`, label: m, description: `${m} thinking display` }));
			}
			if (first === "group" || first === "detail" || first === "extra") {
				const second = parts[1] ?? "";
				return TOOL_BOOL_MODES
					.filter((m) => m.startsWith(second))
					.map((m) => ({ value: `${first} ${m}`, label: m, description: `${m} ${first}` }));
			}
			return [];
		},
		async handler(args, ctx) {
			const parts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
			const sub = parts[0] ?? "";
			if (!sub || sub === "status") {
				notifyToolStatus(ctx);
				return;
			}

			if (sub === "group") {
				const next = booleanMode(parts[1], toolGroupingEnabled());
				if (next === undefined) {
					if (ctx.hasUI) ctx.ui.notify(`Usage: /cc-tools group ${TOOL_BOOL_MODES.join("|")}`, "error");
					return;
				}
				if (next === "status") {
					if (ctx.hasUI) ctx.ui.notify(`Tool grouping: ${toolGroupingEnabled() ? "on" : "off"}`, "info");
					return;
				}
				setToolGroupingEnabled(next);
				if (!next) ungroupActiveToolGroups();
				if (ctx.hasUI) {
					ctx.ui.setToolsExpanded(ctx.ui.getToolsExpanded());
					ctx.ui.notify(`Tool grouping: ${next ? "on" : "off"}${next ? " (future adjacent tool rows)" : ""}`, "info");
				}
				return;
			}

			if (sub === "branch") {
				const arg = parts[1] ?? "status";
				if (arg === "status" || !arg) {
					notifyToolStatus(ctx);
					return;
				}
				if (arg === "reset") {
					writeSettingsKey("toolBranchRgbGray", undefined);
					writeSettingsKey("toolBranchColorMode", undefined);
					if (ctx.hasUI) refreshAllToolBranchVisuals(ctx);
					if (ctx.hasUI) ctx.ui.notify(`Branch color → fixed rgb(${DEFAULT_TOOL_BRANCH_GRAY}) (default)`, "info");
					return;
				}
				if (arg === "theme") {
					writeSettingsKey("toolBranchColorMode", "theme");
					if (ctx.hasUI) refreshAllToolBranchVisuals(ctx);
					if (ctx.hasUI) ctx.ui.notify("Branch color → follow syCode theme (dim/muted)", "info");
					return;
				}
				if (arg === "fixed") {
					writeSettingsKey("toolBranchColorMode", "fixed");
					if (ctx.hasUI) refreshAllToolBranchVisuals(ctx);
					if (ctx.hasUI) ctx.ui.notify(`Branch color → fixed rgb(${getConfiguredToolBranchGray()})`, "info");
					return;
				}
				const gray = Number.parseInt(arg, 10);
				if (!Number.isFinite(gray) || gray < 0 || gray > 255) {
					if (ctx.hasUI) ctx.ui.notify("Usage: /cc-tools branch <0-255> | theme | fixed | reset", "error");
					return;
				}
				writeSettingsKey("toolBranchRgbGray", gray);
				writeSettingsKey("toolBranchColorMode", "fixed");
				if (ctx.hasUI) refreshAllToolBranchVisuals(ctx);
				if (ctx.hasUI) ctx.ui.notify(`Branch color → fixed rgb(${gray})`, "info");
				return;
			}

			if (sub === "thinking") {
				const arg = (parts[1] ?? "status").toLowerCase();
				if (arg === "status" || !arg) {
					if (ctx.hasUI) ctx.ui.notify(`Thinking: ${getThinkingMode()} (live = only streaming thinking expands; full = always expanded)`, "info");
					return;
				}
				if (arg !== "live" && arg !== "full") {
					if (ctx.hasUI) ctx.ui.notify("Usage: /cc-tools thinking live|full|status", "error");
					return;
				}
				writeSettingsKey("thinkingMode", arg);
				if (ctx.hasUI) {
					ctx.ui.setToolsExpanded(ctx.ui.getToolsExpanded());
					ctx.ui.notify(`Thinking → ${arg}${arg === "live" ? " (only the active thinking expands)" : " (thinking always expanded)"}`, "info");
				}
				return;
			}

			if (sub === "detail" || sub === "extra") {
				const next = booleanMode(parts[1], extraToolOutputExpanded);
				if (next === undefined) {
					if (ctx.hasUI) ctx.ui.notify(`Usage: /cc-tools detail ${TOOL_BOOL_MODES.join("|")}`, "error");
					return;
				}
				if (next === "status") {
					if (ctx.hasUI) ctx.ui.notify(`Extra tool detail: ${extraToolOutputExpanded ? "on" : "off"}`, "info");
					return;
				}
				setExtraToolDetailMode(next);
				if (ctx.hasUI) {
					ctx.ui.setToolsExpanded(ctx.ui.getToolsExpanded());
					ctx.ui.notify(`Extra tool detail: ${extraToolOutputExpanded ? "on" : "off"}`, "info");
				}
				return;
			}

			if (!(TOOL_MODES as readonly string[]).includes(sub)) {
				if (ctx.hasUI) ctx.ui.notify(`Unknown option "${sub}". Try /cc-tools status, /cc-tools thinking live, or /cc-tools group toggle.`, "error");
				return;
			}
			toolBackgroundOverride = sub as typeof toolBackgroundMode;
			toolBackgroundMode = toolBackgroundOverride;
			writeSettingsKey("toolBackground", sub);
			if (ctx.hasUI) {
				applyToolBackgroundMode(ctx.ui.theme);
				ctx.ui.notify(`Tool style → ${sub}`, "info");
			}
		},
	});

	// /cc-theme command — toggle syCode-theme-adaptive coloring at runtime.
	const THEME_MODES = ["on", "off", "toggle", "status"] as const;
	pi.registerCommand("cc-theme", {
		description: "Toggle whether tool borders / branch rules / diff colors follow the active syCode theme",
		getArgumentCompletions(prefix) {
			return THEME_MODES
				.filter((m) => m.startsWith(prefix))
				.map((m) => ({
					value: m,
					label: m,
					description:
						m === "on" ? "Derive borders, branch rules, dim text and diff tints from the active syCode theme (default)"
						: m === "off" ? "Keep the fixed Claude-style palette regardless of theme"
						: m === "toggle" ? "Flip between on and off"
						: "Show the current setting and a preview of the derived colors",
				}));
		},
		async handler(args, ctx) {
			const raw = args.trim().toLowerCase();
			const current = themeAdaptiveEnabled();

			if (!raw || raw === "status") {
				if (!ctx.hasUI) return;
				const theme = ctx.ui.theme as any;
				const themeName = theme?.name ?? "unknown";
				const state = current ? "on" : "off";
				if (raw === "status" && current) {
					const settings = readSettings();
					const verbKey = settings.spinnerVerbColor || "borderAccent";
					const statusKey = settings.spinnerStatusColor || "muted";
					const verbAnsi = safeFgAnsi(theme, verbKey) ?? safeFgAnsi(theme, "accent");
					const statusAnsi = safeFgAnsi(theme, statusKey) ?? safeFgAnsi(theme, "muted");
					const chromePreview = resolveThemeChromeFg(theme);
					// Print a short preview of what we derived.
					const preview = [
						`chrome      : ${chromePreview ? `${chromePreview}─┌ User ├─\x1b[39m` : "(unchanged)"}`,
						`  (user box, tool rules, branches)`, 
						`muted text  : ${safeFgAnsi(theme, "muted") ? `${safeFgAnsi(theme, "muted")}example dim text\x1b[39m` : "(unchanged)"}`,
						`diff add    : ${safeFgAnsi(theme, "toolDiffAdded") ? `${safeFgAnsi(theme, "toolDiffAdded")}+ added line\x1b[39m` : "(unchanged)"}`,
						`diff del    : ${safeFgAnsi(theme, "toolDiffRemoved") ? `${safeFgAnsi(theme, "toolDiffRemoved")}- removed line\x1b[39m` : "(unchanged)"}`,
						`spinner verb: ${verbAnsi ? `${verbAnsi}Cooking…\x1b[39m` : "(unchanged)"} (key: ${verbKey})`,
						`spinner stat: ${statusAnsi ? `${statusAnsi}(thinking · ↓ 10 tokens · 2s)\x1b[39m` : "(unchanged)"} (key: ${statusKey})`,
					].join("\n  ");
					ctx.ui.notify(`Theme adaptive: ${state} (theme "${themeName}")\n  ${preview}`, "info");
				} else {
					ctx.ui.notify(`Theme adaptive: ${state} (theme "${themeName}")`, "info");
				}
				return;
			}

			let next: boolean;
			if (raw === "on") next = true;
			else if (raw === "off") next = false;
			else if (raw === "toggle") next = !current;
			else {
				if (ctx.hasUI) ctx.ui.notify(`Unknown option "${raw}". Options: ${THEME_MODES.join(", ")}`, "error");
				return;
			}

			writeSettingsKey("themeAdaptive", next);
			bustSpinnerSettingsCache();
			// Invalidate caches so the next render re-derives from the active
			// theme (or falls back to the fixed Claude palette).
			invalidateThemePaletteCache();
			autoDerivePending = true;
			if (next) {
				if (ctx.hasUI) applyThemePaletteIfNeeded(ctx.ui.theme);
			} else {
				resetThemePalette();
			}
			if (ctx.hasUI) {
				const label = next ? "on — colors follow syCode theme" : "off — fixed Claude palette";
				ctx.ui.notify(`Theme adaptive: ${label}`, "info");
			}
		},
	});

	// /cc-spinner command — pick which theme color keys drive the spinner verb
	// and status suffix.
	const COMMON_COLOR_KEYS: readonly string[] = [
		"accent", "borderAccent", "success", "error", "warning",
		"muted", "dim", "text", "thinkingText",
		"toolTitle", "mdHeading", "mdCode", "mdLink", "mdListBullet",
		"bashMode",
		"thinkingLow", "thinkingMedium", "thinkingHigh", "thinkingXhigh",
		"syntaxKeyword", "syntaxFunction", "syntaxString", "syntaxType",
	];
	pi.registerCommand("cc-spinner", {
		description: "Set the spinner verb or status theme color, or preview current values",
		getArgumentCompletions(prefix) {
			const subCommands = ["verb", "status", "reset", "preview"];
			const parts = prefix.split(/\s+/);
			if (parts.length <= 1) {
				return subCommands
					.filter((c) => c.startsWith(parts[0] ?? ""))
					.map((c) => ({
						value: c,
						label: c,
						description:
							c === "verb" ? "Set the color key used for the spinner verb (e.g. 'Cooking…')"
							: c === "status" ? "Set the color key used for the spinner status suffix"
							: c === "reset" ? "Reset both verb and status to defaults (borderAccent, muted)"
							: "Preview every theme color key with its current sample",
					}));
			}
			// Second arg: color key completions for verb/status.
			if (parts[0] === "verb" || parts[0] === "status") {
				const keyPrefix = (parts[1] ?? "").toLowerCase();
				return COMMON_COLOR_KEYS
					.filter((k) => k.toLowerCase().startsWith(keyPrefix))
					.map((k) => ({ value: k, label: k, description: `theme.fg("${k}", …)` }));
			}
			return [];
		},
		async handler(args, ctx) {
			const parts = args.trim().split(/\s+/).filter((p) => p.length > 0);
			const sub = (parts[0] ?? "").toLowerCase();
			const theme = ctx.hasUI ? (ctx.ui.theme as any) : null;
			const settings = readSettings();
			const currentVerb = settings.spinnerVerbColor || "borderAccent";
			const currentStatus = settings.spinnerStatusColor || "muted";

			if (!sub || sub === "preview") {
				if (!ctx.hasUI) return;
				if (!theme) {
					ctx.ui.notify(`Spinner verb: ${currentVerb}, status: ${currentStatus} (no theme)`, "info");
					return;
				}
				const lines: string[] = [
					`Current: verb=${currentVerb}, status=${currentStatus}`,
					"",
					"Preview of common theme keys (pick one for verb or status):",
				];
				for (const key of COMMON_COLOR_KEYS) {
					const ansi = safeFgAnsi(theme, key);
					const marker = key === currentVerb ? "(verb)" : key === currentStatus ? "(status)" : "";
					const sample = ansi ? `${ansi}Cooking…\x1b[39m` : "(unmapped)";
					lines.push(`  ${key.padEnd(16)} ${sample} ${marker}`);
				}
				ctx.ui.notify(lines.join("\n"), "info");
				return;
			}

			if (sub === "reset") {
				writeSettingsKey("spinnerVerbColor", undefined);
				writeSettingsKey("spinnerStatusColor", undefined);
				bustSpinnerSettingsCache();
				if (ctx.hasUI) ctx.ui.notify("Spinner colors reset to defaults (verb=borderAccent, status=muted)", "info");
				return;
			}

			if (sub !== "verb" && sub !== "status") {
				if (ctx.hasUI) ctx.ui.notify(`Usage: /cc-spinner verb <key> | status <key> | reset | preview`, "error");
				return;
			}

			const key = parts[1];
			if (!key) {
				if (ctx.hasUI) ctx.ui.notify(`Missing color key. Try /cc-spinner preview to see available keys.`, "error");
				return;
			}

			// Validate the key resolves to *some* color in the active theme;
			// accept anyway if the user insists so themes with custom keys work.
			const ansi = theme ? safeFgAnsi(theme, key) : null;
			const settingKey = sub === "verb" ? "spinnerVerbColor" : "spinnerStatusColor";
			writeSettingsKey(settingKey, key);
			bustSpinnerSettingsCache();
			if (ctx.hasUI) {
				const sample = ansi ? `${ansi}sample\x1b[39m` : "(key unmapped in current theme)";
				ctx.ui.notify(`Spinner ${sub} → ${key} ${sample}`, "info");
			}
		},
	});

	pi.on("session_start", async (event, ctx) => {
		clearRtkRewriteState();
		if (!ctx.hasUI) return;
		patchUiNotifications(ctx.ui);
		// Session switch (/resume, /new) can leave tool chrome from the previous
		// theme; rebind from ctx.ui.theme (other extensions may setTheme in the
		// same tick — deferred passes pick up the final theme without coupling).
		rebindUiChromeToTheme(ctx);
		scheduleDeferredChromeRebind(ctx, 0);
		const reason = (event as { reason?: string })?.reason;
		if (reason === "resume" || reason === "new" || reason === "fork") {
			scheduleDeferredChromeRebind(ctx, 48);
			// Chat history rebuild can run after session_start; re-sync transparent tool bgs.
			scheduleDeferredChromeRebind(ctx, 120);
		}
	});

	pi.on("turn_start", async (_event, ctx) => {
		if (!ctx.hasUI) return;
		patchUiNotifications(ctx.ui);
		applyToolBackgroundMode(ctx.ui.theme);
		applyThemePaletteIfNeeded(ctx.ui.theme);
	});

	pi.on("message_update", async (event) => {
		const content = (event as any)?.message?.content;
		const hasText = Array.isArray(content) && content.some((block: any) => block?.type === "text" && typeof block.text === "string" && block.text.trim().length > 0);
		if (hasText) clearPreservedBashPreviews();
	});

	pi.on("tool_execution_start", async (event) => {
		clearPreservedBashPreviews();
		const toolName = (event as any)?.toolName;
		if (toolName !== "bash") return;
		trackRtkOriginalBashCommand((event as any)?.toolCallId, (event as any)?.args);
	});

	const cwd = process.cwd();
	const sp = (path: string) => shortPath(cwd, path);

	const readTool = createReadTool(cwd);
	pi.registerTool({
		name: "read",
		label: "read",
		description: readTool.description,
		parameters: readTool.parameters,
		async execute(toolCallId, params, signal, onUpdate) {
			return readTool.execute(toolCallId, params, signal, onUpdate);
		},
		renderCall(args, theme, ctx) {
			syncToolCallStatus(ctx);
			// SKILL.md reads: render as [skill] block matching /skill:name style
			const rawPath = String(args?.path ?? "");
			const absPath = resolve(ctx.cwd ?? cwd, rawPath);
			if (basename(absPath) === "SKILL.md") {
				const skillName = basename(dirname(absPath)) || "SKILL.md";
				const line =
					theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
					theme.fg("customMessageText", skillName);
				return makeText(ctx.lastComponent, `${toolStatusDot(ctx, theme)}${line}${liveLineCountTrailing(ctx, theme)}`);
			}
			const summary = stableCallSummary(ctx, "_callSummary", () => {
				let value = sp(args.path ?? "");
				if (args.offset || args.limit) {
					const parts: string[] = [];
					if (args.offset) parts.push(`offset=${args.offset}`);
					if (args.limit) parts.push(`limit=${args.limit}`);
					value += ` ${theme.fg("muted", `(${parts.join(", ")})`)}`;
				}
				return value;
			});
			return makeText(
				ctx.lastComponent,
				toolHeader("Read", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
			);
		},
		renderResult(result, { expanded, isPartial }, theme, ctx) {
			if (isPartial) {
				return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "Reading..."), expanded, theme, ctx));
			}
			clearBlinkTimer(ctx);
			setToolStatus(ctx, ctx.isError ? "error" : "success");
			if (getFirstImageBlock(result)) return renderReadImageResult(result, expanded, theme, ctx);
			const details = result.details as ReadToolDetails | undefined;
			const content = result.content.find((block: any) => block?.type === "text");
			if (content?.type !== "text") return makeText(ctx.lastComponent, withBranch(theme.fg("error", "No text content"), theme));
			const lines = content.text.split("\n");
			let text = theme.fg("muted", `${lines.length} lines loaded`);
			if (details?.truncation?.truncated) text += theme.fg("warning", " (truncated)");
			if (!expanded) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}`, theme));
			text += `\n${buildPreviewText(lines, false, theme, previewLimit(), lines.length, (line) => theme.fg("dim", line || " "))}`;
			return makeText(ctx.lastComponent, withBranch(text, theme));
		},
	});

	const bashTool = createBashTool(cwd);
	pi.registerTool({
		name: "bash",
		label: "bash",
		description: bashTool.description,
		parameters: bashTool.parameters,
		async execute(toolCallId, params, signal, onUpdate) {
			return bashTool.execute(toolCallId, params, signal, onUpdate);
		},
		renderCall(args, theme, ctx) {
			syncToolCallStatus(ctx);
			syncBashDuration(ctx);
			const rewrite = ensureRtkRewriteForContext(ctx, args);
			const command = typeof args.command === "string" ? args.command : "";
			const presentation = buildBashCommandPresentation(command);
			const summary = stableCallSummary(ctx, "_bashHeadline", () => presentation.headline);
			const rtkBadge = rewrite ? theme.fg("muted", " (RTK)") : "";
			const status = ctx?.state?._toolStatus;
			const showCommand = ctx.argsComplete === true && (status === "pending" || status === "error" || ctx.expanded === true);
			const commandBlock = showCommand ? renderBashCommandBlock(command, ctx.expanded === true, theme) : "";
			const headerSummary = ctx.expanded === true && commandBlock ? describeBashSource(presentation) : summary;
			const header = toolHeader(
				"Bash",
				`${headerSummary}${rtkBadge}`,
				theme,
				toolStatusDot(ctx, theme),
				bashHeaderTrailing(ctx, theme),
			).replace(WRAP_MARK, CLIP_MARK);
			return makeText(ctx.lastComponent, commandBlock ? `${header}\n${commandBlock}` : header);
		},
		renderResult(result, { expanded, isPartial }, theme, ctx) {
			syncBashDuration(ctx, isPartial);
			const details = result.details as BashToolDetails | undefined;
			const rewrite = ensureRtkRewriteForContext(ctx, ctx.args);
			const output = result.content[0]?.type === "text" ? result.content[0].text : "";
			if (isPartial) {
				const preview = collectNonEmptyLines(output, expanded ? undefined : liveToolPreviewLimit());
				const running = runningPreviewBlock(result, "", expanded, theme, ctx, {
					lines: preview.lines,
					totalLineCount: preview.total,
					styleLine: (line) => theme.fg("dim", line),
					tail: true,
				});
				const withRewrite = expanded && rewrite
					? [running, withBranch(formatRtkRewriteDetails(rewrite, theme), theme)].filter(Boolean).join("\n")
					: running;
				return makeText(ctx.lastComponent, withRewrite);
			}
			// Collapsed: only keep the live-preview tail (or nothing). Expanded: full list.
			const keepTail = !expanded && liveToolPreviewEnabled() ? liveToolPreviewLimit() : undefined;
			const nonEmpty = collectNonEmptyLines(output, expanded ? undefined : (keepTail ?? 0));
			clearBlinkTimer(ctx);
			setToolStatus(ctx, ctx.isError ? "error" : "success");
			if (nonEmpty.total > 0 && ctx.state?._bashPreviewReleased !== true) {
				preserveBashPreview(ctx);
				if (ctx.state) ctx.state._bashPreviewReleased = true;
			}
			const exitMatch = output.match(/exit code: (\d+)/);
			const exitCode = exitMatch ? Number.parseInt(exitMatch[1], 10) : null;
			let text = exitCode === null || exitCode === 0 ? theme.fg("success", "Done") : theme.fg("error", `Exit ${exitCode}`);
			text += theme.fg("muted", ` (${nonEmpty.total} lines)`);
			if (details?.truncation?.truncated) text += theme.fg("warning", " [truncated]");
			const persistentPreview = shouldPreserveBashPreview(ctx) ? buildPersistentBashPreview(nonEmpty.lines, theme) : "";
			if (!expanded && persistentPreview) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}\n${persistentPreview}`, theme));
			if (!expanded && nonEmpty.total > 0) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}`, theme));
			if (!expanded) return makeText(ctx.lastComponent, withBranch(text, theme));
			const collapsed = bashCollapsedLimit();
			if (rewrite) text += `\n${formatRtkRewriteDetails(rewrite, theme)}`;
			text += `\n${buildPreviewText(nonEmpty.lines, false, theme, collapsed, nonEmpty.total, (line) => theme.fg("dim", line))}`;
			return makeText(ctx.lastComponent, withBranch(text, theme));
		},
	});

	const grepTool = createGrepTool(cwd);
	pi.registerTool({
		name: "grep",
		label: "grep",
		description: grepTool.description,
		parameters: grepTool.parameters,
		async execute(toolCallId, params, signal, onUpdate) {
			return grepTool.execute(toolCallId, params, signal, onUpdate);
		},
		renderCall(args, theme, ctx) {
			syncToolCallStatus(ctx);
			const summary = stableCallSummary(ctx, "_callSummary", () => {
				let value = `\"${summarizeText(args.pattern, 40)}\"`;
				if (args.path) value += ` in ${args.path}`;
				return value;
			});
			return makeText(
				ctx.lastComponent,
				toolHeader("Grep", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
			);
		},
		renderResult(result, { expanded, isPartial }, theme, ctx) {
			if (isPartial) {
				return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "Searching..."), expanded, theme, ctx));
			}
			clearBlinkTimer(ctx);
			setToolStatus(ctx, ctx.isError ? "error" : "success");
			const details = result.details as GrepToolDetails | undefined;
			const matches = (result.content[0]?.type === "text" ? result.content[0].text : "")
				.split("\n")
				.filter((line) => line.trim().length > 0);
			if (matches.length === 0) return makeText(ctx.lastComponent, withBranch(theme.fg("muted", "no matches"), theme));
			let text = theme.fg("muted", `${matches.length} matches`);
			if (details?.truncation?.truncated) text += theme.fg("warning", " (truncated)");
			if (!expanded) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}`, theme));
			text += `\n${buildPreviewText(matches, false, theme, previewLimit(), matches.length, (line) => theme.fg("dim", line))}`;
			return makeText(ctx.lastComponent, withBranch(text, theme));
		},
	});

	const findTool = createFindTool(cwd);
	pi.registerTool({
		name: "find",
		label: "find",
		description: findTool.description,
		parameters: findTool.parameters,
		async execute(toolCallId, params, signal, onUpdate) {
			return findTool.execute(toolCallId, params, signal, onUpdate);
		},
		renderCall(args, theme, ctx) {
			syncToolCallStatus(ctx);
			const summary = stableCallSummary(ctx, "_callSummary", () => {
				let value = `\"${summarizeText(args.pattern, 40)}\"`;
				if (args.path) value += ` in ${args.path}`;
				return value;
			});
			return makeText(
				ctx.lastComponent,
				toolHeader("Find", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
			);
		},
		renderResult(result, { expanded, isPartial }, theme, ctx) {
			if (isPartial) {
				return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "Finding..."), expanded, theme, ctx));
			}
			clearBlinkTimer(ctx);
			setToolStatus(ctx, ctx.isError ? "error" : "success");
			const items = (result.content[0]?.type === "text" ? result.content[0].text : "")
				.split("\n")
				.filter((line) => line.trim().length > 0);
			if (items.length === 0) return makeText(ctx.lastComponent, withBranch(theme.fg("muted", "no files found"), theme));
			let text = theme.fg("muted", `${items.length} files`);
			if (!expanded) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}`, theme));
			// Expanded: grouped find results with icons
			const maxShow = previewLimit();
			const shown = items.slice(0, maxShow);
			const findLines: string[] = [];
			for (let i = 0; i < shown.length; i++) {
				const item = shown[i].trim();
				const icon = fileIcon(item);
				findLines.push(`  ${icon}${theme.fg("dim", item)}`);
			}
			const remaining = items.length - shown.length;
			if (remaining > 0) {
				findLines.push(`  ${theme.fg("muted", `… ${remaining} more files`)}`);
			}
			text += `\n${findLines.join('\n')}`;
			return makeText(ctx.lastComponent, withBranch(text, theme));
		},
	});

	const lsTool = createLsTool(cwd);
	pi.registerTool({
		name: "ls",
		label: "ls",
		description: lsTool.description,
		parameters: lsTool.parameters,
		async execute(toolCallId, params, signal, onUpdate) {
			return lsTool.execute(toolCallId, params, signal, onUpdate);
		},
		renderCall(args, theme, ctx) {
			syncToolCallStatus(ctx);
			const summary = stableCallSummary(ctx, "_callSummary", () => sp(args.path ?? "."));
			return makeText(
				ctx.lastComponent,
				toolHeader("List", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
			);
		},
		renderResult(result, { expanded, isPartial }, theme, ctx) {
			if (isPartial) {
				return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "Listing..."), expanded, theme, ctx));
			}
			clearBlinkTimer(ctx);
			setToolStatus(ctx, ctx.isError ? "error" : "success");
			const items = (result.content[0]?.type === "text" ? result.content[0].text : "")
				.split("\n")
				.filter((line) => line.trim().length > 0);
			if (items.length === 0) return makeText(ctx.lastComponent, withBranch(theme.fg("muted", "empty directory"), theme));
			let text = theme.fg("muted", `${items.length} entries`);
			if (!expanded) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}`, theme));
			// Expanded: tree-view with icons
			const maxShow = previewLimit();
			const shown = items.slice(0, maxShow);
			const treeLines: string[] = [];
			for (let i = 0; i < shown.length; i++) {
				const item = shown[i];
				const isDir = item.endsWith("/");
				const isLast = i === shown.length - 1 && items.length <= maxShow;
				const prefix = isLast ? `${FG_RULE}\u2514\u2500\u2500${D_RST} ` : `${FG_RULE}\u251c\u2500\u2500${D_RST} `;
				const icon = isDir ? dirIcon() : fileIcon(item);
				const name = isDir ? theme.fg("accent", theme.bold(item)) : theme.fg("dim", item);
				treeLines.push(`${prefix}${icon}${name}`);
			}
			const remaining = items.length - shown.length;
			if (remaining > 0) {
				treeLines.push(`${FG_RULE}\u2514\u2500\u2500${D_RST} ${theme.fg("muted", `\u2026 ${remaining} more entries`)}`);
			}
			text += `\n${treeLines.join('\n')}`;
			return makeText(ctx.lastComponent, withBranch(text, theme));
		},
	});

	const writeTool = createWriteTool(cwd);
	pi.registerTool({
		name: "write",
		label: "write",
		description: writeTool.description,
		parameters: writeTool.parameters,
		async execute(toolCallId, params, signal, onUpdate, _ctx) {
			const fp = params.path ?? (params as any).file_path ?? "";
			const fullPath = fp ? resolve(cwd, fp) : "";
			const existedBefore = !!fullPath && fileExistsForTool(cwd, fp);
			WRITE_EXISTED_BEFORE.set(toolCallId, existedBefore);
			let old: string | null = null;
			try {
				if (fullPath && existedBefore) old = readFileSync(fullPath, "utf-8");
			} catch {
				old = null;
			}
			const result = await writeTool.execute(toolCallId, params, signal, onUpdate);
			const content = params.content ?? "";
			if (old !== null && old !== content) {
				const diff = parseDiff(old, content);
				(result as any).details = { _type: "diff", summary: summarizeDiff(diff.added, diff.removed), diff, language: lang(fp) };
			} else if (old === null) {
				(result as any).details = { _type: "new", lines: lineCount(content), filePath: fp };
			} else if (old === content) {
				(result as any).details = { _type: "noChange" };
			}
			return result;
		},
		renderCall(args, theme, ctx) {
			const fp = args?.path ?? (args as any)?.file_path ?? "";
			const revealSummary = shouldRevealCallArgs(ctx) || (!!fp && hasOwnArg(args, "content"));
			syncToolCallStatus(ctx);
			const wasNew = getWriteWasNewFile(ctx, cwd, fp, revealSummary);
			const label = wasNew === true ? "Create" : "Write";
			const summary = stableCallSummary(ctx, "_callSummary", () => {
				const base = sp(fp);
				return shouldRevealCallArgs(ctx) ? `${base} ${theme.fg("muted", `(${lineCount(args.content ?? "")} lines)`)}` : base;
			}, revealSummary);
			const hdr = toolHeader(label, summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme));
			return makeText(ctx.lastComponent, hdr);
		},
		renderResult(result, { expanded, isPartial }, theme, ctx) {
			if (isPartial) {
				return makeText(ctx.lastComponent, runningPreviewBlock(result, "", expanded, theme, ctx));
			}
			clearBlinkTimer(ctx);
			setToolStatus(ctx, ctx.isError ? "error" : "success");
			if (typeof ctx?.toolCallId === "string") WRITE_EXISTED_BEFORE.delete(ctx.toolCallId);
			if (ctx.isError) {
				const e =
					result.content
						?.filter((c: any) => c.type === "text")
						.map((c: any) => c.text || "")
						.join("\n") ?? "Error";
				return makeText(ctx.lastComponent, withBranch(theme.fg("error", e), theme));
			}
			const d = (result as any).details;
			if (d?._type === "diff") {
				const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
				const hunks = d.diff?.lines?.filter((l: any) => l.type === "sep").length + (d.diff?.lines?.length ? 1 : 0);
				const diffWidth = contextDiffWidth(ctx);
				const mode = shouldUseSplit(d.diff, diffWidth, previewLines) ? "split" : "unified";
				const richSummary = diffSummaryWithMeta(d.diff.added, d.diff.removed, hunks, mode);
				const key = `wd:${diffWidth}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}:${ctx.expanded ? 1 : 0}`;
				if (ctx.state._wdk !== key) {
					ctx.state._wdk = key;
					ctx.state._wdt = withFinalBranchBlock(`${richSummary}\n${theme.fg("muted", "rendering diff…")}`, theme);
					const dc = resolveDiffColors(theme);
					renderSplit(d.diff, d.language, previewLines, dc, diffWidth)
						.then((rendered) => {
							if (ctx.state._wdk !== key) return;
							ctx.state._wdt = withFinalBranchBlock(`${richSummary}\n${rendered}`, theme);
							safeInvalidate(ctx);
						})
						.catch(() => {
							if (ctx.state._wdk !== key) return;
							ctx.state._wdt = withBranch(richSummary, theme);
							safeInvalidate(ctx);
						});
				}
				return makeResponsiveDiffText(ctx, ctx.lastComponent, ctx.state._wdt ?? withBranch(richSummary, theme));
			}
			if (d?._type === "noChange") return makeText(ctx.lastComponent, withBranch(theme.fg("muted", "✓ no changes"), theme));
			if (d?._type === "new") {
				const content = typeof ctx.args?.content === "string" ? ctx.args.content : "";
				const lineTotal = typeof d.lines === "number" ? d.lines : lineCount(content);
				const contentHash = hashText(content);
				const syntheticDiff = getCachedParsedDiff(ctx, `nf-diff:${d.filePath}:${contentHash}`, "", content);
				const richSummary = diffSummaryWithMeta(syntheticDiff.added, 0, 1, "new file");
				const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
				const diffWidth = contextDiffWidth(ctx);
				const pk = `nf:${d.filePath}:${contentHash}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
				if (ctx.state._nfk !== pk) {
					ctx.state._nfk = pk;
					ctx.state._nft = withFinalBranchBlock(`${richSummary}\n${theme.fg("muted", "rendering diff…")}`, theme);
					const dc = resolveDiffColors(theme);
					renderUnified(syntheticDiff, lang(d.filePath), previewLines, dc, diffWidth)
						.then((rendered) => {
							if (ctx.state._nfk !== pk) return;
							ctx.state._nft = withFinalBranchBlock(`${richSummary}\n${rendered}`, theme);
							safeInvalidate(ctx);
						})
						.catch(() => {
							if (ctx.state._nfk !== pk) return;
							ctx.state._nft = withBranch(`${richSummary} ${theme.fg("muted", `(${lineTotal} lines)`)}`, theme);
							safeInvalidate(ctx);
						});
				}
				return makeResponsiveDiffText(ctx, ctx.lastComponent, ctx.state._nft ?? withBranch(`${richSummary} ${theme.fg("muted", `(${lineTotal} lines)`)}`, theme));
			}
			return makeText(ctx.lastComponent, withBranch(theme.fg("success", "Written"), theme));
		},
	});

	const editTool = createEditTool(cwd);
	pi.registerTool({
		name: "edit",
		label: "edit",
		description: editTool.description,
		parameters: editTool.parameters,
		async execute(toolCallId, params, signal, onUpdate, _ctx) {
			const fp = params.path ?? (params as any).file_path ?? "";
			const operations = getEditOperations(params);
			const localizedDiffs = operations.length === 1 ? await computeLocalizedEditDiffs(fp, operations, cwd) : null;
			const result = await editTool.execute(toolCallId, params, signal, onUpdate);
			if (operations.length === 0) return result;
			const { diffs, summary, totalLines, totalHunks } = summarizeEditOperations(operations);
			const baseDetails = (((result as any).details ?? {}) as Record<string, unknown>);
			if (operations.length === 1) {
				const localized = localizedDiffs?.[0];
				const editLine = localized?.line ?? (typeof baseDetails.firstChangedLine === "number" ? baseDetails.firstChangedLine : 0);
				const diff = localized?.diff ?? diffs[0];
				(result as any).details = {
					...baseDetails,
					_type: "editInfo",
					summary,
					editLine,
					hunks: countDiffHunks(diff),
					added: diff?.added ?? 0,
					removed: diff?.removed ?? 0,
				};
				return result;
			}
			(result as any).details = {
				...baseDetails,
				_type: "multiEditInfo",
				summary,
				editCount: operations.length,
				diffLineCount: totalLines,
				hunks: totalHunks,
				totalAdded: diffs.reduce((sum, diff) => sum + diff.added, 0),
				totalRemoved: diffs.reduce((sum, diff) => sum + diff.removed, 0),
			};
			return result;
		},
		renderCall(args, theme, ctx) {
			const fp = args?.path ?? (args as any)?.file_path ?? "";
			const operations = getEditOperations(args);
			const revealSummary = shouldRevealCallArgs(ctx) || (!!fp && hasOwnArg(args, "edits"));
			const summary = stableCallSummary(ctx, "_callSummary", () => shouldRevealCallArgs(ctx) && operations.length > 1 ? `${sp(fp)} ${theme.fg("muted", `(${operations.length} edits)`)}` : sp(fp), revealSummary);
			syncToolCallStatus(ctx);
			const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}`, liveLineCountTrailing(ctx, theme));
			if (!(ctx.argsComplete && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
			const diffWidth = contextDiffWidth(ctx, 3);
			const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
			const { diffs: fallbackDiffs, summary: editSummary } = getCachedEditOperationSummary(ctx, key, operations);
			if (ctx.state._pk !== key) {
				ctx.state._pk = key;
				ctx.state._ptBody = theme.fg("muted", "(rendering…)");
				ctx.state._ptDisplay = indentBranchBlock(withBranch(ctx.state._ptBody, theme, false, true));
				const lg = lang(fp);
				void computeLocalizedEditDiffs(fp, operations, cwd)
					.then((localizedDiffs) => {
						if (ctx.state._pk !== key) return;
						const diffs = localizedDiffs?.map((entry) => entry.diff) ?? fallbackDiffs;
						const lines = localizedDiffs?.map((entry) => entry.line) ?? diffs.map((diff) => getFirstChangedNewLine(diff));
						renderEditPreviewBody(ctx, key, theme, lg, operations, diffs, lines, editSummary);
					})
					.catch(() => {
						if (ctx.state._pk !== key) return;
						renderEditPreviewBody(ctx, key, theme, lg, operations, fallbackDiffs, fallbackDiffs.map((diff) => getFirstChangedNewLine(diff)), editSummary);
					});
			}
			const body = liveBranchDisplay(ctx.state, theme) ?? (ctx.state._ptDisplay as string | undefined);
			return makeResponsiveDiffText(ctx, ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
		},
		renderResult(result, { expanded, isPartial }, theme, ctx) {
			if (isPartial) {
				return makeText(ctx.lastComponent, indentBranchBlock(runningPreviewBlock(result, theme.fg("dim", "Editing..."), expanded, theme, ctx)));
			}
			clearBlinkTimer(ctx);
			setToolStatus(ctx, ctx.isError ? "error" : "success");
			if (ctx.isError) {
				const e =
					result.content
						?.filter((c: any) => c.type === "text")
						.map((c: any) => c.text || "")
						.join("\n") ?? "Error";
				return makeText(ctx.lastComponent, indentBranchBlock(withBranch(theme.fg("error", e), theme)));
			}
			if ((result as any).details?._type === "editInfo") {
				const { editLine, hunks, added, removed } = (result as any).details;
				const loc = formatLineMeta(editLine ?? 0, theme);
				const summary = diffSummaryWithMeta(added ?? 0, removed ?? 0, hunks ?? 0, "");
				return makeText(ctx.lastComponent, indentBranchBlock(withBranch(`${summary}${loc}`, theme)));
			}
			if ((result as any).details?._type === "multiEditInfo") {
				const { editCount, diffLineCount, hunks, totalAdded, totalRemoved } = (result as any).details;
				const summary = diffSummaryWithMeta(totalAdded ?? 0, totalRemoved ?? 0, hunks ?? 0, "");
				return makeText(ctx.lastComponent, indentBranchBlock(withBranch(`${editCount} edits ${summary}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}`, theme)));
			}
			return makeText(ctx.lastComponent, indentBranchBlock(withBranch(theme.fg("success", "Applied"), theme)));
		},
	});

	const wrappedOpenAiTools = new Set<string>();
	const registerOpenAiToolOverrides = (): void => {
		let allTools: unknown[] = [];
		try {
			allTools = typeof (pi as any).getAllTools === "function" ? (pi as any).getAllTools() : [];
		} catch {
			allTools = [];
		}
		for (const tool of allTools) {
			if (!isOpenAiToolCandidate(tool)) continue;
			const record = tool as Record<string, unknown>;
			const name = typeof record.name === "string" ? record.name : "";
			if (!name || wrappedOpenAiTools.has(name)) continue;
			const execute = typeof record.execute === "function" ? (record.execute as any) : null;
			if (!execute) continue;
			const rawLabel = typeof record.label === "string" ? record.label.trim() : "";
			const label = rawLabel && rawLabel !== name && !rawLabel.includes("_") ? rawLabel : humanizeToolName(name);
			const description = typeof record.description === "string" ? record.description : label;
			(pi as any).registerTool({
				name,
				label,
				description,
				parameters: record.parameters,
				prepareArguments: typeof record.prepareArguments === "function" ? record.prepareArguments : undefined,
				async execute(toolCallId: string, params: any, signal: AbortSignal | undefined, onUpdate: any, ctx: any) {
					return await Promise.resolve(execute(toolCallId, params, signal, onUpdate, ctx));
				},
				renderCall(args: any, theme: Theme, ctx: any) {
					if (name === "apply_patch") return renderApplyPatchCall(args, theme, ctx, sp);
					syncToolCallStatus(ctx);
					ctx.state._openAiPatchFiles = [];
					const summary = stableCallSummary(ctx, "_callSummary", () => summarizeOpenAiToolCall(name, args, theme, sp));
					return makeText(
						ctx.lastComponent,
						toolHeader(label, summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
					);
				},
				renderResult(result: any, { expanded, isPartial }: any, theme: Theme, ctx: any) {
					if (name === "apply_patch") return renderApplyPatchResult(result, isPartial, theme, ctx);
					return renderOpenAiToolResult(name, result, expanded, isPartial, theme, ctx);
				},
			});
			wrappedOpenAiTools.add(name);
		}
	};

	const wrappedMcpTools = new Set<string>();
	const registerMcpToolOverrides = (): void => {
		let allTools: unknown[] = [];
		try {
			allTools = typeof (pi as any).getAllTools === "function" ? (pi as any).getAllTools() : [];
		} catch {
			allTools = [];
		}
		for (const tool of allTools) {
			if (!isMcpToolCandidate(tool)) continue;
			const record = tool as Record<string, unknown>;
			const name = typeof record.name === "string" ? record.name : "";
			if (!name || wrappedMcpTools.has(name)) continue;
			const execute = typeof record.execute === "function" ? (record.execute as any) : null;
			if (!execute) continue;
			const label = typeof record.label === "string" ? record.label : name === "mcp" ? "MCP" : `MCP ${name}`;
			const description = typeof record.description === "string" ? record.description : "MCP tool";
			(pi as any).registerTool({
				name,
				label,
				description,
				parameters: record.parameters,
				prepareArguments: typeof record.prepareArguments === "function" ? record.prepareArguments : undefined,
				async execute(toolCallId: string, params: any, signal: AbortSignal | undefined, onUpdate: any, ctx: any) {
					return await Promise.resolve(execute(toolCallId, params, signal, onUpdate, ctx));
				},
				renderCall(args: any, theme: Theme, ctx: any) {
					return renderGenericToolCall(name, args, theme, ctx);
				},
				renderResult(result: any, { expanded, isPartial }: any, theme: Theme, ctx: any) {
					return renderMcpToolResult(result, expanded, isPartial, theme, ctx);
				},
			});
			wrappedMcpTools.add(name);
		}
	};

	pi.on("session_start", async () => {
		registerOpenAiToolOverrides();
		registerMcpToolOverrides();
	});
	pi.on("before_agent_start", async () => {
		registerOpenAiToolOverrides();
		registerMcpToolOverrides();
	});

	// Streaming activity keeps the blink timer alive. Do NOT clear blink contexts
	// on turn_end — a turn ends when the assistant message finishes, BEFORE its
	// tools run. agent_end / agent_settled are the real "work finished" signals.
	pi.on("turn_start", async () => { markBlinkActivity(); });
	pi.on("message_start", async () => { markBlinkActivity(); });
	pi.on("message_update", async () => { markBlinkActivity(); });
	pi.on("tool_execution_start", async () => { markBlinkActivity(); });
	// Partial tool output is the main long-running signal (bash streams for minutes).
	pi.on("tool_execution_update", async () => { markBlinkActivity(); });
	pi.on("tool_execution_end", async () => { markBlinkActivity(); });
	// agent_end fires when a low-level run finishes (tools for that assistant message
	// are done). registerThinkingLabels clears currentAgentWorkStartMs on the same
	// event; defer so we only wipe blink state once the work marker is gone.
	// Do not use agent_settled here — older peer types don't include it, and the
	// live-agent heartbeat already keeps quiet long tools blinking until agent_end.
	pi.on("agent_end", async () => {
		queueMicrotask(() => {
			if (currentAgentWorkStartMs !== undefined) {
				markBlinkActivity();
				return;
			}
			_clearAllBlinkContexts();
		});
	});
	// Session rebuild (resume/reload/fork) must not leave history partials blinking.
	pi.on("session_start", async () => {
		_clearAllBlinkContexts();
	});
	pi.on("session_shutdown", async () => {
		_clearAllBlinkContexts();
		clearAllBashDurationContexts();
		clearRtkRewriteState();
		WRITE_EXISTED_BEFORE.clear();
		clearHighlightCache();
		invalidateThemePaletteCache();
		bumpToolBranchVisualEpoch();
	});
}
