@kolisachint/hoocode-agent 0.5.31 → 0.5.33
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 +176 -0
- package/dist/core/agent-session-runtime.d.ts +28 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +61 -2
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/keybindings.d.ts +102 -30
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +123 -29
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +10 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +19 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tool-output-view.d.ts +33 -0
- package/dist/core/tool-output-view.d.ts.map +1 -0
- package/dist/core/tool-output-view.js +31 -0
- package/dist/core/tool-output-view.js.map +1 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +4 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts +20 -0
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +138 -9
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +31 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +13 -12
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-chain-summary.d.ts +72 -0
- package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chain-summary.js +301 -0
- package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -0
- package/dist/modes/interactive/components/tool-chain.d.ts +72 -0
- package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chain.js +169 -0
- package/dist/modes/interactive/components/tool-chain.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +55 -9
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +173 -33
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-signal.d.ts +50 -0
- package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-signal.js +151 -0
- package/dist/modes/interactive/components/tool-signal.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +65 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +278 -34
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +9 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +13 -0
- package/dist/modes/interactive/resource-display.js.map +1 -1
- package/docs/keybindings.md +5 -0
- package/docs/terminal-setup.md +51 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The radar view's chain line: one line for a run of consecutive tool calls.
|
|
3
|
+
*
|
|
4
|
+
* Two renderings of the same chain, because a run that is still going and a run
|
|
5
|
+
* that is over answer different questions.
|
|
6
|
+
*
|
|
7
|
+
* While it runs you want the shape — what it did, in order, and where it broke:
|
|
8
|
+
*
|
|
9
|
+
* ◐ grep › read › bash✗ › edit › bash… 4 done · 1 failed · running
|
|
10
|
+
*
|
|
11
|
+
* Once it is over the order has stopped mattering and what is left is what it
|
|
12
|
+
* amounted to:
|
|
13
|
+
*
|
|
14
|
+
* ● Edited packages/tui/src/keys.ts 5 calls · 1 failed
|
|
15
|
+
*
|
|
16
|
+
* A chain that did *not* finish keeps the running form plus a marker. The
|
|
17
|
+
* settled form is a claim about what was accomplished, and a chain cut off
|
|
18
|
+
* partway through has no such claim to make — the same reason
|
|
19
|
+
* `settleDanglingMainTasks` refuses to mark an aborted turn's plan items done.
|
|
20
|
+
*
|
|
21
|
+
* Everything here is pure and deterministic. A model-written phase label would
|
|
22
|
+
* read better than the inferred one, but it would cost tokens on every turn and
|
|
23
|
+
* this agent's whole premise is that nothing happens you did not pay for on
|
|
24
|
+
* purpose.
|
|
25
|
+
*/
|
|
26
|
+
/** Segments beyond this many are elided in the middle rather than wrapped. */
|
|
27
|
+
const MAX_SEGMENTS = 8;
|
|
28
|
+
/** Kept at the head when eliding. */
|
|
29
|
+
const HEAD_SEGMENTS = 3;
|
|
30
|
+
/** Kept at the tail when eliding. */
|
|
31
|
+
const TAIL_SEGMENTS = 2;
|
|
32
|
+
function tone(entry) {
|
|
33
|
+
if (entry.isError)
|
|
34
|
+
return "error";
|
|
35
|
+
if (entry.isPartial)
|
|
36
|
+
return "running";
|
|
37
|
+
return "ok";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Collapse consecutive calls to the same tool into one segment.
|
|
41
|
+
*
|
|
42
|
+
* A run of five reads is one act, not five, and spelling it out crowds out the
|
|
43
|
+
* calls either side of it. A failure never merges: `read ×4` hiding one broken
|
|
44
|
+
* read would defeat the point of the line.
|
|
45
|
+
*/
|
|
46
|
+
function collapseRepeats(entries) {
|
|
47
|
+
const segments = [];
|
|
48
|
+
let index = 0;
|
|
49
|
+
while (index < entries.length) {
|
|
50
|
+
const entry = entries[index];
|
|
51
|
+
const entryTone = tone(entry);
|
|
52
|
+
let run = 1;
|
|
53
|
+
while (index + run < entries.length &&
|
|
54
|
+
entries[index + run].tool === entry.tool &&
|
|
55
|
+
tone(entries[index + run]) === entryTone &&
|
|
56
|
+
entryTone === "ok") {
|
|
57
|
+
run++;
|
|
58
|
+
}
|
|
59
|
+
const marker = entryTone === "error" ? "✗" : "";
|
|
60
|
+
segments.push({ label: run > 1 ? `${entry.tool} ×${run}` : `${entry.tool}${marker}`, tone: entryTone });
|
|
61
|
+
index += run;
|
|
62
|
+
}
|
|
63
|
+
return segments;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Elide the middle of a long chain, keeping both ends legible.
|
|
67
|
+
*
|
|
68
|
+
* Failures are never elided. Where a run broke is the one thing the line exists
|
|
69
|
+
* to show, and a long chain is exactly when you most need it — hiding the ✗
|
|
70
|
+
* inside "… 22 more …" would leave the stats claiming a failure the line cannot
|
|
71
|
+
* point at.
|
|
72
|
+
*/
|
|
73
|
+
function capSegments(segments) {
|
|
74
|
+
if (segments.length <= MAX_SEGMENTS)
|
|
75
|
+
return segments;
|
|
76
|
+
const keep = new Set();
|
|
77
|
+
for (let i = 0; i < HEAD_SEGMENTS; i++)
|
|
78
|
+
keep.add(i);
|
|
79
|
+
for (let i = segments.length - TAIL_SEGMENTS; i < segments.length; i++)
|
|
80
|
+
keep.add(i);
|
|
81
|
+
segments.forEach((segment, i) => {
|
|
82
|
+
if (segment.tone === "error")
|
|
83
|
+
keep.add(i);
|
|
84
|
+
});
|
|
85
|
+
const out = [];
|
|
86
|
+
let gap = 0;
|
|
87
|
+
const flushGap = () => {
|
|
88
|
+
if (gap > 0)
|
|
89
|
+
out.push({ label: `… ${gap} more …`, tone: "ok" });
|
|
90
|
+
gap = 0;
|
|
91
|
+
};
|
|
92
|
+
segments.forEach((segment, i) => {
|
|
93
|
+
if (keep.has(i)) {
|
|
94
|
+
flushGap();
|
|
95
|
+
out.push(segment);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
gap++;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
flushGap();
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** The `grep › read × 3 › bash✗` part of a running chain line. */
|
|
105
|
+
export function chainSegments(entries) {
|
|
106
|
+
return capSegments(collapseRepeats(entries));
|
|
107
|
+
}
|
|
108
|
+
function plural(n, one, many = `${one}s`) {
|
|
109
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
110
|
+
}
|
|
111
|
+
/** The flush-right stats for either rendering. */
|
|
112
|
+
export function chainStats(entries, state) {
|
|
113
|
+
const failed = entries.filter((e) => e.isError).length;
|
|
114
|
+
const parts = [];
|
|
115
|
+
if (state === "running") {
|
|
116
|
+
parts.push(`${entries.filter((e) => !e.isPartial).length} done`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
parts.push(plural(entries.length, "call"));
|
|
120
|
+
}
|
|
121
|
+
if (failed > 0)
|
|
122
|
+
parts.push(`${failed} failed`);
|
|
123
|
+
if (state === "running") {
|
|
124
|
+
parts.push("running");
|
|
125
|
+
}
|
|
126
|
+
else if (state === "interrupted") {
|
|
127
|
+
parts.push("interrupted");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const lines = entries.reduce((sum, e) => sum + e.outputLines, 0);
|
|
131
|
+
if (lines > 0)
|
|
132
|
+
parts.push(plural(lines, "line"));
|
|
133
|
+
}
|
|
134
|
+
return parts.join(" · ");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Tool families, in the order a settled chain reports them.
|
|
138
|
+
*
|
|
139
|
+
* The ordering is the whole trick: a chain that read six files and then changed
|
|
140
|
+
* one is remembered as the edit. Mutation outranks execution outranks reading
|
|
141
|
+
* outranks searching, so the phrase names the most consequential thing the
|
|
142
|
+
* chain did rather than the most frequent.
|
|
143
|
+
*
|
|
144
|
+
* Priority alone is only sound while the chain is small enough for every call
|
|
145
|
+
* to plausibly serve the headline act. See `INCIDENTAL_SHARE`.
|
|
146
|
+
*/
|
|
147
|
+
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"] },
|
|
154
|
+
];
|
|
155
|
+
/**
|
|
156
|
+
* Calls from here up, a chain is long enough that priority order alone starts
|
|
157
|
+
* lying, and the three rules below switch on.
|
|
158
|
+
*
|
|
159
|
+
* Below it the existing behaviour is left exactly as it was. Measured over this
|
|
160
|
+
* repo's own sessions, 87% of chains are three calls or fewer and 75% touch a
|
|
161
|
+
* single family; the failure these rules address does not exist down there, so
|
|
162
|
+
* neither should the rules.
|
|
163
|
+
*/
|
|
164
|
+
const LONG_CHAIN_CALLS = 10;
|
|
165
|
+
/**
|
|
166
|
+
* A family this far below the chain's own size did not characterise it.
|
|
167
|
+
*
|
|
168
|
+
* Priority order says a chain that read six files and changed one is the edit,
|
|
169
|
+
* and at seven calls that is true. At seventy-three it is not: 28 greps, 37
|
|
170
|
+
* reads and one edit is an investigation that ended in a small change, and
|
|
171
|
+
* calling it "Edited subagent.ts" describes 1 call out of 73.
|
|
172
|
+
*/
|
|
173
|
+
const INCIDENTAL_SHARE = 0.1;
|
|
174
|
+
/** Below this, a secondary family is a footnote and not worth the width. */
|
|
175
|
+
const MIN_SECONDARY_CALLS = 3;
|
|
176
|
+
/**
|
|
177
|
+
* Longest common directory prefix of the given paths, or "" when they share none.
|
|
178
|
+
*
|
|
179
|
+
* `long` chains additionally reject a single-segment prefix: across forty files
|
|
180
|
+
* the shared root collapses to something like `packages`, which names a location
|
|
181
|
+
* so broad it is worse than the count it displaced. Short chains keep theirs —
|
|
182
|
+
* `Edited docs` is a genuine location, and three files rarely share a vague one.
|
|
183
|
+
*/
|
|
184
|
+
function commonPathPrefix(paths, long) {
|
|
185
|
+
if (paths.length === 0)
|
|
186
|
+
return "";
|
|
187
|
+
const split = paths.map((p) => p.split("/").filter(Boolean));
|
|
188
|
+
const [first, ...rest] = split;
|
|
189
|
+
const prefix = [];
|
|
190
|
+
for (let i = 0; i < first.length; i++) {
|
|
191
|
+
if (rest.every((parts) => parts[i] === first[i]))
|
|
192
|
+
prefix.push(first[i]);
|
|
193
|
+
else
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (prefix.length === 0)
|
|
197
|
+
return "";
|
|
198
|
+
if (long && prefix.length < 2)
|
|
199
|
+
return "";
|
|
200
|
+
const joined = prefix.join("/");
|
|
201
|
+
// `filter(Boolean)` drops the empty leading segment of an absolute path.
|
|
202
|
+
return paths[0].startsWith("/") ? `/${joined}` : joined;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Whether a subject can stand in for a location.
|
|
206
|
+
*
|
|
207
|
+
* Globs are excluded even though they look pathlike: "Searched *.test.ts" reads
|
|
208
|
+
* as a place the chain worked in, which is exactly what it is not.
|
|
209
|
+
*/
|
|
210
|
+
function usableAsTarget(subject) {
|
|
211
|
+
if (/[*?]/.test(subject))
|
|
212
|
+
return false;
|
|
213
|
+
return subject.includes("/") || /\.[a-zA-Z0-9]+$/.test(subject);
|
|
214
|
+
}
|
|
215
|
+
/** The headline clause for one family's calls. */
|
|
216
|
+
function familyPhrase(family, matched, long) {
|
|
217
|
+
const subjects = matched.map((e) => e.subject).filter(Boolean);
|
|
218
|
+
// 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) {
|
|
221
|
+
return `${family.verb} ${subjects[0]}`;
|
|
222
|
+
}
|
|
223
|
+
// Several calls: name the place they share, or fall back to a count.
|
|
224
|
+
const target = commonPathPrefix(subjects.filter(usableAsTarget), long);
|
|
225
|
+
if (target)
|
|
226
|
+
return `${family.verb} ${target}`;
|
|
227
|
+
// Searching has no natural plural noun ("3 searches" says nothing the
|
|
228
|
+
// stats do not), so the bare verb carries it.
|
|
229
|
+
if (family.noun === "search")
|
|
230
|
+
return "Explored";
|
|
231
|
+
return `${family.verb} ${plural(matched.length, family.noun)}`;
|
|
232
|
+
}
|
|
233
|
+
/** The family with the most calls other than `chosen`, if any reaches the floor. */
|
|
234
|
+
function largestOtherFamily(entries, chosen) {
|
|
235
|
+
let best;
|
|
236
|
+
for (const family of FAMILIES) {
|
|
237
|
+
if (family === chosen)
|
|
238
|
+
continue;
|
|
239
|
+
const count = entries.filter((e) => family.tools.includes(e.tool)).length;
|
|
240
|
+
if (count > (best?.count ?? 0))
|
|
241
|
+
best = { family, count };
|
|
242
|
+
}
|
|
243
|
+
return best && best.count >= MIN_SECONDARY_CALLS ? best : undefined;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The settled chain's phrase: what this run amounted to.
|
|
247
|
+
*
|
|
248
|
+
* Deliberately close to one clause, not an inventory. "Edited packages/tui, read
|
|
249
|
+
* 6 files, searched packages" is a worse line than "Edited packages/tui" — the
|
|
250
|
+
* count is already in the stats, and the per-call detail is one `alt+u` away.
|
|
251
|
+
*
|
|
252
|
+
* A long chain earns at most one more clause, and only when its headline family
|
|
253
|
+
* is a minority of the calls. That is the case where a single clause stops being
|
|
254
|
+
* terse and starts being false: 31 edits among 182 calls is worth naming, but so
|
|
255
|
+
* is the fact that 76 of the rest were commands. Listing every family it touched
|
|
256
|
+
* is not the alternative — a chain that reports "(ran, delegated, read,
|
|
257
|
+
* searched)" has said only that it was busy.
|
|
258
|
+
*
|
|
259
|
+
* A location is only named when the calls actually share one. Naming a single
|
|
260
|
+
* arbitrary file out of five would read as a claim the chain never made, so
|
|
261
|
+
* several unrelated targets collapse to a count instead.
|
|
262
|
+
*/
|
|
263
|
+
export function chainPhrase(entries) {
|
|
264
|
+
if (entries.length === 0)
|
|
265
|
+
return "No calls";
|
|
266
|
+
const long = entries.length >= LONG_CHAIN_CALLS;
|
|
267
|
+
let chosen;
|
|
268
|
+
let matched = [];
|
|
269
|
+
for (const family of FAMILIES) {
|
|
270
|
+
const m = entries.filter((e) => family.tools.includes(e.tool));
|
|
271
|
+
if (m.length === 0)
|
|
272
|
+
continue;
|
|
273
|
+
if (long && m.length / entries.length < INCIDENTAL_SHARE)
|
|
274
|
+
continue;
|
|
275
|
+
chosen = family;
|
|
276
|
+
matched = m;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
// Every family was incidental — a long chain spread thin across many tools.
|
|
280
|
+
// The largest still describes it better than the priority order's first hit.
|
|
281
|
+
if (!chosen) {
|
|
282
|
+
for (const family of FAMILIES) {
|
|
283
|
+
const m = entries.filter((e) => family.tools.includes(e.tool));
|
|
284
|
+
if (m.length > matched.length) {
|
|
285
|
+
chosen = family;
|
|
286
|
+
matched = m;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// An unrecognised tool set still gets a line rather than a blank.
|
|
291
|
+
if (!chosen) {
|
|
292
|
+
const names = [...new Set(entries.map((e) => e.tool))];
|
|
293
|
+
return names.length === 1 ? `Called ${names[0]}` : `Called ${plural(names.length, "tool")}`;
|
|
294
|
+
}
|
|
295
|
+
const head = familyPhrase(chosen, matched, long);
|
|
296
|
+
if (!long || matched.length / entries.length >= 0.5)
|
|
297
|
+
return head;
|
|
298
|
+
const secondary = largestOtherFamily(entries, chosen);
|
|
299
|
+
return secondary ? `${head} · ${plural(secondary.count, secondary.family.tail)}` : head;
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=tool-chain-summary.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type Component, Container } from "@kolisachint/hoocode-tui";
|
|
2
|
+
import type { ToolOutputView } from "../../../core/tool-output-view.js";
|
|
3
|
+
import type { ToolExecutionComponent } from "./tool-execution.js";
|
|
4
|
+
/**
|
|
5
|
+
* A run of consecutive tool calls, rendered as one line in the radar view.
|
|
6
|
+
*
|
|
7
|
+
* The chain owns its blocks rather than sitting beside them, so there is one
|
|
8
|
+
* place that decides whether you are looking at a summary or at the calls. In
|
|
9
|
+
* every view but a collapsed radar it is a plain pass-through container and the
|
|
10
|
+
* blocks render exactly as they always did.
|
|
11
|
+
*
|
|
12
|
+
* A chain closes when the agent next speaks, or when the turn settles. Closing
|
|
13
|
+
* early matters more than it looks: rewriting a line that has scrolled above
|
|
14
|
+
* the viewport forces the TUI into a full redraw, which clears the terminal's
|
|
15
|
+
* scrollback. Chains that close while they are still the bottom of the screen
|
|
16
|
+
* flip for the price of one line.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ToolChainComponent extends Container {
|
|
19
|
+
private blocks;
|
|
20
|
+
private view;
|
|
21
|
+
private state;
|
|
22
|
+
/** Opened into its per-call rows by `app.tools.unfoldOne`. */
|
|
23
|
+
private opened;
|
|
24
|
+
private memo?;
|
|
25
|
+
constructor(view: ToolOutputView);
|
|
26
|
+
add(block: ToolExecutionComponent): void;
|
|
27
|
+
/** True until the agent speaks or the turn settles. */
|
|
28
|
+
get isOpen(): boolean;
|
|
29
|
+
get isOpened(): boolean;
|
|
30
|
+
/** Whether a summary line is standing in for this chain's calls right now. */
|
|
31
|
+
get isSummarised(): boolean;
|
|
32
|
+
get isEmpty(): boolean;
|
|
33
|
+
get toolBlocks(): readonly ToolExecutionComponent[];
|
|
34
|
+
/**
|
|
35
|
+
* Settle the chain.
|
|
36
|
+
*
|
|
37
|
+
* `interrupted` keeps the running rendering, because the settled phrase is a
|
|
38
|
+
* claim about what the run amounted to and a run cut off partway through has
|
|
39
|
+
* no such claim to make — the same reason an aborted turn's plan items settle
|
|
40
|
+
* to cancelled rather than done.
|
|
41
|
+
*/
|
|
42
|
+
close(outcome: "done" | "interrupted"): void;
|
|
43
|
+
setView(view: ToolOutputView): void;
|
|
44
|
+
/** The global expand key opens every block, which also opens the chain. */
|
|
45
|
+
setExpanded(expanded: boolean): void;
|
|
46
|
+
/** `alt+u` in radar: open this chain into its per-call rows, or fold it back. */
|
|
47
|
+
setOpened(opened: boolean): void;
|
|
48
|
+
invalidate(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Whether this chain is currently drawn as a single summary line.
|
|
51
|
+
*
|
|
52
|
+
* A chain of one is not summarised. Its phrase would be `Ran npm run check`,
|
|
53
|
+
* which is strictly less than the radar row it replaced: the row names the
|
|
54
|
+
* tool and how much came back, and the phrase drops both to say the same
|
|
55
|
+
* thing in prose. Summarising is only worth a lossy rewrite when there is
|
|
56
|
+
* more than one call to fold — and by measurement most chains have exactly
|
|
57
|
+
* one, so this is the common case, not an edge.
|
|
58
|
+
*/
|
|
59
|
+
private isCollapsed;
|
|
60
|
+
render(width: number): string[];
|
|
61
|
+
/**
|
|
62
|
+
* Each failed call's reason, indented under the chain line.
|
|
63
|
+
*
|
|
64
|
+
* A collapsed chain hides its blocks, so without this a failure would be a
|
|
65
|
+
* count in the stats and nothing else. Whatever else these views fold away,
|
|
66
|
+
* they never fold away why something broke.
|
|
67
|
+
*/
|
|
68
|
+
private failureLines;
|
|
69
|
+
}
|
|
70
|
+
/** Narrow a transcript child to a chain. */
|
|
71
|
+
export declare function isToolChain(child: Component): child is ToolChainComponent;
|
|
72
|
+
//# sourceMappingURL=tool-chain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-chain.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-chain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAiC,MAAM,0BAA0B,CAAC;AACpG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAGxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;IAChD,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,IAAI,CAAiB;IAC7B,OAAO,CAAC,KAAK,CAAyB;IACtC,8DAA8D;IAC9D,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAC,CAAmC;IAEhD,YAAY,IAAI,EAAE,cAAc,EAG/B;IAED,GAAG,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAIvC;IAED,uDAAuD;IACvD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,8EAA8E;IAC9E,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,SAAS,sBAAsB,EAAE,CAElD;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAG3C;IAED,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAIlC;IAED,2EAA2E;IAC3E,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAInC;IAED,iFAAiF;IACjF,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAG/B;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED;;;;;;;;;OASG;IACH,OAAO,CAAC,WAAW;IAIV,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA4CvC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;CAcpB;AAED,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,IAAI,kBAAkB,CAEzE","sourcesContent":["import { type Component, Container, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { ToolOutputView } from \"../../../core/tool-output-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { type ChainState, chainPhrase, chainSegments, chainStats } from \"./tool-chain-summary.js\";\nimport type { ToolExecutionComponent } from \"./tool-execution.js\";\n\n/**\n * A run of consecutive tool calls, rendered as one line in the radar view.\n *\n * The chain owns its blocks rather than sitting beside them, so there is one\n * place that decides whether you are looking at a summary or at the calls. In\n * every view but a collapsed radar it is a plain pass-through container and the\n * blocks render exactly as they always did.\n *\n * A chain closes when the agent next speaks, or when the turn settles. Closing\n * early matters more than it looks: rewriting a line that has scrolled above\n * the viewport forces the TUI into a full redraw, which clears the terminal's\n * scrollback. Chains that close while they are still the bottom of the screen\n * flip for the price of one line.\n */\nexport class ToolChainComponent extends Container {\n\tprivate blocks: ToolExecutionComponent[] = [];\n\tprivate view: ToolOutputView;\n\tprivate state: ChainState = \"running\";\n\t/** Opened into its per-call rows by `app.tools.unfoldOne`. */\n\tprivate opened = false;\n\tprivate memo?: { width: number; out: string[] };\n\n\tconstructor(view: ToolOutputView) {\n\t\tsuper();\n\t\tthis.view = view;\n\t}\n\n\tadd(block: ToolExecutionComponent): void {\n\t\tthis.blocks.push(block);\n\t\tthis.addChild(block);\n\t\tthis.memo = undefined;\n\t}\n\n\t/** True until the agent speaks or the turn settles. */\n\tget isOpen(): boolean {\n\t\treturn this.state === \"running\";\n\t}\n\n\tget isOpened(): boolean {\n\t\treturn this.opened;\n\t}\n\n\t/** Whether a summary line is standing in for this chain's calls right now. */\n\tget isSummarised(): boolean {\n\t\treturn this.isCollapsed();\n\t}\n\n\tget isEmpty(): boolean {\n\t\treturn this.blocks.length === 0;\n\t}\n\n\tget toolBlocks(): readonly ToolExecutionComponent[] {\n\t\treturn this.blocks;\n\t}\n\n\t/**\n\t * Settle the chain.\n\t *\n\t * `interrupted` keeps the running rendering, because the settled phrase is a\n\t * claim about what the run amounted to and a run cut off partway through has\n\t * no such claim to make — the same reason an aborted turn's plan items settle\n\t * to cancelled rather than done.\n\t */\n\tclose(outcome: \"done\" | \"interrupted\"): void {\n\t\tthis.state = outcome;\n\t\tthis.memo = undefined;\n\t}\n\n\tsetView(view: ToolOutputView): void {\n\t\tthis.view = view;\n\t\tthis.memo = undefined;\n\t\tfor (const block of this.blocks) block.setView(view);\n\t}\n\n\t/** The global expand key opens every block, which also opens the chain. */\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.opened = expanded;\n\t\tthis.memo = undefined;\n\t\tfor (const block of this.blocks) block.setExpanded(expanded);\n\t}\n\n\t/** `alt+u` in radar: open this chain into its per-call rows, or fold it back. */\n\tsetOpened(opened: boolean): void {\n\t\tthis.opened = opened;\n\t\tthis.memo = undefined;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.memo = undefined;\n\t}\n\n\t/**\n\t * Whether this chain is currently drawn as a single summary line.\n\t *\n\t * A chain of one is not summarised. Its phrase would be `Ran npm run check`,\n\t * which is strictly less than the radar row it replaced: the row names the\n\t * tool and how much came back, and the phrase drops both to say the same\n\t * thing in prose. Summarising is only worth a lossy rewrite when there is\n\t * more than one call to fold — and by measurement most chains have exactly\n\t * one, so this is the common case, not an edge.\n\t */\n\tprivate isCollapsed(): boolean {\n\t\treturn this.view === \"radar\" && !this.opened && this.blocks.length > 1;\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (!this.isCollapsed()) return super.render(width);\n\t\tif (this.memo && this.memo.width === width) return this.memo.out;\n\n\t\tconst entries = this.blocks.map((block) => block.chainEntry());\n\t\tconst running = this.state === \"running\";\n\t\tconst glyph = running ? \"◐\" : \"●\";\n\t\tconst glyphTone = entries.some((e) => e.isError) ? \"error\" : running ? \"warning\" : \"success\";\n\n\t\t// While it runs the chain shows its shape in order; once it is over, what\n\t\t// it amounted to. See tool-chain-summary.ts for why they differ.\n\t\tlet leftPlain: string;\n\t\tlet leftStyled: string;\n\t\tif (this.state === \"done\") {\n\t\t\tconst phrase = chainPhrase(entries);\n\t\t\tleftPlain = phrase;\n\t\t\tleftStyled = theme.fg(\"toolTitle\", phrase);\n\t\t} else {\n\t\t\tconst segments = chainSegments(entries);\n\t\t\tconst sep = \" › \";\n\t\t\tleftPlain = segments.map((s) => s.label).join(sep) + (running ? \"…\" : \"\");\n\t\t\tleftStyled =\n\t\t\t\tsegments\n\t\t\t\t\t.map((s) =>\n\t\t\t\t\t\ttheme.fg(s.tone === \"error\" ? \"error\" : s.tone === \"running\" ? \"warning\" : \"toolTitle\", s.label),\n\t\t\t\t\t)\n\t\t\t\t\t.join(theme.fg(\"dim\", sep)) + (running ? theme.fg(\"dim\", \"…\") : \"\");\n\t\t}\n\n\t\tconst stats = chainStats(entries, this.state);\n\t\t// \" ● \" + left, then stats flush right when there is room for both.\n\t\tconst prefix = `${glyph} `;\n\t\tconst budget = Math.max(0, width - 1 - visibleWidth(prefix));\n\t\tlet line: string;\n\t\tif (visibleWidth(leftPlain) + 2 + visibleWidth(stats) <= budget) {\n\t\t\tconst pad = \" \".repeat(budget - visibleWidth(leftPlain) - visibleWidth(stats));\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${leftStyled}${pad}${theme.fg(\"muted\", stats)}`;\n\t\t} else {\n\t\t\tline = ` ${theme.fg(glyphTone, prefix)}${truncateToWidth(leftStyled, budget)}`;\n\t\t}\n\n\t\tconst out = [line, ...this.failureLines(width)];\n\t\tthis.memo = { width, out };\n\t\treturn out;\n\t}\n\n\t/**\n\t * Each failed call's reason, indented under the chain line.\n\t *\n\t * A collapsed chain hides its blocks, so without this a failure would be a\n\t * count in the stats and nothing else. Whatever else these views fold away,\n\t * they never fold away why something broke.\n\t */\n\tprivate failureLines(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const block of this.blocks) {\n\t\t\tconst entry = block.chainEntry();\n\t\t\tif (!entry.isError) continue;\n\t\t\tconst header = ` ${theme.fg(\"error\", \"✗\")} ${theme.fg(\"toolTitle\", entry.tool)} ${theme.fg(\"toolOutput\", entry.subject)}`;\n\t\t\tlines.push(truncateToWidth(header, width));\n\t\t\tfor (const raw of block.errorText().split(\"\\n\")) {\n\t\t\t\tif (!raw.trim()) continue;\n\t\t\t\tlines.push(truncateToWidth(` ${theme.fg(\"toolOutput\", raw)}`, width));\n\t\t\t}\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/** Narrow a transcript child to a chain. */\nexport function isToolChain(child: Component): child is ToolChainComponent {\n\treturn child instanceof ToolChainComponent;\n}\n"]}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Container, truncateToWidth, visibleWidth } from "@kolisachint/hoocode-tui";
|
|
2
|
+
import { theme } from "../theme/theme.js";
|
|
3
|
+
import { chainPhrase, chainSegments, chainStats } from "./tool-chain-summary.js";
|
|
4
|
+
/**
|
|
5
|
+
* A run of consecutive tool calls, rendered as one line in the radar view.
|
|
6
|
+
*
|
|
7
|
+
* The chain owns its blocks rather than sitting beside them, so there is one
|
|
8
|
+
* place that decides whether you are looking at a summary or at the calls. In
|
|
9
|
+
* every view but a collapsed radar it is a plain pass-through container and the
|
|
10
|
+
* blocks render exactly as they always did.
|
|
11
|
+
*
|
|
12
|
+
* A chain closes when the agent next speaks, or when the turn settles. Closing
|
|
13
|
+
* early matters more than it looks: rewriting a line that has scrolled above
|
|
14
|
+
* the viewport forces the TUI into a full redraw, which clears the terminal's
|
|
15
|
+
* scrollback. Chains that close while they are still the bottom of the screen
|
|
16
|
+
* flip for the price of one line.
|
|
17
|
+
*/
|
|
18
|
+
export class ToolChainComponent extends Container {
|
|
19
|
+
blocks = [];
|
|
20
|
+
view;
|
|
21
|
+
state = "running";
|
|
22
|
+
/** Opened into its per-call rows by `app.tools.unfoldOne`. */
|
|
23
|
+
opened = false;
|
|
24
|
+
memo;
|
|
25
|
+
constructor(view) {
|
|
26
|
+
super();
|
|
27
|
+
this.view = view;
|
|
28
|
+
}
|
|
29
|
+
add(block) {
|
|
30
|
+
this.blocks.push(block);
|
|
31
|
+
this.addChild(block);
|
|
32
|
+
this.memo = undefined;
|
|
33
|
+
}
|
|
34
|
+
/** True until the agent speaks or the turn settles. */
|
|
35
|
+
get isOpen() {
|
|
36
|
+
return this.state === "running";
|
|
37
|
+
}
|
|
38
|
+
get isOpened() {
|
|
39
|
+
return this.opened;
|
|
40
|
+
}
|
|
41
|
+
/** Whether a summary line is standing in for this chain's calls right now. */
|
|
42
|
+
get isSummarised() {
|
|
43
|
+
return this.isCollapsed();
|
|
44
|
+
}
|
|
45
|
+
get isEmpty() {
|
|
46
|
+
return this.blocks.length === 0;
|
|
47
|
+
}
|
|
48
|
+
get toolBlocks() {
|
|
49
|
+
return this.blocks;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Settle the chain.
|
|
53
|
+
*
|
|
54
|
+
* `interrupted` keeps the running rendering, because the settled phrase is a
|
|
55
|
+
* claim about what the run amounted to and a run cut off partway through has
|
|
56
|
+
* no such claim to make — the same reason an aborted turn's plan items settle
|
|
57
|
+
* to cancelled rather than done.
|
|
58
|
+
*/
|
|
59
|
+
close(outcome) {
|
|
60
|
+
this.state = outcome;
|
|
61
|
+
this.memo = undefined;
|
|
62
|
+
}
|
|
63
|
+
setView(view) {
|
|
64
|
+
this.view = view;
|
|
65
|
+
this.memo = undefined;
|
|
66
|
+
for (const block of this.blocks)
|
|
67
|
+
block.setView(view);
|
|
68
|
+
}
|
|
69
|
+
/** The global expand key opens every block, which also opens the chain. */
|
|
70
|
+
setExpanded(expanded) {
|
|
71
|
+
this.opened = expanded;
|
|
72
|
+
this.memo = undefined;
|
|
73
|
+
for (const block of this.blocks)
|
|
74
|
+
block.setExpanded(expanded);
|
|
75
|
+
}
|
|
76
|
+
/** `alt+u` in radar: open this chain into its per-call rows, or fold it back. */
|
|
77
|
+
setOpened(opened) {
|
|
78
|
+
this.opened = opened;
|
|
79
|
+
this.memo = undefined;
|
|
80
|
+
}
|
|
81
|
+
invalidate() {
|
|
82
|
+
super.invalidate();
|
|
83
|
+
this.memo = undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether this chain is currently drawn as a single summary line.
|
|
87
|
+
*
|
|
88
|
+
* A chain of one is not summarised. Its phrase would be `Ran npm run check`,
|
|
89
|
+
* which is strictly less than the radar row it replaced: the row names the
|
|
90
|
+
* tool and how much came back, and the phrase drops both to say the same
|
|
91
|
+
* thing in prose. Summarising is only worth a lossy rewrite when there is
|
|
92
|
+
* more than one call to fold — and by measurement most chains have exactly
|
|
93
|
+
* one, so this is the common case, not an edge.
|
|
94
|
+
*/
|
|
95
|
+
isCollapsed() {
|
|
96
|
+
return this.view === "radar" && !this.opened && this.blocks.length > 1;
|
|
97
|
+
}
|
|
98
|
+
render(width) {
|
|
99
|
+
if (!this.isCollapsed())
|
|
100
|
+
return super.render(width);
|
|
101
|
+
if (this.memo && this.memo.width === width)
|
|
102
|
+
return this.memo.out;
|
|
103
|
+
const entries = this.blocks.map((block) => block.chainEntry());
|
|
104
|
+
const running = this.state === "running";
|
|
105
|
+
const glyph = running ? "◐" : "●";
|
|
106
|
+
const glyphTone = entries.some((e) => e.isError) ? "error" : running ? "warning" : "success";
|
|
107
|
+
// While it runs the chain shows its shape in order; once it is over, what
|
|
108
|
+
// it amounted to. See tool-chain-summary.ts for why they differ.
|
|
109
|
+
let leftPlain;
|
|
110
|
+
let leftStyled;
|
|
111
|
+
if (this.state === "done") {
|
|
112
|
+
const phrase = chainPhrase(entries);
|
|
113
|
+
leftPlain = phrase;
|
|
114
|
+
leftStyled = theme.fg("toolTitle", phrase);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const segments = chainSegments(entries);
|
|
118
|
+
const sep = " › ";
|
|
119
|
+
leftPlain = segments.map((s) => s.label).join(sep) + (running ? "…" : "");
|
|
120
|
+
leftStyled =
|
|
121
|
+
segments
|
|
122
|
+
.map((s) => theme.fg(s.tone === "error" ? "error" : s.tone === "running" ? "warning" : "toolTitle", s.label))
|
|
123
|
+
.join(theme.fg("dim", sep)) + (running ? theme.fg("dim", "…") : "");
|
|
124
|
+
}
|
|
125
|
+
const stats = chainStats(entries, this.state);
|
|
126
|
+
// " ● " + left, then stats flush right when there is room for both.
|
|
127
|
+
const prefix = `${glyph} `;
|
|
128
|
+
const budget = Math.max(0, width - 1 - visibleWidth(prefix));
|
|
129
|
+
let line;
|
|
130
|
+
if (visibleWidth(leftPlain) + 2 + visibleWidth(stats) <= budget) {
|
|
131
|
+
const pad = " ".repeat(budget - visibleWidth(leftPlain) - visibleWidth(stats));
|
|
132
|
+
line = ` ${theme.fg(glyphTone, prefix)}${leftStyled}${pad}${theme.fg("muted", stats)}`;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
line = ` ${theme.fg(glyphTone, prefix)}${truncateToWidth(leftStyled, budget)}`;
|
|
136
|
+
}
|
|
137
|
+
const out = [line, ...this.failureLines(width)];
|
|
138
|
+
this.memo = { width, out };
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Each failed call's reason, indented under the chain line.
|
|
143
|
+
*
|
|
144
|
+
* A collapsed chain hides its blocks, so without this a failure would be a
|
|
145
|
+
* count in the stats and nothing else. Whatever else these views fold away,
|
|
146
|
+
* they never fold away why something broke.
|
|
147
|
+
*/
|
|
148
|
+
failureLines(width) {
|
|
149
|
+
const lines = [];
|
|
150
|
+
for (const block of this.blocks) {
|
|
151
|
+
const entry = block.chainEntry();
|
|
152
|
+
if (!entry.isError)
|
|
153
|
+
continue;
|
|
154
|
+
const header = ` ${theme.fg("error", "✗")} ${theme.fg("toolTitle", entry.tool)} ${theme.fg("toolOutput", entry.subject)}`;
|
|
155
|
+
lines.push(truncateToWidth(header, width));
|
|
156
|
+
for (const raw of block.errorText().split("\n")) {
|
|
157
|
+
if (!raw.trim())
|
|
158
|
+
continue;
|
|
159
|
+
lines.push(truncateToWidth(` ${theme.fg("toolOutput", raw)}`, width));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return lines;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Narrow a transcript child to a chain. */
|
|
166
|
+
export function isToolChain(child) {
|
|
167
|
+
return child instanceof ToolChainComponent;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=tool-chain.js.map
|