@kolisachint/hoocode-agent 0.5.33 → 0.5.35
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 +79 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +2 -2
- package/dist/core/tools/read.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +16 -3
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +18 -10
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +2 -2
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +2 -2
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +3 -2
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +5 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/progress-bar.d.ts.map +1 -1
- package/dist/modes/interactive/components/progress-bar.js +1 -1
- package/dist/modes/interactive/components/progress-bar.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +3 -3
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-chain-summary.js +28 -10
- package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +2 -1
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +13 -9
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +30 -23
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +24 -0
- package/dist/modes/interactive/theme/theme.d.ts +19 -2
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +66 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/interactive/theme/vox-cutout-dark.json +116 -0
- package/dist/modes/interactive/theme/vox-cutout-light.json +115 -0
- package/docs/themes.md +101 -1
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"progress-bar.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/progress-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa,EAAU;IACtD,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AAAA,CAClD;AAED;;;;;;;;;;;GAWG;AACH,SAAS,KAAK,CAAC,KAAa,EAAE,KAAa,EAAU;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"progress-bar.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/progress-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa,EAAU;IACtD,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AAAA,CAClD;AAED;;;;;;;;;;;GAWG;AACH,SAAS,KAAK,CAAC,KAAa,EAAE,KAAa,EAAU;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,KAAG,CAAC,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC;AAAA,CACjG;AAOD;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,MAAc,EAAE,OAAO,GAAuB,EAAE,EAAU;IAC1G,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,kBAAkB,CAAC;IAClD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC;IAChD,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAK,MAAM,EAAE,CAAC,EAAE,CAAC;AAAA,CAClG;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACrC,aAAqB,EACrB,UAAyB,EACzB,OAAO,GAAuB,EAAE,EACvB;IACT,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QAC5C,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,eAAe,CAAC,aAAa,CAAC,KAAG,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,eAAe,CAAC,aAAa,CAAC,MAAM,eAAe,CAAC,UAAU,CAAC,EAAE,CAAC;IACpF,OAAO,iBAAiB,CAAC,aAAa,GAAG,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAAA,CACtE","sourcesContent":["/**\n * The one progress bar. Every surface that makes the user wait on measurable\n * work renders it through here.\n *\n * There used to be two, written months apart, and they had drifted in three\n * separate ways: the footer drew `▰▱` while the voice panel drew `·` over `·`,\n * the footer said `2.0 MB` while the voice panel said `2 MB`, and each carried\n * its own copy of the percent-and-detail layout. None of that is a decision\n * anyone made — it is just what happens when the same widget is written twice.\n *\n * Bar *width* is still the caller's, because that is a genuine property of the\n * surface: a footer line shares its row with other status, a panel has the room\n * to be finer-grained. Everything else — glyphs, colours, percent, separator,\n * byte formatting — is fixed here, so two bars on screen at once always agree.\n */\n\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Default cells in a bar. Compact enough that several stacked footer lines\n * still fit a narrow terminal.\n */\nexport const PROGRESS_BAR_CELLS = 12;\n\n/**\n * Megabytes, to one decimal.\n *\n * The decimal is the point: whole megabytes make a slow download look frozen\n * for tens of seconds at a time, which is precisely when the user is deciding\n * whether it has hung.\n */\nexport function formatMegabytes(bytes: number): string {\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/**\n * The filled/empty track.\n *\n * The glyphs differ in shape, not only in colour, matching the context gauge in\n * the footer. An earlier version drew both halves as `·` and left the whole\n * reading to the colour, so at a glance — or on a low-contrast theme, or piped\n * somewhere that drops styling — a bar at 10% and one at 90% were identical.\n *\n * A started-but-tiny ratio keeps one filled cell rather than rounding to an\n * empty bar: \"nothing has happened yet\" and \"the first of forty is done\" are\n * different things to be told.\n */\nfunction track(ratio: number, cells: number): string {\n\tconst clamped = Math.max(0, Math.min(1, ratio));\n\tconst filled = Math.max(clamped > 0 ? 1 : 0, Math.min(cells, Math.round(clamped * cells)));\n\treturn theme.fg(\"accent\", \"▰\".repeat(filled)) + theme.fg(\"halftone\", \"▱\".repeat(cells - filled));\n}\n\nexport interface ProgressBarOptions {\n\t/** Track width. Defaults to {@link PROGRESS_BAR_CELLS}. */\n\tcells?: number;\n}\n\n/**\n * A determinate bar: track, percent, and a trailing detail such as\n * `3/12 sessions` or `2.0 MB / 8.0 MB`.\n *\n * Returns a styled string with no leading or trailing space, so callers can\n * place it after their own label. Callers width-clamp.\n */\nexport function renderProgressBar(ratio: number, detail: string, options: ProgressBarOptions = {}): string {\n\tconst cells = options.cells ?? PROGRESS_BAR_CELLS;\n\tconst clamped = Math.max(0, Math.min(1, ratio));\n\tconst percent = `${Math.round(clamped * 100)}%`;\n\treturn `${track(clamped, cells)} ${theme.fg(\"muted\", percent)} ${theme.fg(\"dim\", `· ${detail}`)}`;\n}\n\n/**\n * A download's progress, determinate when the server sent a length and a\n * running byte count when it did not.\n *\n * The indeterminate case deliberately drops the bar rather than animating a\n * fake one: a moving byte count already says \"still going\", and a bar that\n * cannot reach the end is worse than no bar.\n */\nexport function renderDownloadProgress(\n\treceivedBytes: number,\n\ttotalBytes: number | null,\n\toptions: ProgressBarOptions = {},\n): string {\n\tif (totalBytes === null || totalBytes <= 0) {\n\t\treturn theme.fg(\"dim\", `${formatMegabytes(receivedBytes)}…`);\n\t}\n\tconst detail = `${formatMegabytes(receivedBytes)} / ${formatMegabytes(totalBytes)}`;\n\treturn renderProgressBar(receivedBytes / totalBytes, detail, options);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-invocation-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/skill-invocation-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAY,KAAK,aAAa,EAAQ,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAIvE;;;;GAIG;AACH,qBAAa,+BAAgC,SAAQ,GAAG;IACvD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,aAAa,CAAgB;IAErC,YAAY,UAAU,EAAE,gBAAgB,EAAE,aAAa,GAAE,aAAkC,EAK1F;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,aAAa;CAsBrB","sourcesContent":["import { Box, Markdown, type MarkdownTheme, Text } from \"@kolisachint/hoocode-tui\";\nimport type { ParsedSkillBlock } from \"../../../core/agent-session.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\nimport { keyText } from \"./keybinding-hints.js\";\n\n/**\n * Component that renders a skill invocation message with collapsed/expanded state.\n * Uses same background color as custom messages for visual consistency.\n * Only renders the skill block itself - user message is rendered separately.\n */\nexport class SkillInvocationMessageComponent extends Box {\n\tprivate expanded = false;\n\tprivate skillBlock: ParsedSkillBlock;\n\tprivate markdownTheme: MarkdownTheme;\n\n\tconstructor(skillBlock: ParsedSkillBlock, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tthis.skillBlock = skillBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.clear();\n\n\t\tif (this.expanded) {\n\t\t\t// Expanded: label + skill name header + full content\n\t\t\tconst label =
|
|
1
|
+
{"version":3,"file":"skill-invocation-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/skill-invocation-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAY,KAAK,aAAa,EAAQ,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAIvE;;;;GAIG;AACH,qBAAa,+BAAgC,SAAQ,GAAG;IACvD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,aAAa,CAAgB;IAErC,YAAY,UAAU,EAAE,gBAAgB,EAAE,aAAa,GAAE,aAAkC,EAK1F;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,aAAa;CAsBrB","sourcesContent":["import { Box, Markdown, type MarkdownTheme, Text } from \"@kolisachint/hoocode-tui\";\nimport type { ParsedSkillBlock } from \"../../../core/agent-session.js\";\nimport { getMarkdownTheme, messageLabel, theme } from \"../theme/theme.js\";\nimport { keyText } from \"./keybinding-hints.js\";\n\n/**\n * Component that renders a skill invocation message with collapsed/expanded state.\n * Uses same background color as custom messages for visual consistency.\n * Only renders the skill block itself - user message is rendered separately.\n */\nexport class SkillInvocationMessageComponent extends Box {\n\tprivate expanded = false;\n\tprivate skillBlock: ParsedSkillBlock;\n\tprivate markdownTheme: MarkdownTheme;\n\n\tconstructor(skillBlock: ParsedSkillBlock, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tthis.skillBlock = skillBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.clear();\n\n\t\tif (this.expanded) {\n\t\t\t// Expanded: label + skill name header + full content\n\t\t\tconst label = messageLabel(\"skill\");\n\t\t\tthis.addChild(new Text(label, 0, 0));\n\t\t\tconst header = `**${this.skillBlock.name}**\\n\\n`;\n\t\t\tthis.addChild(\n\t\t\t\tnew Markdown(header + this.skillBlock.content, 0, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\t// Collapsed: single line - [skill] name (hint to expand)\n\t\t\tconst line =\n\t\t\t\t`${messageLabel(\"skill\")} ` +\n\t\t\t\ttheme.fg(\"customMessageText\", this.skillBlock.name) +\n\t\t\t\ttheme.fg(\"dim\", ` (${keyText(\"app.tools.expand\")} to expand)`);\n\t\t\tthis.addChild(new Text(line, 0, 0));\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Markdown, Text } from "@kolisachint/hoocode-tui";
|
|
2
|
-
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
2
|
+
import { getMarkdownTheme, messageLabel, theme } from "../theme/theme.js";
|
|
3
3
|
import { keyText } from "./keybinding-hints.js";
|
|
4
4
|
/**
|
|
5
5
|
* Component that renders a skill invocation message with collapsed/expanded state.
|
|
@@ -28,7 +28,7 @@ export class SkillInvocationMessageComponent extends Box {
|
|
|
28
28
|
this.clear();
|
|
29
29
|
if (this.expanded) {
|
|
30
30
|
// Expanded: label + skill name header + full content
|
|
31
|
-
const label =
|
|
31
|
+
const label = messageLabel("skill");
|
|
32
32
|
this.addChild(new Text(label, 0, 0));
|
|
33
33
|
const header = `**${this.skillBlock.name}**\n\n`;
|
|
34
34
|
this.addChild(new Markdown(header + this.skillBlock.content, 0, 0, this.markdownTheme, {
|
|
@@ -37,7 +37,7 @@ export class SkillInvocationMessageComponent extends Box {
|
|
|
37
37
|
}
|
|
38
38
|
else {
|
|
39
39
|
// Collapsed: single line - [skill] name (hint to expand)
|
|
40
|
-
const line =
|
|
40
|
+
const line = `${messageLabel("skill")} ` +
|
|
41
41
|
theme.fg("customMessageText", this.skillBlock.name) +
|
|
42
42
|
theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`);
|
|
43
43
|
this.addChild(new Text(line, 0, 0));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-invocation-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/skill-invocation-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAsB,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAEnF,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"skill-invocation-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/skill-invocation-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAsB,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAEnF,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAEhD;;;;GAIG;AACH,MAAM,OAAO,+BAAgC,SAAQ,GAAG;IAC/C,QAAQ,GAAG,KAAK,CAAC;IACjB,UAAU,CAAmB;IAC7B,aAAa,CAAgB;IAErC,YAAY,UAA4B,EAAE,aAAa,GAAkB,gBAAgB,EAAE,EAAE;QAC5F,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAED,WAAW,CAAC,QAAiB,EAAQ;QACpC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAEQ,UAAU,GAAS;QAC3B,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAEO,aAAa,GAAS;QAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,qDAAqD;YACrD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;YACpC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,MAAM,GAAG,KAAK,IAAI,CAAC,UAAU,CAAC,IAAI,QAAQ,CAAC;YACjD,IAAI,CAAC,QAAQ,CACZ,IAAI,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE;gBACxE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,IAAI,CAAC;aAC5D,CAAC,CACF,CAAC;QACH,CAAC;aAAM,CAAC;YACP,yDAAyD;YACzD,MAAM,IAAI,GACT,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG;gBAC3B,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;gBACnD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,OAAO,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;YAChE,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACrC,CAAC;IAAA,CACD;CACD","sourcesContent":["import { Box, Markdown, type MarkdownTheme, Text } from \"@kolisachint/hoocode-tui\";\nimport type { ParsedSkillBlock } from \"../../../core/agent-session.js\";\nimport { getMarkdownTheme, messageLabel, theme } from \"../theme/theme.js\";\nimport { keyText } from \"./keybinding-hints.js\";\n\n/**\n * Component that renders a skill invocation message with collapsed/expanded state.\n * Uses same background color as custom messages for visual consistency.\n * Only renders the skill block itself - user message is rendered separately.\n */\nexport class SkillInvocationMessageComponent extends Box {\n\tprivate expanded = false;\n\tprivate skillBlock: ParsedSkillBlock;\n\tprivate markdownTheme: MarkdownTheme;\n\n\tconstructor(skillBlock: ParsedSkillBlock, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\t\tthis.skillBlock = skillBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.clear();\n\n\t\tif (this.expanded) {\n\t\t\t// Expanded: label + skill name header + full content\n\t\t\tconst label = messageLabel(\"skill\");\n\t\t\tthis.addChild(new Text(label, 0, 0));\n\t\t\tconst header = `**${this.skillBlock.name}**\\n\\n`;\n\t\t\tthis.addChild(\n\t\t\t\tnew Markdown(header + this.skillBlock.content, 0, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t} else {\n\t\t\t// Collapsed: single line - [skill] name (hint to expand)\n\t\t\tconst line =\n\t\t\t\t`${messageLabel(\"skill\")} ` +\n\t\t\t\ttheme.fg(\"customMessageText\", this.skillBlock.name) +\n\t\t\t\ttheme.fg(\"dim\", ` (${keyText(\"app.tools.expand\")} to expand)`);\n\t\t\tthis.addChild(new Text(line, 0, 0));\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
|
-
{
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/user-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,SAAS,EAAY,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAOxF;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;IAClD,OAAO,CAAC,UAAU,CAAM;IAExB,YAAY,IAAI,EAAE,MAAM,EAAE,aAAa,GAAE,aAAkC,
|
|
1
|
+
{"version":3,"file":"user-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/user-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,SAAS,EAAY,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAOxF;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;IAClD,OAAO,CAAC,UAAU,CAAM;IAExB,YAAY,IAAI,EAAE,MAAM,EAAE,aAAa,GAAE,aAAkC,EAU1E;IAED;;8DAE0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,CAAmC;IAE3C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAcvC;CACD","sourcesContent":["import { Box, Container, Markdown, type MarkdownTheme } from \"@kolisachint/hoocode-tui\";\nimport { getMarkdownTheme, getPaperShadowFn, 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/**\n * Component that renders a user message\n */\nexport class UserMessageComponent extends Container {\n\tprivate contentBox: Box;\n\n\tconstructor(text: string, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper();\n\t\tthis.contentBox = new Box(1, 1, (content: string) => theme.bg(\"userMessageBg\", content));\n\t\tthis.contentBox.setShadowFn(getPaperShadowFn());\n\t\tthis.contentBox.addChild(\n\t\t\tnew Markdown(text, 0, 0, markdownTheme, {\n\t\t\t\tcolor: (content: string) => theme.fg(\"userMessageText\", content),\n\t\t\t}),\n\t\t);\n\t\tthis.addChild(this.contentBox);\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 (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"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Container, Markdown } from "@kolisachint/hoocode-tui";
|
|
2
|
-
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
2
|
+
import { getMarkdownTheme, getPaperShadowFn, theme } from "../theme/theme.js";
|
|
3
3
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
4
4
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
5
5
|
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
|
|
@@ -11,6 +11,7 @@ export class UserMessageComponent extends Container {
|
|
|
11
11
|
constructor(text, markdownTheme = getMarkdownTheme()) {
|
|
12
12
|
super();
|
|
13
13
|
this.contentBox = new Box(1, 1, (content) => theme.bg("userMessageBg", content));
|
|
14
|
+
this.contentBox.setShadowFn(getPaperShadowFn());
|
|
14
15
|
this.contentBox.addChild(new Markdown(text, 0, 0, markdownTheme, {
|
|
15
16
|
color: (content) => theme.fg("userMessageText", content),
|
|
16
17
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/user-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAsB,MAAM,0BAA0B,CAAC;AACxF,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"user-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/user-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAsB,MAAM,0BAA0B,CAAC;AACxF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE9E,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAC3C,MAAM,eAAe,GAAG,gBAAgB,CAAC;AACzC,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAE3C;;GAEG;AACH,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IAC1C,UAAU,CAAM;IAExB,YAAY,IAAY,EAAE,aAAa,GAAkB,gBAAgB,EAAE,EAAE;QAC5E,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC;QACzF,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,QAAQ,CACvB,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE;YACvC,KAAK,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,OAAO,CAAC;SAChE,CAAC,CACF,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAAA,CAC/B;IAED;;8DAE0D;IAClD,QAAQ,CAAoC;IAE3C,MAAM,CAAC,KAAa,EAAY;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,KAAK,CAAC;QACd,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,EAAE,GAAG,KAAK,KAAK,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC1B,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;QAC1B,GAAG,CAAC,CAAC,CAAC,GAAG,iBAAiB,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACpC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,eAAe,GAAG,iBAAiB,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAChF,IAAI,CAAC,QAAQ,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QACpC,OAAO,GAAG,CAAC;IAAA,CACX;CACD","sourcesContent":["import { Box, Container, Markdown, type MarkdownTheme } from \"@kolisachint/hoocode-tui\";\nimport { getMarkdownTheme, getPaperShadowFn, 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/**\n * Component that renders a user message\n */\nexport class UserMessageComponent extends Container {\n\tprivate contentBox: Box;\n\n\tconstructor(text: string, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper();\n\t\tthis.contentBox = new Box(1, 1, (content: string) => theme.bg(\"userMessageBg\", content));\n\t\tthis.contentBox.setShadowFn(getPaperShadowFn());\n\t\tthis.contentBox.addChild(\n\t\t\tnew Markdown(text, 0, 0, markdownTheme, {\n\t\t\t\tcolor: (content: string) => theme.fg(\"userMessageText\", content),\n\t\t\t}),\n\t\t);\n\t\tthis.addChild(this.contentBox);\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 (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"]}
|
|
@@ -345,20 +345,24 @@ export declare class InteractiveMode {
|
|
|
345
345
|
*/
|
|
346
346
|
private cycleToolOutputView;
|
|
347
347
|
/**
|
|
348
|
-
*
|
|
348
|
+
* How much of a thinking trace this view shows.
|
|
349
349
|
*
|
|
350
|
-
* Radar
|
|
351
|
-
* once — "how much do I ever want to see" — and a view whose
|
|
352
|
-
* fold a tool
|
|
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
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
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
|
|
365
|
+
private thinkingDisplayForView;
|
|
362
366
|
private applyToolOutputView;
|
|
363
367
|
private setToolsExpanded;
|
|
364
368
|
private cycleAgentMode;
|