{"version":3,"file":"tool-chain.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAiC,MAAM,0BAA0B,CAAC;AACpG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;IAChD,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,IAAI,CAAiB;IAC7B,OAAO,CAAC,KAAK,CAAyB;IACtC,wDAAwD;IACxD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAC,CAAmC;IAChD,2EAA2E;IAC3E,OAAO,CAAC,MAAM,CAAC,CAAmC;IAElD,YAAY,IAAI,EAAE,cAAc,EAG/B;IAED,2EAA2E;IAC3E,OAAO,CAAC,MAAM;IAKd,GAAG,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAIvC;IAED,uDAAuD;IACvD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,8EAA8E;IAC9E,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,SAAS,sBAAsB,EAAE,CAElD;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAG3C;IAED,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAIlC;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAI/B;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED;;;;;;;;;OASG;IACH,OAAO,CAAC,WAAW;IAInB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,WAAW;IAMV,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CASvC;IAED,OAAO,CAAC,IAAI;IAkDZ;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;CAcpB;AAED,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,IAAI,kBAAkB,CAEzE","sourcesContent":["import { type Component, Container, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { type ChainState, chainPhrase, chainSegments, chainStats } from \"./tool-chain-summary.js\";\nimport type { ToolExecutionComponent } from \"./tool-execution.js\";\n\n/**\n * A run of consecutive tool calls, rendered as one line in the radar view.\n *\n * The chain owns its blocks rather than sitting beside them, so there is one\n * place that decides whether you are looking at a summary or at the calls. In\n * every view but a collapsed radar it is a plain pass-through container and the\n * blocks render exactly as they always did.\n *\n * A chain closes when the agent next speaks, or when the turn settles. Closing\n * early matters more than it looks: rewriting a line that has scrolled above\n * the viewport forces the TUI into a full redraw, which clears the terminal's\n * scrollback. Chains that close while they are still the bottom of the screen\n * flip for the price of one line.\n */\nexport class ToolChainComponent extends Container {\n\tprivate blocks: ToolExecutionComponent[] = [];\n\tprivate view: ToolOutputView;\n\tprivate state: ChainState = \"running\";\n\t/** The newest run in the transcript; radar marks it. */\n\tprivate latest = false;\n\tprivate memo?: { width: number; out: string[] };\n\t/** Lead-in memo, keyed on the rows it wraps, so the array stays stable. */\n\tprivate leadIn?: { src: string[]; out: string[] };\n\n\tconstructor(view: ToolOutputView) {\n\t\tsuper();\n\t\tthis.view = view;\n\t}\n\n\t/** Every cache this chain keeps. The two go stale for the same reasons. */\n\tprivate forget(): void {\n\t\tthis.memo = undefined;\n\t\tthis.leadIn = undefined;\n\t}\n\n\tadd(block: ToolExecutionComponent): void {\n\t\tthis.blocks.push(block);\n\t\tthis.addChild(block);\n\t\tthis.forget();\n\t}\n\n\t/** True until the agent speaks or the turn settles. */\n\tget isOpen(): boolean {\n\t\treturn this.state === \"running\";\n\t}\n\n\t/** Whether a summary line is standing in for this chain's calls right now. */\n\tget isSummarised(): boolean {\n\t\treturn this.isCollapsed();\n\t}\n\n\tget isEmpty(): boolean {\n\t\treturn this.blocks.length === 0;\n\t}\n\n\tget toolBlocks(): readonly ToolExecutionComponent[] {\n\t\treturn this.blocks;\n\t}\n\n\t/**\n\t * Settle the chain.\n\t *\n\t * `interrupted` keeps the running rendering, because the settled phrase is a\n\t * claim about what the run amounted to and a run cut off partway through has\n\t * no such claim to make — the same reason an aborted turn's plan items settle\n\t * to cancelled rather than done.\n\t */\n\tclose(outcome: \"done\" | \"interrupted\"): void {\n\t\tthis.state = outcome;\n\t\tthis.forget();\n\t}\n\n\tsetView(view: ToolOutputView): void {\n\t\tthis.view = view;\n\t\tthis.forget();\n\t\tfor (const block of this.blocks) block.setView(view);\n\t}\n\n\t/**\n\t * Mark this run as the newest in the transcript, or no longer it.\n\t *\n\t * The per-call rows carry the same mark, but they are not what radar usually\n\t * shows: a run of more than one call folds to this single line, so without\n\t * marking the line too the stroke would be invisible in the view it was\n\t * built for.\n\t */\n\tsetLatest(latest: boolean): void {\n\t\tif (this.latest === latest) return;\n\t\tthis.latest = latest;\n\t\tthis.forget();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.forget();\n\t}\n\n\t/**\n\t * Whether this chain is currently drawn as a single summary line.\n\t *\n\t * A chain of one is not summarised. Its phrase would be `Ran npm run check`,\n\t * which is strictly less than the radar row it replaced: the row names the\n\t * tool and how much came back, and the phrase drops both to say the same\n\t * thing in prose. Summarising is only worth a lossy rewrite when there is\n\t * more than one call to fold — and by measurement most chains have exactly\n\t * one, so this is the common case, not an edge.\n\t */\n\tprivate isCollapsed(): boolean {\n\t\treturn this.view === \"radar\" && this.blocks.length > 1;\n\t}\n\n\t/**\n\t * The blank row that holds a run off whatever came before it.\n\t *\n\t * Radar's rows stack without gaps on purpose, and that is right *between*\n\t * rows — but it left the first row of a run pressed against the prose that\n\t * introduced it, with the turn above and the run below reading as one\n\t * paragraph. The gap belongs to the run rather than to its rows: one blank\n\t * line at the top of the chain, and the rows go on stacking underneath it.\n\t *\n\t * Only radar needs it. Every other view gives each block a leading spacer of\n\t * its own, so asking the first block whether it draws one keeps the two from\n\t * doubling up.\n\t */\n\tprivate needsLeadIn(): boolean {\n\t\tif (this.view !== \"radar\") return false;\n\t\tif (this.isCollapsed()) return true;\n\t\treturn this.blocks[0]?.drawsLeadingGap() === false;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst rows = this.rows(width);\n\t\tif (rows.length === 0 || !this.needsLeadIn()) return rows;\n\t\t// Keep the wrapped array reference-stable: the TUI diffs whole subtrees\n\t\t// by identity, and a fresh array every frame would defeat that.\n\t\tif (this.leadIn?.src === rows) return this.leadIn.out;\n\t\tconst out = [\"\", ...rows];\n\t\tthis.leadIn = { src: rows, out };\n\t\treturn out;\n\t}\n\n\tprivate rows(width: number): string[] {\n\t\tif (!this.isCollapsed()) return super.render(width);\n\t\tif (this.memo && this.memo.width === width) return this.memo.out;\n\n\t\tconst entries = this.blocks.map((block) => block.chainEntry());\n\t\tconst running = this.state === \"running\";\n\t\tconst glyph = running ? \"◐\" : \"●\";\n\t\tconst glyphTone = entries.some((e) => e.isError) ? \"error\" : running ? \"warning\" : \"success\";\n\n\t\t// While it runs the chain shows its shape in order; once it is over, what\n\t\t// it amounted to. See tool-chain-summary.ts for why they differ.\n\t\tlet leftPlain: string;\n\t\tlet leftStyled: string;\n\t\tif (this.state === \"done\") {\n\t\t\tconst phrase = chainPhrase(entries);\n\t\t\tleftPlain = phrase;\n\t\t\tleftStyled = theme.fg(\"toolTitle\", phrase);\n\t\t} else {\n\t\t\tconst segments = chainSegments(entries);\n\t\t\tconst sep = \" › \";\n\t\t\tleftPlain = segments.map((s) => s.label).join(sep) + (running ? \"…\" : \"\");\n\t\t\tleftStyled =\n\t\t\t\tsegments\n\t\t\t\t\t.map((s) =>\n\t\t\t\t\t\ttheme.fg(s.tone === \"error\" ? \"error\" : s.tone === \"running\" ? \"warning\" : \"toolTitle\", s.label),\n\t\t\t\t\t)\n\t\t\t\t\t.join(theme.fg(\"dim\", sep)) + (running ? theme.fg(\"dim\", \"…\") : \"\");\n\t\t}\n\n\t\tconst stats = chainStats(entries, this.state);\n\t\t// The marker stroke runs over what you read and stops before the stats,\n\t\t// the same shape it takes on a per-call row.\n\t\tconst stroke = this.latest && theme.hasBg(\"activeToolBg\");\n\t\tconst mark = (text: string) => (stroke ? theme.bg(\"activeToolBg\", text) : text);\n\t\t// \" ● \" + left, then stats flush right when there is room for both.\n\t\tconst prefix = `${glyph} `;\n\t\tconst budget = Math.max(0, width - 1 - visibleWidth(prefix));\n\t\tlet line: string;\n\t\tif (visibleWidth(leftPlain) + 2 + visibleWidth(stats) <= budget) {\n\t\t\tconst pad = \" \".repeat(budget - visibleWidth(leftPlain) - visibleWidth(stats));\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${mark(leftStyled)}${pad}${theme.fg(\"muted\", stats)}`;\n\t\t} else {\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${mark(truncateToWidth(leftStyled, budget))}`;\n\t\t}\n\n\t\tconst out = [line, ...this.failureLines(width)];\n\t\tthis.memo = { width, out };\n\t\treturn out;\n\t}\n\n\t/**\n\t * Each failed call's reason, indented under the chain line.\n\t *\n\t * A collapsed chain hides its blocks, so without this a failure would be a\n\t * count in the stats and nothing else. Whatever else these views fold away,\n\t * they never fold away why something broke.\n\t */\n\tprivate failureLines(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const block of this.blocks) {\n\t\t\tconst entry = block.chainEntry();\n\t\t\tif (!entry.isError) continue;\n\t\t\tconst header = `   ${theme.fg(\"error\", \"✗\")} ${theme.fg(\"toolTitle\", entry.tool)}  ${theme.fg(\"toolOutput\", entry.subject)}`;\n\t\t\tlines.push(truncateToWidth(header, width));\n\t\t\tfor (const raw of block.errorText().split(\"\\n\")) {\n\t\t\t\tif (!raw.trim()) continue;\n\t\t\t\tlines.push(truncateToWidth(`     ${theme.fg(\"toolOutput\", raw)}`, width));\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/** Narrow a transcript child to a chain. */\nexport function isToolChain(child: Component): child is ToolChainComponent {\n\treturn child instanceof ToolChainComponent;\n}\n"]}