lsd-pi 1.3.9 → 1.3.11

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.
Files changed (53) hide show
  1. package/dist/loader.js +0 -0
  2. package/dist/resources/agents/scout.md +1 -0
  3. package/dist/resources/extensions/mcp-client/index.js +191 -83
  4. package/dist/resources/extensions/mcp-client/mcp-manager-component.js +220 -0
  5. package/dist/resources/extensions/slash-commands/plan.js +67 -13
  6. package/dist/resources/extensions/subagent/agents.js +7 -0
  7. package/dist/resources/extensions/subagent/index.js +25 -8
  8. package/dist/resources/extensions/subagent/model-resolution.js +1 -0
  9. package/package.json +1 -1
  10. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +104 -2
  11. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
  12. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +39 -2
  13. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  14. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +135 -18
  15. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  16. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +21 -2
  17. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
  18. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +146 -8
  19. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
  20. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +51 -13
  21. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
  22. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  23. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +75 -4
  24. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  25. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
  26. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  28. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  29. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +31 -2
  31. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  32. package/packages/pi-coding-agent/package.json +1 -1
  33. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +129 -2
  34. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +158 -18
  35. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +163 -9
  36. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +60 -13
  37. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +86 -5
  38. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
  39. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -2
  40. package/pkg/package.json +1 -1
  41. package/src/resources/agents/scout.md +1 -0
  42. package/src/resources/extensions/mcp-client/index.ts +212 -90
  43. package/src/resources/extensions/mcp-client/mcp-manager-component.ts +256 -0
  44. package/src/resources/extensions/mcp-client/tests/mcp-manager-component.test.ts +141 -0
  45. package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +18 -2
  46. package/src/resources/extensions/slash-commands/plan.ts +70 -13
  47. package/src/resources/extensions/subagent/agents.ts +9 -0
  48. package/src/resources/extensions/subagent/index.ts +30 -8
  49. package/src/resources/extensions/subagent/model-resolution.ts +1 -0
  50. package/src/resources/extensions/voice/tests/linux-ready.test.ts +34 -1
  51. package/dist/headless-query.d.ts +0 -40
  52. package/dist/headless-query.js +0 -77
  53. package/src/resources/extensions/gsd/tests/test-helpers.ts +0 -61
@@ -1,7 +1,45 @@
1
1
  import { Container, Markdown, Spacer, Text } from "@gsd/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../theme/theme.js";
3
3
  /**
4
- * Component that renders a complete assistant message
4
+ * Create the response marker prefixed to the first visible text block.
5
+ * Lazy to avoid calling theme.fg() at module load time (fails in tests).
6
+ */
7
+ function getResponseMarker() {
8
+ return `${theme.fg("accent", "●")} `;
9
+ }
10
+ /**
11
+ * Create a Markdown component for an assistant text block.
12
+ * @param text - Text content (should be trimmed by caller)
13
+ * @param withMarker - Whether to prefix with the response marker
14
+ * @param markdownTheme - Markdown theme
15
+ */
16
+ export function createTextMarkdown(text, withMarker, markdownTheme) {
17
+ const withMarker_ = withMarker ? `${getResponseMarker()}${text}` : text;
18
+ return new Markdown(withMarker_, 1, 0, markdownTheme);
19
+ }
20
+ /**
21
+ * Create a Markdown component for a thinking block.
22
+ */
23
+ export function createThinkingMarkdown(thinking, markdownTheme) {
24
+ return new Markdown(thinking.trim(), 1, 0, markdownTheme, {
25
+ color: (text) => theme.fg("thinkingText", text),
26
+ italic: true,
27
+ });
28
+ }
29
+ /**
30
+ * Create an error/abort Text component.
31
+ */
32
+ export function createErrorText(message) {
33
+ return new Text(theme.fg("error", message), 1, 0);
34
+ }
35
+ /**
36
+ * Component that renders a complete assistant message.
37
+ *
38
+ * Supports two rendering modes:
39
+ * 1. Legacy: `updateContent(message)` renders all text/thinking into a contentContainer.
40
+ * Tool rows are expected to be added as siblings in the parent container.
41
+ * 2. Interleaved: `updateContentOrdered(message, toolComponents)` renders text/thinking
42
+ * AND tool components in content order. Tool components become children of this container.
5
43
  */
