@ai-agent-forge/agent-forge 0.88.4 → 0.88.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/bundled-plugins/agent-loop-policy/agent-forge.json +1 -1
- package/bundled-plugins/agent-loop-policy/plugin.json +1 -1
- package/bundled-plugins/alignment/agent-forge.json +19 -0
- package/bundled-plugins/alignment/dist/baseline.d.ts +12 -0
- package/bundled-plugins/alignment/dist/baseline.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/baseline.js +68 -0
- package/bundled-plugins/alignment/dist/baseline.js.map +1 -0
- package/bundled-plugins/alignment/dist/engine.d.ts +102 -0
- package/bundled-plugins/alignment/dist/engine.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/engine.js +654 -0
- package/bundled-plugins/alignment/dist/engine.js.map +1 -0
- package/bundled-plugins/alignment/dist/entry.d.ts +14 -0
- package/bundled-plugins/alignment/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/entry.js +23 -0
- package/bundled-plugins/alignment/dist/entry.js.map +1 -0
- package/bundled-plugins/alignment/dist/export.d.ts +15 -0
- package/bundled-plugins/alignment/dist/export.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/export.js +122 -0
- package/bundled-plugins/alignment/dist/export.js.map +1 -0
- package/bundled-plugins/alignment/dist/http-server.d.ts +33 -0
- package/bundled-plugins/alignment/dist/http-server.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/http-server.js +119 -0
- package/bundled-plugins/alignment/dist/http-server.js.map +1 -0
- package/bundled-plugins/alignment/dist/index.d.ts +24 -0
- package/bundled-plugins/alignment/dist/index.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/index.js +719 -0
- package/bundled-plugins/alignment/dist/index.js.map +1 -0
- package/bundled-plugins/alignment/dist/page.d.ts +15 -0
- package/bundled-plugins/alignment/dist/page.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/page.js +832 -0
- package/bundled-plugins/alignment/dist/page.js.map +1 -0
- package/bundled-plugins/alignment/dist/render.d.ts +40 -0
- package/bundled-plugins/alignment/dist/render.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/render.js +194 -0
- package/bundled-plugins/alignment/dist/render.js.map +1 -0
- package/bundled-plugins/alignment/dist/types.d.ts +217 -0
- package/bundled-plugins/alignment/dist/types.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/types.js +30 -0
- package/bundled-plugins/alignment/dist/types.js.map +1 -0
- package/bundled-plugins/alignment/plugin.json +12 -0
- package/bundled-plugins/browser-playwright/MANUAL.md +60 -0
- package/bundled-plugins/browser-playwright/agent-forge.json +25 -0
- package/bundled-plugins/browser-playwright/dist/capability.d.ts +25 -0
- package/bundled-plugins/browser-playwright/dist/capability.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/capability.js +612 -0
- package/bundled-plugins/browser-playwright/dist/capability.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/entry.d.ts +13 -0
- package/bundled-plugins/browser-playwright/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/entry.js +14 -0
- package/bundled-plugins/browser-playwright/dist/entry.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/index.d.ts +7 -0
- package/bundled-plugins/browser-playwright/dist/index.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/index.js +5 -0
- package/bundled-plugins/browser-playwright/dist/index.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/operations.d.ts +69 -0
- package/bundled-plugins/browser-playwright/dist/operations.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/operations.js +149 -0
- package/bundled-plugins/browser-playwright/dist/operations.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.d.ts +275 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.js +953 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/setup.d.ts +88 -0
- package/bundled-plugins/browser-playwright/dist/setup.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/setup.js +252 -0
- package/bundled-plugins/browser-playwright/dist/setup.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/types.d.ts +270 -0
- package/bundled-plugins/browser-playwright/dist/types.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/types.js +2 -0
- package/bundled-plugins/browser-playwright/dist/types.js.map +1 -0
- package/bundled-plugins/browser-playwright/plugin.json +20 -0
- package/bundled-plugins/coding-tools/agent-forge.json +1 -1
- package/bundled-plugins/coding-tools/dist/bash.d.ts +2 -7
- package/bundled-plugins/coding-tools/dist/bash.d.ts.map +1 -1
- package/bundled-plugins/coding-tools/dist/bash.js +2 -34
- package/bundled-plugins/coding-tools/dist/bash.js.map +1 -1
- package/bundled-plugins/coding-tools/plugin.json +1 -1
- package/bundled-plugins/compaction-policy/agent-forge.json +1 -1
- package/bundled-plugins/compaction-policy/plugin.json +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.d.ts.map +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.js +84 -31
- package/bundled-plugins/mcp-http/dist/mcp-http.js.map +1 -1
- package/bundled-plugins/overflow-policy/agent-forge.json +1 -1
- package/bundled-plugins/overflow-policy/plugin.json +1 -1
- package/bundled-plugins/provider-failover/agent-forge.json +1 -1
- package/bundled-plugins/provider-failover/plugin.json +1 -1
- package/bundled-plugins/retry-policy/agent-forge.json +1 -1
- package/bundled-plugins/retry-policy/plugin.json +1 -1
- package/dist/bundle/.build-manifest.json +60 -58
- package/dist/bundle/chunks/{anthropic-messages-3FJHRAJL.js → anthropic-messages-NL4Y6WPB.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-GCL47IDS.js → azure-openai-responses-CPXC42HO.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/chunk-5LRQFZFE.js +2 -0
- package/dist/bundle/chunks/{chunk-HTEVUY7F.js → chunk-7HSJPQKN.js} +1 -1
- package/dist/bundle/chunks/{chunk-AC2FIS4G.js → chunk-EDO7Q6XR.js} +1 -1
- package/dist/bundle/chunks/{chunk-755VNWIF.js → chunk-ISLHFRER.js} +6 -6
- package/dist/bundle/chunks/{chunk-HV64ASMT.js → chunk-K3S6GTL7.js} +1 -1
- package/dist/bundle/chunks/{chunk-HEL6TAWE.js → chunk-POLDAUYA.js} +37 -36
- package/dist/bundle/chunks/{chunk-V3ACW7VD.js → chunk-WJSKB3FC.js} +1 -1
- package/dist/bundle/chunks/{chunk-32YDZI4J.js → chunk-ZS5JEI76.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-6BMXWA44.js → google-generative-ai-UMZ53EFA.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-6H7NLRTK.js → google-vertex-ZNR5NS6H.js} +1 -1
- package/dist/bundle/chunks/image-resize-worker.js +1 -1
- package/dist/bundle/chunks/{mistral-conversations-IXUHSFSJ.js → mistral-conversations-IT66WUNN.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-YHUAEVCM.js → openai-codex-responses-FOVCVG6I.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-ULQCFC72.js → openai-completions-FLQ7CBOL.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-NLLAJDPS.js → openai-responses-ABGYHS2T.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-EJTI2EJ3.js → pi-messages-6K5W6LUN.js} +1 -1
- package/dist/bundle/chunks/{virtual-modules-TQWEGNNZ.js → virtual-modules-2QGILFV7.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/client.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/client/remote-session.d.ts.map +1 -1
- package/dist/client/remote-session.js +65 -6
- package/dist/client/remote-session.js.map +1 -1
- package/dist/core/agent-session-store.d.ts.map +1 -1
- package/dist/core/agent-session-store.js +20 -3
- package/dist/core/agent-session-store.js.map +1 -1
- package/dist/core/extensions/agent-composition.d.ts.map +1 -1
- package/dist/core/extensions/agent-composition.js +20 -1
- package/dist/core/extensions/agent-composition.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.js +1 -0
- package/dist/core/i18n/locales/zh-cn/core-errors.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js +10 -3
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js.map +1 -1
- package/dist/core/keybindings.d.ts +35 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +34 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +62 -15
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +11 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +2 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/capability-mount.d.ts.map +1 -1
- package/dist/core/tools/capability-mount.js +4 -21
- package/dist/core/tools/capability-mount.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -2
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +2 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +25 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/changelog-pager.d.ts +53 -0
- package/dist/modes/interactive/components/changelog-pager.d.ts.map +1 -0
- package/dist/modes/interactive/components/changelog-pager.js +239 -0
- package/dist/modes/interactive/components/changelog-pager.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.d.ts +41 -11
- package/dist/modes/interactive/components/tool-run-group.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.js +126 -14
- package/dist/modes/interactive/components/tool-run-group.js.map +1 -1
- package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-commands.js +23 -13
- package/dist/modes/interactive/interactive-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-events.d.ts +14 -4
- package/dist/modes/interactive/interactive-events.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-events.js +94 -12
- package/dist/modes/interactive/interactive-events.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +6 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +21 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/plugins/ecosystem/node-local-package-source.d.ts +2 -1
- package/dist/plugins/ecosystem/node-local-package-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-local-package-source.js +110 -4
- package/dist/plugins/ecosystem/node-local-package-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.js +106 -39
- package/dist/plugins/ecosystem/node-package-install-host.js.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js +59 -2
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js.map +1 -1
- package/dist/suites/builtin-suites.d.ts.map +1 -1
- package/dist/suites/builtin-suites.js +2 -2
- package/dist/suites/builtin-suites.js.map +1 -1
- package/dist/utils/changelog.d.ts +27 -0
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +110 -0
- package/dist/utils/changelog.js.map +1 -1
- package/docs/keybindings.md +14 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/extensions/with-deps/plugin.json +1 -1
- package/package.json +1 -1
- package/dist/bundle/chunks/chunk-A3JYRWB6.js +0 -2
|
@@ -11,6 +11,14 @@ import { keyText } from "./keybinding-hints.js";
|
|
|
11
11
|
import { truncateToVisualLines } from "./visual-truncate.js";
|
|
12
12
|
// Preview line limit when not expanded (matches tool execution behavior)
|
|
13
13
|
const PREVIEW_LINES = 20;
|
|
14
|
+
/**
|
|
15
|
+
* Component-side retention budget for streaming output. The bash executor
|
|
16
|
+
* already keeps only a rolling tail buffer (bash-executor.ts) and streams
|
|
17
|
+
* every chunk here; without a cap the UI-side accumulation is unbounded for
|
|
18
|
+
* long-running, chatty commands and outlives the command. Same budget and
|
|
19
|
+
* char-length accounting as the executor's rolling buffer.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_RETAINED_OUTPUT_CHARS = DEFAULT_MAX_BYTES;
|
|
14
22
|
export class BashExecutionComponent extends Container {
|
|
15
23
|
command;
|
|
16
24
|
outputLines = [];
|
|
@@ -68,8 +76,25 @@ export class BashExecutionComponent extends Container {
|
|
|
68
76
|
else {
|
|
69
77
|
this.outputLines.push(...newLines);
|
|
70
78
|
}
|
|
79
|
+
this.enforceRetention();
|
|
71
80
|
this.updateDisplay();
|
|
72
81
|
}
|
|
82
|
+
/** Keep only the tail within {@link MAX_RETAINED_OUTPUT_CHARS}, mirroring the executor's rolling buffer. */
|
|
83
|
+
enforceRetention() {
|
|
84
|
+
let total = 0;
|
|
85
|
+
for (const line of this.outputLines)
|
|
86
|
+
total += line.length + 1;
|
|
87
|
+
if (total <= MAX_RETAINED_OUTPUT_CHARS)
|
|
88
|
+
return;
|
|
89
|
+
let drop = 0;
|
|
90
|
+
while (drop < this.outputLines.length - 1 &&
|
|
91
|
+
total - (this.outputLines[drop].length + 1) > MAX_RETAINED_OUTPUT_CHARS) {
|
|
92
|
+
total -= this.outputLines[drop].length + 1;
|
|
93
|
+
drop++;
|
|
94
|
+
}
|
|
95
|
+
if (drop > 0)
|
|
96
|
+
this.outputLines.splice(0, drop);
|
|
97
|
+
}
|
|
73
98
|
setComplete(exitCode, cancelled, truncationResult, fullOutputPath) {
|
|
74
99
|
this.exitCode = exitCode;
|
|
75
100
|
this.status = cancelled
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash-execution.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/bash-execution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Component for displaying bash command execution with streaming output.\n */\n\nimport { Container, Loader, Spacer, Text, type TUI } from \"@agent-forge/tui\";\nimport { t } from \"../../../core/i18n/index.ts\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tDEFAULT_MAX_LINES,\n\ttype TruncationResult,\n\ttruncateTail,\n} from \"../../../core/tools/truncate.ts\";\nimport { theme } from \"../../../theme/theme.ts\";\nimport { stripAnsi } from \"../../../utils/ansi.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyText } from \"./keybinding-hints.ts\";\nimport { truncateToVisualLines } from \"./visual-truncate.ts\";\n\n// Preview line limit when not expanded (matches tool execution behavior)\nconst PREVIEW_LINES = 20;\n\nexport class BashExecutionComponent extends Container {\n\tprivate command: string;\n\tprivate outputLines: string[] = [];\n\tprivate status: \"running\" | \"complete\" | \"cancelled\" | \"error\" = \"running\";\n\tprivate exitCode: number | undefined = undefined;\n\tprivate loader: Loader;\n\tprivate truncationResult?: TruncationResult;\n\tprivate fullOutputPath?: string;\n\tprivate expanded = false;\n\tprivate contentContainer: Container;\n\n\tconstructor(command: string, ui: TUI, excludeFromContext = false) {\n\t\tsuper();\n\t\tthis.command = command;\n\n\t\t// Use dim border for excluded-from-context commands (!! prefix)\n\t\tconst colorKey = excludeFromContext ? \"dim\" : \"bashMode\";\n\t\tconst borderColor = (str: string) => theme.fg(colorKey, str);\n\n\t\t// Add spacer\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Top border\n\t\tthis.addChild(new DynamicBorder(borderColor));\n\n\t\t// Content container (holds dynamic content between borders)\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\t// Command header\n\t\tconst header = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0);\n\t\tthis.contentContainer.addChild(header);\n\n\t\t// Loader\n\t\tthis.loader = new Loader(\n\t\t\tui,\n\t\t\t(spinner) => theme.fg(colorKey, spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\tt(\"Running... ({key} to cancel)\", { key: keyText(\"tui.select.cancel\") }), // Plain text for loader\n\t\t);\n\t\tthis.contentContainer.addChild(this.loader);\n\n\t\t// Bottom border\n\t\tthis.addChild(new DynamicBorder(borderColor));\n\t}\n\n\t/**\n\t * Set whether the output is expanded (shows full output) or collapsed (preview only).\n\t */\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tappendOutput(chunk: string): void {\n\t\t// Strip ANSI codes and normalize line endings\n\t\t// Note: binary data is already sanitized in tui-renderer.ts executeBashCommand\n\t\tconst clean = stripAnsi(chunk).replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n\n\t\t// Append to output lines\n\t\tconst newLines = clean.split(\"\\n\");\n\t\tif (this.outputLines.length > 0 && newLines.length > 0) {\n\t\t\t// Append first chunk to last line (incomplete line continuation)\n\t\t\tthis.outputLines[this.outputLines.length - 1] += newLines[0];\n\t\t\tthis.outputLines.push(...newLines.slice(1));\n\t\t} else {\n\t\t\tthis.outputLines.push(...newLines);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tsetComplete(\n\t\texitCode: number | undefined,\n\t\tcancelled: boolean,\n\t\ttruncationResult?: TruncationResult,\n\t\tfullOutputPath?: string,\n\t): void {\n\t\tthis.exitCode = exitCode;\n\t\tthis.status = cancelled\n\t\t\t? \"cancelled\"\n\t\t\t: exitCode !== 0 && exitCode !== undefined && exitCode !== null\n\t\t\t\t? \"error\"\n\t\t\t\t: \"complete\";\n\t\tthis.truncationResult = truncationResult;\n\t\tthis.fullOutputPath = fullOutputPath;\n\n\t\t// Stop loader\n\t\tthis.loader.stop();\n\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\t// Apply truncation for LLM context limits (same limits as bash tool)\n\t\tconst fullOutput = this.outputLines.join(\"\\n\");\n\t\tconst contextTruncation = truncateTail(fullOutput, {\n\t\t\tmaxLines: DEFAULT_MAX_LINES,\n\t\t\tmaxBytes: DEFAULT_MAX_BYTES,\n\t\t});\n\n\t\t// Get the lines to potentially display (after context truncation)\n\t\tconst availableLines = contextTruncation.content ? contextTruncation.content.split(\"\\n\") : [];\n\n\t\t// Apply preview truncation based on expanded state\n\t\tconst previewLogicalLines = availableLines.slice(-PREVIEW_LINES);\n\t\tconst hiddenLineCount = availableLines.length - previewLogicalLines.length;\n\n\t\t// Rebuild content container\n\t\tthis.contentContainer.clear();\n\n\t\t// Command header\n\t\tconst header = new Text(theme.fg(\"bashMode\", theme.bold(`$ ${this.command}`)), 1, 0);\n\t\tthis.contentContainer.addChild(header);\n\n\t\t// Output\n\t\tif (availableLines.length > 0) {\n\t\t\tif (this.expanded) {\n\t\t\t\t// Show all lines\n\t\t\t\tconst displayText = availableLines.map((line) => theme.fg(\"muted\", line)).join(\"\\n\");\n\t\t\t\tthis.contentContainer.addChild(new Text(`\\n${displayText}`, 1, 0));\n\t\t\t} else {\n\t\t\t\t// Use shared visual truncation utility with width-aware caching\n\t\t\t\tconst styledOutput = previewLogicalLines.map((line) => theme.fg(\"muted\", line)).join(\"\\n\");\n\t\t\t\tconst styledInput = `\\n${styledOutput}`;\n\t\t\t\tlet cachedWidth: number | undefined;\n\t\t\t\tlet cachedLines: string[] | undefined;\n\t\t\t\tthis.contentContainer.addChild({\n\t\t\t\t\trender: (width: number) => {\n\t\t\t\t\t\tif (cachedLines === undefined || cachedWidth !== width) {\n\t\t\t\t\t\t\tconst result = truncateToVisualLines(styledInput, PREVIEW_LINES, width, 1);\n\t\t\t\t\t\t\tcachedLines = result.visualLines;\n\t\t\t\t\t\t\tcachedWidth = width;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// v8 ignore next -- cached lines are always computed before this render callback returns\n\t\t\t\t\t\treturn cachedLines ?? [];\n\t\t\t\t\t},\n\t\t\t\t\tinvalidate: () => {\n\t\t\t\t\t\tcachedWidth = undefined;\n\t\t\t\t\t\tcachedLines = undefined;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Loader or status\n\t\tif (this.status === \"running\") {\n\t\t\tthis.contentContainer.addChild(this.loader);\n\t\t} else {\n\t\t\tconst statusParts: string[] = [];\n\n\t\t\t// Show how many lines are hidden (collapsed preview)\n\t\t\tif (hiddenLineCount > 0) {\n\t\t\t\tif (this.expanded) {\n\t\t\t\t\t// i18n: 原三段样式拼接合并为整句单 key,键位片段的 dim 样式损失按批次④特殊裁决 1 接受\n\t\t\t\t\tstatusParts.push(theme.fg(\"muted\", t(\"({key} to collapse)\", { key: keyText(\"app.tools.expand\") })));\n\t\t\t\t} else {\n\t\t\t\t\tstatusParts.push(\n\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\"muted\",\n\t\t\t\t\t\t\tt(\"... {count} more lines ({key} to expand)\", {\n\t\t\t\t\t\t\t\tcount: hiddenLineCount,\n\t\t\t\t\t\t\t\tkey: keyText(\"app.tools.expand\"),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.status === \"cancelled\") {\n\t\t\t\tstatusParts.push(theme.fg(\"warning\", t(\"(cancelled)\")));\n\t\t\t} else if (this.status === \"error\") {\n\t\t\t\tstatusParts.push(theme.fg(\"error\", t(\"(exit {code})\", { code: this.exitCode ?? \"unknown\" })));\n\t\t\t}\n\n\t\t\t// Add truncation warning (context truncation, not preview truncation)\n\t\t\tconst wasTruncated = this.truncationResult?.truncated || contextTruncation.truncated;\n\t\t\tif (wasTruncated && this.fullOutputPath) {\n\t\t\t\tstatusParts.push(\n\t\t\t\t\ttheme.fg(\"warning\", t(\"Output truncated. Full output: {path}\", { path: this.fullOutputPath })),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (statusParts.length > 0) {\n\t\t\t\tthis.contentContainer.addChild(new Text(`\\n${statusParts.join(\"\\n\")}`, 1, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the raw output for creating BashExecutionMessage.\n\t */\n\tgetOutput(): string {\n\t\treturn this.outputLines.join(\"\\n\");\n\t}\n\n\t/**\n\t * Get the command that was executed.\n\t */\n\tgetCommand(): string {\n\t\treturn this.command;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bash-execution.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/bash-execution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Component for displaying bash command execution with streaming output.\n */\n\nimport { Container, Loader, Spacer, Text, type TUI } from \"@agent-forge/tui\";\nimport { t } from \"../../../core/i18n/index.ts\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tDEFAULT_MAX_LINES,\n\ttype TruncationResult,\n\ttruncateTail,\n} from \"../../../core/tools/truncate.ts\";\nimport { theme } from \"../../../theme/theme.ts\";\nimport { stripAnsi } from \"../../../utils/ansi.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyText } from \"./keybinding-hints.ts\";\nimport { truncateToVisualLines } from \"./visual-truncate.ts\";\n\n// Preview line limit when not expanded (matches tool execution behavior)\nconst PREVIEW_LINES = 20;\n\n/**\n * Component-side retention budget for streaming output. The bash executor\n * already keeps only a rolling tail buffer (bash-executor.ts) and streams\n * every chunk here; without a cap the UI-side accumulation is unbounded for\n * long-running, chatty commands and outlives the command. Same budget and\n * char-length accounting as the executor's rolling buffer.\n */\nconst MAX_RETAINED_OUTPUT_CHARS = DEFAULT_MAX_BYTES;\n\nexport class BashExecutionComponent extends Container {\n\tprivate command: string;\n\tprivate outputLines: string[] = [];\n\tprivate status: \"running\" | \"complete\" | \"cancelled\" | \"error\" = \"running\";\n\tprivate exitCode: number | undefined = undefined;\n\tprivate loader: Loader;\n\tprivate truncationResult?: TruncationResult;\n\tprivate fullOutputPath?: string;\n\tprivate expanded = false;\n\tprivate contentContainer: Container;\n\n\tconstructor(command: string, ui: TUI, excludeFromContext = false) {\n\t\tsuper();\n\t\tthis.command = command;\n\n\t\t// Use dim border for excluded-from-context commands (!! prefix)\n\t\tconst colorKey = excludeFromContext ? \"dim\" : \"bashMode\";\n\t\tconst borderColor = (str: string) => theme.fg(colorKey, str);\n\n\t\t// Add spacer\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Top border\n\t\tthis.addChild(new DynamicBorder(borderColor));\n\n\t\t// Content container (holds dynamic content between borders)\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\t// Command header\n\t\tconst header = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0);\n\t\tthis.contentContainer.addChild(header);\n\n\t\t// Loader\n\t\tthis.loader = new Loader(\n\t\t\tui,\n\t\t\t(spinner) => theme.fg(colorKey, spinner),\n\t\t\t(text) => theme.fg(\"muted\", text),\n\t\t\tt(\"Running... ({key} to cancel)\", { key: keyText(\"tui.select.cancel\") }), // Plain text for loader\n\t\t);\n\t\tthis.contentContainer.addChild(this.loader);\n\n\t\t// Bottom border\n\t\tthis.addChild(new DynamicBorder(borderColor));\n\t}\n\n\t/**\n\t * Set whether the output is expanded (shows full output) or collapsed (preview only).\n\t */\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tappendOutput(chunk: string): void {\n\t\t// Strip ANSI codes and normalize line endings\n\t\t// Note: binary data is already sanitized in tui-renderer.ts executeBashCommand\n\t\tconst clean = stripAnsi(chunk).replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n\n\t\t// Append to output lines\n\t\tconst newLines = clean.split(\"\\n\");\n\t\tif (this.outputLines.length > 0 && newLines.length > 0) {\n\t\t\t// Append first chunk to last line (incomplete line continuation)\n\t\t\tthis.outputLines[this.outputLines.length - 1] += newLines[0];\n\t\t\tthis.outputLines.push(...newLines.slice(1));\n\t\t} else {\n\t\t\tthis.outputLines.push(...newLines);\n\t\t}\n\n\t\tthis.enforceRetention();\n\t\tthis.updateDisplay();\n\t}\n\n\t/** Keep only the tail within {@link MAX_RETAINED_OUTPUT_CHARS}, mirroring the executor's rolling buffer. */\n\tprivate enforceRetention(): void {\n\t\tlet total = 0;\n\t\tfor (const line of this.outputLines) total += line.length + 1;\n\t\tif (total <= MAX_RETAINED_OUTPUT_CHARS) return;\n\t\tlet drop = 0;\n\t\twhile (\n\t\t\tdrop < this.outputLines.length - 1 &&\n\t\t\ttotal - (this.outputLines[drop].length + 1) > MAX_RETAINED_OUTPUT_CHARS\n\t\t) {\n\t\t\ttotal -= this.outputLines[drop].length + 1;\n\t\t\tdrop++;\n\t\t}\n\t\tif (drop > 0) this.outputLines.splice(0, drop);\n\t}\n\n\tsetComplete(\n\t\texitCode: number | undefined,\n\t\tcancelled: boolean,\n\t\ttruncationResult?: TruncationResult,\n\t\tfullOutputPath?: string,\n\t): void {\n\t\tthis.exitCode = exitCode;\n\t\tthis.status = cancelled\n\t\t\t? \"cancelled\"\n\t\t\t: exitCode !== 0 && exitCode !== undefined && exitCode !== null\n\t\t\t\t? \"error\"\n\t\t\t\t: \"complete\";\n\t\tthis.truncationResult = truncationResult;\n\t\tthis.fullOutputPath = fullOutputPath;\n\n\t\t// Stop loader\n\t\tthis.loader.stop();\n\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\t// Apply truncation for LLM context limits (same limits as bash tool)\n\t\tconst fullOutput = this.outputLines.join(\"\\n\");\n\t\tconst contextTruncation = truncateTail(fullOutput, {\n\t\t\tmaxLines: DEFAULT_MAX_LINES,\n\t\t\tmaxBytes: DEFAULT_MAX_BYTES,\n\t\t});\n\n\t\t// Get the lines to potentially display (after context truncation)\n\t\tconst availableLines = contextTruncation.content ? contextTruncation.content.split(\"\\n\") : [];\n\n\t\t// Apply preview truncation based on expanded state\n\t\tconst previewLogicalLines = availableLines.slice(-PREVIEW_LINES);\n\t\tconst hiddenLineCount = availableLines.length - previewLogicalLines.length;\n\n\t\t// Rebuild content container\n\t\tthis.contentContainer.clear();\n\n\t\t// Command header\n\t\tconst header = new Text(theme.fg(\"bashMode\", theme.bold(`$ ${this.command}`)), 1, 0);\n\t\tthis.contentContainer.addChild(header);\n\n\t\t// Output\n\t\tif (availableLines.length > 0) {\n\t\t\tif (this.expanded) {\n\t\t\t\t// Show all lines\n\t\t\t\tconst displayText = availableLines.map((line) => theme.fg(\"muted\", line)).join(\"\\n\");\n\t\t\t\tthis.contentContainer.addChild(new Text(`\\n${displayText}`, 1, 0));\n\t\t\t} else {\n\t\t\t\t// Use shared visual truncation utility with width-aware caching\n\t\t\t\tconst styledOutput = previewLogicalLines.map((line) => theme.fg(\"muted\", line)).join(\"\\n\");\n\t\t\t\tconst styledInput = `\\n${styledOutput}`;\n\t\t\t\tlet cachedWidth: number | undefined;\n\t\t\t\tlet cachedLines: string[] | undefined;\n\t\t\t\tthis.contentContainer.addChild({\n\t\t\t\t\trender: (width: number) => {\n\t\t\t\t\t\tif (cachedLines === undefined || cachedWidth !== width) {\n\t\t\t\t\t\t\tconst result = truncateToVisualLines(styledInput, PREVIEW_LINES, width, 1);\n\t\t\t\t\t\t\tcachedLines = result.visualLines;\n\t\t\t\t\t\t\tcachedWidth = width;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// v8 ignore next -- cached lines are always computed before this render callback returns\n\t\t\t\t\t\treturn cachedLines ?? [];\n\t\t\t\t\t},\n\t\t\t\t\tinvalidate: () => {\n\t\t\t\t\t\tcachedWidth = undefined;\n\t\t\t\t\t\tcachedLines = undefined;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Loader or status\n\t\tif (this.status === \"running\") {\n\t\t\tthis.contentContainer.addChild(this.loader);\n\t\t} else {\n\t\t\tconst statusParts: string[] = [];\n\n\t\t\t// Show how many lines are hidden (collapsed preview)\n\t\t\tif (hiddenLineCount > 0) {\n\t\t\t\tif (this.expanded) {\n\t\t\t\t\t// i18n: 原三段样式拼接合并为整句单 key,键位片段的 dim 样式损失按批次④特殊裁决 1 接受\n\t\t\t\t\tstatusParts.push(theme.fg(\"muted\", t(\"({key} to collapse)\", { key: keyText(\"app.tools.expand\") })));\n\t\t\t\t} else {\n\t\t\t\t\tstatusParts.push(\n\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\"muted\",\n\t\t\t\t\t\t\tt(\"... {count} more lines ({key} to expand)\", {\n\t\t\t\t\t\t\t\tcount: hiddenLineCount,\n\t\t\t\t\t\t\t\tkey: keyText(\"app.tools.expand\"),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.status === \"cancelled\") {\n\t\t\t\tstatusParts.push(theme.fg(\"warning\", t(\"(cancelled)\")));\n\t\t\t} else if (this.status === \"error\") {\n\t\t\t\tstatusParts.push(theme.fg(\"error\", t(\"(exit {code})\", { code: this.exitCode ?? \"unknown\" })));\n\t\t\t}\n\n\t\t\t// Add truncation warning (context truncation, not preview truncation)\n\t\t\tconst wasTruncated = this.truncationResult?.truncated || contextTruncation.truncated;\n\t\t\tif (wasTruncated && this.fullOutputPath) {\n\t\t\t\tstatusParts.push(\n\t\t\t\t\ttheme.fg(\"warning\", t(\"Output truncated. Full output: {path}\", { path: this.fullOutputPath })),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (statusParts.length > 0) {\n\t\t\t\tthis.contentContainer.addChild(new Text(`\\n${statusParts.join(\"\\n\")}`, 1, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the raw output for creating BashExecutionMessage.\n\t */\n\tgetOutput(): string {\n\t\treturn this.outputLines.join(\"\\n\");\n\t}\n\n\t/**\n\t * Get the command that was executed.\n\t */\n\tgetCommand(): string {\n\t\treturn this.command;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type MarkdownTheme } from "@agent-forge/tui";
|
|
2
|
+
import type { ChangelogPagerBlockV1 } from "../../../utils/changelog.ts";
|
|
3
|
+
export interface ChangelogPagerOptions {
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly blocks: readonly ChangelogPagerBlockV1[];
|
|
6
|
+
/** Fixed visible row count for the scrolling window (title/status excluded). */
|
|
7
|
+
readonly viewportRows: number;
|
|
8
|
+
readonly markdownTheme: MarkdownTheme;
|
|
9
|
+
readonly onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Paged preview for the changelog (设计 docs/design/版本新特性面板与
|
|
13
|
+
* Changelog分页.md): paging, scrolling, search — nothing else, on universal
|
|
14
|
+
* keys (arrows, PgUp/PgDn, Home/End, ctrl+f), not vi-style (2026-10-08 user
|
|
15
|
+
* ruling). Blocks render lazily — only what the visible window (or a search
|
|
16
|
+
* jump) needs — and closing leaves nothing in the chat scrollback.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ChangelogPager {
|
|
19
|
+
focused: boolean;
|
|
20
|
+
private readonly options;
|
|
21
|
+
private scrollTop;
|
|
22
|
+
private cachedWidth;
|
|
23
|
+
private readonly blockComponents;
|
|
24
|
+
/** Flattened rendered stream: block lines with one blank separator between blocks. */
|
|
25
|
+
private readonly lines;
|
|
26
|
+
private blockStartLines;
|
|
27
|
+
private renderedBlocks;
|
|
28
|
+
private queryMode;
|
|
29
|
+
private queryBuffer;
|
|
30
|
+
private activeQuery;
|
|
31
|
+
private matchBlocks;
|
|
32
|
+
private matchIndex;
|
|
33
|
+
constructor(options: ChangelogPagerOptions);
|
|
34
|
+
/** Drop the rendered-line cache (Component contract); blocks re-render on demand. */
|
|
35
|
+
invalidate(): void;
|
|
36
|
+
/** Render blocks until the window at scrollTop is filled or blocks run out. */
|
|
37
|
+
private fillWindow;
|
|
38
|
+
/** Render blocks sequentially up to and including index. */
|
|
39
|
+
private ensureRenderedUpTo;
|
|
40
|
+
private renderBlockUpTo;
|
|
41
|
+
private maxScrollTop;
|
|
42
|
+
private scrollBy;
|
|
43
|
+
private scrollToBlock;
|
|
44
|
+
private computeMatches;
|
|
45
|
+
/** Jump to the current match; wraps around the match list. */
|
|
46
|
+
private jumpToMatch;
|
|
47
|
+
private acceptQuery;
|
|
48
|
+
handleInput(data: string): void;
|
|
49
|
+
render(width: number): string[];
|
|
50
|
+
private statusLine;
|
|
51
|
+
private blockIndexAt;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=changelog-pager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changelog-pager.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/changelog-pager.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGhF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAGzE,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAClD,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC7B;AAeD;;;;;;GAMG;AACH,qBAAa,cAAc;IAC1B,OAAO,UAAS;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,sFAAsF;IACtF,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAgB;IACvC,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,UAAU,CAAK;IAEvB,YAAY,OAAO,EAAE,qBAAqB,EAGzC;IAED,qFAAqF;IACrF,UAAU,IAAI,IAAI,CAKjB;IAED,+EAA+E;IAC/E,OAAO,CAAC,UAAU;IAOlB,4DAA4D;IAC5D,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,cAAc;IAStB,8DAA8D;IAC9D,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,WAAW;IAenB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAyE9B;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAY9B;IAED,OAAO,CAAC,UAAU;IAqBlB,OAAO,CAAC,YAAY;CAQpB","sourcesContent":["import { getKeybindings, Markdown, type MarkdownTheme } from \"@agent-forge/tui\";\nimport { t } from \"../../../core/i18n/index.ts\";\nimport { theme } from \"../../../theme/theme.ts\";\nimport type { ChangelogPagerBlockV1 } from \"../../../utils/changelog.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\nexport interface ChangelogPagerOptions {\n\treadonly title: string;\n\treadonly blocks: readonly ChangelogPagerBlockV1[];\n\t/** Fixed visible row count for the scrolling window (title/status excluded). */\n\treadonly viewportRows: number;\n\treadonly markdownTheme: MarkdownTheme;\n\treadonly onClose: () => void;\n}\n\n/** Lines moved per mouse-wheel notch; the focused overlay receives raw SGR wheel events. */\nconst WHEEL_LINES_PER_NOTCH = 3;\n\n/** SGR wheel event (`\\x1b[<button;x;yM`), direction 0 = up / 1 = down. */\nfunction parseWheelDirection(data: string): -1 | 1 | undefined {\n\tconst sgr = /^\\x1b\\[<(\\d+);\\d+;\\d+M$/.exec(data);\n\tif (sgr === null) return undefined;\n\tconst button = Number.parseInt(sgr[1], 10);\n\tif ((button & 64) === 0) return undefined;\n\tif (button & 3) return 1;\n\treturn -1;\n}\n\n/**\n * Paged preview for the changelog (设计 docs/design/版本新特性面板与\n * Changelog分页.md): paging, scrolling, search — nothing else, on universal\n * keys (arrows, PgUp/PgDn, Home/End, ctrl+f), not vi-style (2026-10-08 user\n * ruling). Blocks render lazily — only what the visible window (or a search\n * jump) needs — and closing leaves nothing in the chat scrollback.\n */\nexport class ChangelogPager {\n\tfocused = false;\n\tprivate readonly options: ChangelogPagerOptions;\n\tprivate scrollTop = 0;\n\tprivate cachedWidth: number | undefined;\n\tprivate readonly blockComponents: Markdown[] = [];\n\t/** Flattened rendered stream: block lines with one blank separator between blocks. */\n\tprivate readonly lines: string[] = [];\n\tprivate blockStartLines: number[] = [];\n\tprivate renderedBlocks = 0;\n\tprivate queryMode = false;\n\tprivate queryBuffer = \"\";\n\tprivate activeQuery: string | undefined;\n\tprivate matchBlocks: readonly number[] = [];\n\tprivate matchIndex = 0;\n\n\tconstructor(options: ChangelogPagerOptions) {\n\t\tthis.options = options;\n\t\tif (options.viewportRows < 1) throw new Error(t(\"Changelog pager needs at least one visible row\"));\n\t}\n\n\t/** Drop the rendered-line cache (Component contract); blocks re-render on demand. */\n\tinvalidate(): void {\n\t\tthis.blockComponents.length = 0;\n\t\tthis.lines.length = 0;\n\t\tthis.blockStartLines = [];\n\t\tthis.renderedBlocks = 0;\n\t}\n\n\t/** Render blocks until the window at scrollTop is filled or blocks run out. */\n\tprivate fillWindow(width: number): void {\n\t\tconst target = this.scrollTop + this.options.viewportRows;\n\t\twhile (this.renderedBlocks < this.options.blocks.length && this.lines.length <= target) {\n\t\t\tthis.renderBlockUpTo(this.renderedBlocks, width);\n\t\t}\n\t}\n\n\t/** Render blocks sequentially up to and including index. */\n\tprivate ensureRenderedUpTo(index: number, width: number): void {\n\t\twhile (this.renderedBlocks <= Math.min(index, this.options.blocks.length - 1)) {\n\t\t\tthis.renderBlockUpTo(this.renderedBlocks, width);\n\t\t}\n\t}\n\n\tprivate renderBlockUpTo(index: number, width: number): void {\n\t\tconst block = this.options.blocks[index];\n\t\tlet component = this.blockComponents[index];\n\t\tif (component === undefined) {\n\t\t\tcomponent = new Markdown(block.markdown, 1, 0, this.options.markdownTheme);\n\t\t\tthis.blockComponents[index] = component;\n\t\t}\n\t\tthis.blockStartLines[index] = this.lines.length;\n\t\tif (this.lines.length > 0) this.lines.push(\"\");\n\t\tthis.lines.push(...component.render(width));\n\t\tthis.renderedBlocks = index + 1;\n\t}\n\n\tprivate maxScrollTop(): number {\n\t\tconst exhausted = this.renderedBlocks >= this.options.blocks.length;\n\t\treturn exhausted ? Math.max(0, this.lines.length - this.options.viewportRows) : this.lines.length;\n\t}\n\n\tprivate scrollBy(deltaLines: number): void {\n\t\tthis.scrollTop = Math.max(0, Math.min(this.maxScrollTop(), this.scrollTop + deltaLines));\n\t}\n\n\tprivate scrollToBlock(index: number, width: number | undefined): void {\n\t\tif (width !== undefined) this.ensureRenderedUpTo(index, width);\n\t\tthis.scrollTop = this.blockStartLines[index] ?? 0;\n\t}\n\n\tprivate computeMatches(query: string): readonly number[] {\n\t\tconst needle = query.toLowerCase();\n\t\tconst matches: number[] = [];\n\t\tfor (let i = 0; i < this.options.blocks.length; i++) {\n\t\t\tif (this.options.blocks[i].markdown.toLowerCase().includes(needle)) matches.push(i);\n\t\t}\n\t\treturn matches;\n\t}\n\n\t/** Jump to the current match; wraps around the match list. */\n\tprivate jumpToMatch(width: number | undefined): void {\n\t\tif (this.matchBlocks.length === 0 || width === undefined) return;\n\t\tthis.matchIndex =\n\t\t\t((this.matchIndex % this.matchBlocks.length) + this.matchBlocks.length) % this.matchBlocks.length;\n\t\tthis.scrollToBlock(this.matchBlocks[this.matchIndex], width);\n\t}\n\n\tprivate acceptQuery(width: number | undefined): void {\n\t\tconst query = this.queryBuffer.trim();\n\t\tthis.queryMode = false;\n\t\tthis.queryBuffer = \"\";\n\t\tif (query === \"\") {\n\t\t\tthis.activeQuery = undefined;\n\t\t\tthis.matchBlocks = [];\n\t\t\treturn;\n\t\t}\n\t\tthis.activeQuery = query;\n\t\tthis.matchBlocks = this.computeMatches(query);\n\t\tthis.matchIndex = 0;\n\t\tthis.jumpToMatch(width);\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (this.queryMode) {\n\t\t\tif (data === \"\\r\" || data === \"\\n\") {\n\t\t\t\tthis.acceptQuery(this.cachedWidth);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (data === \"\\x1b\") {\n\t\t\t\tthis.queryMode = false;\n\t\t\t\tthis.queryBuffer = \"\";\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (data === \"\\x7f\" || data === \"\\b\") {\n\t\t\t\tthis.queryBuffer = this.queryBuffer.slice(0, -1);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (data.length === 1 && data >= \" \") {\n\t\t\t\tthis.queryBuffer += data;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst wheel = parseWheelDirection(data);\n\t\tif (wheel !== undefined) {\n\t\t\tthis.scrollBy(wheel * WHEEL_LINES_PER_NOTCH);\n\t\t\treturn;\n\t\t}\n\t\t// With an active search, Enter (altScreen.searchNext) cycles matches;\n\t\t// the pager closes via q/escape instead.\n\t\tif (this.activeQuery !== undefined) {\n\t\t\tif (kb.matches(data, \"tui.altScreen.searchNext\")) {\n\t\t\t\tthis.matchIndex++;\n\t\t\t\tthis.jumpToMatch(this.cachedWidth);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(data, \"tui.altScreen.searchPrevious\")) {\n\t\t\t\tthis.matchIndex--;\n\t\t\t\tthis.jumpToMatch(this.cachedWidth);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (kb.matches(data, \"tui.altScreen.search\")) {\n\t\t\tthis.queryMode = true;\n\t\t\tthis.queryBuffer = \"\";\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.close\")) {\n\t\t\tthis.options.onClose();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.up\")) {\n\t\t\tthis.scrollBy(-1);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.down\")) {\n\t\t\tthis.scrollBy(1);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.pageUp\")) {\n\t\t\tthis.scrollBy(-this.options.viewportRows);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.pageDown\")) {\n\t\t\tthis.scrollBy(this.options.viewportRows);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.top\")) {\n\t\t\tthis.scrollTop = 0;\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.bottom\")) {\n\t\t\tthis.scrollTop = Number.MAX_SAFE_INTEGER;\n\t\t\treturn;\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedWidth !== width) {\n\t\t\tthis.invalidate();\n\t\t\tthis.cachedWidth = width;\n\t\t}\n\t\tthis.fillWindow(width);\n\t\tthis.scrollTop = Math.max(0, Math.min(this.maxScrollTop(), this.scrollTop));\n\t\tconst window = this.lines.slice(this.scrollTop, this.scrollTop + this.options.viewportRows);\n\t\twhile (window.length < this.options.viewportRows) window.push(\"\");\n\n\t\tconst statusLine = this.queryMode ? theme.fg(\"accent\", `/${this.queryBuffer}_`) : this.statusLine();\n\t\treturn [theme.bold(theme.fg(\"accent\", this.options.title)), \"\", ...window, \"\", statusLine];\n\t}\n\n\tprivate statusLine(): string {\n\t\tconst topBlock = this.blockIndexAt(this.scrollTop);\n\t\tconst position = t(\"block {index}/{total}\", {\n\t\t\tindex: String(topBlock + 1),\n\t\t\ttotal: String(this.options.blocks.length),\n\t\t});\n\t\tconst parts = [theme.fg(\"muted\", position)];\n\t\tif (this.activeQuery !== undefined) {\n\t\t\tconst matchInfo =\n\t\t\t\tthis.matchBlocks.length === 0\n\t\t\t\t\t? t(\"no matches\")\n\t\t\t\t\t: t(\"{index}/{total} matches\", {\n\t\t\t\t\t\t\tindex: String(this.matchIndex + 1),\n\t\t\t\t\t\t\ttotal: String(this.matchBlocks.length),\n\t\t\t\t\t\t});\n\t\t\tparts.push(theme.fg(\"accent\", `/${this.activeQuery} · ${matchInfo}`));\n\t\t}\n\t\tconst scrollHint = `${keyHint(\"tui.altScreen.search\", t(\"search\"))} · ${keyHint(\"app.changelogPager.close\", t(\"close\"))}`;\n\t\treturn `${parts.join(theme.fg(\"dim\", \" · \"))} ${scrollHint}`.trimEnd();\n\t}\n\n\tprivate blockIndexAt(lineIndex: number): number {\n\t\tlet index = 0;\n\t\tfor (let i = 0; i < this.renderedBlocks; i++) {\n\t\t\tif (this.blockStartLines[i] <= lineIndex) index = i;\n\t\t\telse break;\n\t\t}\n\t\treturn index;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { getKeybindings, Markdown } from "@agent-forge/tui";
|
|
2
|
+
import { t } from "../../../core/i18n/index.js";
|
|
3
|
+
import { theme } from "../../../theme/theme.js";
|
|
4
|
+
import { keyHint } from "./keybinding-hints.js";
|
|
5
|
+
/** Lines moved per mouse-wheel notch; the focused overlay receives raw SGR wheel events. */
|
|
6
|
+
const WHEEL_LINES_PER_NOTCH = 3;
|
|
7
|
+
/** SGR wheel event (`\x1b[<button;x;yM`), direction 0 = up / 1 = down. */
|
|
8
|
+
function parseWheelDirection(data) {
|
|
9
|
+
const sgr = /^\x1b\[<(\d+);\d+;\d+M$/.exec(data);
|
|
10
|
+
if (sgr === null)
|
|
11
|
+
return undefined;
|
|
12
|
+
const button = Number.parseInt(sgr[1], 10);
|
|
13
|
+
if ((button & 64) === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (button & 3)
|
|
16
|
+
return 1;
|
|
17
|
+
return -1;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Paged preview for the changelog (设计 docs/design/版本新特性面板与
|
|
21
|
+
* Changelog分页.md): paging, scrolling, search — nothing else, on universal
|
|
22
|
+
* keys (arrows, PgUp/PgDn, Home/End, ctrl+f), not vi-style (2026-10-08 user
|
|
23
|
+
* ruling). Blocks render lazily — only what the visible window (or a search
|
|
24
|
+
* jump) needs — and closing leaves nothing in the chat scrollback.
|
|
25
|
+
*/
|
|
26
|
+
export class ChangelogPager {
|
|
27
|
+
focused = false;
|
|
28
|
+
options;
|
|
29
|
+
scrollTop = 0;
|
|
30
|
+
cachedWidth;
|
|
31
|
+
blockComponents = [];
|
|
32
|
+
/** Flattened rendered stream: block lines with one blank separator between blocks. */
|
|
33
|
+
lines = [];
|
|
34
|
+
blockStartLines = [];
|
|
35
|
+
renderedBlocks = 0;
|
|
36
|
+
queryMode = false;
|
|
37
|
+
queryBuffer = "";
|
|
38
|
+
activeQuery;
|
|
39
|
+
matchBlocks = [];
|
|
40
|
+
matchIndex = 0;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
if (options.viewportRows < 1)
|
|
44
|
+
throw new Error(t("Changelog pager needs at least one visible row"));
|
|
45
|
+
}
|
|
46
|
+
/** Drop the rendered-line cache (Component contract); blocks re-render on demand. */
|
|
47
|
+
invalidate() {
|
|
48
|
+
this.blockComponents.length = 0;
|
|
49
|
+
this.lines.length = 0;
|
|
50
|
+
this.blockStartLines = [];
|
|
51
|
+
this.renderedBlocks = 0;
|
|
52
|
+
}
|
|
53
|
+
/** Render blocks until the window at scrollTop is filled or blocks run out. */
|
|
54
|
+
fillWindow(width) {
|
|
55
|
+
const target = this.scrollTop + this.options.viewportRows;
|
|
56
|
+
while (this.renderedBlocks < this.options.blocks.length && this.lines.length <= target) {
|
|
57
|
+
this.renderBlockUpTo(this.renderedBlocks, width);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Render blocks sequentially up to and including index. */
|
|
61
|
+
ensureRenderedUpTo(index, width) {
|
|
62
|
+
while (this.renderedBlocks <= Math.min(index, this.options.blocks.length - 1)) {
|
|
63
|
+
this.renderBlockUpTo(this.renderedBlocks, width);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
renderBlockUpTo(index, width) {
|
|
67
|
+
const block = this.options.blocks[index];
|
|
68
|
+
let component = this.blockComponents[index];
|
|
69
|
+
if (component === undefined) {
|
|
70
|
+
component = new Markdown(block.markdown, 1, 0, this.options.markdownTheme);
|
|
71
|
+
this.blockComponents[index] = component;
|
|
72
|
+
}
|
|
73
|
+
this.blockStartLines[index] = this.lines.length;
|
|
74
|
+
if (this.lines.length > 0)
|
|
75
|
+
this.lines.push("");
|
|
76
|
+
this.lines.push(...component.render(width));
|
|
77
|
+
this.renderedBlocks = index + 1;
|
|
78
|
+
}
|
|
79
|
+
maxScrollTop() {
|
|
80
|
+
const exhausted = this.renderedBlocks >= this.options.blocks.length;
|
|
81
|
+
return exhausted ? Math.max(0, this.lines.length - this.options.viewportRows) : this.lines.length;
|
|
82
|
+
}
|
|
83
|
+
scrollBy(deltaLines) {
|
|
84
|
+
this.scrollTop = Math.max(0, Math.min(this.maxScrollTop(), this.scrollTop + deltaLines));
|
|
85
|
+
}
|
|
86
|
+
scrollToBlock(index, width) {
|
|
87
|
+
if (width !== undefined)
|
|
88
|
+
this.ensureRenderedUpTo(index, width);
|
|
89
|
+
this.scrollTop = this.blockStartLines[index] ?? 0;
|
|
90
|
+
}
|
|
91
|
+
computeMatches(query) {
|
|
92
|
+
const needle = query.toLowerCase();
|
|
93
|
+
const matches = [];
|
|
94
|
+
for (let i = 0; i < this.options.blocks.length; i++) {
|
|
95
|
+
if (this.options.blocks[i].markdown.toLowerCase().includes(needle))
|
|
96
|
+
matches.push(i);
|
|
97
|
+
}
|
|
98
|
+
return matches;
|
|
99
|
+
}
|
|
100
|
+
/** Jump to the current match; wraps around the match list. */
|
|
101
|
+
jumpToMatch(width) {
|
|
102
|
+
if (this.matchBlocks.length === 0 || width === undefined)
|
|
103
|
+
return;
|
|
104
|
+
this.matchIndex =
|
|
105
|
+
((this.matchIndex % this.matchBlocks.length) + this.matchBlocks.length) % this.matchBlocks.length;
|
|
106
|
+
this.scrollToBlock(this.matchBlocks[this.matchIndex], width);
|
|
107
|
+
}
|
|
108
|
+
acceptQuery(width) {
|
|
109
|
+
const query = this.queryBuffer.trim();
|
|
110
|
+
this.queryMode = false;
|
|
111
|
+
this.queryBuffer = "";
|
|
112
|
+
if (query === "") {
|
|
113
|
+
this.activeQuery = undefined;
|
|
114
|
+
this.matchBlocks = [];
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.activeQuery = query;
|
|
118
|
+
this.matchBlocks = this.computeMatches(query);
|
|
119
|
+
this.matchIndex = 0;
|
|
120
|
+
this.jumpToMatch(width);
|
|
121
|
+
}
|
|
122
|
+
handleInput(data) {
|
|
123
|
+
const kb = getKeybindings();
|
|
124
|
+
if (this.queryMode) {
|
|
125
|
+
if (data === "\r" || data === "\n") {
|
|
126
|
+
this.acceptQuery(this.cachedWidth);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (data === "\x1b") {
|
|
130
|
+
this.queryMode = false;
|
|
131
|
+
this.queryBuffer = "";
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (data === "\x7f" || data === "\b") {
|
|
135
|
+
this.queryBuffer = this.queryBuffer.slice(0, -1);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (data.length === 1 && data >= " ") {
|
|
139
|
+
this.queryBuffer += data;
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const wheel = parseWheelDirection(data);
|
|
144
|
+
if (wheel !== undefined) {
|
|
145
|
+
this.scrollBy(wheel * WHEEL_LINES_PER_NOTCH);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// With an active search, Enter (altScreen.searchNext) cycles matches;
|
|
149
|
+
// the pager closes via q/escape instead.
|
|
150
|
+
if (this.activeQuery !== undefined) {
|
|
151
|
+
if (kb.matches(data, "tui.altScreen.searchNext")) {
|
|
152
|
+
this.matchIndex++;
|
|
153
|
+
this.jumpToMatch(this.cachedWidth);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (kb.matches(data, "tui.altScreen.searchPrevious")) {
|
|
157
|
+
this.matchIndex--;
|
|
158
|
+
this.jumpToMatch(this.cachedWidth);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (kb.matches(data, "tui.altScreen.search")) {
|
|
163
|
+
this.queryMode = true;
|
|
164
|
+
this.queryBuffer = "";
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (kb.matches(data, "app.changelogPager.close")) {
|
|
168
|
+
this.options.onClose();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (kb.matches(data, "app.changelogPager.up")) {
|
|
172
|
+
this.scrollBy(-1);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (kb.matches(data, "app.changelogPager.down")) {
|
|
176
|
+
this.scrollBy(1);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (kb.matches(data, "app.changelogPager.pageUp")) {
|
|
180
|
+
this.scrollBy(-this.options.viewportRows);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (kb.matches(data, "app.changelogPager.pageDown")) {
|
|
184
|
+
this.scrollBy(this.options.viewportRows);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (kb.matches(data, "app.changelogPager.top")) {
|
|
188
|
+
this.scrollTop = 0;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (kb.matches(data, "app.changelogPager.bottom")) {
|
|
192
|
+
this.scrollTop = Number.MAX_SAFE_INTEGER;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
render(width) {
|
|
197
|
+
if (this.cachedWidth !== width) {
|
|
198
|
+
this.invalidate();
|
|
199
|
+
this.cachedWidth = width;
|
|
200
|
+
}
|
|
201
|
+
this.fillWindow(width);
|
|
202
|
+
this.scrollTop = Math.max(0, Math.min(this.maxScrollTop(), this.scrollTop));
|
|
203
|
+
const window = this.lines.slice(this.scrollTop, this.scrollTop + this.options.viewportRows);
|
|
204
|
+
while (window.length < this.options.viewportRows)
|
|
205
|
+
window.push("");
|
|
206
|
+
const statusLine = this.queryMode ? theme.fg("accent", `/${this.queryBuffer}_`) : this.statusLine();
|
|
207
|
+
return [theme.bold(theme.fg("accent", this.options.title)), "", ...window, "", statusLine];
|
|
208
|
+
}
|
|
209
|
+
statusLine() {
|
|
210
|
+
const topBlock = this.blockIndexAt(this.scrollTop);
|
|
211
|
+
const position = t("block {index}/{total}", {
|
|
212
|
+
index: String(topBlock + 1),
|
|
213
|
+
total: String(this.options.blocks.length),
|
|
214
|
+
});
|
|
215
|
+
const parts = [theme.fg("muted", position)];
|
|
216
|
+
if (this.activeQuery !== undefined) {
|
|
217
|
+
const matchInfo = this.matchBlocks.length === 0
|
|
218
|
+
? t("no matches")
|
|
219
|
+
: t("{index}/{total} matches", {
|
|
220
|
+
index: String(this.matchIndex + 1),
|
|
221
|
+
total: String(this.matchBlocks.length),
|
|
222
|
+
});
|
|
223
|
+
parts.push(theme.fg("accent", `/${this.activeQuery} · ${matchInfo}`));
|
|
224
|
+
}
|
|
225
|
+
const scrollHint = `${keyHint("tui.altScreen.search", t("search"))} · ${keyHint("app.changelogPager.close", t("close"))}`;
|
|
226
|
+
return `${parts.join(theme.fg("dim", " · "))} ${scrollHint}`.trimEnd();
|
|
227
|
+
}
|
|
228
|
+
blockIndexAt(lineIndex) {
|
|
229
|
+
let index = 0;
|
|
230
|
+
for (let i = 0; i < this.renderedBlocks; i++) {
|
|
231
|
+
if (this.blockStartLines[i] <= lineIndex)
|
|
232
|
+
index = i;
|
|
233
|
+
else
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
return index;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=changelog-pager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changelog-pager.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/changelog-pager.ts"],"names":[],"mappings":"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","sourcesContent":["import { getKeybindings, Markdown, type MarkdownTheme } from \"@agent-forge/tui\";\nimport { t } from \"../../../core/i18n/index.ts\";\nimport { theme } from \"../../../theme/theme.ts\";\nimport type { ChangelogPagerBlockV1 } from \"../../../utils/changelog.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\nexport interface ChangelogPagerOptions {\n\treadonly title: string;\n\treadonly blocks: readonly ChangelogPagerBlockV1[];\n\t/** Fixed visible row count for the scrolling window (title/status excluded). */\n\treadonly viewportRows: number;\n\treadonly markdownTheme: MarkdownTheme;\n\treadonly onClose: () => void;\n}\n\n/** Lines moved per mouse-wheel notch; the focused overlay receives raw SGR wheel events. */\nconst WHEEL_LINES_PER_NOTCH = 3;\n\n/** SGR wheel event (`\\x1b[<button;x;yM`), direction 0 = up / 1 = down. */\nfunction parseWheelDirection(data: string): -1 | 1 | undefined {\n\tconst sgr = /^\\x1b\\[<(\\d+);\\d+;\\d+M$/.exec(data);\n\tif (sgr === null) return undefined;\n\tconst button = Number.parseInt(sgr[1], 10);\n\tif ((button & 64) === 0) return undefined;\n\tif (button & 3) return 1;\n\treturn -1;\n}\n\n/**\n * Paged preview for the changelog (设计 docs/design/版本新特性面板与\n * Changelog分页.md): paging, scrolling, search — nothing else, on universal\n * keys (arrows, PgUp/PgDn, Home/End, ctrl+f), not vi-style (2026-10-08 user\n * ruling). Blocks render lazily — only what the visible window (or a search\n * jump) needs — and closing leaves nothing in the chat scrollback.\n */\nexport class ChangelogPager {\n\tfocused = false;\n\tprivate readonly options: ChangelogPagerOptions;\n\tprivate scrollTop = 0;\n\tprivate cachedWidth: number | undefined;\n\tprivate readonly blockComponents: Markdown[] = [];\n\t/** Flattened rendered stream: block lines with one blank separator between blocks. */\n\tprivate readonly lines: string[] = [];\n\tprivate blockStartLines: number[] = [];\n\tprivate renderedBlocks = 0;\n\tprivate queryMode = false;\n\tprivate queryBuffer = \"\";\n\tprivate activeQuery: string | undefined;\n\tprivate matchBlocks: readonly number[] = [];\n\tprivate matchIndex = 0;\n\n\tconstructor(options: ChangelogPagerOptions) {\n\t\tthis.options = options;\n\t\tif (options.viewportRows < 1) throw new Error(t(\"Changelog pager needs at least one visible row\"));\n\t}\n\n\t/** Drop the rendered-line cache (Component contract); blocks re-render on demand. */\n\tinvalidate(): void {\n\t\tthis.blockComponents.length = 0;\n\t\tthis.lines.length = 0;\n\t\tthis.blockStartLines = [];\n\t\tthis.renderedBlocks = 0;\n\t}\n\n\t/** Render blocks until the window at scrollTop is filled or blocks run out. */\n\tprivate fillWindow(width: number): void {\n\t\tconst target = this.scrollTop + this.options.viewportRows;\n\t\twhile (this.renderedBlocks < this.options.blocks.length && this.lines.length <= target) {\n\t\t\tthis.renderBlockUpTo(this.renderedBlocks, width);\n\t\t}\n\t}\n\n\t/** Render blocks sequentially up to and including index. */\n\tprivate ensureRenderedUpTo(index: number, width: number): void {\n\t\twhile (this.renderedBlocks <= Math.min(index, this.options.blocks.length - 1)) {\n\t\t\tthis.renderBlockUpTo(this.renderedBlocks, width);\n\t\t}\n\t}\n\n\tprivate renderBlockUpTo(index: number, width: number): void {\n\t\tconst block = this.options.blocks[index];\n\t\tlet component = this.blockComponents[index];\n\t\tif (component === undefined) {\n\t\t\tcomponent = new Markdown(block.markdown, 1, 0, this.options.markdownTheme);\n\t\t\tthis.blockComponents[index] = component;\n\t\t}\n\t\tthis.blockStartLines[index] = this.lines.length;\n\t\tif (this.lines.length > 0) this.lines.push(\"\");\n\t\tthis.lines.push(...component.render(width));\n\t\tthis.renderedBlocks = index + 1;\n\t}\n\n\tprivate maxScrollTop(): number {\n\t\tconst exhausted = this.renderedBlocks >= this.options.blocks.length;\n\t\treturn exhausted ? Math.max(0, this.lines.length - this.options.viewportRows) : this.lines.length;\n\t}\n\n\tprivate scrollBy(deltaLines: number): void {\n\t\tthis.scrollTop = Math.max(0, Math.min(this.maxScrollTop(), this.scrollTop + deltaLines));\n\t}\n\n\tprivate scrollToBlock(index: number, width: number | undefined): void {\n\t\tif (width !== undefined) this.ensureRenderedUpTo(index, width);\n\t\tthis.scrollTop = this.blockStartLines[index] ?? 0;\n\t}\n\n\tprivate computeMatches(query: string): readonly number[] {\n\t\tconst needle = query.toLowerCase();\n\t\tconst matches: number[] = [];\n\t\tfor (let i = 0; i < this.options.blocks.length; i++) {\n\t\t\tif (this.options.blocks[i].markdown.toLowerCase().includes(needle)) matches.push(i);\n\t\t}\n\t\treturn matches;\n\t}\n\n\t/** Jump to the current match; wraps around the match list. */\n\tprivate jumpToMatch(width: number | undefined): void {\n\t\tif (this.matchBlocks.length === 0 || width === undefined) return;\n\t\tthis.matchIndex =\n\t\t\t((this.matchIndex % this.matchBlocks.length) + this.matchBlocks.length) % this.matchBlocks.length;\n\t\tthis.scrollToBlock(this.matchBlocks[this.matchIndex], width);\n\t}\n\n\tprivate acceptQuery(width: number | undefined): void {\n\t\tconst query = this.queryBuffer.trim();\n\t\tthis.queryMode = false;\n\t\tthis.queryBuffer = \"\";\n\t\tif (query === \"\") {\n\t\t\tthis.activeQuery = undefined;\n\t\t\tthis.matchBlocks = [];\n\t\t\treturn;\n\t\t}\n\t\tthis.activeQuery = query;\n\t\tthis.matchBlocks = this.computeMatches(query);\n\t\tthis.matchIndex = 0;\n\t\tthis.jumpToMatch(width);\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (this.queryMode) {\n\t\t\tif (data === \"\\r\" || data === \"\\n\") {\n\t\t\t\tthis.acceptQuery(this.cachedWidth);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (data === \"\\x1b\") {\n\t\t\t\tthis.queryMode = false;\n\t\t\t\tthis.queryBuffer = \"\";\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (data === \"\\x7f\" || data === \"\\b\") {\n\t\t\t\tthis.queryBuffer = this.queryBuffer.slice(0, -1);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (data.length === 1 && data >= \" \") {\n\t\t\t\tthis.queryBuffer += data;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst wheel = parseWheelDirection(data);\n\t\tif (wheel !== undefined) {\n\t\t\tthis.scrollBy(wheel * WHEEL_LINES_PER_NOTCH);\n\t\t\treturn;\n\t\t}\n\t\t// With an active search, Enter (altScreen.searchNext) cycles matches;\n\t\t// the pager closes via q/escape instead.\n\t\tif (this.activeQuery !== undefined) {\n\t\t\tif (kb.matches(data, \"tui.altScreen.searchNext\")) {\n\t\t\t\tthis.matchIndex++;\n\t\t\t\tthis.jumpToMatch(this.cachedWidth);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(data, \"tui.altScreen.searchPrevious\")) {\n\t\t\t\tthis.matchIndex--;\n\t\t\t\tthis.jumpToMatch(this.cachedWidth);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (kb.matches(data, \"tui.altScreen.search\")) {\n\t\t\tthis.queryMode = true;\n\t\t\tthis.queryBuffer = \"\";\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.close\")) {\n\t\t\tthis.options.onClose();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.up\")) {\n\t\t\tthis.scrollBy(-1);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.down\")) {\n\t\t\tthis.scrollBy(1);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.pageUp\")) {\n\t\t\tthis.scrollBy(-this.options.viewportRows);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.pageDown\")) {\n\t\t\tthis.scrollBy(this.options.viewportRows);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.top\")) {\n\t\t\tthis.scrollTop = 0;\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"app.changelogPager.bottom\")) {\n\t\t\tthis.scrollTop = Number.MAX_SAFE_INTEGER;\n\t\t\treturn;\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedWidth !== width) {\n\t\t\tthis.invalidate();\n\t\t\tthis.cachedWidth = width;\n\t\t}\n\t\tthis.fillWindow(width);\n\t\tthis.scrollTop = Math.max(0, Math.min(this.maxScrollTop(), this.scrollTop));\n\t\tconst window = this.lines.slice(this.scrollTop, this.scrollTop + this.options.viewportRows);\n\t\twhile (window.length < this.options.viewportRows) window.push(\"\");\n\n\t\tconst statusLine = this.queryMode ? theme.fg(\"accent\", `/${this.queryBuffer}_`) : this.statusLine();\n\t\treturn [theme.bold(theme.fg(\"accent\", this.options.title)), \"\", ...window, \"\", statusLine];\n\t}\n\n\tprivate statusLine(): string {\n\t\tconst topBlock = this.blockIndexAt(this.scrollTop);\n\t\tconst position = t(\"block {index}/{total}\", {\n\t\t\tindex: String(topBlock + 1),\n\t\t\ttotal: String(this.options.blocks.length),\n\t\t});\n\t\tconst parts = [theme.fg(\"muted\", position)];\n\t\tif (this.activeQuery !== undefined) {\n\t\t\tconst matchInfo =\n\t\t\t\tthis.matchBlocks.length === 0\n\t\t\t\t\t? t(\"no matches\")\n\t\t\t\t\t: t(\"{index}/{total} matches\", {\n\t\t\t\t\t\t\tindex: String(this.matchIndex + 1),\n\t\t\t\t\t\t\ttotal: String(this.matchBlocks.length),\n\t\t\t\t\t\t});\n\t\t\tparts.push(theme.fg(\"accent\", `/${this.activeQuery} · ${matchInfo}`));\n\t\t}\n\t\tconst scrollHint = `${keyHint(\"tui.altScreen.search\", t(\"search\"))} · ${keyHint(\"app.changelogPager.close\", t(\"close\"))}`;\n\t\treturn `${parts.join(theme.fg(\"dim\", \" · \"))} ${scrollHint}`.trimEnd();\n\t}\n\n\tprivate blockIndexAt(lineIndex: number): number {\n\t\tlet index = 0;\n\t\tfor (let i = 0; i < this.renderedBlocks; i++) {\n\t\t\tif (this.blockStartLines[i] <= lineIndex) index = i;\n\t\t\telse break;\n\t\t}\n\t\treturn index;\n\t}\n}\n"]}
|
|
@@ -54,6 +54,12 @@ export declare class ToolExecutionComponent extends Container {
|
|
|
54
54
|
* renderable images.
|
|
55
55
|
*/
|
|
56
56
|
renderQuietImages(width: number): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Quiet-transcript live tail (collapsed running group, D-083 运行态): the
|
|
59
|
+
* component's own render (pending background included) cut to the last
|
|
60
|
+
* `maxLines` visual lines. Hidden components render to [] and stay hidden.
|
|
61
|
+
*/
|
|
62
|
+
renderQuietLiveOutput(width: number, maxLines: number): string[];
|
|
57
63
|
updateArgs(args: unknown): void;
|
|
58
64
|
markExecutionStarted(): void;
|
|
59
65
|
setArgsComplete(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent, TextContent } from \"@agent-forge/ai\";\nimport {\n\tBox,\n\ttype Component,\n\tContainer,\n\tgetCapabilities,\n\tImage,\n\tSpacer,\n\tText,\n\ttype TUI,\n\ttruncateToWidth,\n} from \"@agent-forge/tui\";\nimport { t } from \"../../../core/i18n/index.ts\";\nimport { formatToolCallWithArgs, getTextOutput as getRenderedTextOutput } from \"../../../core/tools/render-utils.ts\";\nimport type { ToolDefinition, ToolRenderContext } from \"../../../core/tools/tool-definition.ts\";\nimport { theme } from \"../../../theme/theme.ts\";\nimport { convertToPng } from \"../../../utils/image-convert.ts\";\nimport { keyText } from \"./keybinding-hints.ts\";\n\n/** Visual (wrapped) result lines shown before fallback output is expanded. */\nconst FALLBACK_PREVIEW_LINES = 10;\n\n/**\n * 单条工具结果展示预算 (M2, 终端输出模式设计 §3.2「展开仍复用统一的输出截断」):\n * 任何渲染器 (含第三方插件) 的结果输出在视觉行上超过预算时, 保留 head+tail、\n * 中间以一行截断说明替代 (截断原因 + 总量 + 完整输出引用, 引用存在时)。这是\n * compact 预览之上的兜底——折叠态有自己的小预览, detail 态 (ctrl+o) 的全文\n * 渲染由此封顶。错误结果豁免 (错误可见性优先, 与 collapsedPreviewText 同惯例)。\n */\nconst TOOL_OUTPUT_MAX_LINES = 400;\n\n/** 预算超出时保留完整输出的末尾视觉行数 (错误摘要与结论常聚在输出末尾)。 */\nconst TOOL_OUTPUT_TAIL_LINES = 100;\n\n/**\n * Pretty-printed JSON up to this length is collapsed to its compact single-line\n * form; longer compact forms are cut here so a short tool-result ticket shows\n * as one preview line instead of sprawling across the chat. Expansion always\n * shows the original text.\n */\nconst COLLAPSED_JSON_PREVIEW_CHARS = 220;\n\n/**\n * Collapsed preview text for fallback results: pretty-printed JSON (the default\n * serialization for capability-tool results) becomes one compact line, and any\n * other text previews verbatim. Unparseable JSON-like text stays verbatim too.\n * Error results never compact — error visibility beats compactness, so they\n * keep the (threshold-limited) full form with the error background.\n */\nfunction collapsedPreviewText(output: string, isError: boolean): string {\n\tif (isError) {\n\t\treturn output;\n\t}\n\tconst trimmed = output.trimStart();\n\tif (!trimmed.startsWith(\"{\") && !trimmed.startsWith(\"[\")) {\n\t\treturn output;\n\t}\n\ttry {\n\t\tconst compact = JSON.stringify(JSON.parse(output));\n\t\treturn compact.length > COLLAPSED_JSON_PREVIEW_CHARS\n\t\t\t? `${compact.slice(0, COLLAPSED_JSON_PREVIEW_CHARS - 1)}…`\n\t\t\t: compact;\n\t} catch {\n\t\treturn output;\n\t}\n}\n\n/**\n * Collapsed fallback output limited to a number of visual (wrapped) lines. Limiting logical\n * lines instead lets a single long line (such as minified JSON) wrap across the whole screen.\n * Caches its lines per width, since it renders on every frame for every result in the transcript.\n */\nclass VisualLinePreview implements Component {\n\tprivate text: string;\n\tprivate maxVisualLines: number;\n\tprivate formatHint: (hidden: number) => string;\n\tprivate cachedWidth: number | undefined;\n\tprivate cachedLines: string[] | undefined;\n\n\tconstructor(text: string, maxVisualLines: number, formatHint: (hidden: number) => string) {\n\t\tthis.text = text;\n\t\tthis.maxVisualLines = maxVisualLines;\n\t\tthis.formatHint = formatHint;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines === undefined || this.cachedWidth !== width) {\n\t\t\tconst allVisualLines = new Text(this.text, 0, 0).render(width);\n\t\t\tconst lines =\n\t\t\t\tallVisualLines.length > this.maxVisualLines\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t...allVisualLines.slice(0, this.maxVisualLines),\n\t\t\t\t\t\t\ttruncateToWidth(this.formatHint(allVisualLines.length - this.maxVisualLines), width, \"...\"),\n\t\t\t\t\t\t]\n\t\t\t\t\t: allVisualLines;\n\t\t\tthis.cachedLines = lines;\n\t\t\tthis.cachedWidth = width;\n\t\t\treturn lines;\n\t\t}\n\t\treturn this.cachedLines;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n}\n\n/**\n * bash 系工具在 details 携带的全量输出文件引用 (截断说明行的\"完整输出\"引用;\n * 终端输出模式设计 §3.2 的引用要求在通用层的最大努力形态)。\n */\nfunction resultFullOutputRef(details: unknown): string | undefined {\n\tif (details === null || typeof details !== \"object\" || Array.isArray(details)) return undefined;\n\tconst ref = (details as { fullOutputPath?: unknown }).fullOutputPath;\n\treturn typeof ref === \"string\" && ref.trim() !== \"\" ? ref : undefined;\n}\n\n/**\n * 结果渲染器输出的展示预算兜底 (M2): 包装 renderResult/fallback 产出的子组件,\n * render(width) 时对视觉行超预算的输出做 head+截断说明+tail。渲染器契约不变\n * (renderResult 仍收 expanded 原值, lastComponent 仍传裸子组件); 缓存按宽度,\n * invalidate 透传子组件 (流式增量渲染依赖它)。\n */\nclass BoundedResultView implements Component {\n\tprivate child: Component;\n\tprivate maxLines: number;\n\tprivate tailLines: number;\n\tprivate isError: boolean;\n\tprivate fullOutputRef: string | undefined;\n\tprivate cachedWidth: number | undefined;\n\tprivate cachedLines: string[] | undefined;\n\n\tconstructor(\n\t\tchild: Component,\n\t\tmaxLines: number,\n\t\ttailLines: number,\n\t\tisError: boolean,\n\t\tfullOutputRef: string | undefined,\n\t) {\n\t\tthis.child = child;\n\t\tthis.maxLines = maxLines;\n\t\tthis.tailLines = tailLines;\n\t\tthis.isError = isError;\n\t\tthis.fullOutputRef = fullOutputRef;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines === undefined || this.cachedWidth !== width) {\n\t\t\tconst lines = this.child.render(width);\n\t\t\tif (this.isError || lines.length <= this.maxLines) {\n\t\t\t\tthis.cachedLines = lines;\n\t\t\t} else {\n\t\t\t\tconst headLines = this.maxLines - this.tailLines - 1;\n\t\t\t\tconst hidden = lines.length - headLines - this.tailLines;\n\t\t\t\tconst hint =\n\t\t\t\t\tthis.fullOutputRef === undefined\n\t\t\t\t\t\t? t(\"... ({count} lines truncated)\", { count: hidden })\n\t\t\t\t\t\t: t(\"... ({count} lines truncated; full output: {path})\", {\n\t\t\t\t\t\t\t\tcount: hidden,\n\t\t\t\t\t\t\t\tpath: this.fullOutputRef,\n\t\t\t\t\t\t\t});\n\t\t\t\tthis.cachedLines = [\n\t\t\t\t\t...lines.slice(0, headLines),\n\t\t\t\t\ttruncateToWidth(theme.fg(\"muted\", hint), width, \"...\"),\n\t\t\t\t\t...lines.slice(lines.length - this.tailLines),\n\t\t\t\t];\n\t\t\t}\n\t\t\tthis.cachedWidth = width;\n\t\t\treturn this.cachedLines;\n\t\t}\n\t\treturn this.cachedLines;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.child.invalidate();\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n}\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean;\n\timageWidthCells?: number;\n}\n\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox: Box;\n\tprivate contentText: Text;\n\tprivate selfRenderContainer: Container;\n\tprivate callRendererComponent?: Component;\n\tprivate resultRendererComponent?: Component;\n\tprivate rendererState: Record<string, unknown> = {};\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate toolCallId: string;\n\tprivate args: unknown;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate executionStarted = false;\n\tprivate argsComplete = false;\n\tprivate result?: {\n\t\tcontent: (TextContent | ImageContent)[];\n\t\tisError: boolean;\n\t\tdetails?: unknown;\n\t};\n\tprivate convertedImages: Map<\n\t\tnumber,\n\t\t{ sourceData: string; sourceMimeType: string; data: string; mimeType: string }\n\t> = new Map();\n\tprivate hideComponent = false;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: unknown,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition | undefined,\n\t\tui: TUI,\n\t\tcwd: string,\n\t) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.toolCallId = toolCallId;\n\t\tthis.args = args;\n\t\t// The caller passes the session registry's definition\n\t\t// (interactive-mode getRegisteredToolDefinition → facade.getToolDefinition).\n\t\t// Since D-082 批 1 the default coding tool definitions live in the\n\t\t// coding-tools plugin, so the registry is the only definition source;\n\t\t// names missing from the registry render through the generic fallback.\n\t\tthis.toolDefinition = toolDefinition;\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Always create all shell variants. contentBox is used for default renderer-based composition.\n\t\t// selfRenderContainer is used when the tool renders its own framing.\n\t\t// contentText is reserved for generic fallback rendering when no tool definition exists.\n\t\tthis.contentBox = new Box(1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\tthis.contentText = new Text(\"\", 1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\tthis.selfRenderContainer = new Container();\n\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.addChild(this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox);\n\t\t} else {\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getCallRenderer(): ToolDefinition[\"renderCall\"] | undefined {\n\t\treturn this.toolDefinition?.renderCall;\n\t}\n\n\tprivate getResultRenderer(): ToolDefinition[\"renderResult\"] | undefined {\n\t\treturn this.toolDefinition?.renderResult;\n\t}\n\n\tprivate hasRendererDefinition(): boolean {\n\t\treturn this.toolDefinition !== undefined;\n\t}\n\n\tprivate getRenderShell(): \"default\" | \"self\" {\n\t\treturn this.toolDefinition?.renderShell ?? \"default\";\n\t}\n\n\tprivate getRenderContext(lastComponent: Component | undefined): ToolRenderContext {\n\t\treturn {\n\t\t\targs: this.args,\n\t\t\ttoolCallId: this.toolCallId,\n\t\t\tinvalidate: () => {\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\tlastComponent,\n\t\t\tstate: this.rendererState,\n\t\t\tcwd: this.cwd,\n\t\t\texecutionStarted: this.executionStarted,\n\t\t\targsComplete: this.argsComplete,\n\t\t\tisPartial: this.isPartial,\n\t\t\texpanded: this.expanded,\n\t\t\tshowImages: this.showImages,\n\t\t\tisError: this.result?.isError ?? false,\n\t\t};\n\t}\n\n\tprivate createCallFallback(): Component {\n\t\treturn new Text(formatToolCallWithArgs(this.toolName, this.args, theme, this.expanded), 0, 0);\n\t}\n\n\tprivate createResultFallback(): Component | undefined {\n\t\tconst output = this.getTextOutput();\n\t\tif (!output) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (this.expanded) {\n\t\t\treturn new Text(\n\t\t\t\toutput\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t);\n\t\t}\n\t\tconst previewText = collapsedPreviewText(output, this.result?.isError ?? false);\n\t\t// A compacted JSON preview is one line by construction, so it never\n\t\t// reaches the preview-line threshold — append the expand hint directly.\n\t\tif (previewText !== output) {\n\t\t\t// i18n: 原三段样式拼接合并为整句单 key,键位片段的 dim 样式损失按批次④特殊裁决 1 接受\n\t\t\tconst hint = theme.fg(\"muted\", t(\"({key} to expand)\", { key: keyText(\"app.tools.expand\") }));\n\t\t\treturn new Text(`${theme.fg(\"toolOutput\", previewText)} ${hint}`, 0, 0);\n\t\t}\n\t\tconst styled = previewText\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\t\treturn new VisualLinePreview(styled, FALLBACK_PREVIEW_LINES, (hidden) =>\n\t\t\ttheme.fg(\n\t\t\t\t\"muted\",\n\t\t\t\tt(\"... ({count} more lines, {key} to expand)\", {\n\t\t\t\t\tcount: hidden,\n\t\t\t\t\tkey: keyText(\"app.tools.expand\"),\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t}\n\n\t/** Failure fact for the enclosing ToolRunGroupComponent (D-083 quiet marker). */\n\tget isError(): boolean {\n\t\treturn this.result?.isError ?? false;\n\t}\n\n\t/** Tool name for the group marker's sole-tool form (restore path has no setActiveTool). */\n\tget name(): string {\n\t\treturn this.toolName;\n\t}\n\n\t/**\n\t * First non-empty line of a failed result's text output — the quiet group\n\t * marker's failure summary (D-083 增补:失败首行摘要). Successes and\n\t * result-less calls return undefined.\n\t */\n\tget errorSummary(): string | undefined {\n\t\tif (!this.result?.isError) return undefined;\n\t\treturn this.getTextOutput()\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trim())\n\t\t\t.find((line) => line !== \"\");\n\t}\n\n\t/**\n\t * Quiet-transcript image lines (D-083 增补:图片是用户可见的结论): the\n\t * collapsed group marker hides the text card but still renders result\n\t * images (desktop/browser screenshots). Empty when the result has no\n\t * renderable images.\n\t */\n\trenderQuietImages(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (let i = 0; i < this.imageComponents.length; i++) {\n\t\t\tconst spacer = this.imageSpacers[i];\n\t\t\t// v8 ignore next -- spacers and image components are always pushed in pairs\n\t\t\tif (spacer) {\n\t\t\t\tlines.push(...spacer.render(width));\n\t\t\t}\n\t\t\tconst imageComponent = this.imageComponents[i];\n\t\t\t// v8 ignore next -- spacers and image components are always pushed in pairs\n\t\t\tif (imageComponent) {\n\t\t\t\tlines.push(...imageComponent.render(width));\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n\n\tupdateArgs(args: unknown): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\tmarkExecutionStarted(): void {\n\t\tthis.executionStarted = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tsetArgsComplete(): void {\n\t\tthis.argsComplete = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: (TextContent | ImageContent)[];\n\t\t\tdetails?: unknown;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.updateDisplay();\n\t\tthis.maybeConvertImagesForKitty();\n\t}\n\n\tprivate maybeConvertImagesForKitty(): void {\n\t\tconst caps = getCapabilities();\n\t\tif (caps.images !== \"kitty\") return;\n\t\t// v8 ignore next -- sole caller updateResult always assigns this.result first\n\t\tif (!this.result) return;\n\n\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\tconst img = imageBlocks[i];\n\t\t\tif (!img.data || !img.mimeType) continue;\n\t\t\tconst sourceData = img.data;\n\t\t\tconst sourceMimeType = img.mimeType;\n\t\t\tif (sourceMimeType === \"image/png\") continue;\n\t\t\tconst cached = this.convertedImages.get(i);\n\t\t\tif (cached?.sourceData === sourceData && cached.sourceMimeType === sourceMimeType) continue;\n\n\t\t\tconst index = i;\n\t\t\tconvertToPng(sourceData, sourceMimeType).then((converted) => {\n\t\t\t\tconst currentImage = this.result?.content.filter((content) => content.type === \"image\")[index];\n\t\t\t\tif (!converted || currentImage?.data !== sourceData || currentImage.mimeType !== sourceMimeType) return;\n\t\t\t\tthis.convertedImages.set(index, {\n\t\t\t\t\tsourceData,\n\t\t\t\t\tsourceMimeType,\n\t\t\t\t\t...converted,\n\t\t\t\t});\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tthis.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hideComponent) {\n\t\t\treturn [];\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && this.getRenderShell() === \"self\") {\n\t\t\tconst contentLines = this.selfRenderContainer.render(width);\n\t\t\tif (contentLines.length === 0 && this.imageComponents.length === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\tconst lines: string[] = [];\n\t\t\tif (contentLines.length > 0) {\n\t\t\t\tlines.push(\"\");\n\t\t\t\tlines.push(...contentLines);\n\t\t\t}\n\t\t\tfor (let i = 0; i < this.imageComponents.length; i++) {\n\t\t\t\tconst spacer = this.imageSpacers[i];\n\t\t\t\t// v8 ignore next -- spacers and image components are always pushed in pairs\n\t\t\t\tif (spacer) {\n\t\t\t\t\tlines.push(...spacer.render(width));\n\t\t\t\t}\n\t\t\t\tconst imageComponent = this.imageComponents[i];\n\t\t\t\t// v8 ignore next -- spacers and image components are always pushed in pairs\n\t\t\t\tif (imageComponent) {\n\t\t\t\t\tlines.push(...imageComponent.render(width));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\treturn super.render(width);\n\t}\n\n\t/** M2 展示预算: 结果子组件统一过有界视图 (错误豁免), 见 BoundedResultView。 */\n\tprivate boundedResult(component: Component): Component {\n\t\treturn new BoundedResultView(\n\t\t\tcomponent,\n\t\t\tTOOL_OUTPUT_MAX_LINES,\n\t\t\tTOOL_OUTPUT_TAIL_LINES,\n\t\t\tthis.result?.isError ?? false,\n\t\t\tresultFullOutputRef(this.result?.details),\n\t\t);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tconst bgFn = this.isPartial\n\t\t\t? (text: string) => theme.bg(\"toolPendingBg\", text)\n\t\t\t: this.result?.isError\n\t\t\t\t? (text: string) => theme.bg(\"toolErrorBg\", text)\n\t\t\t\t: (text: string) => theme.bg(\"toolSuccessBg\", text);\n\n\t\tlet hasContent = false;\n\t\tthis.hideComponent = false;\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tconst renderContainer = this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox;\n\t\t\tif (renderContainer instanceof Box) {\n\t\t\t\trenderContainer.setBgFn(bgFn);\n\t\t\t}\n\t\t\trenderContainer.clear();\n\n\t\t\tconst callRenderer = this.getCallRenderer();\n\t\t\tif (!callRenderer) {\n\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\thasContent = true;\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\tconst component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));\n\t\t\t\t\t// 渲染器可能返回 undefined(选择不渲染);此时交给无内容隐藏\n\t\t\t\t\t// 逻辑,不能把 undefined 塞进 Box.children(invalidate 会崩溃)。\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\tthis.callRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tthis.callRendererComponent = undefined;\n\t\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\t\thasContent = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst resultRenderer = this.getResultRenderer();\n\t\t\t\tif (!resultRenderer) {\n\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\trenderContainer.addChild(this.boundedResult(component));\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst component = resultRenderer(\n\t\t\t\t\t\t\t{ content: this.result.content, details: this.result.details },\n\t\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\t\ttheme,\n\t\t\t\t\t\t\tthis.getRenderContext(this.resultRendererComponent),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\tthis.resultRendererComponent = component;\n\t\t\t\t\t\t\trenderContainer.addChild(this.boundedResult(component));\n\t\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthis.resultRendererComponent = undefined;\n\t\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.contentText.setCustomBgFn(bgFn);\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t\thasContent = true;\n\t\t}\n\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\t\tfor (const spacer of this.imageSpacers) {\n\t\t\tthis.removeChild(spacer);\n\t\t}\n\t\tthis.imageSpacers = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\t\tconst caps = getCapabilities();\n\t\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\t\tconst img = imageBlocks[i];\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\tconst cached = this.convertedImages.get(i);\n\t\t\t\t\tconst converted =\n\t\t\t\t\t\tcached?.sourceData === img.data && cached.sourceMimeType === img.mimeType ? cached : undefined;\n\t\t\t\t\tconst imageData = converted?.data ?? img.data;\n\t\t\t\t\tconst imageMimeType = converted?.mimeType ?? img.mimeType;\n\t\t\t\t\tif (caps.images === \"kitty\" && imageMimeType !== \"image/png\") continue;\n\n\t\t\t\t\tconst spacer = new Spacer(1);\n\t\t\t\t\tthis.addChild(spacer);\n\t\t\t\t\tthis.imageSpacers.push(spacer);\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timageData,\n\t\t\t\t\t\timageMimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: this.imageWidthCells },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && !hasContent && this.imageComponents.length === 0) {\n\t\t\tthis.hideComponent = true;\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\treturn getRenderedTextOutput(this.result, this.showImages);\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\tif (content) {\n\t\t\ttext += `\\n\\n${content}`;\n\t\t}\n\t\tconst output = this.getTextOutput();\n\t\tif (output) {\n\t\t\t// 无定义工具的通用回退同样封顶 (M2, 逻辑行粒度): 超预算 head+说明+tail。\n\t\t\tconst lines = output.split(\"\\n\");\n\t\t\tif (lines.length <= TOOL_OUTPUT_MAX_LINES) {\n\t\t\t\ttext += `\\n${output}`;\n\t\t\t} else {\n\t\t\t\tconst headLines = TOOL_OUTPUT_MAX_LINES - TOOL_OUTPUT_TAIL_LINES - 1;\n\t\t\t\tconst hint = t(\"... ({count} lines truncated)\", {\n\t\t\t\t\tcount: lines.length - headLines - TOOL_OUTPUT_TAIL_LINES,\n\t\t\t\t});\n\t\t\t\ttext += `\\n${[\n\t\t\t\t\t...lines.slice(0, headLines),\n\t\t\t\t\thint,\n\t\t\t\t\t...lines.slice(lines.length - TOOL_OUTPUT_TAIL_LINES),\n\t\t\t\t].join(\"\\n\")}`;\n\t\t\t}\n\t\t}\n\t\treturn text;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent, TextContent } from \"@agent-forge/ai\";\nimport {\n\tBox,\n\ttype Component,\n\tContainer,\n\tgetCapabilities,\n\tImage,\n\tSpacer,\n\tText,\n\ttype TUI,\n\ttruncateToWidth,\n} from \"@agent-forge/tui\";\nimport { t } from \"../../../core/i18n/index.ts\";\nimport { formatToolCallWithArgs, getTextOutput as getRenderedTextOutput } from \"../../../core/tools/render-utils.ts\";\nimport type { ToolDefinition, ToolRenderContext } from \"../../../core/tools/tool-definition.ts\";\nimport { theme } from \"../../../theme/theme.ts\";\nimport { convertToPng } from \"../../../utils/image-convert.ts\";\nimport { keyText } from \"./keybinding-hints.ts\";\n\n/** Visual (wrapped) result lines shown before fallback output is expanded. */\nconst FALLBACK_PREVIEW_LINES = 10;\n\n/**\n * 单条工具结果展示预算 (M2, 终端输出模式设计 §3.2「展开仍复用统一的输出截断」):\n * 任何渲染器 (含第三方插件) 的结果输出在视觉行上超过预算时, 保留 head+tail、\n * 中间以一行截断说明替代 (截断原因 + 总量 + 完整输出引用, 引用存在时)。这是\n * compact 预览之上的兜底——折叠态有自己的小预览, detail 态 (ctrl+o) 的全文\n * 渲染由此封顶。错误结果豁免 (错误可见性优先, 与 collapsedPreviewText 同惯例)。\n */\nconst TOOL_OUTPUT_MAX_LINES = 400;\n\n/** 预算超出时保留完整输出的末尾视觉行数 (错误摘要与结论常聚在输出末尾)。 */\nconst TOOL_OUTPUT_TAIL_LINES = 100;\n\n/**\n * Pretty-printed JSON up to this length is collapsed to its compact single-line\n * form; longer compact forms are cut here so a short tool-result ticket shows\n * as one preview line instead of sprawling across the chat. Expansion always\n * shows the original text.\n */\nconst COLLAPSED_JSON_PREVIEW_CHARS = 220;\n\n/**\n * Collapsed preview text for fallback results: pretty-printed JSON (the default\n * serialization for capability-tool results) becomes one compact line, and any\n * other text previews verbatim. Unparseable JSON-like text stays verbatim too.\n * Error results never compact — error visibility beats compactness, so they\n * keep the (threshold-limited) full form with the error background.\n */\nfunction collapsedPreviewText(output: string, isError: boolean): string {\n\tif (isError) {\n\t\treturn output;\n\t}\n\tconst trimmed = output.trimStart();\n\tif (!trimmed.startsWith(\"{\") && !trimmed.startsWith(\"[\")) {\n\t\treturn output;\n\t}\n\ttry {\n\t\tconst compact = JSON.stringify(JSON.parse(output));\n\t\treturn compact.length > COLLAPSED_JSON_PREVIEW_CHARS\n\t\t\t? `${compact.slice(0, COLLAPSED_JSON_PREVIEW_CHARS - 1)}…`\n\t\t\t: compact;\n\t} catch {\n\t\treturn output;\n\t}\n}\n\n/**\n * Collapsed fallback output limited to a number of visual (wrapped) lines. Limiting logical\n * lines instead lets a single long line (such as minified JSON) wrap across the whole screen.\n * Caches its lines per width, since it renders on every frame for every result in the transcript.\n */\nclass VisualLinePreview implements Component {\n\tprivate text: string;\n\tprivate maxVisualLines: number;\n\tprivate formatHint: (hidden: number) => string;\n\tprivate cachedWidth: number | undefined;\n\tprivate cachedLines: string[] | undefined;\n\n\tconstructor(text: string, maxVisualLines: number, formatHint: (hidden: number) => string) {\n\t\tthis.text = text;\n\t\tthis.maxVisualLines = maxVisualLines;\n\t\tthis.formatHint = formatHint;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines === undefined || this.cachedWidth !== width) {\n\t\t\tconst allVisualLines = new Text(this.text, 0, 0).render(width);\n\t\t\tconst lines =\n\t\t\t\tallVisualLines.length > this.maxVisualLines\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t...allVisualLines.slice(0, this.maxVisualLines),\n\t\t\t\t\t\t\ttruncateToWidth(this.formatHint(allVisualLines.length - this.maxVisualLines), width, \"...\"),\n\t\t\t\t\t\t]\n\t\t\t\t\t: allVisualLines;\n\t\t\tthis.cachedLines = lines;\n\t\t\tthis.cachedWidth = width;\n\t\t\treturn lines;\n\t\t}\n\t\treturn this.cachedLines;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n}\n\n/**\n * bash 系工具在 details 携带的全量输出文件引用 (截断说明行的\"完整输出\"引用;\n * 终端输出模式设计 §3.2 的引用要求在通用层的最大努力形态)。\n */\nfunction resultFullOutputRef(details: unknown): string | undefined {\n\tif (details === null || typeof details !== \"object\" || Array.isArray(details)) return undefined;\n\tconst ref = (details as { fullOutputPath?: unknown }).fullOutputPath;\n\treturn typeof ref === \"string\" && ref.trim() !== \"\" ? ref : undefined;\n}\n\n/**\n * 结果渲染器输出的展示预算兜底 (M2): 包装 renderResult/fallback 产出的子组件,\n * render(width) 时对视觉行超预算的输出做 head+截断说明+tail。渲染器契约不变\n * (renderResult 仍收 expanded 原值, lastComponent 仍传裸子组件); 缓存按宽度,\n * invalidate 透传子组件 (流式增量渲染依赖它)。\n */\nclass BoundedResultView implements Component {\n\tprivate child: Component;\n\tprivate maxLines: number;\n\tprivate tailLines: number;\n\tprivate isError: boolean;\n\tprivate fullOutputRef: string | undefined;\n\tprivate cachedWidth: number | undefined;\n\tprivate cachedLines: string[] | undefined;\n\n\tconstructor(\n\t\tchild: Component,\n\t\tmaxLines: number,\n\t\ttailLines: number,\n\t\tisError: boolean,\n\t\tfullOutputRef: string | undefined,\n\t) {\n\t\tthis.child = child;\n\t\tthis.maxLines = maxLines;\n\t\tthis.tailLines = tailLines;\n\t\tthis.isError = isError;\n\t\tthis.fullOutputRef = fullOutputRef;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines === undefined || this.cachedWidth !== width) {\n\t\t\tconst lines = this.child.render(width);\n\t\t\tif (this.isError || lines.length <= this.maxLines) {\n\t\t\t\tthis.cachedLines = lines;\n\t\t\t} else {\n\t\t\t\tconst headLines = this.maxLines - this.tailLines - 1;\n\t\t\t\tconst hidden = lines.length - headLines - this.tailLines;\n\t\t\t\tconst hint =\n\t\t\t\t\tthis.fullOutputRef === undefined\n\t\t\t\t\t\t? t(\"... ({count} lines truncated)\", { count: hidden })\n\t\t\t\t\t\t: t(\"... ({count} lines truncated; full output: {path})\", {\n\t\t\t\t\t\t\t\tcount: hidden,\n\t\t\t\t\t\t\t\tpath: this.fullOutputRef,\n\t\t\t\t\t\t\t});\n\t\t\t\tthis.cachedLines = [\n\t\t\t\t\t...lines.slice(0, headLines),\n\t\t\t\t\ttruncateToWidth(theme.fg(\"muted\", hint), width, \"...\"),\n\t\t\t\t\t...lines.slice(lines.length - this.tailLines),\n\t\t\t\t];\n\t\t\t}\n\t\t\tthis.cachedWidth = width;\n\t\t\treturn this.cachedLines;\n\t\t}\n\t\treturn this.cachedLines;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.child.invalidate();\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n}\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean;\n\timageWidthCells?: number;\n}\n\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox: Box;\n\tprivate contentText: Text;\n\tprivate selfRenderContainer: Container;\n\tprivate callRendererComponent?: Component;\n\tprivate resultRendererComponent?: Component;\n\tprivate rendererState: Record<string, unknown> = {};\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate toolCallId: string;\n\tprivate args: unknown;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate executionStarted = false;\n\tprivate argsComplete = false;\n\tprivate result?: {\n\t\tcontent: (TextContent | ImageContent)[];\n\t\tisError: boolean;\n\t\tdetails?: unknown;\n\t};\n\tprivate convertedImages: Map<\n\t\tnumber,\n\t\t{ sourceData: string; sourceMimeType: string; data: string; mimeType: string }\n\t> = new Map();\n\tprivate hideComponent = false;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: unknown,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition | undefined,\n\t\tui: TUI,\n\t\tcwd: string,\n\t) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.toolCallId = toolCallId;\n\t\tthis.args = args;\n\t\t// The caller passes the session registry's definition\n\t\t// (interactive-mode getRegisteredToolDefinition → facade.getToolDefinition).\n\t\t// Since D-082 批 1 the default coding tool definitions live in the\n\t\t// coding-tools plugin, so the registry is the only definition source;\n\t\t// names missing from the registry render through the generic fallback.\n\t\tthis.toolDefinition = toolDefinition;\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Always create all shell variants. contentBox is used for default renderer-based composition.\n\t\t// selfRenderContainer is used when the tool renders its own framing.\n\t\t// contentText is reserved for generic fallback rendering when no tool definition exists.\n\t\tthis.contentBox = new Box(1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\tthis.contentText = new Text(\"\", 1, 1, (text: string) => theme.bg(\"toolPendingBg\", text));\n\t\tthis.selfRenderContainer = new Container();\n\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.addChild(this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox);\n\t\t} else {\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getCallRenderer(): ToolDefinition[\"renderCall\"] | undefined {\n\t\treturn this.toolDefinition?.renderCall;\n\t}\n\n\tprivate getResultRenderer(): ToolDefinition[\"renderResult\"] | undefined {\n\t\treturn this.toolDefinition?.renderResult;\n\t}\n\n\tprivate hasRendererDefinition(): boolean {\n\t\treturn this.toolDefinition !== undefined;\n\t}\n\n\tprivate getRenderShell(): \"default\" | \"self\" {\n\t\treturn this.toolDefinition?.renderShell ?? \"default\";\n\t}\n\n\tprivate getRenderContext(lastComponent: Component | undefined): ToolRenderContext {\n\t\treturn {\n\t\t\targs: this.args,\n\t\t\ttoolCallId: this.toolCallId,\n\t\t\tinvalidate: () => {\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\tlastComponent,\n\t\t\tstate: this.rendererState,\n\t\t\tcwd: this.cwd,\n\t\t\texecutionStarted: this.executionStarted,\n\t\t\targsComplete: this.argsComplete,\n\t\t\tisPartial: this.isPartial,\n\t\t\texpanded: this.expanded,\n\t\t\tshowImages: this.showImages,\n\t\t\tisError: this.result?.isError ?? false,\n\t\t};\n\t}\n\n\tprivate createCallFallback(): Component {\n\t\treturn new Text(formatToolCallWithArgs(this.toolName, this.args, theme, this.expanded), 0, 0);\n\t}\n\n\tprivate createResultFallback(): Component | undefined {\n\t\tconst output = this.getTextOutput();\n\t\tif (!output) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (this.expanded) {\n\t\t\treturn new Text(\n\t\t\t\toutput\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t);\n\t\t}\n\t\tconst previewText = collapsedPreviewText(output, this.result?.isError ?? false);\n\t\t// A compacted JSON preview is one line by construction, so it never\n\t\t// reaches the preview-line threshold — append the expand hint directly.\n\t\tif (previewText !== output) {\n\t\t\t// i18n: 原三段样式拼接合并为整句单 key,键位片段的 dim 样式损失按批次④特殊裁决 1 接受\n\t\t\tconst hint = theme.fg(\"muted\", t(\"({key} to expand)\", { key: keyText(\"app.tools.expand\") }));\n\t\t\treturn new Text(`${theme.fg(\"toolOutput\", previewText)} ${hint}`, 0, 0);\n\t\t}\n\t\tconst styled = previewText\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => theme.fg(\"toolOutput\", line))\n\t\t\t.join(\"\\n\");\n\t\treturn new VisualLinePreview(styled, FALLBACK_PREVIEW_LINES, (hidden) =>\n\t\t\ttheme.fg(\n\t\t\t\t\"muted\",\n\t\t\t\tt(\"... ({count} more lines, {key} to expand)\", {\n\t\t\t\t\tcount: hidden,\n\t\t\t\t\tkey: keyText(\"app.tools.expand\"),\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t}\n\n\t/** Failure fact for the enclosing ToolRunGroupComponent (D-083 quiet marker). */\n\tget isError(): boolean {\n\t\treturn this.result?.isError ?? false;\n\t}\n\n\t/** Tool name for the group marker's sole-tool form (restore path has no setActiveTool). */\n\tget name(): string {\n\t\treturn this.toolName;\n\t}\n\n\t/**\n\t * First non-empty line of a failed result's text output — the quiet group\n\t * marker's failure summary (D-083 增补:失败首行摘要). Successes and\n\t * result-less calls return undefined.\n\t */\n\tget errorSummary(): string | undefined {\n\t\tif (!this.result?.isError) return undefined;\n\t\treturn this.getTextOutput()\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trim())\n\t\t\t.find((line) => line !== \"\");\n\t}\n\n\t/**\n\t * Quiet-transcript image lines (D-083 增补:图片是用户可见的结论): the\n\t * collapsed group marker hides the text card but still renders result\n\t * images (desktop/browser screenshots). Empty when the result has no\n\t * renderable images.\n\t */\n\trenderQuietImages(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (let i = 0; i < this.imageComponents.length; i++) {\n\t\t\tconst spacer = this.imageSpacers[i];\n\t\t\t// v8 ignore next -- spacers and image components are always pushed in pairs\n\t\t\tif (spacer) {\n\t\t\t\tlines.push(...spacer.render(width));\n\t\t\t}\n\t\t\tconst imageComponent = this.imageComponents[i];\n\t\t\t// v8 ignore next -- spacers and image components are always pushed in pairs\n\t\t\tif (imageComponent) {\n\t\t\t\tlines.push(...imageComponent.render(width));\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Quiet-transcript live tail (collapsed running group, D-083 运行态): the\n\t * component's own render (pending background included) cut to the last\n\t * `maxLines` visual lines. Hidden components render to [] and stay hidden.\n\t */\n\trenderQuietLiveOutput(width: number, maxLines: number): string[] {\n\t\tconst lines = this.render(width);\n\t\tif (lines.length <= maxLines) {\n\t\t\treturn lines;\n\t\t}\n\t\treturn lines.slice(lines.length - maxLines);\n\t}\n\n\tupdateArgs(args: unknown): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\tmarkExecutionStarted(): void {\n\t\tthis.executionStarted = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tsetArgsComplete(): void {\n\t\tthis.argsComplete = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: (TextContent | ImageContent)[];\n\t\t\tdetails?: unknown;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tthis.updateDisplay();\n\t\tthis.maybeConvertImagesForKitty();\n\t}\n\n\tprivate maybeConvertImagesForKitty(): void {\n\t\tconst caps = getCapabilities();\n\t\tif (caps.images !== \"kitty\") return;\n\t\t// v8 ignore next -- sole caller updateResult always assigns this.result first\n\t\tif (!this.result) return;\n\n\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\tconst img = imageBlocks[i];\n\t\t\tif (!img.data || !img.mimeType) continue;\n\t\t\tconst sourceData = img.data;\n\t\t\tconst sourceMimeType = img.mimeType;\n\t\t\tif (sourceMimeType === \"image/png\") continue;\n\t\t\tconst cached = this.convertedImages.get(i);\n\t\t\tif (cached?.sourceData === sourceData && cached.sourceMimeType === sourceMimeType) continue;\n\n\t\t\tconst index = i;\n\t\t\tconvertToPng(sourceData, sourceMimeType).then((converted) => {\n\t\t\t\tconst currentImage = this.result?.content.filter((content) => content.type === \"image\")[index];\n\t\t\t\tif (!converted || currentImage?.data !== sourceData || currentImage.mimeType !== sourceMimeType) return;\n\t\t\t\tthis.convertedImages.set(index, {\n\t\t\t\t\tsourceData,\n\t\t\t\t\tsourceMimeType,\n\t\t\t\t\t...converted,\n\t\t\t\t});\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tthis.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hideComponent) {\n\t\t\treturn [];\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && this.getRenderShell() === \"self\") {\n\t\t\tconst contentLines = this.selfRenderContainer.render(width);\n\t\t\tif (contentLines.length === 0 && this.imageComponents.length === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\tconst lines: string[] = [];\n\t\t\tif (contentLines.length > 0) {\n\t\t\t\tlines.push(\"\");\n\t\t\t\tlines.push(...contentLines);\n\t\t\t}\n\t\t\tfor (let i = 0; i < this.imageComponents.length; i++) {\n\t\t\t\tconst spacer = this.imageSpacers[i];\n\t\t\t\t// v8 ignore next -- spacers and image components are always pushed in pairs\n\t\t\t\tif (spacer) {\n\t\t\t\t\tlines.push(...spacer.render(width));\n\t\t\t\t}\n\t\t\t\tconst imageComponent = this.imageComponents[i];\n\t\t\t\t// v8 ignore next -- spacers and image components are always pushed in pairs\n\t\t\t\tif (imageComponent) {\n\t\t\t\t\tlines.push(...imageComponent.render(width));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\treturn super.render(width);\n\t}\n\n\t/** M2 展示预算: 结果子组件统一过有界视图 (错误豁免), 见 BoundedResultView。 */\n\tprivate boundedResult(component: Component): Component {\n\t\treturn new BoundedResultView(\n\t\t\tcomponent,\n\t\t\tTOOL_OUTPUT_MAX_LINES,\n\t\t\tTOOL_OUTPUT_TAIL_LINES,\n\t\t\tthis.result?.isError ?? false,\n\t\t\tresultFullOutputRef(this.result?.details),\n\t\t);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tconst bgFn = this.isPartial\n\t\t\t? (text: string) => theme.bg(\"toolPendingBg\", text)\n\t\t\t: this.result?.isError\n\t\t\t\t? (text: string) => theme.bg(\"toolErrorBg\", text)\n\t\t\t\t: (text: string) => theme.bg(\"toolSuccessBg\", text);\n\n\t\tlet hasContent = false;\n\t\tthis.hideComponent = false;\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tconst renderContainer = this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox;\n\t\t\tif (renderContainer instanceof Box) {\n\t\t\t\trenderContainer.setBgFn(bgFn);\n\t\t\t}\n\t\t\trenderContainer.clear();\n\n\t\t\tconst callRenderer = this.getCallRenderer();\n\t\t\tif (!callRenderer) {\n\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\thasContent = true;\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\tconst component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));\n\t\t\t\t\t// 渲染器可能返回 undefined(选择不渲染);此时交给无内容隐藏\n\t\t\t\t\t// 逻辑,不能把 undefined 塞进 Box.children(invalidate 会崩溃)。\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\tthis.callRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tthis.callRendererComponent = undefined;\n\t\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\t\thasContent = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst resultRenderer = this.getResultRenderer();\n\t\t\t\tif (!resultRenderer) {\n\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\trenderContainer.addChild(this.boundedResult(component));\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst component = resultRenderer(\n\t\t\t\t\t\t\t{ content: this.result.content, details: this.result.details },\n\t\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\t\ttheme,\n\t\t\t\t\t\t\tthis.getRenderContext(this.resultRendererComponent),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\tthis.resultRendererComponent = component;\n\t\t\t\t\t\t\trenderContainer.addChild(this.boundedResult(component));\n\t\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthis.resultRendererComponent = undefined;\n\t\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.contentText.setCustomBgFn(bgFn);\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t\thasContent = true;\n\t\t}\n\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\t\tfor (const spacer of this.imageSpacers) {\n\t\t\tthis.removeChild(spacer);\n\t\t}\n\t\tthis.imageSpacers = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\t\tconst caps = getCapabilities();\n\t\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\t\tconst img = imageBlocks[i];\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\tconst cached = this.convertedImages.get(i);\n\t\t\t\t\tconst converted =\n\t\t\t\t\t\tcached?.sourceData === img.data && cached.sourceMimeType === img.mimeType ? cached : undefined;\n\t\t\t\t\tconst imageData = converted?.data ?? img.data;\n\t\t\t\t\tconst imageMimeType = converted?.mimeType ?? img.mimeType;\n\t\t\t\t\tif (caps.images === \"kitty\" && imageMimeType !== \"image/png\") continue;\n\n\t\t\t\t\tconst spacer = new Spacer(1);\n\t\t\t\t\tthis.addChild(spacer);\n\t\t\t\t\tthis.imageSpacers.push(spacer);\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timageData,\n\t\t\t\t\t\timageMimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: this.imageWidthCells },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && !hasContent && this.imageComponents.length === 0) {\n\t\t\tthis.hideComponent = true;\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\treturn getRenderedTextOutput(this.result, this.showImages);\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\tif (content) {\n\t\t\ttext += `\\n\\n${content}`;\n\t\t}\n\t\tconst output = this.getTextOutput();\n\t\tif (output) {\n\t\t\t// 无定义工具的通用回退同样封顶 (M2, 逻辑行粒度): 超预算 head+说明+tail。\n\t\t\tconst lines = output.split(\"\\n\");\n\t\t\tif (lines.length <= TOOL_OUTPUT_MAX_LINES) {\n\t\t\t\ttext += `\\n${output}`;\n\t\t\t} else {\n\t\t\t\tconst headLines = TOOL_OUTPUT_MAX_LINES - TOOL_OUTPUT_TAIL_LINES - 1;\n\t\t\t\tconst hint = t(\"... ({count} lines truncated)\", {\n\t\t\t\t\tcount: lines.length - headLines - TOOL_OUTPUT_TAIL_LINES,\n\t\t\t\t});\n\t\t\t\ttext += `\\n${[\n\t\t\t\t\t...lines.slice(0, headLines),\n\t\t\t\t\thint,\n\t\t\t\t\t...lines.slice(lines.length - TOOL_OUTPUT_TAIL_LINES),\n\t\t\t\t].join(\"\\n\")}`;\n\t\t\t}\n\t\t}\n\t\treturn text;\n\t}\n}\n"]}
|