@kolisachint/hoocode-agent 0.5.35 → 0.5.36
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 +45 -0
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +2 -2
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/tool-chain.d.ts +11 -0
- package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-chain.js +22 -2
- package/dist/modes/interactive/components/tool-chain.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +8 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +15 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-signal.d.ts +2 -0
- package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-signal.js +6 -4
- package/dist/modes/interactive/components/tool-signal.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +2 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +21 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +4 -0
- package/dist/modes/interactive/theme/theme.d.ts +14 -2
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +36 -2
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/interactive/theme/vox-cutout-dark.json +2 -1
- package/dist/modes/interactive/theme/vox-cutout-light.json +2 -1
- package/docs/themes.md +10 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.36] - 2026-08-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Radar marks the newest run with a marker stroke. The view answers a different
|
|
8
|
+
question than the other two — not what a tool said but the shape of the run —
|
|
9
|
+
and what it could not say was which run you are on. `activeToolBg` covers the
|
|
10
|
+
verb and the subject and stops before the flush-right signal, so the status
|
|
11
|
+
colour stays on the page: a highlighter runs over the words, not the whole
|
|
12
|
+
line. It marks two shapes because radar shows two, the folded chain line and
|
|
13
|
+
the per-call row; marking only the row would have left it invisible in the
|
|
14
|
+
common case, since a run of more than one call folds to a single line. Optional
|
|
15
|
+
like the rest, and unset it draws nothing.
|
|
16
|
+
|
|
17
|
+
- Filled message blocks are cut into sheets rather than run as full-width bands.
|
|
18
|
+
A block that reaches both margins has no right edge to show, which is why the
|
|
19
|
+
shadow could only ever be a bottom edge: there was no column past the last one
|
|
20
|
+
to put anything in. Blocks that take `paperShadow` now hold three columns back
|
|
21
|
+
from the right margin, cast a shadow along that edge as well as under it, and
|
|
22
|
+
have the edge cut rather than ruled — roughly one row in five gives up a
|
|
23
|
+
single column, always out of padding and never out of content.
|
|
24
|
+
|
|
25
|
+
The cut comes from a hash of the block's own content, so it is stable between
|
|
26
|
+
frames and different between blocks. A coin flip per row was the first attempt
|
|
27
|
+
and it was wrong: alternating columns read as a sawtooth, which is the
|
|
28
|
+
opposite of hand-cut. Scissors leave a mostly straight line with the
|
|
29
|
+
occasional nick.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- A filled `Box` keeps its background behind anything that paints its own fill.
|
|
34
|
+
A child that draws a background — a chip, a label strip, a highlighted span —
|
|
35
|
+
has to close it, and that reset ended the band's background as well, so the
|
|
36
|
+
row finished on the terminal's own canvas instead of on the block. The band
|
|
37
|
+
now re-opens its background after any inner reset, recovering the opener from
|
|
38
|
+
the `bgFn` it was handed rather than assuming a colour, which makes it right
|
|
39
|
+
for any nested fill and a no-op for a `bgFn` that adds no codes.
|
|
40
|
+
|
|
41
|
+
- A heading chip no longer wraps the `###` marker along with the text. Below
|
|
42
|
+
level two the renderer prints that marker, and a marker inside a filled chip
|
|
43
|
+
says the same thing twice — the chip *is* the marker. The hook takes the level
|
|
44
|
+
now (`heading(text, level)`, `headingBlock(line, level)`) and only the top two
|
|
45
|
+
levels take a bar, which is the better typography regardless: a filled bar on
|
|
46
|
+
every `###` in a long answer is exhausting.
|
|
47
|
+
|
|
3
48
|
## [0.5.35] - 2026-08-25
|
|
4
49
|
|
|
5
50
|
### Added
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,EAAO,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,0BAA0B,CAAC;AACtG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzE;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAC,CAAY;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAAS;IAE1B,YACC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAC/B,cAAc,CAAC,EAAE,eAAe,EAChC,aAAa,GAAE,aAAkC,EAcjD;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;CAiDf","sourcesContent":["import type { CustomMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { TextContent } from \"@kolisachint/hoocode-ai\";\nimport type { Component } from \"@kolisachint/hoocode-tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@kolisachint/hoocode-tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.js\";\nimport {
|
|
1
|
+
{"version":3,"file":"custom-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,EAAO,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,0BAA0B,CAAC;AACtG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAGzE;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAC,CAAY;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAAS;IAE1B,YACC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAC/B,cAAc,CAAC,EAAE,eAAe,EAChC,aAAa,GAAE,aAAkC,EAcjD;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;CAiDf","sourcesContent":["import type { CustomMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { TextContent } from \"@kolisachint/hoocode-ai\";\nimport type { Component } from \"@kolisachint/hoocode-tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@kolisachint/hoocode-tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.js\";\nimport { applyPaperSheet, getMarkdownTheme, messageLabel, theme } from \"../theme/theme.js\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create box with purple background (used for default rendering)\n\t\tthis.box = new Box(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tapplyPaperSheet(this.box);\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\t// Remove previous content component\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\t// Try custom renderer first - it handles its own styling\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Custom renderer provides its own styled component\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering\n\t\t\t}\n\t\t}\n\n\t\t// Default rendering uses our box\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\t// Default rendering: label + content\n\t\tconst label = messageLabel(this.message.customType);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\t// Extract text content\n\t\tlet text: string;\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\ttext = this.message.content;\n\t\t} else {\n\t\t\ttext = this.message.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(text, 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Container, Markdown, Spacer, Text } from "@kolisachint/hoocode-tui";
|
|
2
|
-
import {
|
|
2
|
+
import { applyPaperSheet, getMarkdownTheme, messageLabel, theme } from "../theme/theme.js";
|
|
3
3
|
/**
|
|
4
4
|
* Component that renders a custom message entry from extensions.
|
|
5
5
|
* Uses distinct styling to differentiate from user messages.
|
|
@@ -19,7 +19,7 @@ export class CustomMessageComponent extends Container {
|
|
|
19
19
|
this.addChild(new Spacer(1));
|
|
20
20
|
// Create box with purple background (used for default rendering)
|
|
21
21
|
this.box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
22
|
-
this.box
|
|
22
|
+
applyPaperSheet(this.box);
|
|
23
23
|
this.rebuild();
|
|
24
24
|
}
|
|
25
25
|
setExpanded(expanded) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAsB,MAAM,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAEtG,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"custom-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAsB,MAAM,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAEtG,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE3F;;;GAGG;AACH,MAAM,OAAO,sBAAuB,SAAQ,SAAS;IAC5C,OAAO,CAAyB;IAChC,cAAc,CAAmB;IACjC,GAAG,CAAM;IACT,eAAe,CAAa;IAC5B,aAAa,CAAgB;IAC7B,SAAS,GAAG,KAAK,CAAC;IAE1B,YACC,OAA+B,EAC/B,cAAgC,EAChC,aAAa,GAAkB,gBAAgB,EAAE,EAChD;QACD,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QAEnC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7B,iEAAiE;QACjE,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,CAAC;QAChE,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAE1B,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAED,WAAW,CAAC,QAAiB,EAAQ;QACpC,IAAI,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;YAC1B,IAAI,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC;IAAA,CACD;IAEQ,UAAU,GAAS;QAC3B,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAEO,OAAO,GAAS;QACvB,oCAAoC;QACpC,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC1B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YACvC,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QAClC,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAE3B,yDAAyD;QACzD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,IAAI,CAAC;gBACJ,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,KAAK,CAAC,CAAC;gBACzF,IAAI,SAAS,EAAE,CAAC;oBACf,oDAAoD;oBACpD,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;oBACjC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;oBACzB,OAAO;gBACR,CAAC;YACF,CAAC;YAAC,MAAM,CAAC;gBACR,oCAAoC;YACrC,CAAC;QACF,CAAC;QAED,iCAAiC;QACjC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QAEjB,qCAAqC;QACrC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACpD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAEjC,uBAAuB;QACvB,IAAI,IAAY,CAAC;QACjB,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC9C,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;QAC7B,CAAC;aAAM,CAAC;YACP,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO;iBACzB,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;iBAClD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;iBAClB,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,QAAQ,CAChB,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE;YAC5C,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,IAAI,CAAC;SAC5D,CAAC,CACF,CAAC;IAAA,CACF;CACD","sourcesContent":["import type { CustomMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { TextContent } from \"@kolisachint/hoocode-ai\";\nimport type { Component } from \"@kolisachint/hoocode-tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@kolisachint/hoocode-tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.js\";\nimport { applyPaperSheet, getMarkdownTheme, messageLabel, theme } from \"../theme/theme.js\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create box with purple background (used for default rendering)\n\t\tthis.box = new Box(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tapplyPaperSheet(this.box);\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\t// Remove previous content component\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\t// Try custom renderer first - it handles its own styling\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Custom renderer provides its own styled component\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering\n\t\t\t}\n\t\t}\n\n\t\t// Default rendering uses our box\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\t// Default rendering: label + content\n\t\tconst label = messageLabel(this.message.customType);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\t// Extract text content\n\t\tlet text: string;\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\ttext = this.message.content;\n\t\t} else {\n\t\t\ttext = this.message.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(text, 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
|
|
@@ -21,6 +21,8 @@ export declare class ToolChainComponent extends Container {
|
|
|
21
21
|
private state;
|
|
22
22
|
/** Opened into its per-call rows by `app.tools.unfoldOne`. */
|
|
23
23
|
private opened;
|
|
24
|
+
/** The newest run in the transcript; radar marks it. */
|
|
25
|
+
private latest;
|
|
24
26
|
private memo?;
|
|
25
27
|
constructor(view: ToolOutputView);
|
|
26
28
|
add(block: ToolExecutionComponent): void;
|
|
@@ -45,6 +47,15 @@ export declare class ToolChainComponent extends Container {
|
|
|
45
47
|
setExpanded(expanded: boolean): void;
|
|
46
48
|
/** `alt+u` in radar: open this chain into its per-call rows, or fold it back. */
|
|
47
49
|
setOpened(opened: boolean): void;
|
|
50
|
+
/**
|
|
51
|
+
* Mark this run as the newest in the transcript, or no longer it.
|
|
52
|
+
*
|
|
53
|
+
* The per-call rows carry the same mark, but they are not what radar usually
|
|
54
|
+
* shows: a run of more than one call folds to this single line, so without
|
|
55
|
+
* marking the line too the stroke would be invisible in the view it was
|
|
56
|
+
* built for.
|
|
57
|
+
*/
|
|
58
|
+
setLatest(latest: boolean): void;
|
|
48
59
|
invalidate(): void;
|
|
49
60
|
/**
|
|
50
61
|
* Whether this chain is currently drawn as a single summary line.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-chain.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAiC,MAAM,0BAA0B,CAAC;AACpG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;IAChD,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,IAAI,CAAiB;IAC7B,OAAO,CAAC,KAAK,CAAyB;IACtC,8DAA8D;IAC9D,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAC,CAAmC;IAEhD,YAAY,IAAI,EAAE,cAAc,EAG/B;IAED,GAAG,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAIvC;IAED,uDAAuD;IACvD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,8EAA8E;IAC9E,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,SAAS,sBAAsB,EAAE,CAElD;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAG3C;IAED,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAIlC;IAED,2EAA2E;IAC3E,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAInC;IAED,iFAAiF;IACjF,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAG/B;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED;;;;;;;;;OASG;IACH,OAAO,CAAC,WAAW;IAIV,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"tool-chain.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAiC,MAAM,0BAA0B,CAAC;AACpG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;IAChD,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,IAAI,CAAiB;IAC7B,OAAO,CAAC,KAAK,CAAyB;IACtC,8DAA8D;IAC9D,OAAO,CAAC,MAAM,CAAS;IACvB,wDAAwD;IACxD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAC,CAAmC;IAEhD,YAAY,IAAI,EAAE,cAAc,EAG/B;IAED,GAAG,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAIvC;IAED,uDAAuD;IACvD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,8EAA8E;IAC9E,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,SAAS,sBAAsB,EAAE,CAElD;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAG3C;IAED,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAIlC;IAED,2EAA2E;IAC3E,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAInC;IAED,iFAAiF;IACjF,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAG/B;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAI/B;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED;;;;;;;;;OASG;IACH,OAAO,CAAC,WAAW;IAIV,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgDvC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;CAcpB;AAED,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,IAAI,kBAAkB,CAEzE","sourcesContent":["import { type Component, Container, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { type ChainState, chainPhrase, chainSegments, chainStats } from \"./tool-chain-summary.js\";\nimport type { ToolExecutionComponent } from \"./tool-execution.js\";\n\n/**\n * A run of consecutive tool calls, rendered as one line in the radar view.\n *\n * The chain owns its blocks rather than sitting beside them, so there is one\n * place that decides whether you are looking at a summary or at the calls. In\n * every view but a collapsed radar it is a plain pass-through container and the\n * blocks render exactly as they always did.\n *\n * A chain closes when the agent next speaks, or when the turn settles. Closing\n * early matters more than it looks: rewriting a line that has scrolled above\n * the viewport forces the TUI into a full redraw, which clears the terminal's\n * scrollback. Chains that close while they are still the bottom of the screen\n * flip for the price of one line.\n */\nexport class ToolChainComponent extends Container {\n\tprivate blocks: ToolExecutionComponent[] = [];\n\tprivate view: ToolOutputView;\n\tprivate state: ChainState = \"running\";\n\t/** Opened into its per-call rows by `app.tools.unfoldOne`. */\n\tprivate opened = false;\n\t/** The newest run in the transcript; radar marks it. */\n\tprivate latest = false;\n\tprivate memo?: { width: number; out: string[] };\n\n\tconstructor(view: ToolOutputView) {\n\t\tsuper();\n\t\tthis.view = view;\n\t}\n\n\tadd(block: ToolExecutionComponent): void {\n\t\tthis.blocks.push(block);\n\t\tthis.addChild(block);\n\t\tthis.memo = undefined;\n\t}\n\n\t/** True until the agent speaks or the turn settles. */\n\tget isOpen(): boolean {\n\t\treturn this.state === \"running\";\n\t}\n\n\tget isOpened(): boolean {\n\t\treturn this.opened;\n\t}\n\n\t/** Whether a summary line is standing in for this chain's calls right now. */\n\tget isSummarised(): boolean {\n\t\treturn this.isCollapsed();\n\t}\n\n\tget isEmpty(): boolean {\n\t\treturn this.blocks.length === 0;\n\t}\n\n\tget toolBlocks(): readonly ToolExecutionComponent[] {\n\t\treturn this.blocks;\n\t}\n\n\t/**\n\t * Settle the chain.\n\t *\n\t * `interrupted` keeps the running rendering, because the settled phrase is a\n\t * claim about what the run amounted to and a run cut off partway through has\n\t * no such claim to make — the same reason an aborted turn's plan items settle\n\t * to cancelled rather than done.\n\t */\n\tclose(outcome: \"done\" | \"interrupted\"): void {\n\t\tthis.state = outcome;\n\t\tthis.memo = undefined;\n\t}\n\n\tsetView(view: ToolOutputView): void {\n\t\tthis.view = view;\n\t\tthis.memo = undefined;\n\t\tfor (const block of this.blocks) block.setView(view);\n\t}\n\n\t/** The global expand key opens every block, which also opens the chain. */\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.opened = expanded;\n\t\tthis.memo = undefined;\n\t\tfor (const block of this.blocks) block.setExpanded(expanded);\n\t}\n\n\t/** `alt+u` in radar: open this chain into its per-call rows, or fold it back. */\n\tsetOpened(opened: boolean): void {\n\t\tthis.opened = opened;\n\t\tthis.memo = undefined;\n\t}\n\n\t/**\n\t * Mark this run as the newest in the transcript, or no longer it.\n\t *\n\t * The per-call rows carry the same mark, but they are not what radar usually\n\t * shows: a run of more than one call folds to this single line, so without\n\t * marking the line too the stroke would be invisible in the view it was\n\t * built for.\n\t */\n\tsetLatest(latest: boolean): void {\n\t\tif (this.latest === latest) return;\n\t\tthis.latest = latest;\n\t\tthis.memo = undefined;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.memo = undefined;\n\t}\n\n\t/**\n\t * Whether this chain is currently drawn as a single summary line.\n\t *\n\t * A chain of one is not summarised. Its phrase would be `Ran npm run check`,\n\t * which is strictly less than the radar row it replaced: the row names the\n\t * tool and how much came back, and the phrase drops both to say the same\n\t * thing in prose. Summarising is only worth a lossy rewrite when there is\n\t * more than one call to fold — and by measurement most chains have exactly\n\t * one, so this is the common case, not an edge.\n\t */\n\tprivate isCollapsed(): boolean {\n\t\treturn this.view === \"radar\" && !this.opened && this.blocks.length > 1;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (!this.isCollapsed()) return super.render(width);\n\t\tif (this.memo && this.memo.width === width) return this.memo.out;\n\n\t\tconst entries = this.blocks.map((block) => block.chainEntry());\n\t\tconst running = this.state === \"running\";\n\t\tconst glyph = running ? \"◐\" : \"●\";\n\t\tconst glyphTone = entries.some((e) => e.isError) ? \"error\" : running ? \"warning\" : \"success\";\n\n\t\t// While it runs the chain shows its shape in order; once it is over, what\n\t\t// it amounted to. See tool-chain-summary.ts for why they differ.\n\t\tlet leftPlain: string;\n\t\tlet leftStyled: string;\n\t\tif (this.state === \"done\") {\n\t\t\tconst phrase = chainPhrase(entries);\n\t\t\tleftPlain = phrase;\n\t\t\tleftStyled = theme.fg(\"toolTitle\", phrase);\n\t\t} else {\n\t\t\tconst segments = chainSegments(entries);\n\t\t\tconst sep = \" › \";\n\t\t\tleftPlain = segments.map((s) => s.label).join(sep) + (running ? \"…\" : \"\");\n\t\t\tleftStyled =\n\t\t\t\tsegments\n\t\t\t\t\t.map((s) =>\n\t\t\t\t\t\ttheme.fg(s.tone === \"error\" ? \"error\" : s.tone === \"running\" ? \"warning\" : \"toolTitle\", s.label),\n\t\t\t\t\t)\n\t\t\t\t\t.join(theme.fg(\"dim\", sep)) + (running ? theme.fg(\"dim\", \"…\") : \"\");\n\t\t}\n\n\t\tconst stats = chainStats(entries, this.state);\n\t\t// The marker stroke runs over what you read and stops before the stats,\n\t\t// the same shape it takes on a per-call row.\n\t\tconst stroke = this.latest && theme.hasBg(\"activeToolBg\");\n\t\tconst mark = (text: string) => (stroke ? theme.bg(\"activeToolBg\", text) : text);\n\t\t// \" ● \" + left, then stats flush right when there is room for both.\n\t\tconst prefix = `${glyph} `;\n\t\tconst budget = Math.max(0, width - 1 - visibleWidth(prefix));\n\t\tlet line: string;\n\t\tif (visibleWidth(leftPlain) + 2 + visibleWidth(stats) <= budget) {\n\t\t\tconst pad = \" \".repeat(budget - visibleWidth(leftPlain) - visibleWidth(stats));\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${mark(leftStyled)}${pad}${theme.fg(\"muted\", stats)}`;\n\t\t} else {\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${mark(truncateToWidth(leftStyled, budget))}`;\n\t\t}\n\n\t\tconst out = [line, ...this.failureLines(width)];\n\t\tthis.memo = { width, out };\n\t\treturn out;\n\t}\n\n\t/**\n\t * Each failed call's reason, indented under the chain line.\n\t *\n\t * A collapsed chain hides its blocks, so without this a failure would be a\n\t * count in the stats and nothing else. Whatever else these views fold away,\n\t * they never fold away why something broke.\n\t */\n\tprivate failureLines(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const block of this.blocks) {\n\t\t\tconst entry = block.chainEntry();\n\t\t\tif (!entry.isError) continue;\n\t\t\tconst header = ` ${theme.fg(\"error\", \"✗\")} ${theme.fg(\"toolTitle\", entry.tool)} ${theme.fg(\"toolOutput\", entry.subject)}`;\n\t\t\tlines.push(truncateToWidth(header, width));\n\t\t\tfor (const raw of block.errorText().split(\"\\n\")) {\n\t\t\t\tif (!raw.trim()) continue;\n\t\t\t\tlines.push(truncateToWidth(` ${theme.fg(\"toolOutput\", raw)}`, width));\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/** Narrow a transcript child to a chain. */\nexport function isToolChain(child: Component): child is ToolChainComponent {\n\treturn child instanceof ToolChainComponent;\n}\n"]}
|
|
@@ -21,6 +21,8 @@ export class ToolChainComponent extends Container {
|
|
|
21
21
|
state = "running";
|
|
22
22
|
/** Opened into its per-call rows by `app.tools.unfoldOne`. */
|
|
23
23
|
opened = false;
|
|
24
|
+
/** The newest run in the transcript; radar marks it. */
|
|
25
|
+
latest = false;
|
|
24
26
|
memo;
|
|
25
27
|
constructor(view) {
|
|
26
28
|
super();
|
|
@@ -78,6 +80,20 @@ export class ToolChainComponent extends Container {
|
|
|
78
80
|
this.opened = opened;
|
|
79
81
|
this.memo = undefined;
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Mark this run as the newest in the transcript, or no longer it.
|
|
85
|
+
*
|
|
86
|
+
* The per-call rows carry the same mark, but they are not what radar usually
|
|
87
|
+
* shows: a run of more than one call folds to this single line, so without
|
|
88
|
+
* marking the line too the stroke would be invisible in the view it was
|
|
89
|
+
* built for.
|
|
90
|
+
*/
|
|
91
|
+
setLatest(latest) {
|
|
92
|
+
if (this.latest === latest)
|
|
93
|
+
return;
|
|
94
|
+
this.latest = latest;
|
|
95
|
+
this.memo = undefined;
|
|
96
|
+
}
|
|
81
97
|
invalidate() {
|
|
82
98
|
super.invalidate();
|
|
83
99
|
this.memo = undefined;
|
|
@@ -123,16 +139,20 @@ export class ToolChainComponent extends Container {
|
|
|
123
139
|
.join(theme.fg("dim", sep)) + (running ? theme.fg("dim", "…") : "");
|
|
124
140
|
}
|
|
125
141
|
const stats = chainStats(entries, this.state);
|
|
142
|
+
// The marker stroke runs over what you read and stops before the stats,
|
|
143
|
+
// the same shape it takes on a per-call row.
|
|
144
|
+
const stroke = this.latest && theme.hasBg("activeToolBg");
|
|
145
|
+
const mark = (text) => (stroke ? theme.bg("activeToolBg", text) : text);
|
|
126
146
|
// " ● " + left, then stats flush right when there is room for both.
|
|
127
147
|
const prefix = `${glyph} `;
|
|
128
148
|
const budget = Math.max(0, width - 1 - visibleWidth(prefix));
|
|
129
149
|
let line;
|
|
130
150
|
if (visibleWidth(leftPlain) + 2 + visibleWidth(stats) <= budget) {
|
|
131
151
|
const pad = " ".repeat(budget - visibleWidth(leftPlain) - visibleWidth(stats));
|
|
132
|
-
line = ` ${theme.fg(glyphTone, prefix)}${leftStyled}${pad}${theme.fg("muted", stats)}`;
|
|
152
|
+
line = ` ${theme.fg(glyphTone, prefix)}${mark(leftStyled)}${pad}${theme.fg("muted", stats)}`;
|
|
133
153
|
}
|
|
134
154
|
else {
|
|
135
|
-
line = ` ${theme.fg(glyphTone, prefix)}${truncateToWidth(leftStyled, budget)}`;
|
|
155
|
+
line = ` ${theme.fg(glyphTone, prefix)}${mark(truncateToWidth(leftStyled, budget))}`;
|
|
136
156
|
}
|
|
137
157
|
const out = [line, ...this.failureLines(width)];
|
|
138
158
|
this.memo = { width, out };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-chain.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, Container, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { type ChainState, chainPhrase, chainSegments, chainStats } from \"./tool-chain-summary.js\";\nimport type { ToolExecutionComponent } from \"./tool-execution.js\";\n\n/**\n * A run of consecutive tool calls, rendered as one line in the radar view.\n *\n * The chain owns its blocks rather than sitting beside them, so there is one\n * place that decides whether you are looking at a summary or at the calls. In\n * every view but a collapsed radar it is a plain pass-through container and the\n * blocks render exactly as they always did.\n *\n * A chain closes when the agent next speaks, or when the turn settles. Closing\n * early matters more than it looks: rewriting a line that has scrolled above\n * the viewport forces the TUI into a full redraw, which clears the terminal's\n * scrollback. Chains that close while they are still the bottom of the screen\n * flip for the price of one line.\n */\nexport class ToolChainComponent extends Container {\n\tprivate blocks: ToolExecutionComponent[] = [];\n\tprivate view: ToolOutputView;\n\tprivate state: ChainState = \"running\";\n\t/** Opened into its per-call rows by `app.tools.unfoldOne`. */\n\tprivate opened = false;\n\tprivate memo?: { width: number; out: string[] };\n\n\tconstructor(view: ToolOutputView) {\n\t\tsuper();\n\t\tthis.view = view;\n\t}\n\n\tadd(block: ToolExecutionComponent): void {\n\t\tthis.blocks.push(block);\n\t\tthis.addChild(block);\n\t\tthis.memo = undefined;\n\t}\n\n\t/** True until the agent speaks or the turn settles. */\n\tget isOpen(): boolean {\n\t\treturn this.state === \"running\";\n\t}\n\n\tget isOpened(): boolean {\n\t\treturn this.opened;\n\t}\n\n\t/** Whether a summary line is standing in for this chain's calls right now. */\n\tget isSummarised(): boolean {\n\t\treturn this.isCollapsed();\n\t}\n\n\tget isEmpty(): boolean {\n\t\treturn this.blocks.length === 0;\n\t}\n\n\tget toolBlocks(): readonly ToolExecutionComponent[] {\n\t\treturn this.blocks;\n\t}\n\n\t/**\n\t * Settle the chain.\n\t *\n\t * `interrupted` keeps the running rendering, because the settled phrase is a\n\t * claim about what the run amounted to and a run cut off partway through has\n\t * no such claim to make — the same reason an aborted turn's plan items settle\n\t * to cancelled rather than done.\n\t */\n\tclose(outcome: \"done\" | \"interrupted\"): void {\n\t\tthis.state = outcome;\n\t\tthis.memo = undefined;\n\t}\n\n\tsetView(view: ToolOutputView): void {\n\t\tthis.view = view;\n\t\tthis.memo = undefined;\n\t\tfor (const block of this.blocks) block.setView(view);\n\t}\n\n\t/** The global expand key opens every block, which also opens the chain. */\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.opened = expanded;\n\t\tthis.memo = undefined;\n\t\tfor (const block of this.blocks) block.setExpanded(expanded);\n\t}\n\n\t/** `alt+u` in radar: open this chain into its per-call rows, or fold it back. */\n\tsetOpened(opened: boolean): void {\n\t\tthis.opened = opened;\n\t\tthis.memo = undefined;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.memo = undefined;\n\t}\n\n\t/**\n\t * Whether this chain is currently drawn as a single summary line.\n\t *\n\t * A chain of one is not summarised. Its phrase would be `Ran npm run check`,\n\t * which is strictly less than the radar row it replaced: the row names the\n\t * tool and how much came back, and the phrase drops both to say the same\n\t * thing in prose. Summarising is only worth a lossy rewrite when there is\n\t * more than one call to fold — and by measurement most chains have exactly\n\t * one, so this is the common case, not an edge.\n\t */\n\tprivate isCollapsed(): boolean {\n\t\treturn this.view === \"radar\" && !this.opened && this.blocks.length > 1;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (!this.isCollapsed()) return super.render(width);\n\t\tif (this.memo && this.memo.width === width) return this.memo.out;\n\n\t\tconst entries = this.blocks.map((block) => block.chainEntry());\n\t\tconst running = this.state === \"running\";\n\t\tconst glyph = running ? \"◐\" : \"●\";\n\t\tconst glyphTone = entries.some((e) => e.isError) ? \"error\" : running ? \"warning\" : \"success\";\n\n\t\t// While it runs the chain shows its shape in order; once it is over, what\n\t\t// it amounted to. See tool-chain-summary.ts for why they differ.\n\t\tlet leftPlain: string;\n\t\tlet leftStyled: string;\n\t\tif (this.state === \"done\") {\n\t\t\tconst phrase = chainPhrase(entries);\n\t\t\tleftPlain = phrase;\n\t\t\tleftStyled = theme.fg(\"toolTitle\", phrase);\n\t\t} else {\n\t\t\tconst segments = chainSegments(entries);\n\t\t\tconst sep = \" › \";\n\t\t\tleftPlain = segments.map((s) => s.label).join(sep) + (running ? \"…\" : \"\");\n\t\t\tleftStyled =\n\t\t\t\tsegments\n\t\t\t\t\t.map((s) =>\n\t\t\t\t\t\ttheme.fg(s.tone === \"error\" ? \"error\" : s.tone === \"running\" ? \"warning\" : \"toolTitle\", s.label),\n\t\t\t\t\t)\n\t\t\t\t\t.join(theme.fg(\"dim\", sep)) + (running ? theme.fg(\"dim\", \"…\") : \"\");\n\t\t}\n\n\t\tconst stats = chainStats(entries, this.state);\n\t\t// \" ● \" + left, then stats flush right when there is room for both.\n\t\tconst prefix = `${glyph} `;\n\t\tconst budget = Math.max(0, width - 1 - visibleWidth(prefix));\n\t\tlet line: string;\n\t\tif (visibleWidth(leftPlain) + 2 + visibleWidth(stats) <= budget) {\n\t\t\tconst pad = \" \".repeat(budget - visibleWidth(leftPlain) - visibleWidth(stats));\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${leftStyled}${pad}${theme.fg(\"muted\", stats)}`;\n\t\t} else {\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${truncateToWidth(leftStyled, budget)}`;\n\t\t}\n\n\t\tconst out = [line, ...this.failureLines(width)];\n\t\tthis.memo = { width, out };\n\t\treturn out;\n\t}\n\n\t/**\n\t * Each failed call's reason, indented under the chain line.\n\t *\n\t * A collapsed chain hides its blocks, so without this a failure would be a\n\t * count in the stats and nothing else. Whatever else these views fold away,\n\t * they never fold away why something broke.\n\t */\n\tprivate failureLines(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const block of this.blocks) {\n\t\t\tconst entry = block.chainEntry();\n\t\t\tif (!entry.isError) continue;\n\t\t\tconst header = ` ${theme.fg(\"error\", \"✗\")} ${theme.fg(\"toolTitle\", entry.tool)} ${theme.fg(\"toolOutput\", entry.subject)}`;\n\t\t\tlines.push(truncateToWidth(header, width));\n\t\t\tfor (const raw of block.errorText().split(\"\\n\")) {\n\t\t\t\tif (!raw.trim()) continue;\n\t\t\t\tlines.push(truncateToWidth(` ${theme.fg(\"toolOutput\", raw)}`, width));\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/** Narrow a transcript child to a chain. */\nexport function isToolChain(child: Component): child is ToolChainComponent {\n\treturn child instanceof ToolChainComponent;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tool-chain.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, Container, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { type ChainState, chainPhrase, chainSegments, chainStats } from \"./tool-chain-summary.js\";\nimport type { ToolExecutionComponent } from \"./tool-execution.js\";\n\n/**\n * A run of consecutive tool calls, rendered as one line in the radar view.\n *\n * The chain owns its blocks rather than sitting beside them, so there is one\n * place that decides whether you are looking at a summary or at the calls. In\n * every view but a collapsed radar it is a plain pass-through container and the\n * blocks render exactly as they always did.\n *\n * A chain closes when the agent next speaks, or when the turn settles. Closing\n * early matters more than it looks: rewriting a line that has scrolled above\n * the viewport forces the TUI into a full redraw, which clears the terminal's\n * scrollback. Chains that close while they are still the bottom of the screen\n * flip for the price of one line.\n */\nexport class ToolChainComponent extends Container {\n\tprivate blocks: ToolExecutionComponent[] = [];\n\tprivate view: ToolOutputView;\n\tprivate state: ChainState = \"running\";\n\t/** Opened into its per-call rows by `app.tools.unfoldOne`. */\n\tprivate opened = false;\n\t/** The newest run in the transcript; radar marks it. */\n\tprivate latest = false;\n\tprivate memo?: { width: number; out: string[] };\n\n\tconstructor(view: ToolOutputView) {\n\t\tsuper();\n\t\tthis.view = view;\n\t}\n\n\tadd(block: ToolExecutionComponent): void {\n\t\tthis.blocks.push(block);\n\t\tthis.addChild(block);\n\t\tthis.memo = undefined;\n\t}\n\n\t/** True until the agent speaks or the turn settles. */\n\tget isOpen(): boolean {\n\t\treturn this.state === \"running\";\n\t}\n\n\tget isOpened(): boolean {\n\t\treturn this.opened;\n\t}\n\n\t/** Whether a summary line is standing in for this chain's calls right now. */\n\tget isSummarised(): boolean {\n\t\treturn this.isCollapsed();\n\t}\n\n\tget isEmpty(): boolean {\n\t\treturn this.blocks.length === 0;\n\t}\n\n\tget toolBlocks(): readonly ToolExecutionComponent[] {\n\t\treturn this.blocks;\n\t}\n\n\t/**\n\t * Settle the chain.\n\t *\n\t * `interrupted` keeps the running rendering, because the settled phrase is a\n\t * claim about what the run amounted to and a run cut off partway through has\n\t * no such claim to make — the same reason an aborted turn's plan items settle\n\t * to cancelled rather than done.\n\t */\n\tclose(outcome: \"done\" | \"interrupted\"): void {\n\t\tthis.state = outcome;\n\t\tthis.memo = undefined;\n\t}\n\n\tsetView(view: ToolOutputView): void {\n\t\tthis.view = view;\n\t\tthis.memo = undefined;\n\t\tfor (const block of this.blocks) block.setView(view);\n\t}\n\n\t/** The global expand key opens every block, which also opens the chain. */\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.opened = expanded;\n\t\tthis.memo = undefined;\n\t\tfor (const block of this.blocks) block.setExpanded(expanded);\n\t}\n\n\t/** `alt+u` in radar: open this chain into its per-call rows, or fold it back. */\n\tsetOpened(opened: boolean): void {\n\t\tthis.opened = opened;\n\t\tthis.memo = undefined;\n\t}\n\n\t/**\n\t * Mark this run as the newest in the transcript, or no longer it.\n\t *\n\t * The per-call rows carry the same mark, but they are not what radar usually\n\t * shows: a run of more than one call folds to this single line, so without\n\t * marking the line too the stroke would be invisible in the view it was\n\t * built for.\n\t */\n\tsetLatest(latest: boolean): void {\n\t\tif (this.latest === latest) return;\n\t\tthis.latest = latest;\n\t\tthis.memo = undefined;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.memo = undefined;\n\t}\n\n\t/**\n\t * Whether this chain is currently drawn as a single summary line.\n\t *\n\t * A chain of one is not summarised. Its phrase would be `Ran npm run check`,\n\t * which is strictly less than the radar row it replaced: the row names the\n\t * tool and how much came back, and the phrase drops both to say the same\n\t * thing in prose. Summarising is only worth a lossy rewrite when there is\n\t * more than one call to fold — and by measurement most chains have exactly\n\t * one, so this is the common case, not an edge.\n\t */\n\tprivate isCollapsed(): boolean {\n\t\treturn this.view === \"radar\" && !this.opened && this.blocks.length > 1;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (!this.isCollapsed()) return super.render(width);\n\t\tif (this.memo && this.memo.width === width) return this.memo.out;\n\n\t\tconst entries = this.blocks.map((block) => block.chainEntry());\n\t\tconst running = this.state === \"running\";\n\t\tconst glyph = running ? \"◐\" : \"●\";\n\t\tconst glyphTone = entries.some((e) => e.isError) ? \"error\" : running ? \"warning\" : \"success\";\n\n\t\t// While it runs the chain shows its shape in order; once it is over, what\n\t\t// it amounted to. See tool-chain-summary.ts for why they differ.\n\t\tlet leftPlain: string;\n\t\tlet leftStyled: string;\n\t\tif (this.state === \"done\") {\n\t\t\tconst phrase = chainPhrase(entries);\n\t\t\tleftPlain = phrase;\n\t\t\tleftStyled = theme.fg(\"toolTitle\", phrase);\n\t\t} else {\n\t\t\tconst segments = chainSegments(entries);\n\t\t\tconst sep = \" › \";\n\t\t\tleftPlain = segments.map((s) => s.label).join(sep) + (running ? \"…\" : \"\");\n\t\t\tleftStyled =\n\t\t\t\tsegments\n\t\t\t\t\t.map((s) =>\n\t\t\t\t\t\ttheme.fg(s.tone === \"error\" ? \"error\" : s.tone === \"running\" ? \"warning\" : \"toolTitle\", s.label),\n\t\t\t\t\t)\n\t\t\t\t\t.join(theme.fg(\"dim\", sep)) + (running ? theme.fg(\"dim\", \"…\") : \"\");\n\t\t}\n\n\t\tconst stats = chainStats(entries, this.state);\n\t\t// The marker stroke runs over what you read and stops before the stats,\n\t\t// the same shape it takes on a per-call row.\n\t\tconst stroke = this.latest && theme.hasBg(\"activeToolBg\");\n\t\tconst mark = (text: string) => (stroke ? theme.bg(\"activeToolBg\", text) : text);\n\t\t// \" ● \" + left, then stats flush right when there is room for both.\n\t\tconst prefix = `${glyph} `;\n\t\tconst budget = Math.max(0, width - 1 - visibleWidth(prefix));\n\t\tlet line: string;\n\t\tif (visibleWidth(leftPlain) + 2 + visibleWidth(stats) <= budget) {\n\t\t\tconst pad = \" \".repeat(budget - visibleWidth(leftPlain) - visibleWidth(stats));\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${mark(leftStyled)}${pad}${theme.fg(\"muted\", stats)}`;\n\t\t} else {\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${mark(truncateToWidth(leftStyled, budget))}`;\n\t\t}\n\n\t\tconst out = [line, ...this.failureLines(width)];\n\t\tthis.memo = { width, out };\n\t\treturn out;\n\t}\n\n\t/**\n\t * Each failed call's reason, indented under the chain line.\n\t *\n\t * A collapsed chain hides its blocks, so without this a failure would be a\n\t * count in the stats and nothing else. Whatever else these views fold away,\n\t * they never fold away why something broke.\n\t */\n\tprivate failureLines(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const block of this.blocks) {\n\t\t\tconst entry = block.chainEntry();\n\t\t\tif (!entry.isError) continue;\n\t\t\tconst header = ` ${theme.fg(\"error\", \"✗\")} ${theme.fg(\"toolTitle\", entry.tool)} ${theme.fg(\"toolOutput\", entry.subject)}`;\n\t\t\tlines.push(truncateToWidth(header, width));\n\t\t\tfor (const raw of block.errorText().split(\"\\n\")) {\n\t\t\t\tif (!raw.trim()) continue;\n\t\t\t\tlines.push(truncateToWidth(` ${theme.fg(\"toolOutput\", raw)}`, width));\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/** Narrow a transcript child to a chain. */\nexport function isToolChain(child: Component): child is ToolChainComponent {\n\treturn child instanceof ToolChainComponent;\n}\n"]}
|
|
@@ -97,12 +97,20 @@ export declare class ToolExecutionComponent extends Container {
|
|
|
97
97
|
* call renderer. A revealed block leaves radar for as long as it is open, so
|
|
98
98
|
* "expand" means the same thing in every view.
|
|
99
99
|
*/
|
|
100
|
+
private isLatest;
|
|
100
101
|
private shouldShowSignalLine;
|
|
101
102
|
/**
|
|
102
103
|
* A failure's body under a radar row hangs off the row; anywhere else it sits
|
|
103
104
|
* directly under its own call line and needs no extra indent.
|
|
104
105
|
*/
|
|
105
106
|
private indentUnderSignalRow;
|
|
107
|
+
/**
|
|
108
|
+
* Mark this block as the newest call in the transcript, or no longer it.
|
|
109
|
+
*
|
|
110
|
+
* Only radar draws anything from it: the other two views already show each
|
|
111
|
+
* call in full, so there is nothing for a "where am I" mark to add.
|
|
112
|
+
*/
|
|
113
|
+
setLatest(isLatest: boolean): void;
|
|
106
114
|
private getSignalComponent;
|
|
107
115
|
setShowImages(show: boolean): void;
|
|
108
116
|
setImageWidthCells(width: number): 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 {\n\tBox,\n\ttype Component,\n\tContainer,\n\tgetCapabilities,\n\tImage,\n\tisImageLine,\n\tSpacer,\n\tText,\n\ttype TUI,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@kolisachint/hoocode-tui\";\nimport type { ToolDefinition, ToolRenderContext } from \"../../../core/extensions/types.js\";\nimport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\nimport { createAllToolDefinitions, type ToolName } from \"../../../core/tools/index.js\";\nimport { getTextOutput as getRenderedTextOutput } from \"../../../core/tools/render-utils.js\";\nimport { convertToPng } from \"../../../utils/image-convert.js\";\nimport { theme } from \"../theme/theme.js\";\nimport type { ChainEntry } from \"./tool-chain-summary.js\";\nimport { ToolSignalComponent, toolSubject } from \"./tool-signal.js\";\n\n/**\n * Re-exported so tool-block callers keep a single import site for the view\n * dial; the type itself lives in core (the settings manager persists it).\n */\nexport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean;\n\timageWidthCells?: number;\n\tview?: ToolOutputView;\n}\n\n/**\n * Renders a child component and prepends a prefix (e.g. a status dot) to its\n * first line, indenting continuation lines so they align under the content.\n *\n * This keeps the dot inline with the first line instead of stacking it on its\n * own line (which happens when a Container holds the dot as a separate child).\n */\nclass PrefixFirstLine implements Component {\n\tprivate prefix: string;\n\tprivate child: Component;\n\tprivate indentWidth: number;\n\t// Keyed on the child's returned array identity so an unchanged child keeps\n\t// this wrapper reference-stable too (parents memoize flattening by ref).\n\tprivate memo?: { src: string[]; width: number; out: string[] };\n\n\tconstructor(prefix: string, child: Component) {\n\t\tthis.prefix = prefix;\n\t\tthis.child = child;\n\t\tthis.indentWidth = visibleWidth(prefix);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.child.invalidate?.();\n\t\tthis.memo = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Reserve room for the prefix so the child wraps within the remaining width.\n\t\tconst childWidth = Math.max(1, width - this.indentWidth);\n\t\tconst lines = this.child.render(childWidth);\n\t\tif (this.memo && this.memo.src === lines && this.memo.width === width) {\n\t\t\treturn this.memo.out;\n\t\t}\n\t\tconst indent = \" \".repeat(this.indentWidth);\n\t\tconst out =\n\t\t\tlines.length === 0 ? [this.prefix] : lines.map((line, i) => (i === 0 ? this.prefix + line : indent + line));\n\t\tthis.memo = { src: lines, width, out };\n\t\treturn out;\n\t}\n}\n\n/**\n * Indents every line of a child, unlike PrefixFirstLine which only offsets the\n * first. Used for a failure's body under a radar row, so the reason reads as\n * hanging off the row rather than starting a new column.\n */\nclass IndentAll implements Component {\n\tprivate child: Component;\n\tprivate indent: string;\n\tprivate memo?: { src: string[]; width: number; out: string[] };\n\n\tconstructor(child: Component, width: number) {\n\t\tthis.child = child;\n\t\tthis.indent = \" \".repeat(width);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.child.invalidate?.();\n\t\tthis.memo = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines = this.child.render(Math.max(1, width - this.indent.length));\n\t\tif (this.memo && this.memo.src === lines && this.memo.width === width) return this.memo.out;\n\t\tconst out = lines.map((line) => (line.trim() === \"\" ? line : this.indent + line));\n\t\tthis.memo = { src: lines, width, out };\n\t\treturn out;\n\t}\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: any = {};\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate toolCallId: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\t// Persisted view (radar/glance/full). Controls how the call line is drawn and\n\t// whether the result body renders at all; `revealed` is the per-block override\n\t// the global expand key flips for radar/glance blocks.\n\tprivate view: ToolOutputView;\n\tprivate revealed = false;\n\tprivate signalComponent?: ToolSignalComponent;\n\tprivate leadingSpacer: Spacer;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition<any, any>;\n\tprivate builtInToolDefinition?: ToolDefinition<any, any>;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate executionStarted = false;\n\tprivate argsComplete = false;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\tprivate convertedImages: Map<number, { data: string; mimeType: string }> = new Map();\n\tprivate hideComponent = false;\n\t// Once a finished tool block has scrolled far out of view the interactive mode\n\t// freezes it (see freeze()): its already-rendered lines are captured and the\n\t// heavy source payloads (full tool result, base64 image copies, per-renderer\n\t// state, child component caches) are released. A frozen block is immutable and\n\t// no longer reflows on resize — old off-screen scrollback keeps its wrapping,\n\t// which self-heals on any full rebuild (theme toggle / session reload, both of\n\t// which reconstruct components from the intact session data).\n\tprivate frozen = false;\n\tprivate frozenLines?: string[];\n\t/** Width the frozen snapshot was captured at; a wider terminal is safe as-is,\n\t * a narrower one needs re-truncation to avoid the TUI's over-width crash guard. */\n\tprivate frozenWidth = 0;\n\tprivate frozenTruncated?: string[];\n\tprivate frozenTruncatedWidth = -1;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: any,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition<any, any> | 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\tthis.toolDefinition = toolDefinition;\n\t\tthis.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.view = options.view ?? \"glance\";\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\t// The separator between consecutive tool blocks. Radar drops it: rows that\n\t\t// are one line each only read as a map when they stack without gaps.\n\t\tthis.leadingSpacer = new Spacer(1);\n\t\tthis.addChild(this.leadingSpacer);\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\t// Boxless: no filled background — hierarchy comes from status dots + indent.\n\t\t// paddingY is 0: the single leading Spacer(1) is the only separator between\n\t\t// tool blocks, so consecutive commands don't stack 3 blank lines between them.\n\t\tthis.contentBox = new Box(1, 0);\n\t\tthis.contentText = new Text(\"\", 1, 0);\n\t\tthis.selfRenderContainer = new Container();\n\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.addChild(this.activeShell());\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<any, any>[\"renderCall\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderCall;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderCall;\n\t\t}\n\t\treturn this.toolDefinition.renderCall ?? this.builtInToolDefinition.renderCall;\n\t}\n\n\tprivate getResultRenderer(): ToolDefinition<any, any>[\"renderResult\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderResult;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderResult;\n\t\t}\n\t\treturn this.toolDefinition.renderResult ?? this.builtInToolDefinition.renderResult;\n\t}\n\n\tprivate hasRendererDefinition(): boolean {\n\t\treturn this.builtInToolDefinition !== undefined || this.toolDefinition !== undefined;\n\t}\n\n\tprivate getRenderShell(): \"default\" | \"self\" {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderShell ?? \"default\";\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderShell ?? \"default\";\n\t\t}\n\t\treturn this.toolDefinition.renderShell ?? this.builtInToolDefinition.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(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 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\t\treturn new Text(theme.fg(\"toolOutput\", output), 0, 0);\n\t}\n\n\tupdateArgs(args: any): 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: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\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\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\tif (img.mimeType === \"image/png\") continue;\n\t\t\tif (this.convertedImages.has(i)) continue;\n\n\t\t\tconst index = i;\n\t\t\tconvertToPng(img.data, img.mimeType).then((converted) => {\n\t\t\t\tif (this.frozen) return;\n\t\t\t\tif (converted) {\n\t\t\t\t\tthis.convertedImages.set(index, converted);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\t// For radar/glance blocks the same global toggle reveals the hidden body;\n\t\t// for full blocks `expanded` alone switches truncated ↔ everything.\n\t\tthis.revealed = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetView(view: ToolOutputView): void {\n\t\tthis.view = view;\n\t\tthis.updateDisplay();\n\t}\n\n\t/**\n\t * Whether this block is currently opened up.\n\t *\n\t * Read by the one-at-a-time unfold, which walks back from the newest block\n\t * to find the first one still folded.\n\t */\n\tisRevealed(): boolean {\n\t\treturn this.revealed;\n\t}\n\n\t/** This call's contribution to its chain's summary line. */\n\tchainEntry(): ChainEntry {\n\t\tconst output = this.result ? this.getTextOutput() : \"\";\n\t\treturn {\n\t\t\ttool: this.toolName,\n\t\t\tsubject: toolSubject(this.args, this.cwd),\n\t\t\tisError: this.result?.isError === true,\n\t\t\tisPartial: this.result === undefined || this.isPartial,\n\t\t\toutputLines: output.trim() ? output.split(\"\\n\").length : 0,\n\t\t};\n\t}\n\n\t/** The failure text, for a collapsed chain to print under its line. */\n\terrorText(): string {\n\t\treturn this.result?.isError ? this.getTextOutput() : \"\";\n\t}\n\n\t/**\n\t * Whether the result body should render.\n\t *\n\t * A failure always shows its reason, in every view. Everything else about\n\t * these views is a judgement about how much detail you want; why something\n\t * broke is not detail, and a red dot with no explanation was the one thing\n\t * the folded views got wrong.\n\t */\n\tprivate shouldShowBody(): boolean {\n\t\treturn this.view === \"full\" || this.revealed || this.result?.isError === true;\n\t}\n\n\t/**\n\t * Whether this block draws its radar signal line instead of the tool's own\n\t * call renderer. A revealed block leaves radar for as long as it is open, so\n\t * \"expand\" means the same thing in every view.\n\t */\n\tprivate shouldShowSignalLine(): boolean {\n\t\treturn this.view === \"radar\" && !this.revealed;\n\t}\n\n\t/**\n\t * A failure's body under a radar row hangs off the row; anywhere else it sits\n\t * directly under its own call line and needs no extra indent.\n\t */\n\tprivate indentUnderSignalRow(component: Component): Component {\n\t\treturn this.shouldShowSignalLine() ? new IndentAll(component, 3) : component;\n\t}\n\n\tprivate getSignalComponent(): ToolSignalComponent {\n\t\tconst input = {\n\t\t\ttoolName: this.toolName,\n\t\t\targs: this.args,\n\t\t\tcwd: this.cwd,\n\t\t\tresult: this.result,\n\t\t\tisPartial: this.isPartial,\n\t\t\tshowImages: this.showImages,\n\t\t};\n\t\tif (this.signalComponent) {\n\t\t\tthis.signalComponent.setInput(input);\n\t\t} else {\n\t\t\tthis.signalComponent = new ToolSignalComponent(input);\n\t\t}\n\t\treturn this.signalComponent;\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\t// A frozen block has released the state updateDisplay would rebuild from;\n\t\t// its captured snapshot is authoritative, so skip the rebuild entirely.\n\t\tif (this.frozen) return;\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\t/**\n\t * True when this block is a finished, visible tool result that still holds its\n\t * heavy source payloads — i.e. a candidate for freeze() once it scrolls out of\n\t * the live window. In-flight (partial) and already-frozen blocks are excluded.\n\t */\n\tisFreezable(): boolean {\n\t\treturn !this.frozen && !this.isPartial && !this.hideComponent && !!this.result;\n\t}\n\n\t/** Mark this block for freezing; the snapshot is captured on the next render\n\t * (at the real render width) and the heavy state released then. */\n\tfreeze(): void {\n\t\tif (this.isFreezable()) this.frozen = true;\n\t}\n\n\tprivate releaseHeavyState(): void {\n\t\tthis.result = undefined;\n\t\tthis.convertedImages.clear();\n\t\tthis.imageComponents = [];\n\t\tthis.imageSpacers = [];\n\t\tthis.rendererState = {};\n\t\tthis.callRendererComponent = undefined;\n\t\tthis.resultRendererComponent = undefined;\n\t\t// Drop child components so their line caches (which embed base64 image\n\t\t// payloads and full text output) become collectable.\n\t\tthis.clear();\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hideComponent) {\n\t\t\treturn [];\n\t\t}\n\t\tif (this.frozenLines) {\n\t\t\t// Captured snapshot. Wider (or equal) terminal: emit as-is. Narrower:\n\t\t\t// re-truncate non-image lines to avoid the TUI's over-width crash guard,\n\t\t\t// caching the result per width so it stays O(1) on unchanged frames.\n\t\t\tif (width >= this.frozenWidth) return this.frozenLines;\n\t\t\tif (this.frozenTruncatedWidth === width && this.frozenTruncated) return this.frozenTruncated;\n\t\t\tthis.frozenTruncated = this.frozenLines.map((line) =>\n\t\t\t\tisImageLine(line) || visibleWidth(line) <= width ? line : truncateToWidth(line, width),\n\t\t\t);\n\t\t\tthis.frozenTruncatedWidth = width;\n\t\t\treturn this.frozenTruncated;\n\t\t}\n\t\tconst lines = super.render(width);\n\t\tif (this.frozen) {\n\t\t\tthis.frozenLines = lines;\n\t\t\tthis.frozenWidth = width;\n\t\t\tthis.releaseHeavyState();\n\t\t}\n\t\treturn lines;\n\t}\n\n\t/**\n\t * The container this block renders into.\n\t *\n\t * A self-rendering tool (`edit` draws its own framed diff) normally gets the\n\t * bare container so its frame can use the full width; everything else gets\n\t * the padded box. That only matters while the body is on screen. With the\n\t * body folded away the block is a single line, and the unpadded container\n\t * put it one column left of every other row — invisible while `peek` was an\n\t * opt-in, glaring now that `glance` is the default. So: padded box whenever\n\t * nothing is expanded, which also gives radar the uniform rows it exists for.\n\t */\n\tprivate activeShell(): Box | Container {\n\t\tif (!this.shouldShowBody()) return this.contentBox;\n\t\treturn this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox;\n\t}\n\n\t/** Swap the attached shell when the view changes which one applies. */\n\tprivate syncShellChild(): void {\n\t\tconst wanted = this.activeShell();\n\t\tconst other = wanted === this.contentBox ? this.selfRenderContainer : this.contentBox;\n\t\tif (this.children.includes(wanted)) return;\n\t\tthis.removeChild(other);\n\t\tother.clear();\n\t\tthis.addChild(wanted);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\t// Frozen blocks are immutable snapshots with their source state released.\n\t\tif (this.frozen) return;\n\t\tlet hasContent = false;\n\t\tthis.hideComponent = false;\n\t\tthis.leadingSpacer.setLines(this.shouldShowSignalLine() ? 0 : 1);\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.syncShellChild();\n\t\t\tconst renderContainer = this.activeShell();\n\t\t\t// Boxless: no background fill on the container.\n\t\t\tif (renderContainer instanceof Box) {\n\t\t\t\trenderContainer.setBgFn(undefined);\n\t\t\t}\n\t\t\trenderContainer.clear();\n\n\t\t\t// Status dot prefix: green for complete success, yellow for pending/partial, red for error.\n\t\t\t// The dot is prepended to the first line of the call renderer so it stays inline\n\t\t\t// (adding it as a separate child would stack it on its own line).\n\t\t\tconst dotColor = this.result?.isError ? \"error\" : this.isPartial ? \"warning\" : \"success\";\n\t\t\t// No disclosure caret. It is a click-target idiom in a TUI with no\n\t\t\t// pointer, it cost two columns on every row for an action that could\n\t\t\t// only ever be reached by keyboard, and reading it as \"this row opens\"\n\t\t\t// set an expectation the view does not meet.\n\t\t\tconst dot = theme.fg(dotColor, \"● \");\n\n\t\t\t// Radar replaces the tool's own call renderer with the uniform signal\n\t\t\t// row, so a screen of mixed tools lines up into one readable map.\n\t\t\tif (this.shouldShowSignalLine()) {\n\t\t\t\trenderContainer.addChild(new PrefixFirstLine(dot, this.getSignalComponent()));\n\t\t\t\thasContent = true;\n\t\t\t} else {\n\t\t\t\tconst callRenderer = this.getCallRenderer();\n\t\t\t\tif (!callRenderer) {\n\t\t\t\t\trenderContainer.addChild(new PrefixFirstLine(dot, this.createCallFallback()));\n\t\t\t\t\thasContent = true;\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));\n\t\t\t\t\t\tthis.callRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(new PrefixFirstLine(dot, component));\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthis.callRendererComponent = undefined;\n\t\t\t\t\t\trenderContainer.addChild(new PrefixFirstLine(dot, this.createCallFallback()));\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.result && this.shouldShowBody()) {\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.indentUnderSignalRow(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 as any, 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\tthis.resultRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(this.indentUnderSignalRow(component));\n\t\t\t\t\t\thasContent = true;\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\t// Boxless: no background fill.\n\t\t\tthis.contentText.setCustomBgFn(undefined);\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 && !this.shouldShowSignalLine()) {\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 converted = this.convertedImages.get(i);\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\tconst dotColor = this.result?.isError ? \"error\" : this.isPartial ? \"warning\" : \"success\";\n\t\t// A tool with no renderer still gets a radar row, just without the\n\t\t// flush-right signal column (this path has no width to align against).\n\t\tif (this.shouldShowSignalLine()) {\n\t\t\tconst subject = toolSubject(this.args, this.cwd);\n\t\t\treturn (\n\t\t\t\ttheme.fg(dotColor, \"● \") +\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(this.toolName)) +\n\t\t\t\t(subject ? ` ${theme.fg(\"toolOutput\", subject)}` : \"\")\n\t\t\t);\n\t\t}\n\t\tlet text = theme.fg(dotColor, \"● \") + 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.shouldShowBody() ? this.getTextOutput() : \"\";\n\t\tif (output) {\n\t\t\ttext += `\\n${output}`;\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 {\n\tBox,\n\ttype Component,\n\tContainer,\n\tgetCapabilities,\n\tImage,\n\tisImageLine,\n\tSpacer,\n\tText,\n\ttype TUI,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@kolisachint/hoocode-tui\";\nimport type { ToolDefinition, ToolRenderContext } from \"../../../core/extensions/types.js\";\nimport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\nimport { createAllToolDefinitions, type ToolName } from \"../../../core/tools/index.js\";\nimport { getTextOutput as getRenderedTextOutput } from \"../../../core/tools/render-utils.js\";\nimport { convertToPng } from \"../../../utils/image-convert.js\";\nimport { theme } from \"../theme/theme.js\";\nimport type { ChainEntry } from \"./tool-chain-summary.js\";\nimport { ToolSignalComponent, toolSubject } from \"./tool-signal.js\";\n\n/**\n * Re-exported so tool-block callers keep a single import site for the view\n * dial; the type itself lives in core (the settings manager persists it).\n */\nexport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean;\n\timageWidthCells?: number;\n\tview?: ToolOutputView;\n}\n\n/**\n * Renders a child component and prepends a prefix (e.g. a status dot) to its\n * first line, indenting continuation lines so they align under the content.\n *\n * This keeps the dot inline with the first line instead of stacking it on its\n * own line (which happens when a Container holds the dot as a separate child).\n */\nclass PrefixFirstLine implements Component {\n\tprivate prefix: string;\n\tprivate child: Component;\n\tprivate indentWidth: number;\n\t// Keyed on the child's returned array identity so an unchanged child keeps\n\t// this wrapper reference-stable too (parents memoize flattening by ref).\n\tprivate memo?: { src: string[]; width: number; out: string[] };\n\n\tconstructor(prefix: string, child: Component) {\n\t\tthis.prefix = prefix;\n\t\tthis.child = child;\n\t\tthis.indentWidth = visibleWidth(prefix);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.child.invalidate?.();\n\t\tthis.memo = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Reserve room for the prefix so the child wraps within the remaining width.\n\t\tconst childWidth = Math.max(1, width - this.indentWidth);\n\t\tconst lines = this.child.render(childWidth);\n\t\tif (this.memo && this.memo.src === lines && this.memo.width === width) {\n\t\t\treturn this.memo.out;\n\t\t}\n\t\tconst indent = \" \".repeat(this.indentWidth);\n\t\tconst out =\n\t\t\tlines.length === 0 ? [this.prefix] : lines.map((line, i) => (i === 0 ? this.prefix + line : indent + line));\n\t\tthis.memo = { src: lines, width, out };\n\t\treturn out;\n\t}\n}\n\n/**\n * Indents every line of a child, unlike PrefixFirstLine which only offsets the\n * first. Used for a failure's body under a radar row, so the reason reads as\n * hanging off the row rather than starting a new column.\n */\nclass IndentAll implements Component {\n\tprivate child: Component;\n\tprivate indent: string;\n\tprivate memo?: { src: string[]; width: number; out: string[] };\n\n\tconstructor(child: Component, width: number) {\n\t\tthis.child = child;\n\t\tthis.indent = \" \".repeat(width);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.child.invalidate?.();\n\t\tthis.memo = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines = this.child.render(Math.max(1, width - this.indent.length));\n\t\tif (this.memo && this.memo.src === lines && this.memo.width === width) return this.memo.out;\n\t\tconst out = lines.map((line) => (line.trim() === \"\" ? line : this.indent + line));\n\t\tthis.memo = { src: lines, width, out };\n\t\treturn out;\n\t}\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: any = {};\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate toolCallId: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\t// Persisted view (radar/glance/full). Controls how the call line is drawn and\n\t// whether the result body renders at all; `revealed` is the per-block override\n\t// the global expand key flips for radar/glance blocks.\n\tprivate view: ToolOutputView;\n\tprivate revealed = false;\n\tprivate signalComponent?: ToolSignalComponent;\n\tprivate leadingSpacer: Spacer;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition<any, any>;\n\tprivate builtInToolDefinition?: ToolDefinition<any, any>;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate executionStarted = false;\n\tprivate argsComplete = false;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\tprivate convertedImages: Map<number, { data: string; mimeType: string }> = new Map();\n\tprivate hideComponent = false;\n\t// Once a finished tool block has scrolled far out of view the interactive mode\n\t// freezes it (see freeze()): its already-rendered lines are captured and the\n\t// heavy source payloads (full tool result, base64 image copies, per-renderer\n\t// state, child component caches) are released. A frozen block is immutable and\n\t// no longer reflows on resize — old off-screen scrollback keeps its wrapping,\n\t// which self-heals on any full rebuild (theme toggle / session reload, both of\n\t// which reconstruct components from the intact session data).\n\tprivate frozen = false;\n\tprivate frozenLines?: string[];\n\t/** Width the frozen snapshot was captured at; a wider terminal is safe as-is,\n\t * a narrower one needs re-truncation to avoid the TUI's over-width crash guard. */\n\tprivate frozenWidth = 0;\n\tprivate frozenTruncated?: string[];\n\tprivate frozenTruncatedWidth = -1;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: any,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition<any, any> | 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\tthis.toolDefinition = toolDefinition;\n\t\tthis.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.view = options.view ?? \"glance\";\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\t// The separator between consecutive tool blocks. Radar drops it: rows that\n\t\t// are one line each only read as a map when they stack without gaps.\n\t\tthis.leadingSpacer = new Spacer(1);\n\t\tthis.addChild(this.leadingSpacer);\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\t// Boxless: no filled background — hierarchy comes from status dots + indent.\n\t\t// paddingY is 0: the single leading Spacer(1) is the only separator between\n\t\t// tool blocks, so consecutive commands don't stack 3 blank lines between them.\n\t\tthis.contentBox = new Box(1, 0);\n\t\tthis.contentText = new Text(\"\", 1, 0);\n\t\tthis.selfRenderContainer = new Container();\n\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.addChild(this.activeShell());\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<any, any>[\"renderCall\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderCall;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderCall;\n\t\t}\n\t\treturn this.toolDefinition.renderCall ?? this.builtInToolDefinition.renderCall;\n\t}\n\n\tprivate getResultRenderer(): ToolDefinition<any, any>[\"renderResult\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderResult;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderResult;\n\t\t}\n\t\treturn this.toolDefinition.renderResult ?? this.builtInToolDefinition.renderResult;\n\t}\n\n\tprivate hasRendererDefinition(): boolean {\n\t\treturn this.builtInToolDefinition !== undefined || this.toolDefinition !== undefined;\n\t}\n\n\tprivate getRenderShell(): \"default\" | \"self\" {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderShell ?? \"default\";\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderShell ?? \"default\";\n\t\t}\n\t\treturn this.toolDefinition.renderShell ?? this.builtInToolDefinition.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(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 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\t\treturn new Text(theme.fg(\"toolOutput\", output), 0, 0);\n\t}\n\n\tupdateArgs(args: any): 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: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\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\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\tif (img.mimeType === \"image/png\") continue;\n\t\t\tif (this.convertedImages.has(i)) continue;\n\n\t\t\tconst index = i;\n\t\t\tconvertToPng(img.data, img.mimeType).then((converted) => {\n\t\t\t\tif (this.frozen) return;\n\t\t\t\tif (converted) {\n\t\t\t\t\tthis.convertedImages.set(index, converted);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\t// For radar/glance blocks the same global toggle reveals the hidden body;\n\t\t// for full blocks `expanded` alone switches truncated ↔ everything.\n\t\tthis.revealed = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetView(view: ToolOutputView): void {\n\t\tthis.view = view;\n\t\tthis.updateDisplay();\n\t}\n\n\t/**\n\t * Whether this block is currently opened up.\n\t *\n\t * Read by the one-at-a-time unfold, which walks back from the newest block\n\t * to find the first one still folded.\n\t */\n\tisRevealed(): boolean {\n\t\treturn this.revealed;\n\t}\n\n\t/** This call's contribution to its chain's summary line. */\n\tchainEntry(): ChainEntry {\n\t\tconst output = this.result ? this.getTextOutput() : \"\";\n\t\treturn {\n\t\t\ttool: this.toolName,\n\t\t\tsubject: toolSubject(this.args, this.cwd),\n\t\t\tisError: this.result?.isError === true,\n\t\t\tisPartial: this.result === undefined || this.isPartial,\n\t\t\toutputLines: output.trim() ? output.split(\"\\n\").length : 0,\n\t\t};\n\t}\n\n\t/** The failure text, for a collapsed chain to print under its line. */\n\terrorText(): string {\n\t\treturn this.result?.isError ? this.getTextOutput() : \"\";\n\t}\n\n\t/**\n\t * Whether the result body should render.\n\t *\n\t * A failure always shows its reason, in every view. Everything else about\n\t * these views is a judgement about how much detail you want; why something\n\t * broke is not detail, and a red dot with no explanation was the one thing\n\t * the folded views got wrong.\n\t */\n\tprivate shouldShowBody(): boolean {\n\t\treturn this.view === \"full\" || this.revealed || this.result?.isError === true;\n\t}\n\n\t/**\n\t * Whether this block draws its radar signal line instead of the tool's own\n\t * call renderer. A revealed block leaves radar for as long as it is open, so\n\t * \"expand\" means the same thing in every view.\n\t */\n\tprivate isLatest = false;\n\n\tprivate shouldShowSignalLine(): boolean {\n\t\treturn this.view === \"radar\" && !this.revealed;\n\t}\n\n\t/**\n\t * A failure's body under a radar row hangs off the row; anywhere else it sits\n\t * directly under its own call line and needs no extra indent.\n\t */\n\tprivate indentUnderSignalRow(component: Component): Component {\n\t\treturn this.shouldShowSignalLine() ? new IndentAll(component, 3) : component;\n\t}\n\n\t/**\n\t * Mark this block as the newest call in the transcript, or no longer it.\n\t *\n\t * Only radar draws anything from it: the other two views already show each\n\t * call in full, so there is nothing for a \"where am I\" mark to add.\n\t */\n\tsetLatest(isLatest: boolean): void {\n\t\tif (this.isLatest === isLatest) return;\n\t\tthis.isLatest = isLatest;\n\t\tthis.signalComponent?.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getSignalComponent(): ToolSignalComponent {\n\t\tconst input = {\n\t\t\ttoolName: this.toolName,\n\t\t\targs: this.args,\n\t\t\tcwd: this.cwd,\n\t\t\tresult: this.result,\n\t\t\tisPartial: this.isPartial,\n\t\t\tshowImages: this.showImages,\n\t\t\tisLatest: this.isLatest,\n\t\t};\n\t\tif (this.signalComponent) {\n\t\t\tthis.signalComponent.setInput(input);\n\t\t} else {\n\t\t\tthis.signalComponent = new ToolSignalComponent(input);\n\t\t}\n\t\treturn this.signalComponent;\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\t// A frozen block has released the state updateDisplay would rebuild from;\n\t\t// its captured snapshot is authoritative, so skip the rebuild entirely.\n\t\tif (this.frozen) return;\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\t/**\n\t * True when this block is a finished, visible tool result that still holds its\n\t * heavy source payloads — i.e. a candidate for freeze() once it scrolls out of\n\t * the live window. In-flight (partial) and already-frozen blocks are excluded.\n\t */\n\tisFreezable(): boolean {\n\t\treturn !this.frozen && !this.isPartial && !this.hideComponent && !!this.result;\n\t}\n\n\t/** Mark this block for freezing; the snapshot is captured on the next render\n\t * (at the real render width) and the heavy state released then. */\n\tfreeze(): void {\n\t\tif (this.isFreezable()) this.frozen = true;\n\t}\n\n\tprivate releaseHeavyState(): void {\n\t\tthis.result = undefined;\n\t\tthis.convertedImages.clear();\n\t\tthis.imageComponents = [];\n\t\tthis.imageSpacers = [];\n\t\tthis.rendererState = {};\n\t\tthis.callRendererComponent = undefined;\n\t\tthis.resultRendererComponent = undefined;\n\t\t// Drop child components so their line caches (which embed base64 image\n\t\t// payloads and full text output) become collectable.\n\t\tthis.clear();\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hideComponent) {\n\t\t\treturn [];\n\t\t}\n\t\tif (this.frozenLines) {\n\t\t\t// Captured snapshot. Wider (or equal) terminal: emit as-is. Narrower:\n\t\t\t// re-truncate non-image lines to avoid the TUI's over-width crash guard,\n\t\t\t// caching the result per width so it stays O(1) on unchanged frames.\n\t\t\tif (width >= this.frozenWidth) return this.frozenLines;\n\t\t\tif (this.frozenTruncatedWidth === width && this.frozenTruncated) return this.frozenTruncated;\n\t\t\tthis.frozenTruncated = this.frozenLines.map((line) =>\n\t\t\t\tisImageLine(line) || visibleWidth(line) <= width ? line : truncateToWidth(line, width),\n\t\t\t);\n\t\t\tthis.frozenTruncatedWidth = width;\n\t\t\treturn this.frozenTruncated;\n\t\t}\n\t\tconst lines = super.render(width);\n\t\tif (this.frozen) {\n\t\t\tthis.frozenLines = lines;\n\t\t\tthis.frozenWidth = width;\n\t\t\tthis.releaseHeavyState();\n\t\t}\n\t\treturn lines;\n\t}\n\n\t/**\n\t * The container this block renders into.\n\t *\n\t * A self-rendering tool (`edit` draws its own framed diff) normally gets the\n\t * bare container so its frame can use the full width; everything else gets\n\t * the padded box. That only matters while the body is on screen. With the\n\t * body folded away the block is a single line, and the unpadded container\n\t * put it one column left of every other row — invisible while `peek` was an\n\t * opt-in, glaring now that `glance` is the default. So: padded box whenever\n\t * nothing is expanded, which also gives radar the uniform rows it exists for.\n\t */\n\tprivate activeShell(): Box | Container {\n\t\tif (!this.shouldShowBody()) return this.contentBox;\n\t\treturn this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox;\n\t}\n\n\t/** Swap the attached shell when the view changes which one applies. */\n\tprivate syncShellChild(): void {\n\t\tconst wanted = this.activeShell();\n\t\tconst other = wanted === this.contentBox ? this.selfRenderContainer : this.contentBox;\n\t\tif (this.children.includes(wanted)) return;\n\t\tthis.removeChild(other);\n\t\tother.clear();\n\t\tthis.addChild(wanted);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\t// Frozen blocks are immutable snapshots with their source state released.\n\t\tif (this.frozen) return;\n\t\tlet hasContent = false;\n\t\tthis.hideComponent = false;\n\t\tthis.leadingSpacer.setLines(this.shouldShowSignalLine() ? 0 : 1);\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.syncShellChild();\n\t\t\tconst renderContainer = this.activeShell();\n\t\t\t// Boxless: no background fill on the container.\n\t\t\tif (renderContainer instanceof Box) {\n\t\t\t\trenderContainer.setBgFn(undefined);\n\t\t\t}\n\t\t\trenderContainer.clear();\n\n\t\t\t// Status dot prefix: green for complete success, yellow for pending/partial, red for error.\n\t\t\t// The dot is prepended to the first line of the call renderer so it stays inline\n\t\t\t// (adding it as a separate child would stack it on its own line).\n\t\t\tconst dotColor = this.result?.isError ? \"error\" : this.isPartial ? \"warning\" : \"success\";\n\t\t\t// No disclosure caret. It is a click-target idiom in a TUI with no\n\t\t\t// pointer, it cost two columns on every row for an action that could\n\t\t\t// only ever be reached by keyboard, and reading it as \"this row opens\"\n\t\t\t// set an expectation the view does not meet.\n\t\t\tconst dot = theme.fg(dotColor, \"● \");\n\n\t\t\t// Radar replaces the tool's own call renderer with the uniform signal\n\t\t\t// row, so a screen of mixed tools lines up into one readable map.\n\t\t\tif (this.shouldShowSignalLine()) {\n\t\t\t\trenderContainer.addChild(new PrefixFirstLine(dot, this.getSignalComponent()));\n\t\t\t\thasContent = true;\n\t\t\t} else {\n\t\t\t\tconst callRenderer = this.getCallRenderer();\n\t\t\t\tif (!callRenderer) {\n\t\t\t\t\trenderContainer.addChild(new PrefixFirstLine(dot, this.createCallFallback()));\n\t\t\t\t\thasContent = true;\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));\n\t\t\t\t\t\tthis.callRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(new PrefixFirstLine(dot, component));\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthis.callRendererComponent = undefined;\n\t\t\t\t\t\trenderContainer.addChild(new PrefixFirstLine(dot, this.createCallFallback()));\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.result && this.shouldShowBody()) {\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.indentUnderSignalRow(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 as any, 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\tthis.resultRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(this.indentUnderSignalRow(component));\n\t\t\t\t\t\thasContent = true;\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\t// Boxless: no background fill.\n\t\t\tthis.contentText.setCustomBgFn(undefined);\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 && !this.shouldShowSignalLine()) {\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 converted = this.convertedImages.get(i);\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\tconst dotColor = this.result?.isError ? \"error\" : this.isPartial ? \"warning\" : \"success\";\n\t\t// A tool with no renderer still gets a radar row, just without the\n\t\t// flush-right signal column (this path has no width to align against).\n\t\tif (this.shouldShowSignalLine()) {\n\t\t\tconst subject = toolSubject(this.args, this.cwd);\n\t\t\treturn (\n\t\t\t\ttheme.fg(dotColor, \"● \") +\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(this.toolName)) +\n\t\t\t\t(subject ? ` ${theme.fg(\"toolOutput\", subject)}` : \"\")\n\t\t\t);\n\t\t}\n\t\tlet text = theme.fg(dotColor, \"● \") + 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.shouldShowBody() ? this.getTextOutput() : \"\";\n\t\tif (output) {\n\t\t\ttext += `\\n${output}`;\n\t\t}\n\t\treturn text;\n\t}\n}\n"]}
|
|
@@ -302,6 +302,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
302
302
|
* call renderer. A revealed block leaves radar for as long as it is open, so
|
|
303
303
|
* "expand" means the same thing in every view.
|
|
304
304
|
*/
|
|
305
|
+
isLatest = false;
|
|
305
306
|
shouldShowSignalLine() {
|
|
306
307
|
return this.view === "radar" && !this.revealed;
|
|
307
308
|
}
|
|
@@ -312,6 +313,19 @@ export class ToolExecutionComponent extends Container {
|
|
|
312
313
|
indentUnderSignalRow(component) {
|
|
313
314
|
return this.shouldShowSignalLine() ? new IndentAll(component, 3) : component;
|
|
314
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Mark this block as the newest call in the transcript, or no longer it.
|
|
318
|
+
*
|
|
319
|
+
* Only radar draws anything from it: the other two views already show each
|
|
320
|
+
* call in full, so there is nothing for a "where am I" mark to add.
|
|
321
|
+
*/
|
|
322
|
+
setLatest(isLatest) {
|
|
323
|
+
if (this.isLatest === isLatest)
|
|
324
|
+
return;
|
|
325
|
+
this.isLatest = isLatest;
|
|
326
|
+
this.signalComponent?.invalidate();
|
|
327
|
+
this.updateDisplay();
|
|
328
|
+
}
|
|
315
329
|
getSignalComponent() {
|
|
316
330
|
const input = {
|
|
317
331
|
toolName: this.toolName,
|
|
@@ -320,6 +334,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
320
334
|
result: this.result,
|
|
321
335
|
isPartial: this.isPartial,
|
|
322
336
|
showImages: this.showImages,
|
|
337
|
+
isLatest: this.isLatest,
|
|
323
338
|
};
|
|
324
339
|
if (this.signalComponent) {
|
|
325
340
|
this.signalComponent.setInput(input);
|