@kolisachint/hoocode-agent 0.5.33 → 0.5.34

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.34] - 2026-08-23
4
+
5
+ ### Fixed
6
+
7
+ - Radar hides thinking traces outright rather than folding them to their
8
+ one-line label. Folding is right when the label sits between things you can
9
+ see, but a message that only thought and called tools has nothing left on
10
+ screen once its calls join the chain — so the label stood alone under the
11
+ summary and a six-call chain rendered as one row plus six `Thinking...` lines,
12
+ which is the noise radar exists to remove. One row per chain now means one row.
13
+
14
+ - A settled radar chain never reads a shared command prefix as a location. A
15
+ command is not a path however much it looks like one: several calls beginning
16
+ `cd /Users/me/repo && …` share a long slash-separated prefix, and naming it
17
+ produced `Ran cd /Users/me/repo` — a headline for the one part of the command
18
+ that did nothing. Only families whose subject is a path reduce to a shared
19
+ location now; a command's leading `cd … &&` is stripped so the phrase names the
20
+ act, and a run that did the same thing every time reports that thing
21
+ (`Ran bun run check`) rather than a count.
22
+
3
23
  ## [0.5.33] - 2026-08-23
4
24
 
5
25
  ### Fixed
@@ -1,5 +1,16 @@
1
1
  import type { AssistantMessage } from "@kolisachint/hoocode-ai";
2
2
  import { Container, type MarkdownTheme } from "@kolisachint/hoocode-tui";
3
+ /**
4
+ * How a thinking block renders.
5
+ *
6
+ * `omit` exists for radar. Folding a trace to its one-line label is right when
7
+ * the label sits between things you can see; under a chain summary it is not,
8
+ * because a message whose only content is thinking plus tool calls has nothing
9
+ * left on screen once its calls collapse into the chain's row. A stack of
10
+ * `Thinking...` lines under a one-line summary is precisely the noise radar
11
+ * exists to remove.
12
+ */
13
+ export type ThinkingDisplay = "full" | "label" | "omit";
3
14
  /**
4
15
  * Split markdown into chunks at blank-line boundaries where each chunk lexes
5
16
  * independently to the same blocks the whole text would. Prefix-stable:
@@ -14,13 +25,13 @@ export declare function segmentStreamingMarkdown(text: string): string[];
14
25
  */
15
26
  export declare class AssistantMessageComponent extends Container {
16
27
  private contentContainer;
17
- private hideThinkingBlock;
28
+ private thinkingDisplay;
18
29
  private markdownTheme;
19
30
  private hiddenThinkingLabel;
20
31
  private lastMessage?;
21
32
  private hasToolCalls;
22
33
  private markdownCache;
23
- constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, markdownTheme?: MarkdownTheme, hiddenThinkingLabel?: string);
34
+ constructor(message?: AssistantMessage, thinkingDisplay?: ThinkingDisplay, markdownTheme?: MarkdownTheme, hiddenThinkingLabel?: string);
24
35
  invalidate(): void;
25
36
  private reuseMarkdown;
26
37
  /** Set when the current children include streaming segments; the next
@@ -34,7 +45,9 @@ export declare class AssistantMessageComponent extends Container {
34
45
  * segmentation artifact is transient by construction.
35
46
  */
36
47
  private addMarkdownBlock;
37
- setHideThinkingBlock(hide: boolean): void;
48
+ setThinkingDisplay(display: ThinkingDisplay): void;
49
+ /** Whether a content block puts anything on screen under the current settings. */
50
+ private isVisibleBlock;
38
51
  setHiddenThinkingLabel(label: string): void;