6
44
  export class AssistantMessageComponent extends Container {
7
45
  constructor(message, hideThinkingBlock = false, markdownTheme = getMarkdownTheme(), timestampFormat = "date-time-iso", thinkingLevel = "off") {
@@ -29,6 +67,12 @@ export class AssistantMessageComponent extends Container {
29
67
  setThinkingLevel(level) {
30
68
  this.thinkingLevel = level;
31
69
  }
70
+ /**
71
+ * Legacy rendering: renders text/thinking blocks into contentContainer.
72
+ * Stops rendering at the first tool-type block (toolCall/serverToolUse).
73
+ * Post-tool text blocks are handled by the chat-controller to preserve
74
+ * content ordering relative to tool rows.
75
+ */
32
76
  updateContent(message) {
33
77
  this.lastMessage = message;
34
78
  // Clear content container
@@ -43,26 +87,26 @@ export class AssistantMessageComponent extends Container {
43
87
  if (hasVisibleContent) {
44
88
  this.contentContainer.addChild(new Spacer(1));
45
89
  }
46
- // Render content in order
90
+ // Render content blocks up to (but not including) the first tool block.
91
+ // Text blocks after tools are rendered by the chat-controller as separate
92
+ // components to maintain correct visual ordering with tool rows.
47
93
  let markerAdded = false;
48
- const responseMarker = `${theme.fg("accent", "●")} `;
49
94
  for (let i = 0; i < message.content.length; i++) {
50
95
  const content = message.content[i];
96
+ // Stop at the first tool-type block — post-tool content is handled externally
97
+ if (content.type === "toolCall" || content.type === "serverToolUse") {
98
+ break;
99
+ }
51
100
  if (content.type === "text" && content.text.trim()) {
52
- // Assistant text messages with no background - trim the text
53
- // Set paddingY=0 to avoid extra spacing before tool executions
54
101
  const text = content.text.trim();
55
- const withMarker = markerAdded ? text : `${responseMarker}${text}`;
102
+ const withMarker = markerAdded ? text : `${getResponseMarker()}${text}`;
56
103
  this.contentContainer.addChild(new Markdown(withMarker, 1, 0, this.markdownTheme));
57
104
  markerAdded = true;
58
105
  }
59
106
  else if (content.type === "thinking" && content.thinking.trim()) {
60
107
  if (this.hideThinkingBlock) {
61
- // Hide thinking content entirely when hide-thinking is enabled.
62
108
  continue;
63
109
  }
64
- // Add spacing only when another visible assistant content block follows.
65
- // This avoids a superfluous blank line before separately-rendered tool execution blocks.
66
110
  const hasVisibleContentAfter = message.content.slice(i + 1).some((c) => {
67
111
  if (c.type === "text")
68
112
  return Boolean(c.text.trim());
@@ -70,7 +114,6 @@ export class AssistantMessageComponent extends Container {
70
114
  return !this.hideThinkingBlock && Boolean(c.thinking.trim());
71
115
  return false;
72
116
  });
73
- // Thinking traces in thinkingText color, italic
74
117
  this.contentContainer.addChild(new Markdown(content.thinking.trim(), 1, 0, this.markdownTheme, {
75
118
  color: (text) => theme.fg("thinkingText", text),
76
119
  italic: true,
@@ -81,8 +124,7 @@ export class AssistantMessageComponent extends Container {
81
124
  }
82
125
  }
83
126
  // Check if aborted - show after partial content
84
- // But only if there are no tool calls (tool execution components will show the error)
85
- const hasToolCalls = message.content.some((c) => c.type === "toolCall");
127
+ const hasToolCalls = message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
86
128
  if (!hasToolCalls) {
87
129
  if (message.stopReason === "aborted") {
88
130
  const abortMessage = message.errorMessage && message.errorMessage !== "Request was aborted"
@@ -102,12 +144,87 @@ export class AssistantMessageComponent extends Container {
102
144
  this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
103
145
  }
104
146
  }
105
- // Timestamp display removed
106
- // Show timestamp when the message is complete (has a stop reason)
107
- // if (message.stopReason && message.timestamp) {
108
- // const timeStr = formatTimestamp(message.timestamp, this.timestampFormat);
109
- // this.contentContainer.addChild(new Text(theme.fg("dim", timeStr), 1, 0));
110
- // }
147
+ }
148
+ /**
149
+ * Interleaved rendering: renders text/thinking AND tool components in content order.
150
+ * Tool components become children of this container, preserving visual ordering.
151
+ *
152
+ * @param message - The assistant message
153
+ * @param toolComponents - Map of content block ID → pre-created Component (ToolExecutionComponent etc.)
154
+ * @returns Map of content block ID → the tool Component that was placed (for pending tool tracking)
155
+ */
156
+ updateContentOrdered(message, toolComponents) {
157
+ this.lastMessage = message;
158
+ // Clear contentContainer so we can re-render all blocks in order
159
+ this.contentContainer.clear();
160
+ const placedTools = new Map();
161
+ // Check if there's any visible content at all
162
+ const hasVisibleContent = message.content.some((c) => {
163
+ if (c.type === "text")
164
+ return Boolean(c.text.trim());
165
+ if (c.type === "thinking")
166
+ return !this.hideThinkingBlock && Boolean(c.thinking.trim());
167
+ return false;
168
+ });
169
+ if (hasVisibleContent) {
170
+ this.contentContainer.addChild(new Spacer(1));
171
+ }
172
+ // Render all content blocks in order
173
+ let markerAdded = false;
174
+ for (let i = 0; i < message.content.length; i++) {
175
+ const block = message.content[i];
176
+ if (block.type === "text" && block.text.trim()) {
177
+ const text = block.text.trim();
178
+ const withMarker = markerAdded ? text : `${getResponseMarker()}${text}`;
179
+ this.contentContainer.addChild(new Markdown(withMarker, 1, 0, this.markdownTheme));
180
+ markerAdded = true;
181
+ }
182
+ else if (block.type === "thinking" && block.thinking.trim()) {
183
+ if (this.hideThinkingBlock) {
184
+ continue;
185
+ }
186
+ // Add spacing only when another visible assistant content block follows.
187
+ const hasVisibleContentAfter = message.content.slice(i + 1).some((c) => {
188
+ if (c.type === "text")
189
+ return Boolean(c.text.trim());
190
+ if (c.type === "thinking")
191
+ return !this.hideThinkingBlock && Boolean(c.thinking.trim());
192
+ return false;
193
+ });
194
+ this.contentContainer.addChild(new Markdown(block.thinking.trim(), 1, 0, this.markdownTheme, {
195
+ color: (text) => theme.fg("thinkingText", text),
196
+ italic: true,
197
+ }));
198
+ if (hasVisibleContentAfter) {
199
+ this.contentContainer.addChild(new Spacer(1));
200
+ }
201
+ }
202
+ else if ((block.type === "toolCall" || block.type === "serverToolUse") && toolComponents?.has(block.id)) {
203
+ // Place the pre-created tool component in content order
204
+ const toolComponent = toolComponents.get(block.id);
205
+ this.contentContainer.addChild(toolComponent);
206
+ placedTools.set(block.id, toolComponent);
207
+ }
208
+ // webSearchResult blocks don't produce their own component;
209
+ // they update the matching serverToolUse component via updateResult()
210
+ }
211
+ // Handle abort/error after content (only if no tool calls)
212
+ const hasToolCalls = message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
213
+ if (!hasToolCalls) {
214
+ if (message.stopReason === "aborted") {
215
+ const abortMessage = message.errorMessage && message.errorMessage !== "Request was aborted"
216
+ ? message.errorMessage
217
+ : "Operation aborted";
218
+ this.contentContainer.addChild(new Spacer(1));
219
+ this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
220
+ }
221
+ else if (message.stopReason === "error") {
222
+ const errorMsg = message.errorMessage || "Unknown error";
223
+ this.contentContainer.addChild(new Spacer(1));
224
+ this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
225
+ }
226
+ }
227
+ return placedTools;
111
228
  }
112
229
  }
113
230
  //# sourceMappingURL=assistant-message.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"@gsd/pi-ai\";\nimport { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@gsd/pi-tui\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\nimport { formatTimestamp, type TimestampFormat } from \"./timestamp.js\";\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\tprivate thinkingLevel: string;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate lastMessage?: AssistantMessage;\n\tprivate timestampFormat: TimestampFormat;\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\ttimestampFormat: TimestampFormat = \"date-time-iso\",\n\t\tthinkingLevel = \"off\",\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.thinkingLevel = thinkingLevel;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.timestampFormat = timestampFormat;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t}\n\n\tsetThinkingLevel(level: string): void {\n\t\tthis.thinkingLevel = level;\n\t}\n\n\tupdateContent(message: AssistantMessage): void {\n\t\tthis.lastMessage = message;\n\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tconst hasVisibleContent = message.content.some((c) => {\n\t\t\tif (c.type === \"text\") return Boolean(c.text.trim());\n\t\t\tif (c.type === \"thinking\") return !this.hideThinkingBlock && Boolean(c.thinking.trim());\n\t\t\treturn false;\n\t\t});\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content in order\n\t\tlet markerAdded = false;\n\t\tconst responseMarker = `${theme.fg(\"accent\", \"●\")} `;\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tconst text = content.text.trim();\n\t\t\t\tconst withMarker = markerAdded ? text : `${responseMarker}${text}`;\n\t\t\t\tthis.contentContainer.addChild(new Markdown(withMarker, 1, 0, this.markdownTheme));\n\t\t\t\tmarkerAdded = true;\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\t// Hide thinking content entirely when hide-thinking is enabled.\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content.slice(i + 1).some((c) => {\n\t\t\t\t\tif (c.type === \"text\") return Boolean(c.text.trim());\n\t\t\t\t\tif (c.type === \"thinking\") return !this.hideThinkingBlock && Boolean(c.thinking.trim());\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\n\t\t\t\t// Thinking traces in thinkingText color, italic\n\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\tnew Markdown(content.thinking.trim(), 1, 0, this.markdownTheme, {\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check if aborted - show after partial content\n\t\t// But only if there are no tool calls (tool execution components will show the error)\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tif (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tif (hasVisibleContent) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t} else {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), 1, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), 1, 0));\n\t\t\t}\n\t\t}\n\n\t\t// Timestamp display removed\n\t\t// Show timestamp when the message is complete (has a stop reason)\n\t\t// if (message.stopReason && message.timestamp) {\n\t\t// \tconst timeStr = formatTimestamp(message.timestamp, this.timestampFormat);\n\t\t// \tthis.contentContainer.addChild(new Text(theme.fg(\"dim\", timeStr), 1, 0));\n\t\t// }\n\t}\n}\n"]}
1
+ {"version":3,"file":"assistant-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"@gsd/pi-ai\";\nimport type { Component } from \"@gsd/pi-tui\";\nimport { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@gsd/pi-tui\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\nimport { formatTimestamp, type TimestampFormat } from \"./timestamp.js\";\n\n/**\n * Create the response marker prefixed to the first visible text block.\n * Lazy to avoid calling theme.fg() at module load time (fails in tests).\n */\nfunction getResponseMarker(): string {\n\treturn `${theme.fg(\"accent\", \"●\")} `;\n}\n\n/**\n * Create a Markdown component for an assistant text block.\n * @param text - Text content (should be trimmed by caller)\n * @param withMarker - Whether to prefix with the response marker\n * @param markdownTheme - Markdown theme\n */\nexport function createTextMarkdown(\n\ttext: string,\n\twithMarker: boolean,\n\tmarkdownTheme: MarkdownTheme,\n): Markdown {\n\tconst withMarker_ = withMarker ? `${getResponseMarker()}${text}` : text;\n\treturn new Markdown(withMarker_, 1, 0, markdownTheme);\n}\n\n/**\n * Create a Markdown component for a thinking block.\n */\nexport function createThinkingMarkdown(\n\tthinking: string,\n\tmarkdownTheme: MarkdownTheme,\n): Markdown {\n\treturn new Markdown(thinking.trim(), 1, 0, markdownTheme, {\n\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\titalic: true,\n\t});\n}\n\n/**\n * Create an error/abort Text component.\n */\nexport function createErrorText(message: string): Text {\n\treturn new Text(theme.fg(\"error\", message), 1, 0);\n}\n\n/**\n * Component that renders a complete assistant message.\n *\n * Supports two rendering modes:\n * 1. Legacy: `updateContent(message)` renders all text/thinking into a contentContainer.\n * Tool rows are expected to be added as siblings in the parent container.\n * 2. Interleaved: `updateContentOrdered(message, toolComponents)` renders text/thinking\n * AND tool components in content order. Tool components become children of this container.\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\tprivate thinkingLevel: string;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate lastMessage?: AssistantMessage;\n\tprivate timestampFormat: TimestampFormat;\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\ttimestampFormat: TimestampFormat = \"date-time-iso\",\n\t\tthinkingLevel = \"off\",\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.thinkingLevel = thinkingLevel;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.timestampFormat = timestampFormat;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t}\n\n\tsetThinkingLevel(level: string): void {\n\t\tthis.thinkingLevel = level;\n\t}\n\n\t/**\n\t * Legacy rendering: renders text/thinking blocks into contentContainer.\n\t * Stops rendering at the first tool-type block (toolCall/serverToolUse).\n\t * Post-tool text blocks are handled by the chat-controller to preserve\n\t * content ordering relative to tool rows.\n\t */\n\tupdateContent(message: AssistantMessage): void {\n\t\tthis.lastMessage = message;\n\n\t\t// Clear content container\n\t\tthis.contentContainer.clear();\n\n\t\tconst hasVisibleContent = message.content.some((c) => {\n\t\t\tif (c.type === \"text\") return Boolean(c.text.trim());\n\t\t\tif (c.type === \"thinking\") return !this.hideThinkingBlock && Boolean(c.thinking.trim());\n\t\t\treturn false;\n\t\t});\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render content blocks up to (but not including) the first tool block.\n\t\t// Text blocks after tools are rendered by the chat-controller as separate\n\t\t// components to maintain correct visual ordering with tool rows.\n\t\tlet markerAdded = false;\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\n\t\t\t// Stop at the first tool-type block — post-tool content is handled externally\n\t\t\tif (content.type === \"toolCall\" || content.type === \"serverToolUse\") {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\tconst text = content.text.trim();\n\t\t\t\tconst withMarker = markerAdded ? text : `${getResponseMarker()}${text}`;\n\t\t\t\tthis.contentContainer.addChild(new Markdown(withMarker, 1, 0, this.markdownTheme));\n\t\t\t\tmarkerAdded = true;\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst hasVisibleContentAfter = message.content.slice(i + 1).some((c) => {\n\t\t\t\t\tif (c.type === \"text\") return Boolean(c.text.trim());\n\t\t\t\t\tif (c.type === \"thinking\") return !this.hideThinkingBlock && Boolean(c.thinking.trim());\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\n\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\tnew Markdown(content.thinking.trim(), 1, 0, this.markdownTheme, {\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check if aborted - show after partial content\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\" || c.type === \"serverToolUse\");\n\t\tif (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tif (hasVisibleContent) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t} else {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), 1, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), 1, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Interleaved rendering: renders text/thinking AND tool components in content order.\n\t * Tool components become children of this container, preserving visual ordering.\n\t *\n\t * @param message - The assistant message\n\t * @param toolComponents - Map of content block ID → pre-created Component (ToolExecutionComponent etc.)\n\t * @returns Map of content block ID → the tool Component that was placed (for pending tool tracking)\n\t */\n\tupdateContentOrdered(\n\t\tmessage: AssistantMessage,\n\t\ttoolComponents?: Map<string, Component>,\n\t): Map<string, Component> {\n\t\tthis.lastMessage = message;\n\n\t\t// Clear contentContainer so we can re-render all blocks in order\n\t\tthis.contentContainer.clear();\n\n\t\tconst placedTools = new Map<string, Component>();\n\n\t\t// Check if there's any visible content at all\n\t\tconst hasVisibleContent = message.content.some((c) => {\n\t\t\tif (c.type === \"text\") return Boolean(c.text.trim());\n\t\t\tif (c.type === \"thinking\") return !this.hideThinkingBlock && Boolean(c.thinking.trim());\n\t\t\treturn false;\n\t\t});\n\n\t\tif (hasVisibleContent) {\n\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\t// Render all content blocks in order\n\t\tlet markerAdded = false;\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst block = message.content[i];\n\n\t\t\tif (block.type === \"text\" && block.text.trim()) {\n\t\t\t\tconst text = block.text.trim();\n\t\t\t\tconst withMarker = markerAdded ? text : `${getResponseMarker()}${text}`;\n\t\t\t\tthis.contentContainer.addChild(new Markdown(withMarker, 1, 0, this.markdownTheme));\n\t\t\t\tmarkerAdded = true;\n\t\t\t} else if (block.type === \"thinking\" && block.thinking.trim()) {\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\tconst hasVisibleContentAfter = message.content.slice(i + 1).some((c) => {\n\t\t\t\t\tif (c.type === \"text\") return Boolean(c.text.trim());\n\t\t\t\t\tif (c.type === \"thinking\") return !this.hideThinkingBlock && Boolean(c.thinking.trim());\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\n\t\t\t\tthis.contentContainer.addChild(\n\t\t\t\t\tnew Markdown(block.thinking.trim(), 1, 0, this.markdownTheme, {\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\t}\n\t\t\t} else if ((block.type === \"toolCall\" || block.type === \"serverToolUse\") && toolComponents?.has(block.id)) {\n\t\t\t\t// Place the pre-created tool component in content order\n\t\t\t\tconst toolComponent = toolComponents.get(block.id)!;\n\t\t\t\tthis.contentContainer.addChild(toolComponent);\n\t\t\t\tplacedTools.set(block.id, toolComponent);\n\t\t\t}\n\t\t\t// webSearchResult blocks don't produce their own component;\n\t\t\t// they update the matching serverToolUse component via updateResult()\n\t\t}\n\n\t\t// Handle abort/error after content (only if no tool calls)\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\" || c.type === \"serverToolUse\");\n\t\tif (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", abortMessage), 1, 0));\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tthis.contentContainer.addChild(new Spacer(1));\n\t\t\t\tthis.contentContainer.addChild(new Text(theme.fg(\"error\", `Error: ${errorMsg}`), 1, 0));\n\t\t\t}\n\t\t}\n\n\t\treturn placedTools;\n\t}\n}\n"]}
@@ -1,10 +1,29 @@
1
- import { Container } from "@gsd/pi-tui";
1
+ import { Container, type TUI } from "@gsd/pi-tui";
2
+ export declare function extractToolLabel(toolName: string, args: Record<string, unknown>): string;
2
3
  export declare class ToolSummaryLine extends Container {
3
4
  private tools;
5
+ private pendingTools;
4
6
  private hidden;
5
7
  private contentText;
8
+ private labelText;
9
+ private expandHint;
10
+ private ui?;
11
+ private spinnerTimer;
12
+ private spinnerFrame;
13
+ private lastToolLabel;
6
14
  canGroupWith(toolName: string): boolean;
7
- constructor();
15
+ constructor(ui?: TUI);
16
+ setUI(ui: TUI): void;
17
+ setExpandHint(hint: string): void;
18
+ addPendingTool(toolCallId: string, name: string, args: Record<string, unknown>): void;
19
+ removePendingTool(toolCallId: string): void;
20
+ hasPendingTools(): boolean;
21
+ hasPendingTool(toolCallId: string): boolean;
22
+ clearPendingTools(): void;
23
+ updatePendingToolArgs(toolCallId: string, args: Record<string, unknown>): void;
24
+ private startSpinner;
25
+ private stopSpinner;
26
+ dispose(): void;
8
27
  addTool(name: string, elapsed: number): void;
9
28
  setHidden(hidden: boolean): void;
10
29
  invalidate(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"tool-summary-line.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-summary-line.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAQ,MAAM,aAAa,CAAC;AAwD9C,qBAAa,eAAgB,SAAQ,SAAS;IAC7C,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAO;IAE1B,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;;IAgBvC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5C,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAIvB,UAAU,IAAI,IAAI;IAKlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAOxC,OAAO,CAAC,aAAa;CAqBrB"}
1
+ {"version":3,"file":"tool-summary-line.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-summary-line.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAQ,KAAK,GAAG,EAAE,MAAM,aAAa,CAAC;AAqExD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAyBxF;AAED,qBAAa,eAAgB,SAAQ,SAAS;IAC7C,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAuC;IAC3D,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,EAAE,CAAC,CAAM;IACjB,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,aAAa,CAAM;IAE3B,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;gBAW3B,EAAE,CAAC,EAAE,GAAG;IASpB,KAAK,CAAC,EAAE,EAAE,GAAG,GAAG,IAAI;IAIpB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKjC,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAQrF,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAe3C,eAAe,IAAI,OAAO;IAI1B,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI3C,iBAAiB,IAAI,IAAI;IAUzB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAQ9E,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,WAAW;IAMnB,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5C,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAIvB,UAAU,IAAI,IAAI;IAKlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAOxC,OAAO,CAAC,aAAa;CA6CrB"}
@@ -1,3 +1,4 @@
1
+ import { basename } from "node:path";
1
2
  import { Container, Text } from "@gsd/pi-tui";
2
3
  import { theme } from "../theme/theme.js";
3
4
  // Tools that can be mixed together in one summary line
@@ -22,6 +23,8 @@ const TOOL_SUMMARY_DESCRIPTORS = {
22
23
  search_and_read: { action: "researching", singular: "topic", plural: "topics" },
23
24
  google_search: { action: "searching web for", singular: "query", plural: "queries" },
24
25
  };
26
+ const SPINNER_FRAMES = ["◯", "◔", "◑", "◕", "●"];
27
+ const SPINNER_INTERVAL_MS = 150;
25
28
  function formatCount(count, singular, plural) {
26
29
  return `${count} ${count === 1 ? singular : plural}`;
27
30
  }
@@ -35,23 +38,137 @@ function summarizeToolGroup(name, count) {
35
38
  }
36
39
  return `${descriptor.action} ${formatCount(count, descriptor.singular, descriptor.plural)}`;
37
40
  }
41
+ function capitalize(s) {
42
+ if (!s)
43
+ return s;
44
+ return s.charAt(0).toUpperCase() + s.slice(1);
45
+ }
46
+ export function extractToolLabel(toolName, args) {
47
+ switch (toolName) {
48
+ case "read": {
49
+ const path = (args.path ?? args.file_path);
50
+ return path ? basename(path) : toolName;
51
+ }
52
+ case "grep":
53
+ case "find": {
54
+ const pattern = args.pattern;
55
+ return pattern ?? toolName;
56
+ }
57
+ case "ls": {
58
+ const path = args.path;
59
+ return path ? basename(path) || path : ".";
60
+ }
61
+ case "lsp": {
62
+ const symbol = args.symbol;
63
+ const file = args.file;
64
+ if (symbol)
65
+ return symbol;
66
+ if (file)
67
+ return basename(file);
68
+ return toolName;
69
+ }
70
+ default:
71
+ return toolName;
72
+ }
73
+ }
38
74
  export class ToolSummaryLine extends Container {
39
75
  canGroupWith(toolName) {
40
- if (this.tools.length === 0)
76
+ if (this.tools.length === 0 && this.pendingTools.size === 0)
41
77
  return true;
42
- // Mixed-groupable tools can share a summary line regardless of order
43
- if (MIXED_GROUPABLE_TOOLS.has(toolName) && this.tools.every((t) => MIXED_GROUPABLE_TOOLS.has(t.name))) {
78
+ const allCompletedMixed = this.tools.every((t) => MIXED_GROUPABLE_TOOLS.has(t.name));
79
+ const allPendingMixed = [...this.pendingTools.values()].every((t) => MIXED_GROUPABLE_TOOLS.has(t.name));
80
+ if (MIXED_GROUPABLE_TOOLS.has(toolName) && allCompletedMixed && allPendingMixed) {
44
81
  return true;
45
82
  }
46
- // Otherwise only same-tool grouping
47
- return this.tools.every((tool) => tool.name === toolName);
83
+ return this.tools.every((tool) => tool.name === toolName)
84
+ && [...this.pendingTools.values()].every((tool) => tool.name === toolName);
48
85
  }
49
- constructor() {
86
+ constructor(ui) {
50
87
  super();
51
88
  this.tools = [];
89
+ this.pendingTools = new Map();
52
90
  this.hidden = false;
91
+ this.expandHint = "";
92
+ this.spinnerTimer = null;
93
+ this.spinnerFrame = 0;
94
+ this.lastToolLabel = "";
95
+ this.ui = ui;
53
96
  this.contentText = new Text("", 1, 0);
97
+ this.labelText = new Text("", 1, 0);
54
98
  this.addChild(this.contentText);
99
+ this.addChild(this.labelText);
100
+ }
101
+ setUI(ui) {
102
+ this.ui = ui;
103
+ }
104
+ setExpandHint(hint) {
105
+ this.expandHint = hint;
106
+ this.updateDisplay();
107
+ }
108
+ addPendingTool(toolCallId, name, args) {
109
+ const label = extractToolLabel(name, args);
110
+ this.pendingTools.set(toolCallId, { name, label });
111
+ this.lastToolLabel = label;
112
+ this.startSpinner();
113
+ this.updateDisplay();
114
+ }
115
+ removePendingTool(toolCallId) {
116
+ const removed = this.pendingTools.get(toolCallId);
117
+ this.pendingTools.delete(toolCallId);
118
+ if (removed) {
119
+ this.lastToolLabel = removed.label;
120
+ }
121
+ if (this.pendingTools.size === 0) {
122
+ this.stopSpinner();
123
+ }
124
+ else {
125
+ const lastPending = [...this.pendingTools.values()].at(-1);
126
+ if (lastPending)
127
+ this.lastToolLabel = lastPending.label;
128
+ }
129
+ this.updateDisplay();
130
+ }
131
+ hasPendingTools() {
132
+ return this.pendingTools.size > 0;
133
+ }
134
+ hasPendingTool(toolCallId) {
135
+ return this.pendingTools.has(toolCallId);
136
+ }
137
+ clearPendingTools() {
138
+ const lastPending = [...this.pendingTools.values()].at(-1);
139
+ if (lastPending) {
140
+ this.lastToolLabel = lastPending.label;
141
+ }
142
+ this.pendingTools.clear();
143
+ this.stopSpinner();
144
+ this.updateDisplay();
145
+ }
146
+ updatePendingToolArgs(toolCallId, args) {
147
+ const pending = this.pendingTools.get(toolCallId);
148
+ if (!pending)
149
+ return;
150
+ pending.label = extractToolLabel(pending.name, args);
151
+ this.lastToolLabel = pending.label;
152
+ this.updateDisplay();
153
+ }
154
+ startSpinner() {
155
+ if (this.spinnerTimer)
156
+ return;
157
+ this.spinnerTimer = setInterval(() => {
158
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
159
+ this.updateDisplay();
160
+ this.ui?.requestRender();
161
+ }, SPINNER_INTERVAL_MS);
162
+ this.spinnerTimer.unref?.();
163
+ }
164
+ stopSpinner() {
165
+ if (!this.spinnerTimer)
166
+ return;
167
+ clearInterval(this.spinnerTimer);
168
+ this.spinnerTimer = null;
169
+ }
170
+ dispose() {
171
+ this.stopSpinner();
55
172
  }
56
173
  addTool(name, elapsed) {
57
174
  this.tools.push({ name, elapsed });
@@ -65,16 +182,37 @@ export class ToolSummaryLine extends Container {
65
182
  this.updateDisplay();
66
183
  }
67
184
  render(width) {
68
- if (this.hidden || this.tools.length === 0) {
185
+ if (this.hidden || (this.tools.length === 0 && this.pendingTools.size === 0)) {
69
186
  return [];
70
187
  }
71
188
  return super.render(width);
72
189
  }
73
190
  updateDisplay() {
74
- if (this.tools.length === 0) {
191
+ if (this.tools.length === 0 && this.pendingTools.size === 0) {
75
192
  this.contentText.setText("");
193
+ this.labelText.setText("");
194
+ return;
195
+ }
196
+ if (this.pendingTools.size > 0) {
197
+ const counts = new Map();
198
+ for (const tool of this.tools) {
199
+ counts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);
200
+ }
201
+ for (const tool of this.pendingTools.values()) {
202
+ counts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);
203
+ }
204
+ const groupedTools = [...counts.entries()]
205
+ .map(([name, count]) => summarizeToolGroup(name, count))
206
+ .map((value, index) => index === 0 ? capitalize(value) : value)
207
+ .join(", ");
208
+ const spinner = theme.fg("accent", SPINNER_FRAMES[this.spinnerFrame]);
209
+ const hint = this.expandHint ? theme.fg("muted", ` ${this.expandHint}`) : "";
210
+ this.contentText.setText(`${spinner} ${theme.fg("text", groupedTools)}${theme.fg("muted", "…")}${hint}`);
211
+ const lastPending = [...this.pendingTools.values()].at(-1);
212
+ this.labelText.setText(lastPending ? theme.fg("muted", ` └ ${lastPending.label}`) : "");
76
213
  return;
77
214
  }
215
+ this.labelText.setText(this.lastToolLabel ? theme.fg("muted", ` └ ${this.lastToolLabel}`) : "");
78
216
  const counts = new Map();
79
217
  let totalElapsed = 0;
80
218
  for (const tool of this.tools) {
@@ -1 +1 @@
1
- {"version":3,"file":"tool-summary-line.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-summary-line.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container, Text } from \"@gsd/pi-tui\";\n\nimport { theme } from \"../theme/theme.js\";\n\ninterface CollapsedTool {\n\tname: string;\n\telapsed: number;\n}\n\n// Tools that can be mixed together in one summary line\nconst MIXED_GROUPABLE_TOOLS = new Set([\n\t\"read\", \"find\", \"ls\", \"grep\", \"lsp\",\n]);\n\ntype SummaryDescriptor = {\n\taction: string;\n\tsingular: string;\n\tplural: string;\n};\n\nconst TOOL_SUMMARY_DESCRIPTORS: Record<string, SummaryDescriptor> = {\n\tread: { action: \"reading\", singular: \"file\", plural: \"files\" },\n\twrite: { action: \"editing\", singular: \"file\", plural: \"files\" },\n\tedit: { action: \"editing\", singular: \"file\", plural: \"files\" },\n\tgrep: { action: \"searching for\", singular: \"pattern\", plural: \"patterns\" },\n\tfind: { action: \"finding\", singular: \"path\", plural: \"paths\" },\n\tls: { action: \"listing\", singular: \"directory\", plural: \"directories\" },\n\tlsp: { action: \"looking up\", singular: \"symbol\", plural: \"symbols\" },\n\tbash: { action: \"running\", singular: \"command\", plural: \"commands\" },\n\tbg_shell: { action: \"running\", singular: \"background command\", plural: \"background commands\" },\n\tfetch_page: { action: \"reading\", singular: \"page\", plural: \"pages\" },\n\tresolve_library: { action: \"searching for\", singular: \"library\", plural: \"libraries\" },\n\tget_library_docs: { action: \"reading\", singular: \"doc\", plural: \"docs\" },\n\tweb_search: { action: \"searching web for\", singular: \"query\", plural: \"queries\" },\n\t\"search-the-web\": { action: \"searching web for\", singular: \"query\", plural: \"queries\" },\n\tsearch_and_read: { action: \"researching\", singular: \"topic\", plural: \"topics\" },\n\tgoogle_search: { action: \"searching web for\", singular: \"query\", plural: \"queries\" },\n};\n\nfunction formatCount(count: number, singular: string, plural: string): string {\n\treturn `${count} ${count === 1 ? singular : plural}`;\n}\n\nfunction summarizeToolGroup(name: string, count: number): string {\n\tif (name.startsWith(\"browser_\")) {\n\t\treturn `using browser for ${formatCount(count, \"step\", \"steps\")}`;\n\t}\n\n\tconst descriptor = TOOL_SUMMARY_DESCRIPTORS[name];\n\tif (!descriptor) {\n\t\treturn count > 1 ? `${name} ×${count}` : name;\n\t}\n\n\treturn `${descriptor.action} ${formatCount(count, descriptor.singular, descriptor.plural)}`;\n}\n\nexport class ToolSummaryLine extends Container {\n\tprivate tools: CollapsedTool[] = [];\n\tprivate hidden = false;\n\tprivate contentText: Text;\n\n\tcanGroupWith(toolName: string): boolean {\n\t\tif (this.tools.length === 0) return true;\n\t\t// Mixed-groupable tools can share a summary line regardless of order\n\t\tif (MIXED_GROUPABLE_TOOLS.has(toolName) && this.tools.every((t) => MIXED_GROUPABLE_TOOLS.has(t.name))) {\n\t\t\treturn true;\n\t\t}\n\t\t// Otherwise only same-tool grouping\n\t\treturn this.tools.every((tool) => tool.name === toolName);\n\t}\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.contentText = new Text(\"\", 1, 0);\n\t\tthis.addChild(this.contentText);\n\t}\n\n\taddTool(name: string, elapsed: number): void {\n\t\tthis.tools.push({ name, elapsed });\n\t\tthis.updateDisplay();\n\t}\n\n\tsetHidden(hidden: boolean): void {\n\t\tthis.hidden = hidden;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hidden || this.tools.length === 0) {\n\t\t\treturn [];\n\t\t}\n\t\treturn super.render(width);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tif (this.tools.length === 0) {\n\t\t\tthis.contentText.setText(\"\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst counts = new Map<string, number>();\n\t\tlet totalElapsed = 0;\n\t\tfor (const tool of this.tools) {\n\t\t\tcounts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);\n\t\t\ttotalElapsed += tool.elapsed;\n\t\t}\n\n\t\tconst groupedTools = [...counts.entries()]\n\t\t\t.map(([name, count]) => summarizeToolGroup(name, count))\n\t\t\t.join(\" · \");\n\t\tconst elapsed = (totalElapsed / 1000).toFixed(1);\n\t\tconst indicator = theme.fg(\"success\", \"●\");\n\t\tconst details = theme.fg(\"text\", groupedTools) + theme.fg(\"muted\", ` · ${elapsed}s`);\n\t\tthis.contentText.setText(`${indicator} ${details}`);\n\t}\n}\n"]}
1
+ {"version":3,"file":"tool-summary-line.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-summary-line.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename } from \"node:path\";\nimport { Container, Text, type TUI } from \"@gsd/pi-tui\";\n\nimport { theme } from \"../theme/theme.js\";\n\ninterface CollapsedTool {\n\tname: string;\n\telapsed: number;\n}\n\ninterface PendingTool {\n\tname: string;\n\tlabel: string;\n}\n\n// Tools that can be mixed together in one summary line\nconst MIXED_GROUPABLE_TOOLS = new Set([\n\t\"read\", \"find\", \"ls\", \"grep\", \"lsp\",\n]);\n\ntype SummaryDescriptor = {\n\taction: string;\n\tsingular: string;\n\tplural: string;\n};\n\nconst TOOL_SUMMARY_DESCRIPTORS: Record<string, SummaryDescriptor> = {\n\tread: { action: \"reading\", singular: \"file\", plural: \"files\" },\n\twrite: { action: \"editing\", singular: \"file\", plural: \"files\" },\n\tedit: { action: \"editing\", singular: \"file\", plural: \"files\" },\n\tgrep: { action: \"searching for\", singular: \"pattern\", plural: \"patterns\" },\n\tfind: { action: \"finding\", singular: \"path\", plural: \"paths\" },\n\tls: { action: \"listing\", singular: \"directory\", plural: \"directories\" },\n\tlsp: { action: \"looking up\", singular: \"symbol\", plural: \"symbols\" },\n\tbash: { action: \"running\", singular: \"command\", plural: \"commands\" },\n\tbg_shell: { action: \"running\", singular: \"background command\", plural: \"background commands\" },\n\tfetch_page: { action: \"reading\", singular: \"page\", plural: \"pages\" },\n\tresolve_library: { action: \"searching for\", singular: \"library\", plural: \"libraries\" },\n\tget_library_docs: { action: \"reading\", singular: \"doc\", plural: \"docs\" },\n\tweb_search: { action: \"searching web for\", singular: \"query\", plural: \"queries\" },\n\t\"search-the-web\": { action: \"searching web for\", singular: \"query\", plural: \"queries\" },\n\tsearch_and_read: { action: \"researching\", singular: \"topic\", plural: \"topics\" },\n\tgoogle_search: { action: \"searching web for\", singular: \"query\", plural: \"queries\" },\n};\n\nconst SPINNER_FRAMES = [\"◯\", \"◔\", \"◑\", \"◕\", \"●\"];\nconst SPINNER_INTERVAL_MS = 150;\n\nfunction formatCount(count: number, singular: string, plural: string): string {\n\treturn `${count} ${count === 1 ? singular : plural}`;\n}\n\nfunction summarizeToolGroup(name: string, count: number): string {\n\tif (name.startsWith(\"browser_\")) {\n\t\treturn `using browser for ${formatCount(count, \"step\", \"steps\")}`;\n\t}\n\n\tconst descriptor = TOOL_SUMMARY_DESCRIPTORS[name];\n\tif (!descriptor) {\n\t\treturn count > 1 ? `${name} ×${count}` : name;\n\t}\n\n\treturn `${descriptor.action} ${formatCount(count, descriptor.singular, descriptor.plural)}`;\n}\n\nfunction capitalize(s: string): string {\n\tif (!s) return s;\n\treturn s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nexport function extractToolLabel(toolName: string, args: Record<string, unknown>): string {\n\tswitch (toolName) {\n\t\tcase \"read\": {\n\t\t\tconst path = (args.path ?? args.file_path) as string | undefined;\n\t\t\treturn path ? basename(path) : toolName;\n\t\t}\n\t\tcase \"grep\":\n\t\tcase \"find\": {\n\t\t\tconst pattern = args.pattern as string | undefined;\n\t\t\treturn pattern ?? toolName;\n\t\t}\n\t\tcase \"ls\": {\n\t\t\tconst path = args.path as string | undefined;\n\t\t\treturn path ? basename(path) || path : \".\";\n\t\t}\n\t\tcase \"lsp\": {\n\t\t\tconst symbol = args.symbol as string | undefined;\n\t\t\tconst file = args.file as string | undefined;\n\t\t\tif (symbol) return symbol;\n\t\t\tif (file) return basename(file);\n\t\t\treturn toolName;\n\t\t}\n\t\tdefault:\n\t\t\treturn toolName;\n\t}\n}\n\nexport class ToolSummaryLine extends Container {\n\tprivate tools: CollapsedTool[] = [];\n\tprivate pendingTools: Map<string, PendingTool> = new Map();\n\tprivate hidden = false;\n\tprivate contentText: Text;\n\tprivate labelText: Text;\n\tprivate expandHint = \"\";\n\tprivate ui?: TUI;\n\tprivate spinnerTimer: NodeJS.Timeout | null = null;\n\tprivate spinnerFrame = 0;\n\tprivate lastToolLabel = \"\";\n\n\tcanGroupWith(toolName: string): boolean {\n\t\tif (this.tools.length === 0 && this.pendingTools.size === 0) return true;\n\t\tconst allCompletedMixed = this.tools.every((t) => MIXED_GROUPABLE_TOOLS.has(t.name));\n\t\tconst allPendingMixed = [...this.pendingTools.values()].every((t) => MIXED_GROUPABLE_TOOLS.has(t.name));\n\t\tif (MIXED_GROUPABLE_TOOLS.has(toolName) && allCompletedMixed && allPendingMixed) {\n\t\t\treturn true;\n\t\t}\n\t\treturn this.tools.every((tool) => tool.name === toolName)\n\t\t\t&& [...this.pendingTools.values()].every((tool) => tool.name === toolName);\n\t}\n\n\tconstructor(ui?: TUI) {\n\t\tsuper();\n\t\tthis.ui = ui;\n\t\tthis.contentText = new Text(\"\", 1, 0);\n\t\tthis.labelText = new Text(\"\", 1, 0);\n\t\tthis.addChild(this.contentText);\n\t\tthis.addChild(this.labelText);\n\t}\n\n\tsetUI(ui: TUI): void {\n\t\tthis.ui = ui;\n\t}\n\n\tsetExpandHint(hint: string): void {\n\t\tthis.expandHint = hint;\n\t\tthis.updateDisplay();\n\t}\n\n\taddPendingTool(toolCallId: string, name: string, args: Record<string, unknown>): void {\n\t\tconst label = extractToolLabel(name, args);\n\t\tthis.pendingTools.set(toolCallId, { name, label });\n\t\tthis.lastToolLabel = label;\n\t\tthis.startSpinner();\n\t\tthis.updateDisplay();\n\t}\n\n\tremovePendingTool(toolCallId: string): void {\n\t\tconst removed = this.pendingTools.get(toolCallId);\n\t\tthis.pendingTools.delete(toolCallId);\n\t\tif (removed) {\n\t\t\tthis.lastToolLabel = removed.label;\n\t\t}\n\t\tif (this.pendingTools.size === 0) {\n\t\t\tthis.stopSpinner();\n\t\t} else {\n\t\t\tconst lastPending = [...this.pendingTools.values()].at(-1);\n\t\t\tif (lastPending) this.lastToolLabel = lastPending.label;\n\t\t}\n\t\tthis.updateDisplay();\n\t}\n\n\thasPendingTools(): boolean {\n\t\treturn this.pendingTools.size > 0;\n\t}\n\n\thasPendingTool(toolCallId: string): boolean {\n\t\treturn this.pendingTools.has(toolCallId);\n\t}\n\n\tclearPendingTools(): void {\n\t\tconst lastPending = [...this.pendingTools.values()].at(-1);\n\t\tif (lastPending) {\n\t\t\tthis.lastToolLabel = lastPending.label;\n\t\t}\n\t\tthis.pendingTools.clear();\n\t\tthis.stopSpinner();\n\t\tthis.updateDisplay();\n\t}\n\n\tupdatePendingToolArgs(toolCallId: string, args: Record<string, unknown>): void {\n\t\tconst pending = this.pendingTools.get(toolCallId);\n\t\tif (!pending) return;\n\t\tpending.label = extractToolLabel(pending.name, args);\n\t\tthis.lastToolLabel = pending.label;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate startSpinner(): void {\n\t\tif (this.spinnerTimer) return;\n\t\tthis.spinnerTimer = setInterval(() => {\n\t\t\tthis.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;\n\t\t\tthis.updateDisplay();\n\t\t\tthis.ui?.requestRender();\n\t\t}, SPINNER_INTERVAL_MS);\n\t\tthis.spinnerTimer.unref?.();\n\t}\n\n\tprivate stopSpinner(): void {\n\t\tif (!this.spinnerTimer) return;\n\t\tclearInterval(this.spinnerTimer);\n\t\tthis.spinnerTimer = null;\n\t}\n\n\tdispose(): void {\n\t\tthis.stopSpinner();\n\t}\n\n\taddTool(name: string, elapsed: number): void {\n\t\tthis.tools.push({ name, elapsed });\n\t\tthis.updateDisplay();\n\t}\n\n\tsetHidden(hidden: boolean): void {\n\t\tthis.hidden = hidden;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hidden || (this.tools.length === 0 && this.pendingTools.size === 0)) {\n\t\t\treturn [];\n\t\t}\n\t\treturn super.render(width);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tif (this.tools.length === 0 && this.pendingTools.size === 0) {\n\t\t\tthis.contentText.setText(\"\");\n\t\t\tthis.labelText.setText(\"\");\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.pendingTools.size > 0) {\n\t\t\tconst counts = new Map<string, number>();\n\t\t\tfor (const tool of this.tools) {\n\t\t\t\tcounts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);\n\t\t\t}\n\t\t\tfor (const tool of this.pendingTools.values()) {\n\t\t\t\tcounts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);\n\t\t\t}\n\n\t\t\tconst groupedTools = [...counts.entries()]\n\t\t\t\t.map(([name, count]) => summarizeToolGroup(name, count))\n\t\t\t\t.map((value, index) => index === 0 ? capitalize(value) : value)\n\t\t\t\t.join(\", \");\n\t\t\tconst spinner = theme.fg(\"accent\", SPINNER_FRAMES[this.spinnerFrame]);\n\t\t\tconst hint = this.expandHint ? theme.fg(\"muted\", ` ${this.expandHint}`) : \"\";\n\t\t\tthis.contentText.setText(`${spinner} ${theme.fg(\"text\", groupedTools)}${theme.fg(\"muted\", \"…\")}${hint}`);\n\n\t\t\tconst lastPending = [...this.pendingTools.values()].at(-1);\n\t\t\tthis.labelText.setText(lastPending ? theme.fg(\"muted\", ` └ ${lastPending.label}`) : \"\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.labelText.setText(this.lastToolLabel ? theme.fg(\"muted\", ` └ ${this.lastToolLabel}`) : \"\");\n\t\tconst counts = new Map<string, number>();\n\t\tlet totalElapsed = 0;\n\t\tfor (const tool of this.tools) {\n\t\t\tcounts.set(tool.name, (counts.get(tool.name) ?? 0) + 1);\n\t\t\ttotalElapsed += tool.elapsed;\n\t\t}\n\n\t\tconst groupedTools = [...counts.entries()]\n\t\t\t.map(([name, count]) => summarizeToolGroup(name, count))\n\t\t\t.join(\" · \");\n\t\tconst elapsed = (totalElapsed / 1000).toFixed(1);\n\t\tconst indicator = theme.fg(\"success\", \"●\");\n\t\tconst details = theme.fg(\"text\", groupedTools) + theme.fg(\"muted\", ` · ${elapsed}s`);\n\t\tthis.contentText.setText(`${indicator} ${details}`);\n\t}\n}\n"]}