/** * default mode(config.mode === "on"):Claude Code 风格工具卡渲染。 * * 经 ToolExecutionComponent 原型补丁接管 call/result;edit/write 走 rich diff。 * 生命周期对齐 compact-mode:installDefaultMode → hooks.shutdown。 */ import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent"; import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { config, getToolDisplayConfig, type CompactStyleMode } from "../config/config.ts"; import { isToolCallHovered } from "./mouse/interaction.ts"; import { countLines, fitToolCallSummary, hasExpandableDetail, insetComponent, isToolExpanded, oneLine, outputLineCount, pathSummary, pendingIcon, renderCollapsedToolResultToWidth, renderExpandedToolResult, resolveToolVisualState, scheduleAnimation, settledIcon, setToolVisualState, textFromResult, toolIconColor, toolViewportWidth, type ToolCallSummary, } from "./tool/result.ts"; import { showMoreHintText } from "./show-more-hint.ts"; import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts"; import { getMessageDisplayTheme } from "./message-display.ts"; // 成功勾:亮绿 truecolor(与 message-display 一致) const BRIGHT_GREEN = "\x1b[38;2;80;220;100m"; const ANSI_FG_RESET = "\x1b[39m"; const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch"); const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode"); const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode"); const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for("pi.ccstyle.tool-expanded-background-patch"); const AGENT_FAMILY_TOOL_NAMES = new Set([ "Agent", "Agents", "get_subagent_result", "steer_subagent", ]); // pi-subagents 等扩展为 Agent 提供专用渲染器,ccstyle 必须保留。 const DEDICATED_RENDERER_TOOLS = new Set(["Agent"]); export type DefaultModeHooks = { /** 本安装是否仍持有全局工具渲染补丁。 */ isOwner(): boolean; shutdown(): void; }; type ToolRenderMethods = { hasRendererDefinition: (...args: any[]) => boolean; getRenderShell: (...args: any[]) => "default" | "self"; getCallRenderer: (...args: any[]) => any; getResultRenderer: (...args: any[]) => any; }; type GlobalToolRenderPatch = { version: 2; prototype: any; owner: object; active: boolean; enabled: () => boolean; mode: () => CompactStyleMode; wrap: (tool: any) => any; byDefinition: WeakMap; byName: Map; downstream: ToolRenderMethods; installed: ToolRenderMethods; // 兼容旧扩展读取 Symbol 上的 legacy 字段。 originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"]; originalGetRenderShell: ToolRenderMethods["getRenderShell"]; originalGetCallRenderer: ToolRenderMethods["getCallRenderer"]; originalGetResultRenderer: ToolRenderMethods["getResultRenderer"]; }; type ToolExpandedBackgroundPatch = { active: boolean; prototype: any; installed: (...args: any[]) => void; original: (...args: any[]) => void; dispose: () => void; }; export function shouldRenderRichDiff( mode: CompactStyleMode, toolName: string, isError: boolean, ): boolean { return mode === "on" && !isError && (toolName === "edit" || toolName === "write"); } export function isMcpToolDefinition(definition: any, toolName: string): boolean { const label = typeof definition?.label === "string" ? definition.label.trim() : ""; if (/^MCP(?::|$)/i.test(label)) return true; if (toolName === "mcp" || /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName)) return true; if (label) return false; const description = typeof definition?.description === "string" ? definition.description : ""; return /\bModel Context Protocol\b/i.test(description); } export function humanizeMcpToolName(toolName: string): string { const words = toolName .replace(/^mcp(?:[_:-]+)+/i, "") .split(/[_:-]+/) .filter(Boolean); return words.length ? words.map((word) => word[0]!.toUpperCase() + word.slice(1)).join(" ") : "MCP"; } /** 排除名单内且自带 renderer 的工具保留原渲染。 */ export function preservesOriginalRenderer( extensionDefinition: any, toolName: string, builtInToolDefinition?: any, excludeRenderers: readonly string[] = config.excludeRenderers, ): boolean { if (!excludeRenderers.includes(toolName)) return false; return [extensionDefinition, builtInToolDefinition].some( (definition) => definition?.renderShell === "self" || typeof definition?.renderCall === "function" || typeof definition?.renderResult === "function", ); } function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") { try { if (typeof tool?.[slot] === "function") return tool[slot](...args); } catch { // Fall through to raw fallback. } return new Text(fallback, 0, 0); } function humanizeToolLabel(label: string): string { return label .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/[_-]+/g, " ") .replace(/\b\w/g, (char) => char.toUpperCase()); } function singleToolCallSummary( toolName: string, label: string, args: any, cwd?: string, ): ToolCallSummary { const title = label === toolName ? humanizeToolLabel(label) : label; if (!args || typeof args !== "object") return { main: title, detail: "" }; const name = toolName.toLowerCase(); const value = (fallback: string, ...keys: string[]) => { const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item); return `${title} ${oneLine(found || fallback)}`; }; if (AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) { return { main: `${title} ${oneLine(args.agent_id)}`, detail: "" }; } // Agents 走 ccstyle wrapper;Agent 保留专用渲染器。 if (name === "agents") { return { main: value("launch agents", "description", "prompt"), detail: "", }; } if (name === "skill") return { main: value("run skill", "name"), detail: "" }; if (name === "enterplanmode" || name === "enter_plan_mode") { return { main: `${title} enable read-only planning`, detail: "" }; } if (name === "exitplanmode" || name === "exit_plan_mode") { return { main: `${title} present plan`, detail: "" }; } if (name === "taskcreate") return { main: value("create task", "subject"), detail: "" }; if (name === "tasklist") return { main: `${title} task list`, detail: "" }; if (name === "taskget" || name === "taskupdate") { return { main: value("task", "taskId", "task_id"), detail: "" }; } if (name === "taskoutput" || name === "taskstop") { return { main: value("background task", "task_id", "taskId"), detail: "" }; } if (name === "taskexecute") { const ids = Array.isArray(args.task_ids) ? args.task_ids : Array.isArray(args.taskIds) ? args.taskIds : []; const summary = ids.length ? `${ids[0]}${ids.length > 1 ? ` (+${ids.length - 1} tasks)` : ""}` : "start tasks"; return { main: `${title} ${summary}`, detail: "" }; } if (toolName === "read") { const details = [ args.offset !== undefined ? `offset=${args.offset}` : "", args.limit !== undefined ? `limit=${args.limit}` : "", ].filter(Boolean); const detail = details.length ? ` (${details.join(", ")})` : ""; if (typeof args.path === "string" && args.path) { return pathSummary(title, args.path, cwd, detail); } return { main: title, detail }; } const preferredPath = args.path ?? args.file_path; if (typeof preferredPath === "string" && preferredPath) { return pathSummary(title, preferredPath, cwd); } const preferred = args.command ?? args.query ?? args.question ?? args.pattern ?? args.url ?? args.name ?? args.tool_use_id ?? args.toolCallId ?? args.id ?? args.message; return { main: preferred !== undefined && preferred !== null && typeof preferred !== "object" ? `${title} ${oneLine(preferred)}` : title, detail: "", }; } type ParsedTask = { id: string; status: string; subject: string }; function parseTaskList(text: string): ParsedTask[] { return text .split("\n") .map((line) => line.match(/^#(\d+) \[([^\]]+)] (.+)$/)) .filter((match): match is RegExpMatchArray => Boolean(match)) .map((match) => ({ id: match[1]!, status: match[2]!, subject: match[3]! })); } function taskListSummary(tasks: ParsedTask[]): string { const counts = { pending: 0, in_progress: 0, completed: 0 }; for (const task of tasks) { if (task.status in counts) counts[task.status as keyof typeof counts]++; } return [ `${tasks.length} tasks`, counts.in_progress ? `${counts.in_progress} in progress` : "", counts.pending ? `${counts.pending} pending` : "", counts.completed ? `${counts.completed} completed` : "", ] .filter(Boolean) .join(" • "); } function renderExpandedTaskResult( toolName: string, text: string, theme: any, isError: boolean, ): any | undefined { if (isError) return undefined; if (toolName === "TaskList") { const tasks = parseTaskList(text); if (!tasks.length) return undefined; const limit = Math.max(1, config.expandedPreviewMaxLines); const rows = tasks.slice(0, limit).map((task) => { const color = task.status === "completed" ? "success" : task.status === "in_progress" ? "warning" : "muted"; return ` ${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`; }); if (tasks.length > rows.length) rows.push(theme.fg("muted", ` … ${tasks.length - rows.length} more tasks`)); return new Text(` ↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.join("\n")}`, 0, 0); } const line = text.trim(); if (!line || line.includes("\n")) return undefined; let formatted: string | undefined; let match: RegExpMatchArray | null; if ( toolName === "TaskCreate" && (match = line.match(/^Task #(\d+) created successfully: (.+)$/)) ) { formatted = `${theme.fg("success", "Created task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`; } else if (toolName === "TaskUpdate" && (match = line.match(/^Updated task #(\d+) (.+)$/))) { formatted = `${theme.fg("success", "Updated task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`; } else if (toolName === "TaskExecute") { formatted = `${theme.fg("success", "Started")} ${theme.fg("muted", line)}`; } else if (toolName === "TaskStop") { formatted = `${theme.fg("success", "Stopped")} ${theme.fg("muted", line)}`; } return formatted ? new Text(` ↳ ${formatted}`, 0, 0) : undefined; } /** 用 ccstyle call/result 包装任意工具定义。 */ function createCcstyleTool( originalTool: any, writeExecutionMetadata: WriteExecutionMetadataStore, ): any { const toolName = originalTool.name; const label = isMcpToolDefinition(originalTool, toolName) ? humanizeMcpToolName(toolName) : originalTool.label || toolName; return { ...originalTool, renderShell: "self", renderCall(args: any, theme: any, context: any) { if (config.mode !== "on") { return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName)); } const visualState = resolveToolVisualState(context); const isPending = visualState === "pending" || (!visualState && (context?.isPartial || context?.executionStarted)); if (isPending && context?.executionStarted) scheduleAnimation(context); const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState); const icon = visualState === "success" ? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}` : theme.fg(toolIconColor(context), rawIcon); const summary = singleToolCallSummary(toolName, label, args, context?.cwd); let cachedWidth: number | undefined; let cachedLine: string | undefined; return { render(width: number) { if (cachedLine !== undefined && cachedWidth === width) return [cachedLine]; const viewportWidth = toolViewportWidth(width); const callWidth = Math.max(0, viewportWidth - visibleWidth(icon) - 2); const mainWidth = Math.max(0, callWidth - visibleWidth(summary.detail)); cachedWidth = width; const line = ` ${icon} ${theme.fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${theme.fg("dim", summary.detail)}`; cachedLine = truncateToWidth(line, viewportWidth, ""); return [cachedLine]; }, invalidate() {}, }; }, renderResult(result: any, options: any, theme: any, context: any) { if (config.mode !== "on") { return renderDefault( originalTool, "renderResult", [result, options, theme, context], textFromResult(result), ); } if (options?.isPartial) { return new Text(theme.fg("muted", " ↳ Pending…"), 0, 0); } const isError = options?.isError || context?.isError; setToolVisualState(context, isError ? "error" : "success"); const expanded = isToolExpanded(options, context); const toolCallId = context?.toolCallId; if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) { // getter 保证 Diff indicator / wrap / limits 下次绘制即更新 const richResult = renderRichToolResult( toolName, result, { ...options, expanded, // 全局共享状态避免 /reload 后旧 result renderer 闭包失联 isHovered: () => isToolCallHovered(toolCallId), }, theme, context, writeExecutionMetadata, getToolDisplayConfig, ); if (richResult) return insetComponent(richResult); } const text = textFromResult(result, expanded); const args = context?.args; if (expanded) { const taskResult = renderExpandedTaskResult(toolName, text, theme, Boolean(isError)); if (taskResult) return taskResult; } const tasks = !isError && toolName === "TaskList" ? parseTaskList(text) : []; const outputLines = outputLineCount(result) || countLines(text); const lineWord = outputLines === 1 ? "line" : "lines"; const action = toolName === "read" ? "loaded" : "returned"; const rendered = tasks.length ? taskListSummary(tasks) : isError ? text ? oneLine(text) : "Failed" : outputLines ? `${outputLines} ${lineWord} ${action}` : "Done"; const expandable = !expanded && (tasks.length > 0 || hasExpandableDetail(text, args)); const hintText = showMoreHintText(); const hintPrefix = expandable ? theme.fg("dim", " • ") : ""; const hint = expandable ? hintPrefix + theme.fg("dim", hintText) : ""; const hoveredHint = expandable ? hintPrefix + theme.fg("text", hintText) : ""; if (expanded) { return renderExpandedToolResult( text || "", theme, Boolean(isError), context?.lastComponent, args, context, ); } if (context?.state) context.state.ccstyleIoView = undefined; let cachedWidth: number | undefined; let cachedLine: string | undefined; let cachedHoveredLine: string | undefined; return { render(width: number) { if (cachedLine === undefined || cachedWidth !== width) { cachedWidth = width; cachedLine = theme.fg( isError ? "error" : "muted", renderCollapsedToolResultToWidth(rendered, hint, width), ); cachedHoveredLine = theme.fg( isError ? "error" : "muted", renderCollapsedToolResultToWidth(rendered, hoveredHint, width), ); } return [isToolCallHovered(toolCallId) ? cachedHoveredLine! : cachedLine]; }, invalidate() {}, }; }, }; } function syncToolShell(component: any, shell: "default" | "self"): void { const target = shell === "self" ? component.selfRenderContainer : component.contentBox; if (!target || !Array.isArray(component.children)) return; const candidates = new Set( [component.contentText, component.contentBox, component.selfRenderContainer].filter(Boolean), ); const indexes = component.children .map((child: any, index: number) => (candidates.has(child) ? index : -1)) .filter((index: number) => index >= 0); const targetIndex = indexes[0]; // 构造期 getRenderShell 先于 Pi 挂载 shell;此处勿挂载,否则构造器会二次添加。 if (targetIndex === undefined) return; component.children[targetIndex] = target; for (const index of indexes.sort((left: number, right: number) => right - left)) { if (index !== targetIndex) component.children.splice(index, 1); } } function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean { const extensionDefinition = component.toolDefinition; const builtInDefinition = component.builtInToolDefinition; const definition = extensionDefinition ?? builtInDefinition; const toolName = String(component.toolName || definition?.name || ""); const useCcstyle = patch.mode() === "on" && !DEDICATED_RENDERER_TOOLS.has(toolName) && !preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition); component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle; return useCcstyle; } function shouldUseSelfShell(component: any, _patch: GlobalToolRenderPatch): boolean { component[COMPONENT_TOOL_SELF_SHELL_MODE] = false; return false; } function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): any { const definition = component.toolDefinition ?? component.builtInToolDefinition; if (definition && typeof definition === "object") { let wrapped = patch.byDefinition.get(definition); if (!wrapped) { wrapped = patch.wrap(definition); patch.byDefinition.set(definition, wrapped); } return wrapped; } const name = String(component.toolName || "tool"); let wrapped = patch.byName.get(name); if (!wrapped) { wrapped = patch.wrap({ name, label: name }); patch.byName.set(name, wrapped); } return wrapped; } function prototypeToolRenderMethods(prototype: any): ToolRenderMethods { return { hasRendererDefinition: prototype.hasRendererDefinition, getRenderShell: prototype.getRenderShell, getCallRenderer: prototype.getCallRenderer, getResultRenderer: prototype.getResultRenderer, }; } function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch { if (!value || value.version !== 2 || !value.installed || !value.downstream) return false; return ["hasRendererDefinition", "getRenderShell", "getCallRenderer", "getResultRenderer"].every( (name) => typeof value.installed[name] === "function" && typeof value.downstream[name] === "function", ); } function isLegacyInstalledWrapper(method: unknown, downstreamField: string): boolean { if (typeof method !== "function") return false; try { const source = Function.prototype.toString.call(method); return ( source.includes(downstreamField) && (source.includes("shouldGloballyStyleTool") || source.includes("shouldUseSelfShell") || source.includes("getGloballyStyledTool")) ); } catch { return false; } } function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods { const current = prototypeToolRenderMethods(prototype); if (!previous || previous.prototype !== prototype) return current; if (isOwnershipAwarePatch(previous)) { return { hasRendererDefinition: current.hasRendererDefinition === previous.installed.hasRendererDefinition ? previous.downstream.hasRendererDefinition : current.hasRendererDefinition, getRenderShell: current.getRenderShell === previous.installed.getRenderShell ? previous.downstream.getRenderShell : current.getRenderShell, getCallRenderer: current.getCallRenderer === previous.installed.getCallRenderer ? previous.downstream.getCallRenderer : current.getCallRenderer, getResultRenderer: current.getResultRenderer === previous.installed.getResultRenderer ? previous.downstream.getResultRenderer : current.getResultRenderer, }; } // pre-v2 Symbol 无 wrapper 引用;能识别则回退,否则保留当前方法为 external。 const legacyDownstream = (method: Function, field: string): Function => { const saved = previous[field]; return typeof saved === "function" && isLegacyInstalledWrapper(method, field) ? saved : method; }; return { hasRendererDefinition: legacyDownstream( current.hasRendererDefinition, "originalHasRendererDefinition", ) as ToolRenderMethods["hasRendererDefinition"], getRenderShell: legacyDownstream( current.getRenderShell, "originalGetRenderShell", ) as ToolRenderMethods["getRenderShell"], getCallRenderer: legacyDownstream( current.getCallRenderer, "originalGetCallRenderer", ) as ToolRenderMethods["getCallRenderer"], getResultRenderer: legacyDownstream( current.getResultRenderer, "originalGetResultRenderer", ) as ToolRenderMethods["getResultRenderer"], }; } function disconnectGlobalToolRenderPatch(patch: any): void { if (!patch || typeof patch !== "object") return; patch.active = false; patch.enabled = () => false; patch.mode = () => "off"; patch.wrap = (tool: any) => tool; patch.byDefinition = new WeakMap(); if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear(); else patch.byName = new Map(); } function installGlobalToolRendering( writeExecutionMetadata: WriteExecutionMetadataStore, ): GlobalToolRenderPatch { const prototype = (ToolExecutionComponent as any).prototype; const host = globalThis as any; const previous = host[GLOBAL_TOOL_RENDER_PATCH]; const downstream = downstreamForGlobalToolInstall(prototype, previous); // 外部仍持有的旧 wrapper 先变为无回调 pass-through,再挂新安装。 disconnectGlobalToolRenderPatch(previous); const patch: GlobalToolRenderPatch = { version: 2, prototype, owner: {}, active: true, enabled: () => config.mode === "on", mode: () => config.mode, wrap: (tool: any) => createCcstyleTool(tool, writeExecutionMetadata), byDefinition: new WeakMap(), byName: new Map(), downstream, installed: undefined as any, originalHasRendererDefinition: downstream.hasRendererDefinition, originalGetRenderShell: downstream.getRenderShell, originalGetCallRenderer: downstream.getCallRenderer, originalGetResultRenderer: downstream.getResultRenderer, }; patch.installed = { hasRendererDefinition: function (this: any, ...args: any[]) { if (patch.active && shouldGloballyStyleTool(this, patch)) return true; return patch.downstream.hasRendererDefinition.apply(this, args); }, getRenderShell: function (this: any, ...args: any[]) { if (!patch.active) return patch.downstream.getRenderShell.apply(this, args); const useSelfShell = shouldUseSelfShell(this, patch); const useCcstyle = shouldGloballyStyleTool(this, patch); const shell = useSelfShell || (useCcstyle && !this.expanded) ? "self" : useCcstyle ? "default" : patch.downstream.getRenderShell.apply(this, args); syncToolShell(this, shell); return shell; }, getCallRenderer: function (this: any, ...args: any[]) { if (patch.active && shouldGloballyStyleTool(this, patch)) { return getGloballyStyledTool(this, patch).renderCall; } return patch.downstream.getCallRenderer.apply(this, args); }, getResultRenderer: function (this: any, ...args: any[]) { if (patch.active && shouldGloballyStyleTool(this, patch)) { return getGloballyStyledTool(this, patch).renderResult; } return patch.downstream.getResultRenderer.apply(this, args); }, }; prototype.hasRendererDefinition = patch.installed.hasRendererDefinition; prototype.getRenderShell = patch.installed.getRenderShell; prototype.getCallRenderer = patch.installed.getCallRenderer; prototype.getResultRenderer = patch.installed.getResultRenderer; host[GLOBAL_TOOL_RENDER_PATCH] = patch; return patch; } function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void { if (!patch.active) return; disconnectGlobalToolRenderPatch(patch); const prototype = patch.prototype; if (prototype.hasRendererDefinition === patch.installed.hasRendererDefinition) { prototype.hasRendererDefinition = patch.downstream.hasRendererDefinition; } if (prototype.getRenderShell === patch.installed.getRenderShell) { prototype.getRenderShell = patch.downstream.getRenderShell; } if (prototype.getCallRenderer === patch.installed.getCallRenderer) { prototype.getCallRenderer = patch.downstream.getCallRenderer; } if (prototype.getResultRenderer === patch.installed.getResultRenderer) { prototype.getResultRenderer = patch.downstream.getResultRenderer; } } /** 展开面板背景统一为 user message 背景色;折叠行保持原生状态色。 * 必须在 compact-mode 之后安装,shutdown 时先于 compact-mode 释放。 */ export function installToolExpandedBackground(): () => void { const host = globalThis as any; const previous = host[TOOL_EXPANDED_BACKGROUND_PATCH] as ToolExpandedBackgroundPatch | undefined; if (previous) previous.dispose(); // SAFETY: Pi's runtime ToolExecutionComponent prototype owns updateDisplay; its public type omits it. const prototype = ToolExecutionComponent.prototype as unknown as { updateDisplay: () => void }; const original = prototype.updateDisplay; const patch: ToolExpandedBackgroundPatch = { active: true, prototype, installed: function (this: any) { original.call(this); if (!patch.active || config.mode !== "on" || !this.expanded) return; const theme = getMessageDisplayTheme(); if (!theme?.bg) return; const box = this.contentBox; if (box?.setBgFn) box.setBgFn((text: string) => theme.bg("userMessageBg", text)); }, original, dispose: () => { patch.active = false; if (prototype.updateDisplay === patch.installed) { prototype.updateDisplay = original; } if (host[TOOL_EXPANDED_BACKGROUND_PATCH] === patch) { delete host[TOOL_EXPANDED_BACKGROUND_PATCH]; } }, }; prototype.updateDisplay = patch.installed; host[TOOL_EXPANDED_BACKGROUND_PATCH] = patch; return patch.dispose; } /** 安装 default mode 全局工具渲染补丁(不含展开背景,见 installToolExpandedBackground)。 */ export function installDefaultMode( writeExecutionMetadata: WriteExecutionMetadataStore, ): DefaultModeHooks { const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata); return { isOwner() { return ( (globalThis as any)[GLOBAL_TOOL_RENDER_PATCH] === globalToolRendering && globalToolRendering.active ); }, shutdown() { deactivateGlobalToolRendering(globalToolRendering); }, }; }