39
52
  /** OSC-zone wrap memo: Container.render returns the same array across
40
53
  * frames when nothing changed, so it must not be mutated — the wrapped
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,SAAS,EAAmC,KAAK,aAAa,EAAgB,MAAM,0BAA0B,CAAC;AAkCxH;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC/D;AAED;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,YAAY,CAAS;IAM7B,OAAO,CAAC,aAAa,CAAqD;IAE1E,YACC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,iBAAiB,UAAQ,EACzB,aAAa,GAAE,aAAkC,EACjD,mBAAmB,SAAgB,EAenC;IAEQ,UAAU,IAAI,IAAI,CAW1B;IAED,OAAO,CAAC,aAAa;IAcrB;0EACsE;IACtE,OAAO,CAAC,kBAAkB,CAAS;IAEnC;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAexB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAKxC;IAED,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAK1C;IAED;;8DAE0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,CAAmC;IAE3C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAcvC;IAED,aAAa,CAAC,OAAO,EAAE,gBAAgB,EAAE,SAAS,UAAQ,GAAG,IAAI,CAgFhE;CACD","sourcesContent":["import type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { Container, type DefaultTextStyle, Markdown, type MarkdownTheme, Spacer, Text } from \"@kolisachint/hoocode-tui\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\n// Streaming messages below this size render as a single Markdown; above it the\n// text is segmented at stable block boundaries so each throttle tick re-parses\n// only the growing tail instead of the whole accumulated message.\nconst SEGMENT_MIN_CHARS = 2048;\n\nconst LIST_ITEM_RE = /^ {0,3}(?:[-*+] |\\d{1,9}[.)] )/;\nconst FENCE_RE = /^ {0,3}(?:```|~~~)/;\nconst LINK_DEF_RE = /^ {0,3}\\[[^\\]]+\\]: /m;\nconst SETEXT_UNDERLINE_RE = /^ {0,3}(?:=+|-+)\\s*$/;\n\n/** True when a blank-line gap between `prev` and `next` (both non-blank) is a\n * safe place to cut the markdown into independently-parseable chunks. */\nfunction isSafeBoundary(prev: string, next: string): boolean {\n\t// Indented continuation (loose list item body, indented code) binds to the\n\t// block above the gap.\n\tif (/^\\s/.test(next)) return false;\n\t// A blank line between two list items is a loose list, not two lists.\n\tif (LIST_ITEM_RE.test(prev) && LIST_ITEM_RE.test(next)) return false;\n\t// Tables and raw HTML blocks can span blank lines in surprising ways.\n\tif (next.startsWith(\"|\") || prev.trimStart().startsWith(\"|\")) return false;\n\tif (next.startsWith(\"<\") || prev.trimStart().startsWith(\"<\")) return false;\n\t// A bare ===/--- line after the gap could lex as setext underline or hr\n\t// differently without its preceding text; keep it attached.\n\tif (SETEXT_UNDERLINE_RE.test(next)) return false;\n\treturn true;\n}\n\n/**\n * Split markdown into chunks at blank-line boundaries where each chunk lexes\n * independently to the same blocks the whole text would. Prefix-stable:\n * appending text never changes earlier boundaries (decisions depend only on\n * preceding fence parity and the lines adjacent to each gap), so during\n * streaming every chunk except the last is byte-identical across updates and\n * its Markdown render cache keeps hitting. Exported for tests.\n */\nexport function segmentStreamingMarkdown(text: string): string[] {\n\t// Reference-style link/footnote definitions resolve across the whole\n\t// document; segmenting would break lookups from other chunks.\n\tif (LINK_DEF_RE.test(text)) return [text];\n\tconst lines = text.split(\"\\n\");\n\tconst chunks: string[] = [];\n\tlet chunkStart = 0;\n\tlet fenceOpen = false;\n\tlet prevNonblank = \"\";\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tif (FENCE_RE.test(line)) fenceOpen = !fenceOpen;\n\t\tif (line.trim() === \"\" && !fenceOpen) {\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < lines.length && lines[j].trim() === \"\") j++;\n\t\t\t// Only cut when the gap has content on both sides; a trailing blank\n\t\t\t// run stays attached so the decision never has to be revisited.\n\t\t\tif (j < lines.length && chunkStart < i && isSafeBoundary(prevNonblank, lines[j])) {\n\t\t\t\tchunks.push(lines.slice(chunkStart, i).join(\"\\n\"));\n\t\t\t\tchunkStart = j;\n\t\t\t}\n\t\t\ti = j;\n\t\t\tcontinue;\n\t\t}\n\t\tif (line.trim() !== \"\") prevNonblank = line;\n\t\ti++;\n\t}\n\tconst last = lines.slice(chunkStart).join(\"\\n\");\n\tif (last.trim() !== \"\") {\n\t\tchunks.push(last);\n\t}\n\treturn chunks.length > 0 ? chunks : [text];\n}\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate hiddenThinkingLabel: string;\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\t// Markdown children reused across streaming updates, keyed by content index +\n\t// kind. Markdown caches its rendered lines by (text, width); recreating the\n\t// instances on every streamed delta discarded those caches and re-parsed the\n\t// entire message (thinking trace included) per frame. Reuse keeps finished\n\t// blocks cached so only the block whose text actually changed re-parses.\n\tprivate markdownCache = new Map<string, { md: Markdown; text: string }>();\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\t// Cached Markdown blocks may be detached right now (e.g. hidden thinking);\n\t\t// drop their render caches too so a theme/width change can't resurface\n\t\t// stale styling when they re-attach.\n\t\tfor (const { md } of this.markdownCache.values()) {\n\t\t\tmd.invalidate();\n\t\t}\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tprivate reuseMarkdown(key: string, text: string, style?: DefaultTextStyle): Markdown {\n\t\tconst entry = this.markdownCache.get(key);\n\t\tif (entry) {\n\t\t\tif (entry.text !== text) {\n\t\t\t\tentry.md.setText(text);\n\t\t\t\tentry.text = text;\n\t\t\t}\n\t\t\treturn entry.md;\n\t\t}\n\t\tconst md = new Markdown(text, 1, 0, this.markdownTheme, style);\n\t\tthis.markdownCache.set(key, { md, text });\n\t\treturn md;\n\t}\n\n\t/** Set when the current children include streaming segments; the next\n\t * non-streaming (final/rebuild) render purges their cache entries. */\n\tprivate hasSegmentedBlocks = false;\n\n\t/**\n\t * Add one markdown content block. Large blocks still being streamed are\n\t * segmented at stable boundaries so only the tail chunk re-parses per\n\t * update; every earlier chunk is byte-stable and stays cached. The final\n\t * (non-streaming) render collapses back to one canonical Markdown, so any\n\t * segmentation artifact is transient by construction.\n\t */\n\tprivate addMarkdownBlock(keyBase: string, text: string, streaming: boolean, style?: DefaultTextStyle): void {\n\t\tif (streaming && text.length >= SEGMENT_MIN_CHARS) {\n\t\t\tconst chunks = segmentStreamingMarkdown(text);\n\t\t\tif (chunks.length > 1) {\n\t\t\t\tfor (let k = 0; k < chunks.length; k++) {\n\t\t\t\t\tif (k > 0) this.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.contentContainer.addChild(this.reuseMarkdown(`${keyBase}:seg:${k}`, chunks[k], style));\n\t\t\t\t}\n\t\t\t\tthis.hasSegmentedBlocks = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tthis.contentContainer.addChild(this.reuseMarkdown(keyBase, text, style));\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\t/** OSC-zone wrap memo: Container.render returns the same array across\n\t * frames when nothing changed, so it must not be mutated — the wrapped\n\t * copy is cached keyed on the source array's identity. */\n\tprivate zoneMemo?: { src: string[]; out: string[] };\n\n\toverride render(width: number): string[] {\n\t\tconst lines = super.render(width);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.zoneMemo?.src === lines) {\n\t\t\treturn this.zoneMemo.out;\n\t\t}\n\t\tconst out = lines.slice();\n\t\tout[0] = OSC133_ZONE_START + out[0];\n\t\tout[out.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + out[out.length - 1];\n\t\tthis.zoneMemo = { src: lines, out };\n\t\treturn out;\n\t}\n\n\tupdateContent(message: AssistantMessage, streaming = false): void {\n\t\tthis.lastMessage = message;\n\n\t\t// A final/rebuild render replaces streaming segments with the canonical\n\t\t// single-Markdown form; drop the segment cache entries they used.\n\t\tif (!streaming && this.hasSegmentedBlocks) {\n\t\t\tfor (const key of this.markdownCache.keys()) {\n\t\t\t\tif (key.includes(\":seg:\")) this.markdownCache.delete(key);\n\t\t\t}\n\t\t\tthis.hasSegmentedBlocks = false;\n\t\t}\n\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tconst hasVisibleContent = message.content.some(\n\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t);\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.addMarkdownBlock(`${i}:text`, content.text.trim(), streaming);\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.some((c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()));\n\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\t// Show static thinking label when hidden\n\t\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\t\tnew Text(theme.italic(theme.fg(\"thinkingText\", this.hiddenThinkingLabel)), 1, 0),\n\t\t\t\t\t);\n\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Thinking traces in thinkingText color, italic, with ✻ prefix\n\t\t\t\t\tthis.addMarkdownBlock(`${i}:thinking`, `✻ ${content.thinking.trim()}`, streaming, {\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t});\n\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check if aborted - show after partial content\n\t\t// But only if there are no tool calls (tool execution components will show the error)\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t\tif (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tif (hasVisibleContent) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t} else {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), 1, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), 1, 0));\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,SAAS,EAAmC,KAAK,aAAa,EAAgB,MAAM,0BAA0B,CAAC;AAGxH;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAiCxD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC/D;AAED;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,YAAY,CAAS;IAM7B,OAAO,CAAC,aAAa,CAAqD;IAE1E,YACC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,eAAe,GAAE,eAAwB,EACzC,aAAa,GAAE,aAAkC,EACjD,mBAAmB,SAAgB,EAenC;IAEQ,UAAU,IAAI,IAAI,CAW1B;IAED,OAAO,CAAC,aAAa;IAcrB;0EACsE;IACtE,OAAO,CAAC,kBAAkB,CAAS;IAEnC;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAexB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAKjD;IAED,kFAAkF;IAClF,OAAO,CAAC,cAAc;IAMtB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAK1C;IAED;;8DAE0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,CAAmC;IAE3C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAcvC;IAED,aAAa,CAAC,OAAO,EAAE,gBAAgB,EAAE,SAAS,UAAQ,GAAG,IAAI,CA8EhE;CACD","sourcesContent":["import type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { Container, type DefaultTextStyle, Markdown, type MarkdownTheme, Spacer, Text } from \"@kolisachint/hoocode-tui\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\n/**\n * How a thinking block renders.\n *\n * `omit` exists for radar. Folding a trace to its one-line label is right when\n * the label sits between things you can see; under a chain summary it is not,\n * because a message whose only content is thinking plus tool calls has nothing\n * left on screen once its calls collapse into the chain's row. A stack of\n * `Thinking...` lines under a one-line summary is precisely the noise radar\n * exists to remove.\n */\nexport type ThinkingDisplay = \"full\" | \"label\" | \"omit\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\n// Streaming messages below this size render as a single Markdown; above it the\n// text is segmented at stable block boundaries so each throttle tick re-parses\n// only the growing tail instead of the whole accumulated message.\nconst SEGMENT_MIN_CHARS = 2048;\n\nconst LIST_ITEM_RE = /^ {0,3}(?:[-*+] |\\d{1,9}[.)] )/;\nconst FENCE_RE = /^ {0,3}(?:```|~~~)/;\nconst LINK_DEF_RE = /^ {0,3}\\[[^\\]]+\\]: /m;\nconst SETEXT_UNDERLINE_RE = /^ {0,3}(?:=+|-+)\\s*$/;\n\n/** True when a blank-line gap between `prev` and `next` (both non-blank) is a\n * safe place to cut the markdown into independently-parseable chunks. */\nfunction isSafeBoundary(prev: string, next: string): boolean {\n\t// Indented continuation (loose list item body, indented code) binds to the\n\t// block above the gap.\n\tif (/^\\s/.test(next)) return false;\n\t// A blank line between two list items is a loose list, not two lists.\n\tif (LIST_ITEM_RE.test(prev) && LIST_ITEM_RE.test(next)) return false;\n\t// Tables and raw HTML blocks can span blank lines in surprising ways.\n\tif (next.startsWith(\"|\") || prev.trimStart().startsWith(\"|\")) return false;\n\tif (next.startsWith(\"<\") || prev.trimStart().startsWith(\"<\")) return false;\n\t// A bare ===/--- line after the gap could lex as setext underline or hr\n\t// differently without its preceding text; keep it attached.\n\tif (SETEXT_UNDERLINE_RE.test(next)) return false;\n\treturn true;\n}\n\n/**\n * Split markdown into chunks at blank-line boundaries where each chunk lexes\n * independently to the same blocks the whole text would. Prefix-stable:\n * appending text never changes earlier boundaries (decisions depend only on\n * preceding fence parity and the lines adjacent to each gap), so during\n * streaming every chunk except the last is byte-identical across updates and\n * its Markdown render cache keeps hitting. Exported for tests.\n */\nexport function segmentStreamingMarkdown(text: string): string[] {\n\t// Reference-style link/footnote definitions resolve across the whole\n\t// document; segmenting would break lookups from other chunks.\n\tif (LINK_DEF_RE.test(text)) return [text];\n\tconst lines = text.split(\"\\n\");\n\tconst chunks: string[] = [];\n\tlet chunkStart = 0;\n\tlet fenceOpen = false;\n\tlet prevNonblank = \"\";\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tif (FENCE_RE.test(line)) fenceOpen = !fenceOpen;\n\t\tif (line.trim() === \"\" && !fenceOpen) {\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < lines.length && lines[j].trim() === \"\") j++;\n\t\t\t// Only cut when the gap has content on both sides; a trailing blank\n\t\t\t// run stays attached so the decision never has to be revisited.\n\t\t\tif (j < lines.length && chunkStart < i && isSafeBoundary(prevNonblank, lines[j])) {\n\t\t\t\tchunks.push(lines.slice(chunkStart, i).join(\"\\n\"));\n\t\t\t\tchunkStart = j;\n\t\t\t}\n\t\t\ti = j;\n\t\t\tcontinue;\n\t\t}\n\t\tif (line.trim() !== \"\") prevNonblank = line;\n\t\ti++;\n\t}\n\tconst last = lines.slice(chunkStart).join(\"\\n\");\n\tif (last.trim() !== \"\") {\n\t\tchunks.push(last);\n\t}\n\treturn chunks.length > 0 ? chunks : [text];\n}\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate thinkingDisplay: ThinkingDisplay;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate hiddenThinkingLabel: string;\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\t// Markdown children reused across streaming updates, keyed by content index +\n\t// kind. Markdown caches its rendered lines by (text, width); recreating the\n\t// instances on every streamed delta discarded those caches and re-parsed the\n\t// entire message (thinking trace included) per frame. Reuse keeps finished\n\t// blocks cached so only the block whose text actually changed re-parses.\n\tprivate markdownCache = new Map<string, { md: Markdown; text: string }>();\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\tthinkingDisplay: ThinkingDisplay = \"full\",\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t) {\n\t\tsuper();\n\n\t\tthis.thinkingDisplay = thinkingDisplay;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\t// Cached Markdown blocks may be detached right now (e.g. hidden thinking);\n\t\t// drop their render caches too so a theme/width change can't resurface\n\t\t// stale styling when they re-attach.\n\t\tfor (const { md } of this.markdownCache.values()) {\n\t\t\tmd.invalidate();\n\t\t}\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tprivate reuseMarkdown(key: string, text: string, style?: DefaultTextStyle): Markdown {\n\t\tconst entry = this.markdownCache.get(key);\n\t\tif (entry) {\n\t\t\tif (entry.text !== text) {\n\t\t\t\tentry.md.setText(text);\n\t\t\t\tentry.text = text;\n\t\t\t}\n\t\t\treturn entry.md;\n\t\t}\n\t\tconst md = new Markdown(text, 1, 0, this.markdownTheme, style);\n\t\tthis.markdownCache.set(key, { md, text });\n\t\treturn md;\n\t}\n\n\t/** Set when the current children include streaming segments; the next\n\t * non-streaming (final/rebuild) render purges their cache entries. */\n\tprivate hasSegmentedBlocks = false;\n\n\t/**\n\t * Add one markdown content block. Large blocks still being streamed are\n\t * segmented at stable boundaries so only the tail chunk re-parses per\n\t * update; every earlier chunk is byte-stable and stays cached. The final\n\t * (non-streaming) render collapses back to one canonical Markdown, so any\n\t * segmentation artifact is transient by construction.\n\t */\n\tprivate addMarkdownBlock(keyBase: string, text: string, streaming: boolean, style?: DefaultTextStyle): void {\n\t\tif (streaming && text.length >= SEGMENT_MIN_CHARS) {\n\t\t\tconst chunks = segmentStreamingMarkdown(text);\n\t\t\tif (chunks.length > 1) {\n\t\t\t\tfor (let k = 0; k < chunks.length; k++) {\n\t\t\t\t\tif (k > 0) this.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.contentContainer.addChild(this.reuseMarkdown(`${keyBase}:seg:${k}`, chunks[k], style));\n\t\t\t\t}\n\t\t\t\tthis.hasSegmentedBlocks = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tthis.contentContainer.addChild(this.reuseMarkdown(keyBase, text, style));\n\t}\n\n\tsetThinkingDisplay(display: ThinkingDisplay): void {\n\t\tthis.thinkingDisplay = display;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\t/** Whether a content block puts anything on screen under the current settings. */\n\tprivate isVisibleBlock(content: AssistantMessage[\"content\"][number]): boolean {\n\t\tif (content.type === \"text\") return content.text.trim() !== \"\";\n\t\tif (content.type === \"thinking\") return this.thinkingDisplay !== \"omit\" && content.thinking.trim() !== \"\";\n\t\treturn false;\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\t/** OSC-zone wrap memo: Container.render returns the same array across\n\t * frames when nothing changed, so it must not be mutated — the wrapped\n\t * copy is cached keyed on the source array's identity. */\n\tprivate zoneMemo?: { src: string[]; out: string[] };\n\n\toverride render(width: number): string[] {\n\t\tconst lines = super.render(width);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.zoneMemo?.src === lines) {\n\t\t\treturn this.zoneMemo.out;\n\t\t}\n\t\tconst out = lines.slice();\n\t\tout[0] = OSC133_ZONE_START + out[0];\n\t\tout[out.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + out[out.length - 1];\n\t\tthis.zoneMemo = { src: lines, out };\n\t\treturn out;\n\t}\n\n\tupdateContent(message: AssistantMessage, streaming = false): void {\n\t\tthis.lastMessage = message;\n\n\t\t// A final/rebuild render replaces streaming segments with the canonical\n\t\t// single-Markdown form; drop the segment cache entries they used.\n\t\tif (!streaming && this.hasSegmentedBlocks) {\n\t\t\tfor (const key of this.markdownCache.keys()) {\n\t\t\t\tif (key.includes(\":seg:\")) this.markdownCache.delete(key);\n\t\t\t}\n\t\t\tthis.hasSegmentedBlocks = false;\n\t\t}\n\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tconst hasVisibleContent = message.content.some((c) => this.isVisibleBlock(c));\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.addMarkdownBlock(`${i}:text`, content.text.trim(), streaming);\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\tif (this.thinkingDisplay === \"omit\") continue;\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content.slice(i + 1).some((c) => this.isVisibleBlock(c));\n\n\t\t\t\tif (this.thinkingDisplay === \"label\") {\n\t\t\t\t\t// Show static thinking label when hidden\n\t\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\t\tnew Text(theme.italic(theme.fg(\"thinkingText\", this.hiddenThinkingLabel)), 1, 0),\n\t\t\t\t\t);\n\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Thinking traces in thinkingText color, italic, with ✻ prefix\n\t\t\t\t\tthis.addMarkdownBlock(`${i}:thinking`, `✻ ${content.thinking.trim()}`, streaming, {\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t});\n\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check if aborted - show after partial content\n\t\t// But only if there are no tool calls (tool execution components will show the error)\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t\tif (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tif (hasVisibleContent) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t} else {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), 1, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), 1, 0));\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
@@ -83,7 +83,7 @@ export function segmentStreamingMarkdown(text) {
83
83
  */
84
84
  export class AssistantMessageComponent extends Container {
85
85
  contentContainer;
86
- hideThinkingBlock;
86
+ thinkingDisplay;
87
87
  markdownTheme;
88
88
  hiddenThinkingLabel;
89
89
  lastMessage;
@@ -94,9 +94,9 @@ export class AssistantMessageComponent extends Container {
94
94
  // entire message (thinking trace included) per frame. Reuse keeps finished
95
95
  // blocks cached so only the block whose text actually changed re-parses.
96
96
  markdownCache = new Map();
97
- constructor(message, hideThinkingBlock = false, markdownTheme = getMarkdownTheme(), hiddenThinkingLabel = "Thinking...") {
97
+ constructor(message, thinkingDisplay = "full", markdownTheme = getMarkdownTheme(), hiddenThinkingLabel = "Thinking...") {
98
98
  super();
99
- this.hideThinkingBlock = hideThinkingBlock;
99
+ this.thinkingDisplay = thinkingDisplay;
100
100
  this.markdownTheme = markdownTheme;
101
101
  this.hiddenThinkingLabel = hiddenThinkingLabel;
102
102
  // Container for text/thinking content
@@ -156,12 +156,20 @@ export class AssistantMessageComponent extends Container {
156
156
  }
157
157
  this.contentContainer.addChild(this.reuseMarkdown(keyBase, text, style));
158
158
  }
159
- setHideThinkingBlock(hide) {
160
- this.hideThinkingBlock = hide;
159
+ setThinkingDisplay(display) {
160
+ this.thinkingDisplay = display;
161
161
  if (this.lastMessage) {
162
162
  this.updateContent(this.lastMessage);
163
163
  }
164
164
  }
165
+ /** Whether a content block puts anything on screen under the current settings. */
166
+ isVisibleBlock(content) {
167
+ if (content.type === "text")
168
+ return content.text.trim() !== "";
169
+ if (content.type === "thinking")
170
+ return this.thinkingDisplay !== "omit" && content.thinking.trim() !== "";
171
+ return false;
172
+ }
165
173
  setHiddenThinkingLabel(label) {
166
174
  this.hiddenThinkingLabel = label;
167
175
  if (this.lastMessage) {
@@ -199,7 +207,7 @@ export class AssistantMessageComponent extends Container {
199
207
  }
200
208
  // Clear content container
201
209
  this.contentContainer.clear();
202
- const hasVisibleContent = message.content.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
210
+ const hasVisibleContent = message.content.some((c) => this.isVisibleBlock(c));
203
211
  if (hasVisibleContent) {
204
212
  this.contentContainer.addChild(new Spacer(1));
205
213
  }
@@ -212,12 +220,12 @@ export class AssistantMessageComponent extends Container {
212
220
  this.addMarkdownBlock(`${i}:text`, content.text.trim(), streaming);
213
221
  }
214
222
  else if (content.type === "thinking" && content.thinking.trim()) {
223
+ if (this.thinkingDisplay === "omit")
224
+ continue;
215
225
  // Add spacing only when another visible assistant content block follows.
216
226
  // This avoids a superfluous blank line before separately-rendered tool execution blocks.
217
- const hasVisibleContentAfter = message.content
218
- .slice(i + 1)
219
- .some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
220
- if (this.hideThinkingBlock) {
227
+ const hasVisibleContentAfter = message.content.slice(i + 1).some((c) => this.isVisibleBlock(c));
228
+ if (this.thinkingDisplay === "label") {
221
229
  // Show static thinking label when hidden
222
230
  this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), 1, 0));
223
231
  if (hasVisibleContentAfter) {
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { Container, type DefaultTextStyle, Markdown, type MarkdownTheme, Spacer, Text } from \"@kolisachint/hoocode-tui\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\n// Streaming messages below this size render as a single Markdown; above it the\n// text is segmented at stable block boundaries so each throttle tick re-parses\n// only the growing tail instead of the whole accumulated message.\nconst SEGMENT_MIN_CHARS = 2048;\n\nconst LIST_ITEM_RE = /^ {0,3}(?:[-*+] |\\d{1,9}[.)] )/;\nconst FENCE_RE = /^ {0,3}(?:```|~~~)/;\nconst LINK_DEF_RE = /^ {0,3}\\[[^\\]]+\\]: /m;\nconst SETEXT_UNDERLINE_RE = /^ {0,3}(?:=+|-+)\\s*$/;\n\n/** True when a blank-line gap between `prev` and `next` (both non-blank) is a\n * safe place to cut the markdown into independently-parseable chunks. */\nfunction isSafeBoundary(prev: string, next: string): boolean {\n\t// Indented continuation (loose list item body, indented code) binds to the\n\t// block above the gap.\n\tif (/^\\s/.test(next)) return false;\n\t// A blank line between two list items is a loose list, not two lists.\n\tif (LIST_ITEM_RE.test(prev) && LIST_ITEM_RE.test(next)) return false;\n\t// Tables and raw HTML blocks can span blank lines in surprising ways.\n\tif (next.startsWith(\"|\") || prev.trimStart().startsWith(\"|\")) return false;\n\tif (next.startsWith(\"<\") || prev.trimStart().startsWith(\"<\")) return false;\n\t// A bare ===/--- line after the gap could lex as setext underline or hr\n\t// differently without its preceding text; keep it attached.\n\tif (SETEXT_UNDERLINE_RE.test(next)) return false;\n\treturn true;\n}\n\n/**\n * Split markdown into chunks at blank-line boundaries where each chunk lexes\n * independently to the same blocks the whole text would. Prefix-stable:\n * appending text never changes earlier boundaries (decisions depend only on\n * preceding fence parity and the lines adjacent to each gap), so during\n * streaming every chunk except the last is byte-identical across updates and\n * its Markdown render cache keeps hitting. Exported for tests.\n */\nexport function segmentStreamingMarkdown(text: string): string[] {\n\t// Reference-style link/footnote definitions resolve across the whole\n\t// document; segmenting would break lookups from other chunks.\n\tif (LINK_DEF_RE.test(text)) return [text];\n\tconst lines = text.split(\"\\n\");\n\tconst chunks: string[] = [];\n\tlet chunkStart = 0;\n\tlet fenceOpen = false;\n\tlet prevNonblank = \"\";\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tif (FENCE_RE.test(line)) fenceOpen = !fenceOpen;\n\t\tif (line.trim() === \"\" && !fenceOpen) {\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < lines.length && lines[j].trim() === \"\") j++;\n\t\t\t// Only cut when the gap has content on both sides; a trailing blank\n\t\t\t// run stays attached so the decision never has to be revisited.\n\t\t\tif (j < lines.length && chunkStart < i && isSafeBoundary(prevNonblank, lines[j])) {\n\t\t\t\tchunks.push(lines.slice(chunkStart, i).join(\"\\n\"));\n\t\t\t\tchunkStart = j;\n\t\t\t}\n\t\t\ti = j;\n\t\t\tcontinue;\n\t\t}\n\t\tif (line.trim() !== \"\") prevNonblank = line;\n\t\ti++;\n\t}\n\tconst last = lines.slice(chunkStart).join(\"\\n\");\n\tif (last.trim() !== \"\") {\n\t\tchunks.push(last);\n\t}\n\treturn chunks.length > 0 ? chunks : [text];\n}\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate hiddenThinkingLabel: string;\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\t// Markdown children reused across streaming updates, keyed by content index +\n\t// kind. Markdown caches its rendered lines by (text, width); recreating the\n\t// instances on every streamed delta discarded those caches and re-parsed the\n\t// entire message (thinking trace included) per frame. Reuse keeps finished\n\t// blocks cached so only the block whose text actually changed re-parses.\n\tprivate markdownCache = new Map<string, { md: Markdown; text: string }>();\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\t// Cached Markdown blocks may be detached right now (e.g. hidden thinking);\n\t\t// drop their render caches too so a theme/width change can't resurface\n\t\t// stale styling when they re-attach.\n\t\tfor (const { md } of this.markdownCache.values()) {\n\t\t\tmd.invalidate();\n\t\t}\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tprivate reuseMarkdown(key: string, text: string, style?: DefaultTextStyle): Markdown {\n\t\tconst entry = this.markdownCache.get(key);\n\t\tif (entry) {\n\t\t\tif (entry.text !== text) {\n\t\t\t\tentry.md.setText(text);\n\t\t\t\tentry.text = text;\n\t\t\t}\n\t\t\treturn entry.md;\n\t\t}\n\t\tconst md = new Markdown(text, 1, 0, this.markdownTheme, style);\n\t\tthis.markdownCache.set(key, { md, text });\n\t\treturn md;\n\t}\n\n\t/** Set when the current children include streaming segments; the next\n\t * non-streaming (final/rebuild) render purges their cache entries. */\n\tprivate hasSegmentedBlocks = false;\n\n\t/**\n\t * Add one markdown content block. Large blocks still being streamed are\n\t * segmented at stable boundaries so only the tail chunk re-parses per\n\t * update; every earlier chunk is byte-stable and stays cached. The final\n\t * (non-streaming) render collapses back to one canonical Markdown, so any\n\t * segmentation artifact is transient by construction.\n\t */\n\tprivate addMarkdownBlock(keyBase: string, text: string, streaming: boolean, style?: DefaultTextStyle): void {\n\t\tif (streaming && text.length >= SEGMENT_MIN_CHARS) {\n\t\t\tconst chunks = segmentStreamingMarkdown(text);\n\t\t\tif (chunks.length > 1) {\n\t\t\t\tfor (let k = 0; k < chunks.length; k++) {\n\t\t\t\t\tif (k > 0) this.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.contentContainer.addChild(this.reuseMarkdown(`${keyBase}:seg:${k}`, chunks[k], style));\n\t\t\t\t}\n\t\t\t\tthis.hasSegmentedBlocks = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tthis.contentContainer.addChild(this.reuseMarkdown(keyBase, text, style));\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\t/** OSC-zone wrap memo: Container.render returns the same array across\n\t * frames when nothing changed, so it must not be mutated — the wrapped\n\t * copy is cached keyed on the source array's identity. */\n\tprivate zoneMemo?: { src: string[]; out: string[] };\n\n\toverride render(width: number): string[] {\n\t\tconst lines = super.render(width);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.zoneMemo?.src === lines) {\n\t\t\treturn this.zoneMemo.out;\n\t\t}\n\t\tconst out = lines.slice();\n\t\tout[0] = OSC133_ZONE_START + out[0];\n\t\tout[out.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + out[out.length - 1];\n\t\tthis.zoneMemo = { src: lines, out };\n\t\treturn out;\n\t}\n\n\tupdateContent(message: AssistantMessage, streaming = false): void {\n\t\tthis.lastMessage = message;\n\n\t\t// A final/rebuild render replaces streaming segments with the canonical\n\t\t// single-Markdown form; drop the segment cache entries they used.\n\t\tif (!streaming && this.hasSegmentedBlocks) {\n\t\t\tfor (const key of this.markdownCache.keys()) {\n\t\t\t\tif (key.includes(\":seg:\")) this.markdownCache.delete(key);\n\t\t\t}\n\t\t\tthis.hasSegmentedBlocks = false;\n\t\t}\n\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tconst hasVisibleContent = message.content.some(\n\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t);\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.addMarkdownBlock(`${i}:text`, content.text.trim(), streaming);\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.some((c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()));\n\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\t// Show static thinking label when hidden\n\t\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\t\tnew Text(theme.italic(theme.fg(\"thinkingText\", this.hiddenThinkingLabel)), 1, 0),\n\t\t\t\t\t);\n\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Thinking traces in thinkingText color, italic, with ✻ prefix\n\t\t\t\t\tthis.addMarkdownBlock(`${i}:thinking`, `✻ ${content.thinking.trim()}`, streaming, {\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t});\n\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check if aborted - show after partial content\n\t\t// But only if there are no tool calls (tool execution components will show the error)\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t\tif (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tif (hasVisibleContent) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t} else {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), 1, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), 1, 0));\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"assistant-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { Container, type DefaultTextStyle, Markdown, type MarkdownTheme, Spacer, Text } from \"@kolisachint/hoocode-tui\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\n/**\n * How a thinking block renders.\n *\n * `omit` exists for radar. Folding a trace to its one-line label is right when\n * the label sits between things you can see; under a chain summary it is not,\n * because a message whose only content is thinking plus tool calls has nothing\n * left on screen once its calls collapse into the chain's row. A stack of\n * `Thinking...` lines under a one-line summary is precisely the noise radar\n * exists to remove.\n */\nexport type ThinkingDisplay = \"full\" | \"label\" | \"omit\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\n// Streaming messages below this size render as a single Markdown; above it the\n// text is segmented at stable block boundaries so each throttle tick re-parses\n// only the growing tail instead of the whole accumulated message.\nconst SEGMENT_MIN_CHARS = 2048;\n\nconst LIST_ITEM_RE = /^ {0,3}(?:[-*+] |\\d{1,9}[.)] )/;\nconst FENCE_RE = /^ {0,3}(?:```|~~~)/;\nconst LINK_DEF_RE = /^ {0,3}\\[[^\\]]+\\]: /m;\nconst SETEXT_UNDERLINE_RE = /^ {0,3}(?:=+|-+)\\s*$/;\n\n/** True when a blank-line gap between `prev` and `next` (both non-blank) is a\n * safe place to cut the markdown into independently-parseable chunks. */\nfunction isSafeBoundary(prev: string, next: string): boolean {\n\t// Indented continuation (loose list item body, indented code) binds to the\n\t// block above the gap.\n\tif (/^\\s/.test(next)) return false;\n\t// A blank line between two list items is a loose list, not two lists.\n\tif (LIST_ITEM_RE.test(prev) && LIST_ITEM_RE.test(next)) return false;\n\t// Tables and raw HTML blocks can span blank lines in surprising ways.\n\tif (next.startsWith(\"|\") || prev.trimStart().startsWith(\"|\")) return false;\n\tif (next.startsWith(\"<\") || prev.trimStart().startsWith(\"<\")) return false;\n\t// A bare ===/--- line after the gap could lex as setext underline or hr\n\t// differently without its preceding text; keep it attached.\n\tif (SETEXT_UNDERLINE_RE.test(next)) return false;\n\treturn true;\n}\n\n/**\n * Split markdown into chunks at blank-line boundaries where each chunk lexes\n * independently to the same blocks the whole text would. Prefix-stable:\n * appending text never changes earlier boundaries (decisions depend only on\n * preceding fence parity and the lines adjacent to each gap), so during\n * streaming every chunk except the last is byte-identical across updates and\n * its Markdown render cache keeps hitting. Exported for tests.\n */\nexport function segmentStreamingMarkdown(text: string): string[] {\n\t// Reference-style link/footnote definitions resolve across the whole\n\t// document; segmenting would break lookups from other chunks.\n\tif (LINK_DEF_RE.test(text)) return [text];\n\tconst lines = text.split(\"\\n\");\n\tconst chunks: string[] = [];\n\tlet chunkStart = 0;\n\tlet fenceOpen = false;\n\tlet prevNonblank = \"\";\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tif (FENCE_RE.test(line)) fenceOpen = !fenceOpen;\n\t\tif (line.trim() === \"\" && !fenceOpen) {\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < lines.length && lines[j].trim() === \"\") j++;\n\t\t\t// Only cut when the gap has content on both sides; a trailing blank\n\t\t\t// run stays attached so the decision never has to be revisited.\n\t\t\tif (j < lines.length && chunkStart < i && isSafeBoundary(prevNonblank, lines[j])) {\n\t\t\t\tchunks.push(lines.slice(chunkStart, i).join(\"\\n\"));\n\t\t\t\tchunkStart = j;\n\t\t\t}\n\t\t\ti = j;\n\t\t\tcontinue;\n\t\t}\n\t\tif (line.trim() !== \"\") prevNonblank = line;\n\t\ti++;\n\t}\n\tconst last = lines.slice(chunkStart).join(\"\\n\");\n\tif (last.trim() !== \"\") {\n\t\tchunks.push(last);\n\t}\n\treturn chunks.length > 0 ? chunks : [text];\n}\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate thinkingDisplay: ThinkingDisplay;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate hiddenThinkingLabel: string;\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\t// Markdown children reused across streaming updates, keyed by content index +\n\t// kind. Markdown caches its rendered lines by (text, width); recreating the\n\t// instances on every streamed delta discarded those caches and re-parsed the\n\t// entire message (thinking trace included) per frame. Reuse keeps finished\n\t// blocks cached so only the block whose text actually changed re-parses.\n\tprivate markdownCache = new Map<string, { md: Markdown; text: string }>();\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\tthinkingDisplay: ThinkingDisplay = \"full\",\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t) {\n\t\tsuper();\n\n\t\tthis.thinkingDisplay = thinkingDisplay;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\t// Cached Markdown blocks may be detached right now (e.g. hidden thinking);\n\t\t// drop their render caches too so a theme/width change can't resurface\n\t\t// stale styling when they re-attach.\n\t\tfor (const { md } of this.markdownCache.values()) {\n\t\t\tmd.invalidate();\n\t\t}\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tprivate reuseMarkdown(key: string, text: string, style?: DefaultTextStyle): Markdown {\n\t\tconst entry = this.markdownCache.get(key);\n\t\tif (entry) {\n\t\t\tif (entry.text !== text) {\n\t\t\t\tentry.md.setText(text);\n\t\t\t\tentry.text = text;\n\t\t\t}\n\t\t\treturn entry.md;\n\t\t}\n\t\tconst md = new Markdown(text, 1, 0, this.markdownTheme, style);\n\t\tthis.markdownCache.set(key, { md, text });\n\t\treturn md;\n\t}\n\n\t/** Set when the current children include streaming segments; the next\n\t * non-streaming (final/rebuild) render purges their cache entries. */\n\tprivate hasSegmentedBlocks = false;\n\n\t/**\n\t * Add one markdown content block. Large blocks still being streamed are\n\t * segmented at stable boundaries so only the tail chunk re-parses per\n\t * update; every earlier chunk is byte-stable and stays cached. The final\n\t * (non-streaming) render collapses back to one canonical Markdown, so any\n\t * segmentation artifact is transient by construction.\n\t */\n\tprivate addMarkdownBlock(keyBase: string, text: string, streaming: boolean, style?: DefaultTextStyle): void {\n\t\tif (streaming && text.length >= SEGMENT_MIN_CHARS) {\n\t\t\tconst chunks = segmentStreamingMarkdown(text);\n\t\t\tif (chunks.length > 1) {\n\t\t\t\tfor (let k = 0; k < chunks.length; k++) {\n\t\t\t\t\tif (k > 0) this.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\tthis.contentContainer.addChild(this.reuseMarkdown(`${keyBase}:seg:${k}`, chunks[k], style));\n\t\t\t\t}\n\t\t\t\tthis.hasSegmentedBlocks = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tthis.contentContainer.addChild(this.reuseMarkdown(keyBase, text, style));\n\t}\n\n\tsetThinkingDisplay(display: ThinkingDisplay): void {\n\t\tthis.thinkingDisplay = display;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\t/** Whether a content block puts anything on screen under the current settings. */\n\tprivate isVisibleBlock(content: AssistantMessage[\"content\"][number]): boolean {\n\t\tif (content.type === \"text\") return content.text.trim() !== \"\";\n\t\tif (content.type === \"thinking\") return this.thinkingDisplay !== \"omit\" && content.thinking.trim() !== \"\";\n\t\treturn false;\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\t/** OSC-zone wrap memo: Container.render returns the same array across\n\t * frames when nothing changed, so it must not be mutated — the wrapped\n\t * copy is cached keyed on the source array's identity. */\n\tprivate zoneMemo?: { src: string[]; out: string[] };\n\n\toverride render(width: number): string[] {\n\t\tconst lines = super.render(width);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.zoneMemo?.src === lines) {\n\t\t\treturn this.zoneMemo.out;\n\t\t}\n\t\tconst out = lines.slice();\n\t\tout[0] = OSC133_ZONE_START + out[0];\n\t\tout[out.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + out[out.length - 1];\n\t\tthis.zoneMemo = { src: lines, out };\n\t\treturn out;\n\t}\n\n\tupdateContent(message: AssistantMessage, streaming = false): void {\n\t\tthis.lastMessage = message;\n\n\t\t// A final/rebuild render replaces streaming segments with the canonical\n\t\t// single-Markdown form; drop the segment cache entries they used.\n\t\tif (!streaming && this.hasSegmentedBlocks) {\n\t\t\tfor (const key of this.markdownCache.keys()) {\n\t\t\t\tif (key.includes(\":seg:\")) this.markdownCache.delete(key);\n\t\t\t}\n\t\t\tthis.hasSegmentedBlocks = false;\n\t\t}\n\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tconst hasVisibleContent = message.content.some((c) => this.isVisibleBlock(c));\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.addMarkdownBlock(`${i}:text`, content.text.trim(), streaming);\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\tif (this.thinkingDisplay === \"omit\") continue;\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content.slice(i + 1).some((c) => this.isVisibleBlock(c));\n\n\t\t\t\tif (this.thinkingDisplay === \"label\") {\n\t\t\t\t\t// Show static thinking label when hidden\n\t\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\t\tnew Text(theme.italic(theme.fg(\"thinkingText\", this.hiddenThinkingLabel)), 1, 0),\n\t\t\t\t\t);\n\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Thinking traces in thinkingText color, italic, with ✻ prefix\n\t\t\t\t\tthis.addMarkdownBlock(`${i}:thinking`, `✻ ${content.thinking.trim()}`, streaming, {\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t});\n\t\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check if aborted - show after partial content\n\t\t// But only if there are no tool calls (tool execution components will show the error)\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t\tif (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tif (hasVisibleContent) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t} else {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), 1, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), 1, 0));\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"tool-chain-summary.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,iDAAiD;AACjD,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,2DAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,uBAAuB;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,UAAU;AACrB,6CAA6C;AAC3C,SAAS;AACX,yDAAyD;GACvD,MAAM;AACR,0EAA0E;GACxE,aAAa,CAAC;AAEjB,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,CAAC;AAErD,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;CAClB;AAgFD,yEAAkE;AAClE,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,YAAY,EAAE,CAEnE;AAMD,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAoB3E;AA2HD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAqCzD","sourcesContent":["/**\n * The radar view's chain line: one line for a run of consecutive tool calls.\n *\n * Two renderings of the same chain, because a run that is still going and a run\n * that is over answer different questions.\n *\n * While it runs you want the shape — what it did, in order, and where it broke:\n *\n * ◐ grep › read › bash✗ › edit › bash… 4 done · 1 failed · running\n *\n * Once it is over the order has stopped mattering and what is left is what it\n * amounted to:\n *\n * ● Edited packages/tui/src/keys.ts 5 calls · 1 failed\n *\n * A chain that did *not* finish keeps the running form plus a marker. The\n * settled form is a claim about what was accomplished, and a chain cut off\n * partway through has no such claim to make — the same reason\n * `settleDanglingMainTasks` refuses to mark an aborted turn's plan items done.\n *\n * Everything here is pure and deterministic. A model-written phase label would\n * read better than the inferred one, but it would cost tokens on every turn and\n * this agent's whole premise is that nothing happens you did not pay for on\n * purpose.\n */\n\n/** One tool call's contribution to its chain. */\nexport interface ChainEntry {\n\ttool: string;\n\t/** The call's subject — a path, a command, a pattern. */\n\tsubject: string;\n\tisError: boolean;\n\t/** Still executing. */\n\tisPartial: boolean;\n\t/** Lines of text output the call returned. */\n\toutputLines: number;\n}\n\nexport type ChainState =\n\t/** Calls are still arriving or executing. */\n\t| \"running\"\n\t/** Every call finished and the turn moved on cleanly. */\n\t| \"done\"\n\t/** The turn was aborted, errored, or hit a length cap partway through. */\n\t| \"interrupted\";\n\nexport type SegmentTone = \"ok\" | \"error\" | \"running\";\n\nexport interface ChainSegment {\n\tlabel: string;\n\ttone: SegmentTone;\n}\n\n/** Segments beyond this many are elided in the middle rather than wrapped. */\nconst MAX_SEGMENTS = 8;\n/** Kept at the head when eliding. */\nconst HEAD_SEGMENTS = 3;\n/** Kept at the tail when eliding. */\nconst TAIL_SEGMENTS = 2;\n\nfunction tone(entry: ChainEntry): SegmentTone {\n\tif (entry.isError) return \"error\";\n\tif (entry.isPartial) return \"running\";\n\treturn \"ok\";\n}\n\n/**\n * Collapse consecutive calls to the same tool into one segment.\n *\n * A run of five reads is one act, not five, and spelling it out crowds out the\n * calls either side of it. A failure never merges: `read ×4` hiding one broken\n * read would defeat the point of the line.\n */\nfunction collapseRepeats(entries: ChainEntry[]): ChainSegment[] {\n\tconst segments: ChainSegment[] = [];\n\tlet index = 0;\n\twhile (index < entries.length) {\n\t\tconst entry = entries[index];\n\t\tconst entryTone = tone(entry);\n\t\tlet run = 1;\n\t\twhile (\n\t\t\tindex + run < entries.length &&\n\t\t\tentries[index + run].tool === entry.tool &&\n\t\t\ttone(entries[index + run]) === entryTone &&\n\t\t\tentryTone === \"ok\"\n\t\t) {\n\t\t\trun++;\n\t\t}\n\t\tconst marker = entryTone === \"error\" ? \"✗\" : \"\";\n\t\tsegments.push({ label: run > 1 ? `${entry.tool} ×${run}` : `${entry.tool}${marker}`, tone: entryTone });\n\t\tindex += run;\n\t}\n\treturn segments;\n}\n\n/**\n * Elide the middle of a long chain, keeping both ends legible.\n *\n * Failures are never elided. Where a run broke is the one thing the line exists\n * to show, and a long chain is exactly when you most need it — hiding the ✗\n * inside \"… 22 more …\" would leave the stats claiming a failure the line cannot\n * point at.\n */\nfunction capSegments(segments: ChainSegment[]): ChainSegment[] {\n\tif (segments.length <= MAX_SEGMENTS) return segments;\n\n\tconst keep = new Set<number>();\n\tfor (let i = 0; i < HEAD_SEGMENTS; i++) keep.add(i);\n\tfor (let i = segments.length - TAIL_SEGMENTS; i < segments.length; i++) keep.add(i);\n\tsegments.forEach((segment, i) => {\n\t\tif (segment.tone === \"error\") keep.add(i);\n\t});\n\n\tconst out: ChainSegment[] = [];\n\tlet gap = 0;\n\tconst flushGap = () => {\n\t\tif (gap > 0) out.push({ label: `… ${gap} more …`, tone: \"ok\" });\n\t\tgap = 0;\n\t};\n\tsegments.forEach((segment, i) => {\n\t\tif (keep.has(i)) {\n\t\t\tflushGap();\n\t\t\tout.push(segment);\n\t\t} else {\n\t\t\tgap++;\n\t\t}\n\t});\n\tflushGap();\n\treturn out;\n}\n\n/** The `grep › read × 3 › bash✗` part of a running chain line. */\nexport function chainSegments(entries: ChainEntry[]): ChainSegment[] {\n\treturn capSegments(collapseRepeats(entries));\n}\n\nfunction plural(n: number, one: string, many = `${one}s`): string {\n\treturn `${n} ${n === 1 ? one : many}`;\n}\n\n/** The flush-right stats for either rendering. */\nexport function chainStats(entries: ChainEntry[], state: ChainState): string {\n\tconst failed = entries.filter((e) => e.isError).length;\n\tconst parts: string[] = [];\n\n\tif (state === \"running\") {\n\t\tparts.push(`${entries.filter((e) => !e.isPartial).length} done`);\n\t} else {\n\t\tparts.push(plural(entries.length, \"call\"));\n\t}\n\tif (failed > 0) parts.push(`${failed} failed`);\n\n\tif (state === \"running\") {\n\t\tparts.push(\"running\");\n\t} else if (state === \"interrupted\") {\n\t\tparts.push(\"interrupted\");\n\t} else {\n\t\tconst lines = entries.reduce((sum, e) => sum + e.outputLines, 0);\n\t\tif (lines > 0) parts.push(plural(lines, \"line\"));\n\t}\n\treturn parts.join(\" · \");\n}\n\ninterface ToolFamily {\n\tverb: string;\n\t/** Noun for the headline's count form: \"Edited 3 files\". */\n\tnoun: string;\n\t/** Noun for the secondary clause, which has no verb to lean on: \"· 38 reads\". */\n\ttail: string;\n\ttools: string[];\n}\n\n/**\n * Tool families, in the order a settled chain reports them.\n *\n * The ordering is the whole trick: a chain that read six files and then changed\n * one is remembered as the edit. Mutation outranks execution outranks reading\n * outranks searching, so the phrase names the most consequential thing the\n * chain did rather than the most frequent.\n *\n * Priority alone is only sound while the chain is small enough for every call\n * to plausibly serve the headline act. See `INCIDENTAL_SHARE`.\n */\nconst FAMILIES: ToolFamily[] = [\n\t{ verb: \"Edited\", noun: \"file\", tail: \"edit\", tools: [\"edit\", \"write\", \"MultiEdit\", \"NotebookEdit\"] },\n\t{ verb: \"Ran\", noun: \"command\", tail: \"command\", tools: [\"bash\"] },\n\t{ verb: \"Delegated\", noun: \"task\", tail: \"task\", tools: [\"Task\", \"TaskOutput\"] },\n\t{ verb: \"Fetched\", noun: \"page\", tail: \"fetch\", tools: [\"webfetch\", \"websearch\"] },\n\t{ verb: \"Read\", noun: \"file\", tail: \"read\", tools: [\"read\"] },\n\t{ verb: \"Searched\", noun: \"search\", tail: \"search\", tools: [\"grep\", \"find\", \"search\", \"ls\"] },\n];\n\n/**\n * Calls from here up, a chain is long enough that priority order alone starts\n * lying, and the three rules below switch on.\n *\n * Below it the existing behaviour is left exactly as it was. Measured over this\n * repo's own sessions, 87% of chains are three calls or fewer and 75% touch a\n * single family; the failure these rules address does not exist down there, so\n * neither should the rules.\n */\nconst LONG_CHAIN_CALLS = 10;\n\n/**\n * A family this far below the chain's own size did not characterise it.\n *\n * Priority order says a chain that read six files and changed one is the edit,\n * and at seven calls that is true. At seventy-three it is not: 28 greps, 37\n * reads and one edit is an investigation that ended in a small change, and\n * calling it \"Edited subagent.ts\" describes 1 call out of 73.\n */\nconst INCIDENTAL_SHARE = 0.1;\n\n/** Below this, a secondary family is a footnote and not worth the width. */\nconst MIN_SECONDARY_CALLS = 3;\n\n/**\n * Longest common directory prefix of the given paths, or \"\" when they share none.\n *\n * `long` chains additionally reject a single-segment prefix: across forty files\n * the shared root collapses to something like `packages`, which names a location\n * so broad it is worse than the count it displaced. Short chains keep theirs —\n * `Edited docs` is a genuine location, and three files rarely share a vague one.\n */\nfunction commonPathPrefix(paths: string[], long: boolean): string {\n\tif (paths.length === 0) return \"\";\n\tconst split = paths.map((p) => p.split(\"/\").filter(Boolean));\n\tconst [first, ...rest] = split;\n\tconst prefix: string[] = [];\n\tfor (let i = 0; i < first.length; i++) {\n\t\tif (rest.every((parts) => parts[i] === first[i])) prefix.push(first[i]);\n\t\telse break;\n\t}\n\tif (prefix.length === 0) return \"\";\n\tif (long && prefix.length < 2) return \"\";\n\tconst joined = prefix.join(\"/\");\n\t// `filter(Boolean)` drops the empty leading segment of an absolute path.\n\treturn paths[0].startsWith(\"/\") ? `/${joined}` : joined;\n}\n\n/**\n * Whether a subject can stand in for a location.\n *\n * Globs are excluded even though they look pathlike: \"Searched *.test.ts\" reads\n * as a place the chain worked in, which is exactly what it is not.\n */\nfunction usableAsTarget(subject: string): boolean {\n\tif (/[*?]/.test(subject)) return false;\n\treturn subject.includes(\"/\") || /\\.[a-zA-Z0-9]+$/.test(subject);\n}\n\n/** The headline clause for one family's calls. */\nfunction familyPhrase(family: ToolFamily, matched: ChainEntry[], long: boolean): string {\n\tconst subjects = matched.map((e) => e.subject).filter(Boolean);\n\n\t// A lone call is fully described by its own subject, whatever shape it is:\n\t// a command, a pattern, a URL, a path.\n\tif (matched.length === 1 && subjects.length === 1) {\n\t\treturn `${family.verb} ${subjects[0]}`;\n\t}\n\n\t// Several calls: name the place they share, or fall back to a count.\n\tconst target = commonPathPrefix(subjects.filter(usableAsTarget), long);\n\tif (target) return `${family.verb} ${target}`;\n\t// Searching has no natural plural noun (\"3 searches\" says nothing the\n\t// stats do not), so the bare verb carries it.\n\tif (family.noun === \"search\") return \"Explored\";\n\treturn `${family.verb} ${plural(matched.length, family.noun)}`;\n}\n\n/** The family with the most calls other than `chosen`, if any reaches the floor. */\nfunction largestOtherFamily(\n\tentries: ChainEntry[],\n\tchosen: ToolFamily,\n): { family: ToolFamily; count: number } | undefined {\n\tlet best: { family: ToolFamily; count: number } | undefined;\n\tfor (const family of FAMILIES) {\n\t\tif (family === chosen) continue;\n\t\tconst count = entries.filter((e) => family.tools.includes(e.tool)).length;\n\t\tif (count > (best?.count ?? 0)) best = { family, count };\n\t}\n\treturn best && best.count >= MIN_SECONDARY_CALLS ? best : undefined;\n}\n\n/**\n * The settled chain's phrase: what this run amounted to.\n *\n * Deliberately close to one clause, not an inventory. \"Edited packages/tui, read\n * 6 files, searched packages\" is a worse line than \"Edited packages/tui\" — the\n * count is already in the stats, and the per-call detail is one `alt+u` away.\n *\n * A long chain earns at most one more clause, and only when its headline family\n * is a minority of the calls. That is the case where a single clause stops being\n * terse and starts being false: 31 edits among 182 calls is worth naming, but so\n * is the fact that 76 of the rest were commands. Listing every family it touched\n * is not the alternative — a chain that reports \"(ran, delegated, read,\n * searched)\" has said only that it was busy.\n *\n * A location is only named when the calls actually share one. Naming a single\n * arbitrary file out of five would read as a claim the chain never made, so\n * several unrelated targets collapse to a count instead.\n */\nexport function chainPhrase(entries: ChainEntry[]): string {\n\tif (entries.length === 0) return \"No calls\";\n\tconst long = entries.length >= LONG_CHAIN_CALLS;\n\n\tlet chosen: ToolFamily | undefined;\n\tlet matched: ChainEntry[] = [];\n\tfor (const family of FAMILIES) {\n\t\tconst m = entries.filter((e) => family.tools.includes(e.tool));\n\t\tif (m.length === 0) continue;\n\t\tif (long && m.length / entries.length < INCIDENTAL_SHARE) continue;\n\t\tchosen = family;\n\t\tmatched = m;\n\t\tbreak;\n\t}\n\n\t// Every family was incidental — a long chain spread thin across many tools.\n\t// The largest still describes it better than the priority order's first hit.\n\tif (!chosen) {\n\t\tfor (const family of FAMILIES) {\n\t\t\tconst m = entries.filter((e) => family.tools.includes(e.tool));\n\t\t\tif (m.length > matched.length) {\n\t\t\t\tchosen = family;\n\t\t\t\tmatched = m;\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unrecognised tool set still gets a line rather than a blank.\n\tif (!chosen) {\n\t\tconst names = [...new Set(entries.map((e) => e.tool))];\n\t\treturn names.length === 1 ? `Called ${names[0]}` : `Called ${plural(names.length, \"tool\")}`;\n\t}\n\n\tconst head = familyPhrase(chosen, matched, long);\n\tif (!long || matched.length / entries.length >= 0.5) return head;\n\tconst secondary = largestOtherFamily(entries, chosen);\n\treturn secondary ? `${head} · ${plural(secondary.count, secondary.family.tail)}` : head;\n}\n"]}
1
+ {"version":3,"file":"tool-chain-summary.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,iDAAiD;AACjD,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,2DAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,uBAAuB;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,UAAU;AACrB,6CAA6C;AAC3C,SAAS;AACX,yDAAyD;GACvD,MAAM;AACR,0EAA0E;GACxE,aAAa,CAAC;AAEjB,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,CAAC;AAErD,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;CAClB;AAgFD,yEAAkE;AAClE,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,YAAY,EAAE,CAEnE;AAMD,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAoB3E;AA2JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAqCzD","sourcesContent":["/**\n * The radar view's chain line: one line for a run of consecutive tool calls.\n *\n * Two renderings of the same chain, because a run that is still going and a run\n * that is over answer different questions.\n *\n * While it runs you want the shape — what it did, in order, and where it broke:\n *\n * ◐ grep › read › bash✗ › edit › bash… 4 done · 1 failed · running\n *\n * Once it is over the order has stopped mattering and what is left is what it\n * amounted to:\n *\n * ● Edited packages/tui/src/keys.ts 5 calls · 1 failed\n *\n * A chain that did *not* finish keeps the running form plus a marker. The\n * settled form is a claim about what was accomplished, and a chain cut off\n * partway through has no such claim to make — the same reason\n * `settleDanglingMainTasks` refuses to mark an aborted turn's plan items done.\n *\n * Everything here is pure and deterministic. A model-written phase label would\n * read better than the inferred one, but it would cost tokens on every turn and\n * this agent's whole premise is that nothing happens you did not pay for on\n * purpose.\n */\n\n/** One tool call's contribution to its chain. */\nexport interface ChainEntry {\n\ttool: string;\n\t/** The call's subject — a path, a command, a pattern. */\n\tsubject: string;\n\tisError: boolean;\n\t/** Still executing. */\n\tisPartial: boolean;\n\t/** Lines of text output the call returned. */\n\toutputLines: number;\n}\n\nexport type ChainState =\n\t/** Calls are still arriving or executing. */\n\t| \"running\"\n\t/** Every call finished and the turn moved on cleanly. */\n\t| \"done\"\n\t/** The turn was aborted, errored, or hit a length cap partway through. */\n\t| \"interrupted\";\n\nexport type SegmentTone = \"ok\" | \"error\" | \"running\";\n\nexport interface ChainSegment {\n\tlabel: string;\n\ttone: SegmentTone;\n}\n\n/** Segments beyond this many are elided in the middle rather than wrapped. */\nconst MAX_SEGMENTS = 8;\n/** Kept at the head when eliding. */\nconst HEAD_SEGMENTS = 3;\n/** Kept at the tail when eliding. */\nconst TAIL_SEGMENTS = 2;\n\nfunction tone(entry: ChainEntry): SegmentTone {\n\tif (entry.isError) return \"error\";\n\tif (entry.isPartial) return \"running\";\n\treturn \"ok\";\n}\n\n/**\n * Collapse consecutive calls to the same tool into one segment.\n *\n * A run of five reads is one act, not five, and spelling it out crowds out the\n * calls either side of it. A failure never merges: `read ×4` hiding one broken\n * read would defeat the point of the line.\n */\nfunction collapseRepeats(entries: ChainEntry[]): ChainSegment[] {\n\tconst segments: ChainSegment[] = [];\n\tlet index = 0;\n\twhile (index < entries.length) {\n\t\tconst entry = entries[index];\n\t\tconst entryTone = tone(entry);\n\t\tlet run = 1;\n\t\twhile (\n\t\t\tindex + run < entries.length &&\n\t\t\tentries[index + run].tool === entry.tool &&\n\t\t\ttone(entries[index + run]) === entryTone &&\n\t\t\tentryTone === \"ok\"\n\t\t) {\n\t\t\trun++;\n\t\t}\n\t\tconst marker = entryTone === \"error\" ? \"✗\" : \"\";\n\t\tsegments.push({ label: run > 1 ? `${entry.tool} ×${run}` : `${entry.tool}${marker}`, tone: entryTone });\n\t\tindex += run;\n\t}\n\treturn segments;\n}\n\n/**\n * Elide the middle of a long chain, keeping both ends legible.\n *\n * Failures are never elided. Where a run broke is the one thing the line exists\n * to show, and a long chain is exactly when you most need it — hiding the ✗\n * inside \"… 22 more …\" would leave the stats claiming a failure the line cannot\n * point at.\n */\nfunction capSegments(segments: ChainSegment[]): ChainSegment[] {\n\tif (segments.length <= MAX_SEGMENTS) return segments;\n\n\tconst keep = new Set<number>();\n\tfor (let i = 0; i < HEAD_SEGMENTS; i++) keep.add(i);\n\tfor (let i = segments.length - TAIL_SEGMENTS; i < segments.length; i++) keep.add(i);\n\tsegments.forEach((segment, i) => {\n\t\tif (segment.tone === \"error\") keep.add(i);\n\t});\n\n\tconst out: ChainSegment[] = [];\n\tlet gap = 0;\n\tconst flushGap = () => {\n\t\tif (gap > 0) out.push({ label: `… ${gap} more …`, tone: \"ok\" });\n\t\tgap = 0;\n\t};\n\tsegments.forEach((segment, i) => {\n\t\tif (keep.has(i)) {\n\t\t\tflushGap();\n\t\t\tout.push(segment);\n\t\t} else {\n\t\t\tgap++;\n\t\t}\n\t});\n\tflushGap();\n\treturn out;\n}\n\n/** The `grep › read × 3 › bash✗` part of a running chain line. */\nexport function chainSegments(entries: ChainEntry[]): ChainSegment[] {\n\treturn capSegments(collapseRepeats(entries));\n}\n\nfunction plural(n: number, one: string, many = `${one}s`): string {\n\treturn `${n} ${n === 1 ? one : many}`;\n}\n\n/** The flush-right stats for either rendering. */\nexport function chainStats(entries: ChainEntry[], state: ChainState): string {\n\tconst failed = entries.filter((e) => e.isError).length;\n\tconst parts: string[] = [];\n\n\tif (state === \"running\") {\n\t\tparts.push(`${entries.filter((e) => !e.isPartial).length} done`);\n\t} else {\n\t\tparts.push(plural(entries.length, \"call\"));\n\t}\n\tif (failed > 0) parts.push(`${failed} failed`);\n\n\tif (state === \"running\") {\n\t\tparts.push(\"running\");\n\t} else if (state === \"interrupted\") {\n\t\tparts.push(\"interrupted\");\n\t} else {\n\t\tconst lines = entries.reduce((sum, e) => sum + e.outputLines, 0);\n\t\tif (lines > 0) parts.push(plural(lines, \"line\"));\n\t}\n\treturn parts.join(\" · \");\n}\n\ninterface ToolFamily {\n\tverb: string;\n\t/** Noun for the headline's count form: \"Edited 3 files\". */\n\tnoun: string;\n\t/** Noun for the secondary clause, which has no verb to lean on: \"· 38 reads\". */\n\ttail: string;\n\ttools: string[];\n\t/**\n\t * Whether this family's subjects name a place on disk.\n\t *\n\t * Only these may be reduced to a shared location. A command is not a path\n\t * however much it looks like one: three calls that all begin\n\t * `cd /Users/me/repo && …` share a long slash-separated prefix, and treating\n\t * it as a location produced `Ran cd /Users/me/repo` — a headline naming the\n\t * one part of the command that did nothing.\n\t */\n\tsubjectIsPath: boolean;\n\t/** Reduce a subject to the act it performed, when the raw form carries noise. */\n\tact?: (subject: string) => string;\n}\n\n/** Leading `cd <dir> &&` runs, however many are chained. */\nconst CD_PREFIX_RE = /^(?:cd\\s+(?:\"[^\"]*\"|'[^']*'|[^\\s;&|]+)\\s*&&\\s*)+/;\n\n/**\n * A command's act, with the navigation stripped off the front.\n *\n * `cd packages/tui && bun run check` is one act performed somewhere, not two,\n * and the somewhere is not what the chain did.\n */\nfunction commandAct(subject: string): string {\n\treturn subject.replace(CD_PREFIX_RE, \"\").trim() || subject;\n}\n\n/**\n * Tool families, in the order a settled chain reports them.\n *\n * The ordering is the whole trick: a chain that read six files and then changed\n * one is remembered as the edit. Mutation outranks execution outranks reading\n * outranks searching, so the phrase names the most consequential thing the\n * chain did rather than the most frequent.\n *\n * Priority alone is only sound while the chain is small enough for every call\n * to plausibly serve the headline act. See `INCIDENTAL_SHARE`.\n */\nconst FAMILIES: ToolFamily[] = [\n\t{\n\t\tverb: \"Edited\",\n\t\tnoun: \"file\",\n\t\ttail: \"edit\",\n\t\ttools: [\"edit\", \"write\", \"MultiEdit\", \"NotebookEdit\"],\n\t\tsubjectIsPath: true,\n\t},\n\t{ verb: \"Ran\", noun: \"command\", tail: \"command\", tools: [\"bash\"], subjectIsPath: false, act: commandAct },\n\t{ verb: \"Delegated\", noun: \"task\", tail: \"task\", tools: [\"Task\", \"TaskOutput\"], subjectIsPath: false },\n\t{ verb: \"Fetched\", noun: \"page\", tail: \"fetch\", tools: [\"webfetch\", \"websearch\"], subjectIsPath: false },\n\t{ verb: \"Read\", noun: \"file\", tail: \"read\", tools: [\"read\"], subjectIsPath: true },\n\t{ verb: \"Searched\", noun: \"search\", tail: \"search\", tools: [\"grep\", \"find\", \"search\", \"ls\"], subjectIsPath: true },\n];\n\n/**\n * Calls from here up, a chain is long enough that priority order alone starts\n * lying, and the three rules below switch on.\n *\n * Below it the existing behaviour is left exactly as it was. Measured over this\n * repo's own sessions, 87% of chains are three calls or fewer and 75% touch a\n * single family; the failure these rules address does not exist down there, so\n * neither should the rules.\n */\nconst LONG_CHAIN_CALLS = 10;\n\n/**\n * A family this far below the chain's own size did not characterise it.\n *\n * Priority order says a chain that read six files and changed one is the edit,\n * and at seven calls that is true. At seventy-three it is not: 28 greps, 37\n * reads and one edit is an investigation that ended in a small change, and\n * calling it \"Edited subagent.ts\" describes 1 call out of 73.\n */\nconst INCIDENTAL_SHARE = 0.1;\n\n/** Below this, a secondary family is a footnote and not worth the width. */\nconst MIN_SECONDARY_CALLS = 3;\n\n/**\n * Longest common directory prefix of the given paths, or \"\" when they share none.\n *\n * `long` chains additionally reject a single-segment prefix: across forty files\n * the shared root collapses to something like `packages`, which names a location\n * so broad it is worse than the count it displaced. Short chains keep theirs —\n * `Edited docs` is a genuine location, and three files rarely share a vague one.\n */\nfunction commonPathPrefix(paths: string[], long: boolean): string {\n\tif (paths.length === 0) return \"\";\n\tconst split = paths.map((p) => p.split(\"/\").filter(Boolean));\n\tconst [first, ...rest] = split;\n\tconst prefix: string[] = [];\n\tfor (let i = 0; i < first.length; i++) {\n\t\tif (rest.every((parts) => parts[i] === first[i])) prefix.push(first[i]);\n\t\telse break;\n\t}\n\tif (prefix.length === 0) return \"\";\n\tif (long && prefix.length < 2) return \"\";\n\tconst joined = prefix.join(\"/\");\n\t// `filter(Boolean)` drops the empty leading segment of an absolute path.\n\treturn paths[0].startsWith(\"/\") ? `/${joined}` : joined;\n}\n\n/**\n * Whether a subject can stand in for a location.\n *\n * Globs are excluded even though they look pathlike: \"Searched *.test.ts\" reads\n * as a place the chain worked in, which is exactly what it is not.\n */\nfunction usableAsTarget(subject: string): boolean {\n\tif (/[*?]/.test(subject)) return false;\n\treturn subject.includes(\"/\") || /\\.[a-zA-Z0-9]+$/.test(subject);\n}\n\n/** The headline clause for one family's calls. */\nfunction familyPhrase(family: ToolFamily, matched: ChainEntry[], long: boolean): string {\n\tconst subjects = matched.map((e) => (family.act ? family.act(e.subject) : e.subject)).filter(Boolean);\n\n\t// A lone call is fully described by its own subject, whatever shape it is:\n\t// a command, a pattern, a URL, a path. So is a run that did the same thing\n\t// every time — `bun run check` twice is that command, not \"2 commands\".\n\tif (subjects.length === matched.length && new Set(subjects).size === 1) {\n\t\treturn `${family.verb} ${subjects[0]}`;\n\t}\n\n\t// Several calls: name the place they share, or fall back to a count.\n\tconst target = family.subjectIsPath ? commonPathPrefix(subjects.filter(usableAsTarget), long) : \"\";\n\tif (target) return `${family.verb} ${target}`;\n\t// Searching has no natural plural noun (\"3 searches\" says nothing the\n\t// stats do not), so the bare verb carries it.\n\tif (family.noun === \"search\") return \"Explored\";\n\treturn `${family.verb} ${plural(matched.length, family.noun)}`;\n}\n\n/** The family with the most calls other than `chosen`, if any reaches the floor. */\nfunction largestOtherFamily(\n\tentries: ChainEntry[],\n\tchosen: ToolFamily,\n): { family: ToolFamily; count: number } | undefined {\n\tlet best: { family: ToolFamily; count: number } | undefined;\n\tfor (const family of FAMILIES) {\n\t\tif (family === chosen) continue;\n\t\tconst count = entries.filter((e) => family.tools.includes(e.tool)).length;\n\t\tif (count > (best?.count ?? 0)) best = { family, count };\n\t}\n\treturn best && best.count >= MIN_SECONDARY_CALLS ? best : undefined;\n}\n\n/**\n * The settled chain's phrase: what this run amounted to.\n *\n * Deliberately close to one clause, not an inventory. \"Edited packages/tui, read\n * 6 files, searched packages\" is a worse line than \"Edited packages/tui\" — the\n * count is already in the stats, and the per-call detail is one `alt+u` away.\n *\n * A long chain earns at most one more clause, and only when its headline family\n * is a minority of the calls. That is the case where a single clause stops being\n * terse and starts being false: 31 edits among 182 calls is worth naming, but so\n * is the fact that 76 of the rest were commands. Listing every family it touched\n * is not the alternative — a chain that reports \"(ran, delegated, read,\n * searched)\" has said only that it was busy.\n *\n * A location is only named when the calls actually share one. Naming a single\n * arbitrary file out of five would read as a claim the chain never made, so\n * several unrelated targets collapse to a count instead.\n */\nexport function chainPhrase(entries: ChainEntry[]): string {\n\tif (entries.length === 0) return \"No calls\";\n\tconst long = entries.length >= LONG_CHAIN_CALLS;\n\n\tlet chosen: ToolFamily | undefined;\n\tlet matched: ChainEntry[] = [];\n\tfor (const family of FAMILIES) {\n\t\tconst m = entries.filter((e) => family.tools.includes(e.tool));\n\t\tif (m.length === 0) continue;\n\t\tif (long && m.length / entries.length < INCIDENTAL_SHARE) continue;\n\t\tchosen = family;\n\t\tmatched = m;\n\t\tbreak;\n\t}\n\n\t// Every family was incidental — a long chain spread thin across many tools.\n\t// The largest still describes it better than the priority order's first hit.\n\tif (!chosen) {\n\t\tfor (const family of FAMILIES) {\n\t\t\tconst m = entries.filter((e) => family.tools.includes(e.tool));\n\t\t\tif (m.length > matched.length) {\n\t\t\t\tchosen = family;\n\t\t\t\tmatched = m;\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unrecognised tool set still gets a line rather than a blank.\n\tif (!chosen) {\n\t\tconst names = [...new Set(entries.map((e) => e.tool))];\n\t\treturn names.length === 1 ? `Called ${names[0]}` : `Called ${plural(names.length, \"tool\")}`;\n\t}\n\n\tconst head = familyPhrase(chosen, matched, long);\n\tif (!long || matched.length / entries.length >= 0.5) return head;\n\tconst secondary = largestOtherFamily(entries, chosen);\n\treturn secondary ? `${head} · ${plural(secondary.count, secondary.family.tail)}` : head;\n}\n"]}
@@ -133,6 +133,17 @@ export function chainStats(entries, state) {
133
133
  }
134
134
  return parts.join(" · ");
135
135
  }
136
+ /** Leading `cd <dir> &&` runs, however many are chained. */
137
+ const CD_PREFIX_RE = /^(?:cd\s+(?:"[^"]*"|'[^']*'|[^\s;&|]+)\s*&&\s*)+/;
138
+ /**
139
+ * A command's act, with the navigation stripped off the front.
140
+ *
141
+ * `cd packages/tui && bun run check` is one act performed somewhere, not two,
142
+ * and the somewhere is not what the chain did.
143
+ */
144
+ function commandAct(subject) {
145
+ return subject.replace(CD_PREFIX_RE, "").trim() || subject;
146
+ }
136
147
  /**
137
148
  * Tool families, in the order a settled chain reports them.
138
149
  *
@@ -145,12 +156,18 @@ export function chainStats(entries, state) {
145
156
  * to plausibly serve the headline act. See `INCIDENTAL_SHARE`.
146
157
  */
147
158
  const FAMILIES = [
148
- { verb: "Edited", noun: "file", tail: "edit", tools: ["edit", "write", "MultiEdit", "NotebookEdit"] },
149
- { verb: "Ran", noun: "command", tail: "command", tools: ["bash"] },
150
- { verb: "Delegated", noun: "task", tail: "task", tools: ["Task", "TaskOutput"] },
151
- { verb: "Fetched", noun: "page", tail: "fetch", tools: ["webfetch", "websearch"] },
152
- { verb: "Read", noun: "file", tail: "read", tools: ["read"] },
153
- { verb: "Searched", noun: "search", tail: "search", tools: ["grep", "find", "search", "ls"] },
159
+ {
160
+ verb: "Edited",
161
+ noun: "file",
162
+ tail: "edit",
163
+ tools: ["edit", "write", "MultiEdit", "NotebookEdit"],
164
+ subjectIsPath: true,
165
+ },
166
+ { verb: "Ran", noun: "command", tail: "command", tools: ["bash"], subjectIsPath: false, act: commandAct },
167
+ { verb: "Delegated", noun: "task", tail: "task", tools: ["Task", "TaskOutput"], subjectIsPath: false },
168
+ { verb: "Fetched", noun: "page", tail: "fetch", tools: ["webfetch", "websearch"], subjectIsPath: false },
169
+ { verb: "Read", noun: "file", tail: "read", tools: ["read"], subjectIsPath: true },
170
+ { verb: "Searched", noun: "search", tail: "search", tools: ["grep", "find", "search", "ls"], subjectIsPath: true },
154
171
  ];
155
172
  /**
156
173
  * Calls from here up, a chain is long enough that priority order alone starts
@@ -214,14 +231,15 @@ function usableAsTarget(subject) {
214
231
  }
215
232
  /** The headline clause for one family's calls. */
216
233
  function familyPhrase(family, matched, long) {
217
- const subjects = matched.map((e) => e.subject).filter(Boolean);
234
+ const subjects = matched.map((e) => (family.act ? family.act(e.subject) : e.subject)).filter(Boolean);
218
235
  // A lone call is fully described by its own subject, whatever shape it is:
219
- // a command, a pattern, a URL, a path.
220
- if (matched.length === 1 && subjects.length === 1) {
236
+ // a command, a pattern, a URL, a path. So is a run that did the same thing
237
+ // every time `bun run check` twice is that command, not "2 commands".
238
+ if (subjects.length === matched.length && new Set(subjects).size === 1) {
221
239
  return `${family.verb} ${subjects[0]}`;
222
240
  }
223
241
  // Several calls: name the place they share, or fall back to a count.
224
- const target = commonPathPrefix(subjects.filter(usableAsTarget), long);
242
+ const target = family.subjectIsPath ? commonPathPrefix(subjects.filter(usableAsTarget), long) : "";
225
243
  if (target)
226
244
  return `${family.verb} ${target}`;
227
245
  // Searching has no natural plural noun ("3 searches" says nothing the
@@ -1 +1 @@
1
- {"version":3,"file":"tool-chain-summary.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain-summary.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The radar view's chain line: one line for a run of consecutive tool calls.\n *\n * Two renderings of the same chain, because a run that is still going and a run\n * that is over answer different questions.\n *\n * While it runs you want the shape — what it did, in order, and where it broke:\n *\n * ◐ grep › read › bash✗ › edit › bash… 4 done · 1 failed · running\n *\n * Once it is over the order has stopped mattering and what is left is what it\n * amounted to:\n *\n * ● Edited packages/tui/src/keys.ts 5 calls · 1 failed\n *\n * A chain that did *not* finish keeps the running form plus a marker. The\n * settled form is a claim about what was accomplished, and a chain cut off\n * partway through has no such claim to make — the same reason\n * `settleDanglingMainTasks` refuses to mark an aborted turn's plan items done.\n *\n * Everything here is pure and deterministic. A model-written phase label would\n * read better than the inferred one, but it would cost tokens on every turn and\n * this agent's whole premise is that nothing happens you did not pay for on\n * purpose.\n */\n\n/** One tool call's contribution to its chain. */\nexport interface ChainEntry {\n\ttool: string;\n\t/** The call's subject — a path, a command, a pattern. */\n\tsubject: string;\n\tisError: boolean;\n\t/** Still executing. */\n\tisPartial: boolean;\n\t/** Lines of text output the call returned. */\n\toutputLines: number;\n}\n\nexport type ChainState =\n\t/** Calls are still arriving or executing. */\n\t| \"running\"\n\t/** Every call finished and the turn moved on cleanly. */\n\t| \"done\"\n\t/** The turn was aborted, errored, or hit a length cap partway through. */\n\t| \"interrupted\";\n\nexport type SegmentTone = \"ok\" | \"error\" | \"running\";\n\nexport interface ChainSegment {\n\tlabel: string;\n\ttone: SegmentTone;\n}\n\n/** Segments beyond this many are elided in the middle rather than wrapped. */\nconst MAX_SEGMENTS = 8;\n/** Kept at the head when eliding. */\nconst HEAD_SEGMENTS = 3;\n/** Kept at the tail when eliding. */\nconst TAIL_SEGMENTS = 2;\n\nfunction tone(entry: ChainEntry): SegmentTone {\n\tif (entry.isError) return \"error\";\n\tif (entry.isPartial) return \"running\";\n\treturn \"ok\";\n}\n\n/**\n * Collapse consecutive calls to the same tool into one segment.\n *\n * A run of five reads is one act, not five, and spelling it out crowds out the\n * calls either side of it. A failure never merges: `read ×4` hiding one broken\n * read would defeat the point of the line.\n */\nfunction collapseRepeats(entries: ChainEntry[]): ChainSegment[] {\n\tconst segments: ChainSegment[] = [];\n\tlet index = 0;\n\twhile (index < entries.length) {\n\t\tconst entry = entries[index];\n\t\tconst entryTone = tone(entry);\n\t\tlet run = 1;\n\t\twhile (\n\t\t\tindex + run < entries.length &&\n\t\t\tentries[index + run].tool === entry.tool &&\n\t\t\ttone(entries[index + run]) === entryTone &&\n\t\t\tentryTone === \"ok\"\n\t\t) {\n\t\t\trun++;\n\t\t}\n\t\tconst marker = entryTone === \"error\" ? \"✗\" : \"\";\n\t\tsegments.push({ label: run > 1 ? `${entry.tool} ×${run}` : `${entry.tool}${marker}`, tone: entryTone });\n\t\tindex += run;\n\t}\n\treturn segments;\n}\n\n/**\n * Elide the middle of a long chain, keeping both ends legible.\n *\n * Failures are never elided. Where a run broke is the one thing the line exists\n * to show, and a long chain is exactly when you most need it — hiding the ✗\n * inside \"… 22 more …\" would leave the stats claiming a failure the line cannot\n * point at.\n */\nfunction capSegments(segments: ChainSegment[]): ChainSegment[] {\n\tif (segments.length <= MAX_SEGMENTS) return segments;\n\n\tconst keep = new Set<number>();\n\tfor (let i = 0; i < HEAD_SEGMENTS; i++) keep.add(i);\n\tfor (let i = segments.length - TAIL_SEGMENTS; i < segments.length; i++) keep.add(i);\n\tsegments.forEach((segment, i) => {\n\t\tif (segment.tone === \"error\") keep.add(i);\n\t});\n\n\tconst out: ChainSegment[] = [];\n\tlet gap = 0;\n\tconst flushGap = () => {\n\t\tif (gap > 0) out.push({ label: `… ${gap} more …`, tone: \"ok\" });\n\t\tgap = 0;\n\t};\n\tsegments.forEach((segment, i) => {\n\t\tif (keep.has(i)) {\n\t\t\tflushGap();\n\t\t\tout.push(segment);\n\t\t} else {\n\t\t\tgap++;\n\t\t}\n\t});\n\tflushGap();\n\treturn out;\n}\n\n/** The `grep › read × 3 › bash✗` part of a running chain line. */\nexport function chainSegments(entries: ChainEntry[]): ChainSegment[] {\n\treturn capSegments(collapseRepeats(entries));\n}\n\nfunction plural(n: number, one: string, many = `${one}s`): string {\n\treturn `${n} ${n === 1 ? one : many}`;\n}\n\n/** The flush-right stats for either rendering. */\nexport function chainStats(entries: ChainEntry[], state: ChainState): string {\n\tconst failed = entries.filter((e) => e.isError).length;\n\tconst parts: string[] = [];\n\n\tif (state === \"running\") {\n\t\tparts.push(`${entries.filter((e) => !e.isPartial).length} done`);\n\t} else {\n\t\tparts.push(plural(entries.length, \"call\"));\n\t}\n\tif (failed > 0) parts.push(`${failed} failed`);\n\n\tif (state === \"running\") {\n\t\tparts.push(\"running\");\n\t} else if (state === \"interrupted\") {\n\t\tparts.push(\"interrupted\");\n\t} else {\n\t\tconst lines = entries.reduce((sum, e) => sum + e.outputLines, 0);\n\t\tif (lines > 0) parts.push(plural(lines, \"line\"));\n\t}\n\treturn parts.join(\" · \");\n}\n\ninterface ToolFamily {\n\tverb: string;\n\t/** Noun for the headline's count form: \"Edited 3 files\". */\n\tnoun: string;\n\t/** Noun for the secondary clause, which has no verb to lean on: \"· 38 reads\". */\n\ttail: string;\n\ttools: string[];\n}\n\n/**\n * Tool families, in the order a settled chain reports them.\n *\n * The ordering is the whole trick: a chain that read six files and then changed\n * one is remembered as the edit. Mutation outranks execution outranks reading\n * outranks searching, so the phrase names the most consequential thing the\n * chain did rather than the most frequent.\n *\n * Priority alone is only sound while the chain is small enough for every call\n * to plausibly serve the headline act. See `INCIDENTAL_SHARE`.\n */\nconst FAMILIES: ToolFamily[] = [\n\t{ verb: \"Edited\", noun: \"file\", tail: \"edit\", tools: [\"edit\", \"write\", \"MultiEdit\", \"NotebookEdit\"] },\n\t{ verb: \"Ran\", noun: \"command\", tail: \"command\", tools: [\"bash\"] },\n\t{ verb: \"Delegated\", noun: \"task\", tail: \"task\", tools: [\"Task\", \"TaskOutput\"] },\n\t{ verb: \"Fetched\", noun: \"page\", tail: \"fetch\", tools: [\"webfetch\", \"websearch\"] },\n\t{ verb: \"Read\", noun: \"file\", tail: \"read\", tools: [\"read\"] },\n\t{ verb: \"Searched\", noun: \"search\", tail: \"search\", tools: [\"grep\", \"find\", \"search\", \"ls\"] },\n];\n\n/**\n * Calls from here up, a chain is long enough that priority order alone starts\n * lying, and the three rules below switch on.\n *\n * Below it the existing behaviour is left exactly as it was. Measured over this\n * repo's own sessions, 87% of chains are three calls or fewer and 75% touch a\n * single family; the failure these rules address does not exist down there, so\n * neither should the rules.\n */\nconst LONG_CHAIN_CALLS = 10;\n\n/**\n * A family this far below the chain's own size did not characterise it.\n *\n * Priority order says a chain that read six files and changed one is the edit,\n * and at seven calls that is true. At seventy-three it is not: 28 greps, 37\n * reads and one edit is an investigation that ended in a small change, and\n * calling it \"Edited subagent.ts\" describes 1 call out of 73.\n */\nconst INCIDENTAL_SHARE = 0.1;\n\n/** Below this, a secondary family is a footnote and not worth the width. */\nconst MIN_SECONDARY_CALLS = 3;\n\n/**\n * Longest common directory prefix of the given paths, or \"\" when they share none.\n *\n * `long` chains additionally reject a single-segment prefix: across forty files\n * the shared root collapses to something like `packages`, which names a location\n * so broad it is worse than the count it displaced. Short chains keep theirs —\n * `Edited docs` is a genuine location, and three files rarely share a vague one.\n */\nfunction commonPathPrefix(paths: string[], long: boolean): string {\n\tif (paths.length === 0) return \"\";\n\tconst split = paths.map((p) => p.split(\"/\").filter(Boolean));\n\tconst [first, ...rest] = split;\n\tconst prefix: string[] = [];\n\tfor (let i = 0; i < first.length; i++) {\n\t\tif (rest.every((parts) => parts[i] === first[i])) prefix.push(first[i]);\n\t\telse break;\n\t}\n\tif (prefix.length === 0) return \"\";\n\tif (long && prefix.length < 2) return \"\";\n\tconst joined = prefix.join(\"/\");\n\t// `filter(Boolean)` drops the empty leading segment of an absolute path.\n\treturn paths[0].startsWith(\"/\") ? `/${joined}` : joined;\n}\n\n/**\n * Whether a subject can stand in for a location.\n *\n * Globs are excluded even though they look pathlike: \"Searched *.test.ts\" reads\n * as a place the chain worked in, which is exactly what it is not.\n */\nfunction usableAsTarget(subject: string): boolean {\n\tif (/[*?]/.test(subject)) return false;\n\treturn subject.includes(\"/\") || /\\.[a-zA-Z0-9]+$/.test(subject);\n}\n\n/** The headline clause for one family's calls. */\nfunction familyPhrase(family: ToolFamily, matched: ChainEntry[], long: boolean): string {\n\tconst subjects = matched.map((e) => e.subject).filter(Boolean);\n\n\t// A lone call is fully described by its own subject, whatever shape it is:\n\t// a command, a pattern, a URL, a path.\n\tif (matched.length === 1 && subjects.length === 1) {\n\t\treturn `${family.verb} ${subjects[0]}`;\n\t}\n\n\t// Several calls: name the place they share, or fall back to a count.\n\tconst target = commonPathPrefix(subjects.filter(usableAsTarget), long);\n\tif (target) return `${family.verb} ${target}`;\n\t// Searching has no natural plural noun (\"3 searches\" says nothing the\n\t// stats do not), so the bare verb carries it.\n\tif (family.noun === \"search\") return \"Explored\";\n\treturn `${family.verb} ${plural(matched.length, family.noun)}`;\n}\n\n/** The family with the most calls other than `chosen`, if any reaches the floor. */\nfunction largestOtherFamily(\n\tentries: ChainEntry[],\n\tchosen: ToolFamily,\n): { family: ToolFamily; count: number } | undefined {\n\tlet best: { family: ToolFamily; count: number } | undefined;\n\tfor (const family of FAMILIES) {\n\t\tif (family === chosen) continue;\n\t\tconst count = entries.filter((e) => family.tools.includes(e.tool)).length;\n\t\tif (count > (best?.count ?? 0)) best = { family, count };\n\t}\n\treturn best && best.count >= MIN_SECONDARY_CALLS ? best : undefined;\n}\n\n/**\n * The settled chain's phrase: what this run amounted to.\n *\n * Deliberately close to one clause, not an inventory. \"Edited packages/tui, read\n * 6 files, searched packages\" is a worse line than \"Edited packages/tui\" — the\n * count is already in the stats, and the per-call detail is one `alt+u` away.\n *\n * A long chain earns at most one more clause, and only when its headline family\n * is a minority of the calls. That is the case where a single clause stops being\n * terse and starts being false: 31 edits among 182 calls is worth naming, but so\n * is the fact that 76 of the rest were commands. Listing every family it touched\n * is not the alternative — a chain that reports \"(ran, delegated, read,\n * searched)\" has said only that it was busy.\n *\n * A location is only named when the calls actually share one. Naming a single\n * arbitrary file out of five would read as a claim the chain never made, so\n * several unrelated targets collapse to a count instead.\n */\nexport function chainPhrase(entries: ChainEntry[]): string {\n\tif (entries.length === 0) return \"No calls\";\n\tconst long = entries.length >= LONG_CHAIN_CALLS;\n\n\tlet chosen: ToolFamily | undefined;\n\tlet matched: ChainEntry[] = [];\n\tfor (const family of FAMILIES) {\n\t\tconst m = entries.filter((e) => family.tools.includes(e.tool));\n\t\tif (m.length === 0) continue;\n\t\tif (long && m.length / entries.length < INCIDENTAL_SHARE) continue;\n\t\tchosen = family;\n\t\tmatched = m;\n\t\tbreak;\n\t}\n\n\t// Every family was incidental — a long chain spread thin across many tools.\n\t// The largest still describes it better than the priority order's first hit.\n\tif (!chosen) {\n\t\tfor (const family of FAMILIES) {\n\t\t\tconst m = entries.filter((e) => family.tools.includes(e.tool));\n\t\t\tif (m.length > matched.length) {\n\t\t\t\tchosen = family;\n\t\t\t\tmatched = m;\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unrecognised tool set still gets a line rather than a blank.\n\tif (!chosen) {\n\t\tconst names = [...new Set(entries.map((e) => e.tool))];\n\t\treturn names.length === 1 ? `Called ${names[0]}` : `Called ${plural(names.length, \"tool\")}`;\n\t}\n\n\tconst head = familyPhrase(chosen, matched, long);\n\tif (!long || matched.length / entries.length >= 0.5) return head;\n\tconst secondary = largestOtherFamily(entries, chosen);\n\treturn secondary ? `${head} · ${plural(secondary.count, secondary.family.tail)}` : head;\n}\n"]}
1
+ {"version":3,"file":"tool-chain-summary.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain-summary.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The radar view's chain line: one line for a run of consecutive tool calls.\n *\n * Two renderings of the same chain, because a run that is still going and a run\n * that is over answer different questions.\n *\n * While it runs you want the shape — what it did, in order, and where it broke:\n *\n * ◐ grep › read › bash✗ › edit › bash… 4 done · 1 failed · running\n *\n * Once it is over the order has stopped mattering and what is left is what it\n * amounted to:\n *\n * ● Edited packages/tui/src/keys.ts 5 calls · 1 failed\n *\n * A chain that did *not* finish keeps the running form plus a marker. The\n * settled form is a claim about what was accomplished, and a chain cut off\n * partway through has no such claim to make — the same reason\n * `settleDanglingMainTasks` refuses to mark an aborted turn's plan items done.\n *\n * Everything here is pure and deterministic. A model-written phase label would\n * read better than the inferred one, but it would cost tokens on every turn and\n * this agent's whole premise is that nothing happens you did not pay for on\n * purpose.\n */\n\n/** One tool call's contribution to its chain. */\nexport interface ChainEntry {\n\ttool: string;\n\t/** The call's subject — a path, a command, a pattern. */\n\tsubject: string;\n\tisError: boolean;\n\t/** Still executing. */\n\tisPartial: boolean;\n\t/** Lines of text output the call returned. */\n\toutputLines: number;\n}\n\nexport type ChainState =\n\t/** Calls are still arriving or executing. */\n\t| \"running\"\n\t/** Every call finished and the turn moved on cleanly. */\n\t| \"done\"\n\t/** The turn was aborted, errored, or hit a length cap partway through. */\n\t| \"interrupted\";\n\nexport type SegmentTone = \"ok\" | \"error\" | \"running\";\n\nexport interface ChainSegment {\n\tlabel: string;\n\ttone: SegmentTone;\n}\n\n/** Segments beyond this many are elided in the middle rather than wrapped. */\nconst MAX_SEGMENTS = 8;\n/** Kept at the head when eliding. */\nconst HEAD_SEGMENTS = 3;\n/** Kept at the tail when eliding. */\nconst TAIL_SEGMENTS = 2;\n\nfunction tone(entry: ChainEntry): SegmentTone {\n\tif (entry.isError) return \"error\";\n\tif (entry.isPartial) return \"running\";\n\treturn \"ok\";\n}\n\n/**\n * Collapse consecutive calls to the same tool into one segment.\n *\n * A run of five reads is one act, not five, and spelling it out crowds out the\n * calls either side of it. A failure never merges: `read ×4` hiding one broken\n * read would defeat the point of the line.\n */\nfunction collapseRepeats(entries: ChainEntry[]): ChainSegment[] {\n\tconst segments: ChainSegment[] = [];\n\tlet index = 0;\n\twhile (index < entries.length) {\n\t\tconst entry = entries[index];\n\t\tconst entryTone = tone(entry);\n\t\tlet run = 1;\n\t\twhile (\n\t\t\tindex + run < entries.length &&\n\t\t\tentries[index + run].tool === entry.tool &&\n\t\t\ttone(entries[index + run]) === entryTone &&\n\t\t\tentryTone === \"ok\"\n\t\t) {\n\t\t\trun++;\n\t\t}\n\t\tconst marker = entryTone === \"error\" ? \"✗\" : \"\";\n\t\tsegments.push({ label: run > 1 ? `${entry.tool} ×${run}` : `${entry.tool}${marker}`, tone: entryTone });\n\t\tindex += run;\n\t}\n\treturn segments;\n}\n\n/**\n * Elide the middle of a long chain, keeping both ends legible.\n *\n * Failures are never elided. Where a run broke is the one thing the line exists\n * to show, and a long chain is exactly when you most need it — hiding the ✗\n * inside \"… 22 more …\" would leave the stats claiming a failure the line cannot\n * point at.\n */\nfunction capSegments(segments: ChainSegment[]): ChainSegment[] {\n\tif (segments.length <= MAX_SEGMENTS) return segments;\n\n\tconst keep = new Set<number>();\n\tfor (let i = 0; i < HEAD_SEGMENTS; i++) keep.add(i);\n\tfor (let i = segments.length - TAIL_SEGMENTS; i < segments.length; i++) keep.add(i);\n\tsegments.forEach((segment, i) => {\n\t\tif (segment.tone === \"error\") keep.add(i);\n\t});\n\n\tconst out: ChainSegment[] = [];\n\tlet gap = 0;\n\tconst flushGap = () => {\n\t\tif (gap > 0) out.push({ label: `… ${gap} more …`, tone: \"ok\" });\n\t\tgap = 0;\n\t};\n\tsegments.forEach((segment, i) => {\n\t\tif (keep.has(i)) {\n\t\t\tflushGap();\n\t\t\tout.push(segment);\n\t\t} else {\n\t\t\tgap++;\n\t\t}\n\t});\n\tflushGap();\n\treturn out;\n}\n\n/** The `grep › read × 3 › bash✗` part of a running chain line. */\nexport function chainSegments(entries: ChainEntry[]): ChainSegment[] {\n\treturn capSegments(collapseRepeats(entries));\n}\n\nfunction plural(n: number, one: string, many = `${one}s`): string {\n\treturn `${n} ${n === 1 ? one : many}`;\n}\n\n/** The flush-right stats for either rendering. */\nexport function chainStats(entries: ChainEntry[], state: ChainState): string {\n\tconst failed = entries.filter((e) => e.isError).length;\n\tconst parts: string[] = [];\n\n\tif (state === \"running\") {\n\t\tparts.push(`${entries.filter((e) => !e.isPartial).length} done`);\n\t} else {\n\t\tparts.push(plural(entries.length, \"call\"));\n\t}\n\tif (failed > 0) parts.push(`${failed} failed`);\n\n\tif (state === \"running\") {\n\t\tparts.push(\"running\");\n\t} else if (state === \"interrupted\") {\n\t\tparts.push(\"interrupted\");\n\t} else {\n\t\tconst lines = entries.reduce((sum, e) => sum + e.outputLines, 0);\n\t\tif (lines > 0) parts.push(plural(lines, \"line\"));\n\t}\n\treturn parts.join(\" · \");\n}\n\ninterface ToolFamily {\n\tverb: string;\n\t/** Noun for the headline's count form: \"Edited 3 files\". */\n\tnoun: string;\n\t/** Noun for the secondary clause, which has no verb to lean on: \"· 38 reads\". */\n\ttail: string;\n\ttools: string[];\n\t/**\n\t * Whether this family's subjects name a place on disk.\n\t *\n\t * Only these may be reduced to a shared location. A command is not a path\n\t * however much it looks like one: three calls that all begin\n\t * `cd /Users/me/repo && …` share a long slash-separated prefix, and treating\n\t * it as a location produced `Ran cd /Users/me/repo` — a headline naming the\n\t * one part of the command that did nothing.\n\t */\n\tsubjectIsPath: boolean;\n\t/** Reduce a subject to the act it performed, when the raw form carries noise. */\n\tact?: (subject: string) => string;\n}\n\n/** Leading `cd <dir> &&` runs, however many are chained. */\nconst CD_PREFIX_RE = /^(?:cd\\s+(?:\"[^\"]*\"|'[^']*'|[^\\s;&|]+)\\s*&&\\s*)+/;\n\n/**\n * A command's act, with the navigation stripped off the front.\n *\n * `cd packages/tui && bun run check` is one act performed somewhere, not two,\n * and the somewhere is not what the chain did.\n */\nfunction commandAct(subject: string): string {\n\treturn subject.replace(CD_PREFIX_RE, \"\").trim() || subject;\n}\n\n/**\n * Tool families, in the order a settled chain reports them.\n *\n * The ordering is the whole trick: a chain that read six files and then changed\n * one is remembered as the edit. Mutation outranks execution outranks reading\n * outranks searching, so the phrase names the most consequential thing the\n * chain did rather than the most frequent.\n *\n * Priority alone is only sound while the chain is small enough for every call\n * to plausibly serve the headline act. See `INCIDENTAL_SHARE`.\n */\nconst FAMILIES: ToolFamily[] = [\n\t{\n\t\tverb: \"Edited\",\n\t\tnoun: \"file\",\n\t\ttail: \"edit\",\n\t\ttools: [\"edit\", \"write\", \"MultiEdit\", \"NotebookEdit\"],\n\t\tsubjectIsPath: true,\n\t},\n\t{ verb: \"Ran\", noun: \"command\", tail: \"command\", tools: [\"bash\"], subjectIsPath: false, act: commandAct },\n\t{ verb: \"Delegated\", noun: \"task\", tail: \"task\", tools: [\"Task\", \"TaskOutput\"], subjectIsPath: false },\n\t{ verb: \"Fetched\", noun: \"page\", tail: \"fetch\", tools: [\"webfetch\", \"websearch\"], subjectIsPath: false },\n\t{ verb: \"Read\", noun: \"file\", tail: \"read\", tools: [\"read\"], subjectIsPath: true },\n\t{ verb: \"Searched\", noun: \"search\", tail: \"search\", tools: [\"grep\", \"find\", \"search\", \"ls\"], subjectIsPath: true },\n];\n\n/**\n * Calls from here up, a chain is long enough that priority order alone starts\n * lying, and the three rules below switch on.\n *\n * Below it the existing behaviour is left exactly as it was. Measured over this\n * repo's own sessions, 87% of chains are three calls or fewer and 75% touch a\n * single family; the failure these rules address does not exist down there, so\n * neither should the rules.\n */\nconst LONG_CHAIN_CALLS = 10;\n\n/**\n * A family this far below the chain's own size did not characterise it.\n *\n * Priority order says a chain that read six files and changed one is the edit,\n * and at seven calls that is true. At seventy-three it is not: 28 greps, 37\n * reads and one edit is an investigation that ended in a small change, and\n * calling it \"Edited subagent.ts\" describes 1 call out of 73.\n */\nconst INCIDENTAL_SHARE = 0.1;\n\n/** Below this, a secondary family is a footnote and not worth the width. */\nconst MIN_SECONDARY_CALLS = 3;\n\n/**\n * Longest common directory prefix of the given paths, or \"\" when they share none.\n *\n * `long` chains additionally reject a single-segment prefix: across forty files\n * the shared root collapses to something like `packages`, which names a location\n * so broad it is worse than the count it displaced. Short chains keep theirs —\n * `Edited docs` is a genuine location, and three files rarely share a vague one.\n */\nfunction commonPathPrefix(paths: string[], long: boolean): string {\n\tif (paths.length === 0) return \"\";\n\tconst split = paths.map((p) => p.split(\"/\").filter(Boolean));\n\tconst [first, ...rest] = split;\n\tconst prefix: string[] = [];\n\tfor (let i = 0; i < first.length; i++) {\n\t\tif (rest.every((parts) => parts[i] === first[i])) prefix.push(first[i]);\n\t\telse break;\n\t}\n\tif (prefix.length === 0) return \"\";\n\tif (long && prefix.length < 2) return \"\";\n\tconst joined = prefix.join(\"/\");\n\t// `filter(Boolean)` drops the empty leading segment of an absolute path.\n\treturn paths[0].startsWith(\"/\") ? `/${joined}` : joined;\n}\n\n/**\n * Whether a subject can stand in for a location.\n *\n * Globs are excluded even though they look pathlike: \"Searched *.test.ts\" reads\n * as a place the chain worked in, which is exactly what it is not.\n */\nfunction usableAsTarget(subject: string): boolean {\n\tif (/[*?]/.test(subject)) return false;\n\treturn subject.includes(\"/\") || /\\.[a-zA-Z0-9]+$/.test(subject);\n}\n\n/** The headline clause for one family's calls. */\nfunction familyPhrase(family: ToolFamily, matched: ChainEntry[], long: boolean): string {\n\tconst subjects = matched.map((e) => (family.act ? family.act(e.subject) : e.subject)).filter(Boolean);\n\n\t// A lone call is fully described by its own subject, whatever shape it is:\n\t// a command, a pattern, a URL, a path. So is a run that did the same thing\n\t// every time — `bun run check` twice is that command, not \"2 commands\".\n\tif (subjects.length === matched.length && new Set(subjects).size === 1) {\n\t\treturn `${family.verb} ${subjects[0]}`;\n\t}\n\n\t// Several calls: name the place they share, or fall back to a count.\n\tconst target = family.subjectIsPath ? commonPathPrefix(subjects.filter(usableAsTarget), long) : \"\";\n\tif (target) return `${family.verb} ${target}`;\n\t// Searching has no natural plural noun (\"3 searches\" says nothing the\n\t// stats do not), so the bare verb carries it.\n\tif (family.noun === \"search\") return \"Explored\";\n\treturn `${family.verb} ${plural(matched.length, family.noun)}`;\n}\n\n/** The family with the most calls other than `chosen`, if any reaches the floor. */\nfunction largestOtherFamily(\n\tentries: ChainEntry[],\n\tchosen: ToolFamily,\n): { family: ToolFamily; count: number } | undefined {\n\tlet best: { family: ToolFamily; count: number } | undefined;\n\tfor (const family of FAMILIES) {\n\t\tif (family === chosen) continue;\n\t\tconst count = entries.filter((e) => family.tools.includes(e.tool)).length;\n\t\tif (count > (best?.count ?? 0)) best = { family, count };\n\t}\n\treturn best && best.count >= MIN_SECONDARY_CALLS ? best : undefined;\n}\n\n/**\n * The settled chain's phrase: what this run amounted to.\n *\n * Deliberately close to one clause, not an inventory. \"Edited packages/tui, read\n * 6 files, searched packages\" is a worse line than \"Edited packages/tui\" — the\n * count is already in the stats, and the per-call detail is one `alt+u` away.\n *\n * A long chain earns at most one more clause, and only when its headline family\n * is a minority of the calls. That is the case where a single clause stops being\n * terse and starts being false: 31 edits among 182 calls is worth naming, but so\n * is the fact that 76 of the rest were commands. Listing every family it touched\n * is not the alternative — a chain that reports \"(ran, delegated, read,\n * searched)\" has said only that it was busy.\n *\n * A location is only named when the calls actually share one. Naming a single\n * arbitrary file out of five would read as a claim the chain never made, so\n * several unrelated targets collapse to a count instead.\n */\nexport function chainPhrase(entries: ChainEntry[]): string {\n\tif (entries.length === 0) return \"No calls\";\n\tconst long = entries.length >= LONG_CHAIN_CALLS;\n\n\tlet chosen: ToolFamily | undefined;\n\tlet matched: ChainEntry[] = [];\n\tfor (const family of FAMILIES) {\n\t\tconst m = entries.filter((e) => family.tools.includes(e.tool));\n\t\tif (m.length === 0) continue;\n\t\tif (long && m.length / entries.length < INCIDENTAL_SHARE) continue;\n\t\tchosen = family;\n\t\tmatched = m;\n\t\tbreak;\n\t}\n\n\t// Every family was incidental — a long chain spread thin across many tools.\n\t// The largest still describes it better than the priority order's first hit.\n\tif (!chosen) {\n\t\tfor (const family of FAMILIES) {\n\t\t\tconst m = entries.filter((e) => family.tools.includes(e.tool));\n\t\t\tif (m.length > matched.length) {\n\t\t\t\tchosen = family;\n\t\t\t\tmatched = m;\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unrecognised tool set still gets a line rather than a blank.\n\tif (!chosen) {\n\t\tconst names = [...new Set(entries.map((e) => e.tool))];\n\t\treturn names.length === 1 ? `Called ${names[0]}` : `Called ${plural(names.length, \"tool\")}`;\n\t}\n\n\tconst head = familyPhrase(chosen, matched, long);\n\tif (!long || matched.length / entries.length >= 0.5) return head;\n\tconst secondary = largestOtherFamily(entries, chosen);\n\treturn secondary ? `${head} · ${plural(secondary.count, secondary.family.tail)}` : head;\n}\n"]}
@@ -345,20 +345,24 @@ export declare class InteractiveMode {
345
345
  */
346
346
  private cycleToolOutputView;
347
347
  /**
348
- * Whether thinking traces render folded to their one-line label.
348
+ * How much of a thinking trace this view shows.
349
349
  *
350
- * Radar folds them regardless of the setting. The dial is one question asked
351
- * once — "how much do I ever want to see" — and a view whose whole job is to
352
- * fold a tool's output down to a row cannot then spend forty lines on the
353
- * reasoning that led to it.
350
+ * Radar drops them outright, regardless of the setting. The dial is one
351
+ * question asked once — "how much do I ever want to see" — and a view whose
352
+ * whole job is to fold a run of tool calls down to a row cannot then spend
353
+ * forty lines on the reasoning that led to it. Folding to the label is not
354
+ * enough either: a message that only thought and called tools has nothing
355
+ * else on screen once its calls join the chain, so its label would stand
356
+ * alone under the summary and one row per chain would become one row plus a
357
+ * label per call.
354
358
  *
355
359
  * It is also what keeps a running chain on screen. A chain stays open across
356
360
  * a thinking block (thinking is not text, so it is not a chain boundary), and
357
- * an unfolded trace streaming in below pushes the chain's own summary line off
358
- * the top — where every subsequent call rewrites a line above the viewport and
359
- * forces the full redraw that clears terminal scrollback.
361
+ * a trace rendering below pushes the chain's own summary line off the top —
362
+ * where every subsequent call rewrites a line above the viewport and forces
363
+ * the full redraw that clears terminal scrollback.
360
364
  */
361
- private thinkingHiddenForView;
365
+ private thinkingDisplayForView;
362
366
  private applyToolOutputView;
363
367
  private setToolsExpanded;
364
368
  private cycleAgentMode;