@hheei/pi-ext-ui 0.3.0 → 0.4.1

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/dist/diff-view.js CHANGED
@@ -153,9 +153,12 @@ export class DiffView {
153
153
  if (!this.#parsed.ok)
154
154
  rows.push(this.#theme.fg("warning", `! ${this.#parsed.diagnostic}`));
155
155
  for (const raw of (this.#rawText ?? "").split(/\r?\n/)) {
156
- rows.push(this.#theme.fg("dim", raw));
156
+ const wrapped = wrapTextWithAnsi(raw, width);
157
+ for (const line of wrapped.length > 0 ? wrapped : [""]) {
158
+ rows.push(this.#theme.fg("dim", line));
159
+ }
157
160
  }
158
- return rows.map((row) => truncateToWidth(row, width, "…"));
161
+ return rows;
159
162
  }
160
163
  #renderDiff(width, hunks) {
161
164
  const numberCol = numberWidth(hunks);
package/dist/extension.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import { messageViewPoint, registerExtensionHook, registerExtensionLifecycle, toolRendererPoint, } from "@hheei/pi-ext-core";
2
2
  import { createMessageView } from "./message-view.js";
3
3
  import { createOptionalToolRendererFactory, registerCollapsibleToolRenderer } from "./renderer.js";
4
+ import { clearCapturedNestedCalls } from "./tracker.js";
4
5
  export default function (pi) {
5
6
  registerCollapsibleToolRenderer(pi);
6
7
  registerExtensionLifecycle(pi, {
7
8
  key: "@hheei/pi-ext-ui",
8
9
  start(context) {
10
+ context.resources.add("tracker-cleanup", () => {
11
+ clearCapturedNestedCalls();
12
+ });
9
13
  const toolRenderers = registerExtensionHook(pi, toolRendererPoint, createOptionalToolRendererFactory(pi));
10
14
  context.resources.add("tool-renderer", () => toolRenderers.dispose());
11
15
  const handle = registerExtensionHook(context.pi, messageViewPoint, createMessageView);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  export { type DiffHunk, type DiffLine, type DiffSource, type DiffStats, DiffView, type DiffViewOptions, diffStats, } from "./diff-view.js";
2
2
  export { createMessageView, type MessageRows, type MessageTone, MessageView, type MessageViewHeader, type MessageViewSpec, } from "./message-view.js";
3
3
  export { extractChildToolCopyText, NestedCallsView, type NestedCallsViewProps, } from "./nested-calls-view.js";
4
- export { type CollapsibleToolRendererOptions, createCollapsibleToolRendererResolver, estimateStreamingTokens, inlinePresentation, type NestedCallRecord, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, type SpecializedToolRendererName, type ToolRenderContext, WEB_ACCESS_TOOLS, type WebAccessToolName, } from "./renderer.js";
4
+ export { type CollapsibleToolRendererOptions, createCollapsibleToolRendererResolver, estimateStreamingTokens, inlinePresentation, isMcpTool, type NestedCallRecord, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, type SpecializedToolRendererName, type ToolRenderContext, } from "./renderer.js";
5
5
  export { type Collapsible, createToolView, type FoldLevel, formatStatusGlyph, formatToolHeaderText, isCollapsible, type RailColorToken, type RailSection, type ToolStatusKind, ToolView, type ToolViewCopySpec, type ToolViewHeaderSpec, type ToolViewSpec, transitionFoldLevel, } from "./tool-view.js";
6
6
  export { type CapturedToolCall, clearCapturedNestedCalls, getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
7
- export { formatTurnFlow, type TurnFlowData, type TurnToolCallSummary, } from "./turn-copy.js";
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export { DiffView, diffStats, } from "./diff-view.js";
2
2
  export { createMessageView, MessageView, } from "./message-view.js";
3
3
  export { extractChildToolCopyText, NestedCallsView, } from "./nested-calls-view.js";
4
- export { createCollapsibleToolRendererResolver, estimateStreamingTokens, inlinePresentation, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, WEB_ACCESS_TOOLS, } from "./renderer.js";
4
+ export { createCollapsibleToolRendererResolver, estimateStreamingTokens, inlinePresentation, isMcpTool, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, } from "./renderer.js";
5
5
  export { createToolView, formatStatusGlyph, formatToolHeaderText, isCollapsible, ToolView, transitionFoldLevel, } from "./tool-view.js";
6
6
  export { clearCapturedNestedCalls, getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
7
- export { formatTurnFlow, } from "./turn-copy.js";
@@ -1,4 +1,4 @@
1
- import { truncateToWidth, } from "@earendil-works/pi-tui";
1
+ import { truncateToWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
2
2
  import { toContentRows, toSingleRow } from "./tool-view.js";
3
3
  /**
4
4
  * A transcript notice, not a tool execution.
@@ -38,7 +38,14 @@ function layRows(source, width) {
38
38
  const raw = isComponent(source)
39
39
  ? source.render(inner).flatMap((line) => toContentRows(line))
40
40
  : (typeof source === "string" ? [source] : source).flatMap((line) => toContentRows(line));
41
- return raw.map((line) => truncate(` ${line}`, width));
41
+ const rows = [];
42
+ for (const line of raw) {
43
+ const wrapped = wrapTextWithAnsi(line, inner);
44
+ for (const chunk of wrapped.length > 0 ? wrapped : [""]) {
45
+ rows.push(truncate(` ${chunk}`, width));
46
+ }
47
+ }
48
+ return rows;
42
49
  }
43
50
  function invalidateRows(source) {
44
51
  if (source !== undefined && isComponent(source))
@@ -1,5 +1,5 @@
1
1
  import { copyToClipboard as copyToSystemClipboard, getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
2
- import { stripTerminalSequences, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
2
+ import { stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
3
3
  import { inlinePresentation } from "./renderer.js";
4
4
  import { formatToolHeaderText, ToolView, toContentRows, transitionFoldLevel, } from "./tool-view.js";
5
5
  function truncate(text, width) {
@@ -268,15 +268,20 @@ export class NestedCallsView {
268
268
  copyRegion: { startX: width - copySlotWidth - 1, endX: width },
269
269
  };
270
270
  if (isExpanded) {
271
- const bodyLines = this.#renderChildBody(call, Math.max(1, width - 2), expansionLevel);
271
+ const childWidth = Math.max(1, width - 2);
272
+ const bodyLines = this.#renderChildBody(call, childWidth, expansionLevel);
272
273
  for (const rawLine of bodyLines) {
273
274
  for (const bodyLine of toContentRows(rawLine)) {
274
- const bodyLineIdx = lines.length;
275
- lines.push(truncate(` ${bodyLine}`, width));
276
- this.#lineMapping[bodyLineIdx] = {
277
- callIndex: i,
278
- isHeader: false,
279
- };
275
+ const wrapped = wrapTextWithAnsi(bodyLine, childWidth);
276
+ const chunks = wrapped.length > 0 ? wrapped : [""];
277
+ for (const chunk of chunks) {
278
+ const bodyLineIdx = lines.length;
279
+ lines.push(truncate(` ${chunk}`, width));
280
+ this.#lineMapping[bodyLineIdx] = {
281
+ callIndex: i,
282
+ isHeader: false,
283
+ };
284
+ }
280
285
  }
281
286
  }
282
287
  }
@@ -343,7 +348,12 @@ export class NestedCallsView {
343
348
  else {
344
349
  lines = text.split("\n");
345
350
  }
346
- if (expansionLevel === 2 || lines.length <= 20) {
351
+ if (expansionLevel === 2) {
352
+ if (lines.length <= 500)
353
+ return lines;
354
+ return [...lines.slice(0, 500), `... (${lines.length - 500} more lines)`];
355
+ }
356
+ if (lines.length <= 20) {
347
357
  return lines;
348
358
  }
349
359
  return [...lines.slice(0, 20), `... (${lines.length - 20} more lines)`];
@@ -125,32 +125,55 @@ export function extractSummaryArg(toolName, args) {
125
125
  const parsed = parseCallArgs(args);
126
126
  if (!parsed)
127
127
  return "";
128
- for (const key of [
129
- "command",
130
- "query",
131
- "queries",
132
- "url",
133
- "urls",
134
- "source",
135
- "claim",
136
- "id",
137
- "search_id",
138
- "topic",
139
- "path",
140
- "file_path",
141
- "pattern",
142
- "agent",
143
- ]) {
144
- const val = parsed[key];
145
- if (typeof val === "string" && val.trim() !== "") {
146
- return stripTerminalSequences(val.trim());
128
+ const firstLine = (val) => {
129
+ if (typeof val !== "string" || val.trim() === "")
130
+ return "";
131
+ const clean = stripTerminalSequences(val.trim());
132
+ return clean.split("\n")[0]?.trim() ?? clean;
133
+ };
134
+ switch (toolName) {
135
+ case "bash":
136
+ case "bg_bash_spawn":
137
+ return firstLine(parsed.command);
138
+ case "read":
139
+ case "edit":
140
+ case "write": {
141
+ const path = typeof parsed.path === "string"
142
+ ? parsed.path.trim()
143
+ : typeof parsed.file_path === "string"
144
+ ? parsed.file_path.trim()
145
+ : "";
146
+ return stripTerminalSequences(path);
147
+ }
148
+ case "grep":
149
+ case "find": {
150
+ const pattern = typeof parsed.pattern === "string" ? parsed.pattern.trim() : "";
151
+ const path = typeof parsed.path === "string" ? parsed.path.trim() : "";
152
+ if (!pattern)
153
+ return path ? stripTerminalSequences(path) : "";
154
+ const target = path && path !== "." ? ` ${path}` : "";
155
+ return `${pattern}${target}`;
147
156
  }
148
- if (Array.isArray(val) && val.length > 0 && typeof val[0] === "string") {
149
- const first = stripTerminalSequences(val[0].trim());
150
- return val.length > 1 ? `${first} (+${String(val.length - 1)})` : first;
157
+ case "python_eval":
158
+ return firstLine(parsed.code);
159
+ case "bg_bash_get":
160
+ case "bg_bash_stop":
161
+ case "agent_send":
162
+ case "agent_get":
163
+ case "agent_stop":
164
+ return typeof parsed.id === "string" ? parsed.id.trim() : "";
165
+ case "agent_spawn": {
166
+ const agent = typeof parsed.agent === "string" ? parsed.agent.trim() : "";
167
+ const task = firstLine(parsed.task);
168
+ if (agent && task)
169
+ return `${agent}: ${task}`;
170
+ return agent || task;
151
171
  }
172
+ case "tool_search":
173
+ return typeof parsed.query === "string" ? parsed.query.trim() : "";
174
+ default:
175
+ return "";
152
176
  }
153
- return "";
154
177
  }
155
178
  export function extractHeavyInputCommand(toolName, args) {
156
179
  if (!isRecord(args))
@@ -7,9 +7,6 @@ export type ToolRenderContext = Parameters<NonNullable<ToolRenderers["renderCall
7
7
  */
8
8
  export declare const SPECIALIZED_TOOL_RENDERERS: readonly ["codemode"];
9
9
  export type SpecializedToolRendererName = (typeof SPECIALIZED_TOOL_RENDERERS)[number];
10
- /** Web-access tools use the shared folding shell while retaining their native body renderer. */
11
- export declare const WEB_ACCESS_TOOLS: readonly ["web_search", "web_fetch", "web_open"];
12
- export type WebAccessToolName = (typeof WEB_ACCESS_TOOLS)[number];
13
10
  export { estimateStreamingTokens, inlinePresentation, type NestedCallRecord, } from "./payload-parser.js";
14
11
  export interface CollapsibleToolRendererOptions {
15
12
  readonly tools?: readonly string[] | undefined;
@@ -18,7 +15,8 @@ export interface CollapsibleToolRendererOptions {
18
15
  readonly defaultCollapsed?: boolean | undefined;
19
16
  readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
20
17
  }
18
+ export declare function isMcpTool(toolName: string): boolean;
21
19
  export declare function createCollapsibleToolRendererResolver(options?: CollapsibleToolRendererOptions): ToolRendererResolver;
22
20
  /** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
23
21
  export declare function createOptionalToolRendererFactory(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): import("@hheei/pi-ext-core").ToolRendererFactory;
24
- export declare function registerCollapsibleToolRenderer(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): void;
22
+ export declare function registerCollapsibleToolRenderer(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): () => void;
package/dist/renderer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
2
2
  import { stripTerminalSequences } from "@earendil-works/pi-tui";
3
- import { agentResultText, getRegisteredToolPresentationProvider, getToolPresentationProvider, hasOptionalToolRenderers, } from "@hheei/pi-ext-core";
3
+ import { agentResultText, getRegisteredToolPresentationProvider, getRegisteredToolRenderers, getToolPresentationProvider, hasOptionalToolRenderers, } from "@hheei/pi-ext-core";
4
4
  import { DiffView } from "./diff-view.js";
5
5
  import { NestedCallsView } from "./nested-calls-view.js";
6
6
  import { formatToolHeaderText, ToolView, } from "./tool-view.js";
@@ -11,18 +11,17 @@ import { getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStar
11
11
  * appear here.
12
12
  */
13
13
  export const SPECIALIZED_TOOL_RENDERERS = ["codemode"];
14
- /** Web-access tools use the shared folding shell while retaining their native body renderer. */
15
- export const WEB_ACCESS_TOOLS = ["web_search", "web_fetch", "web_open"];
16
- import { callStatsParts, estimateStreamingTokens, extractHeavyInputCommand, extractNestedCalls, extractResultText, extractSummaryArg, formatCostUsd, formatDurationMs, inlinePresentation, isRecord, resolveContainerStatus, toolPath, } from "./payload-parser.js";
14
+ import { callStatsParts, estimateStreamingTokens, extractCodemodeHeaderFacts, extractHeavyInputCommand, extractNestedCalls, extractResultText, extractSummaryArg, formatCostUsd, formatDurationMs, inlinePresentation, isRecord, resolveContainerStatus, } from "./payload-parser.js";
17
15
  export { estimateStreamingTokens, inlinePresentation, } from "./payload-parser.js";
18
16
  const SPECIALIZED_TOOL_SET = new Set(SPECIALIZED_TOOL_RENDERERS);
19
- const WEB_ACCESS_TOOL_SET = new Set(WEB_ACCESS_TOOLS);
20
17
  const CALL_VIEW_STATE_KEY = "__piExtUiCallView";
21
18
  function detectCodeLanguage(toolName) {
22
19
  if (toolName === "bash")
23
20
  return "bash";
24
21
  if (toolName === "codemode")
25
22
  return "javascript";
23
+ if (toolName === "python_eval")
24
+ return "python";
26
25
  return undefined;
27
26
  }
28
27
  function createInputCodeSection(toolName, args, theme, railToken = "muted") {
@@ -46,6 +45,39 @@ function createInputCodeSection(toolName, args, theme, railToken = "muted") {
46
45
  }
47
46
  return { railToken, content: lines };
48
47
  }
48
+ function resolveToolCallSummary(params) {
49
+ const { toolName, args, originalRenderCall, theme, context } = params;
50
+ if (toolName === "codemode") {
51
+ return extractCodemodeHeaderFacts(args);
52
+ }
53
+ // Tier 1: Host Native RenderCall Delegation
54
+ if (originalRenderCall !== undefined) {
55
+ try {
56
+ const upstreamLines = originalRenderCall(args, theme, context).render(120);
57
+ const firstNonEmpty = upstreamLines
58
+ .map((line) => stripTerminalSequences(line).trim())
59
+ .find((line) => line !== "");
60
+ if (firstNonEmpty !== undefined) {
61
+ if (firstNonEmpty.startsWith("$ ")) {
62
+ return firstNonEmpty.slice(2).trim();
63
+ }
64
+ if (firstNonEmpty === toolName) {
65
+ return "";
66
+ }
67
+ if (firstNonEmpty.startsWith(toolName) &&
68
+ /^[\s(:=/[]/.test(firstNonEmpty.slice(toolName.length))) {
69
+ return firstNonEmpty.slice(toolName.length).trim();
70
+ }
71
+ return firstNonEmpty;
72
+ }
73
+ }
74
+ catch {
75
+ // Upstream threw, fall through to fallback
76
+ }
77
+ }
78
+ // Tier 2: Contractual Fallback for built-in tools
79
+ return extractSummaryArg(toolName, args);
80
+ }
49
81
  function formatArgsPreview(args) {
50
82
  if (args === undefined || args === null)
51
83
  return "";
@@ -104,6 +136,12 @@ function formatNestedCallRow(call, theme) {
104
136
  .join(" · "),
105
137
  }, theme);
106
138
  }
139
+ export function isMcpTool(toolName) {
140
+ return (toolName.startsWith("mcp__") ||
141
+ toolName === "read_mcp_resource" ||
142
+ toolName === "list_mcp_resources" ||
143
+ toolName === "list_mcp_resource_templates");
144
+ }
107
145
  function shouldInterceptTool(toolName, options) {
108
146
  if (options?.catchAll === true) {
109
147
  return options.match !== undefined ? options.match(toolName) : true;
@@ -115,10 +153,10 @@ function shouldInterceptTool(toolName, options) {
115
153
  if (toolList !== undefined) {
116
154
  return toolList.includes(toolName);
117
155
  }
118
- return SPECIALIZED_TOOL_SET.has(toolName) || WEB_ACCESS_TOOL_SET.has(toolName);
156
+ return SPECIALIZED_TOOL_SET.has(toolName) || isMcpTool(toolName);
119
157
  }
120
158
  function renderGenericResult(params) {
121
- const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, resolvePresentation, options, } = params;
159
+ const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, resolvePresentation, resolveNativeRenderer, options, } = params;
122
160
  const heavyCmd = extractHeavyInputCommand(toolName, context.args);
123
161
  const nestedCalls = extractNestedCalls(result);
124
162
  const captured = toolName === "codemode"
@@ -150,19 +188,15 @@ function renderGenericResult(params) {
150
188
  ? "running"
151
189
  : "ok";
152
190
  const summaryParts = [];
153
- const argSummary = extractSummaryArg(toolName, context.args);
154
- if (argSummary !== "") {
191
+ const argSummary = resolveToolCallSummary({
192
+ toolName,
193
+ args: context.args,
194
+ originalRenderCall,
195
+ theme,
196
+ context,
197
+ });
198
+ if (argSummary !== "")
155
199
  summaryParts.push(argSummary);
156
- }
157
- else if (originalRenderCall !== undefined && toolName !== "codemode") {
158
- const upstreamLines = originalRenderCall(context.args, theme, context).render(120);
159
- const firstLine = stripTerminalSequences(upstreamLines[0] ?? "").trim();
160
- const stripped = firstLine.startsWith(toolName)
161
- ? firstLine.slice(toolName.length).trim()
162
- : firstLine;
163
- if (stripped !== "")
164
- summaryParts.push(stripped);
165
- }
166
200
  if (isMetaTool && childCalls.length > 0) {
167
201
  const failCount = childCalls.filter((c) => Boolean(c.isError || ("status" in c && c.status === "error"))).length;
168
202
  summaryParts.push(...callStatsParts(childCalls.length, failCount));
@@ -195,9 +229,12 @@ function renderGenericResult(params) {
195
229
  theme: childTheme,
196
230
  context: childCtx,
197
231
  defaultLevel: childOptions.expanded ? 2 : 1,
232
+ resolvePresentation,
233
+ resolveNativeRenderer,
198
234
  options,
199
235
  });
200
236
  }
237
+ const registered = resolveNativeRenderer?.(childName);
201
238
  return renderGenericResult({
202
239
  toolName: childName,
203
240
  theme: childTheme,
@@ -205,9 +242,10 @@ function renderGenericResult(params) {
205
242
  result: childResult,
206
243
  renderOptions: childOptions,
207
244
  defaultLevel: childOptions.expanded ? 2 : 1,
208
- originalRenderCall: undefined,
209
- originalRenderResult: undefined,
245
+ originalRenderCall: registered?.renderCall,
246
+ originalRenderResult: registered?.renderResult,
210
247
  resolvePresentation,
248
+ resolveNativeRenderer,
211
249
  options,
212
250
  });
213
251
  },
@@ -231,7 +269,16 @@ function renderGenericResult(params) {
231
269
  : "success";
232
270
  // Upper room (input code) uses neutral/muted rail; lower room (output) uses toolRailToken (success/error)
233
271
  const inputSection = createInputCodeSection(toolName, context.args, theme, "muted");
234
- const sections = inputSection !== undefined ? [inputSection] : [];
272
+ const sections = [];
273
+ if (inputSection !== undefined) {
274
+ sections.push(inputSection);
275
+ }
276
+ else {
277
+ const preview = formatArgsPreview(context.args);
278
+ if (preview !== "" && preview !== "{}") {
279
+ sections.push({ railToken: "muted", content: preview });
280
+ }
281
+ }
235
282
  if (toolName === "codemode") {
236
283
  // Nested tool calls belong only to collapsedRows (Level 0), hidden when expanded
237
284
  if (rawOutput !== "") {
@@ -267,36 +314,14 @@ function renderGenericResult(params) {
267
314
  sections.push({
268
315
  railToken: toolRailToken,
269
316
  content: lazyUpstreamComponent,
270
- maxLines: effectiveExpanded ? Number.POSITIVE_INFINITY : 9,
317
+ maxLines: effectiveExpanded ? Number.POSITIVE_INFINITY : undefined,
271
318
  });
272
319
  }
273
320
  else if (rawOutput !== "") {
274
321
  const rawLines = rawOutput.split("\n");
275
- let outputLines;
276
- if (isError) {
277
- outputLines = rawLines.map((line) => theme.fg("error", line));
278
- }
279
- else if (toolName === "read" || toolPath(context.args) !== "") {
280
- const filePath = toolPath(context.args);
281
- const lang = filePath !== "" ? getLanguageFromPath(filePath) : undefined;
282
- if (lang !== undefined) {
283
- try {
284
- outputLines = highlightCode(rawOutput, lang);
285
- }
286
- catch {
287
- outputLines = rawLines;
288
- }
289
- }
290
- else {
291
- outputLines = rawLines;
292
- }
293
- }
294
- else if (toolName === "bash") {
295
- outputLines = rawLines.map((line) => theme.fg("dim", line));
296
- }
297
- else {
298
- outputLines = rawLines;
299
- }
322
+ const outputLines = isError
323
+ ? rawLines.map((line) => theme.fg("error", line))
324
+ : rawLines.map((line) => theme.fg("dim", line));
300
325
  sections.push({
301
326
  railToken: toolRailToken,
302
327
  content: outputLines,
@@ -345,7 +370,7 @@ function renderGenericResult(params) {
345
370
  * pass never asks the tool to rebuild its rows.
346
371
  */
347
372
  function renderOwnedPresentation(params) {
348
- const { toolName, presentation, result, theme, context, defaultLevel, options } = params;
373
+ const { toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
349
374
  const isError = context.isError || result?.isError === true;
350
375
  const pending = result === undefined || context.isPartial;
351
376
  const isDiff = !isError && !pending && presentation.kind === "diff";
@@ -385,10 +410,36 @@ function renderOwnedPresentation(params) {
385
410
  }
386
411
  }
387
412
  else if (output !== "") {
388
- const lines = stripTerminalSequences(output).split("\n");
413
+ const rawLines = stripTerminalSequences(output).split("\n");
414
+ let styledLines;
415
+ if (isError) {
416
+ styledLines = rawLines.map((line) => theme.fg("error", line));
417
+ }
418
+ else if (toolName === "read") {
419
+ const rawPath = isRecord(context.args) && typeof context.args.path === "string"
420
+ ? context.args.path
421
+ : isRecord(context.args) && typeof context.args.file_path === "string"
422
+ ? context.args.file_path
423
+ : "";
424
+ const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
425
+ if (lang !== undefined) {
426
+ try {
427
+ styledLines = highlightCode(output.replace(/\t/g, " "), lang);
428
+ }
429
+ catch {
430
+ styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
431
+ }
432
+ }
433
+ else {
434
+ styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
435
+ }
436
+ }
437
+ else {
438
+ styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
439
+ }
389
440
  sections.push({
390
441
  railToken,
391
- content: isError ? lines.map((line) => theme.fg("error", line)) : lines,
442
+ content: styledLines,
392
443
  });
393
444
  }
394
445
  const duration = result === undefined ? "" : formatDurationMs(context.durationMs);
@@ -410,13 +461,68 @@ function renderOwnedPresentation(params) {
410
461
  header: { title: toolName, status, summary },
411
462
  railToken,
412
463
  // The tool owns the folded rows and their tones; the frame only sanitizes them for the host.
413
- collapsedRows: presentation.collapsedRows ??
414
- (isError && output !== ""
464
+ collapsedRows: (() => {
465
+ if (presentation.collapsedRows !== undefined) {
466
+ return presentation.collapsedRows;
467
+ }
468
+ const captured = result !== undefined
469
+ ? (getCapturedNestedCalls(context.toolCallId) ??
470
+ (isRecord(result.details) && Array.isArray(result.details.__piExtUiCapturedCalls)
471
+ ? result.details.__piExtUiCapturedCalls
472
+ : undefined))
473
+ : undefined;
474
+ const nestedCalls = extractNestedCalls(result);
475
+ const childCalls = captured ?? nestedCalls;
476
+ if (childCalls.length > 0) {
477
+ return new NestedCallsView({
478
+ calls: childCalls,
479
+ theme,
480
+ context,
481
+ resolveChildRenderer: (childName) => ({
482
+ renderResult: (childResult, childOptions, childTheme, childCtx) => {
483
+ const childProvider = resolvePresentation?.(childName);
484
+ if (childProvider !== undefined) {
485
+ return renderOwnedPresentation({
486
+ toolName: childName,
487
+ presentation: childProvider(childCtx.args, childResult, childTheme, childCtx),
488
+ result: childResult,
489
+ theme: childTheme,
490
+ context: childCtx,
491
+ defaultLevel: childOptions.expanded ? 2 : 1,
492
+ resolvePresentation,
493
+ resolveNativeRenderer,
494
+ options,
495
+ });
496
+ }
497
+ const registered = resolveNativeRenderer?.(childName);
498
+ return renderGenericResult({
499
+ toolName: childName,
500
+ theme: childTheme,
501
+ context: childCtx,
502
+ result: childResult,
503
+ renderOptions: childOptions,
504
+ defaultLevel: childOptions.expanded ? 2 : 1,
505
+ originalRenderCall: registered?.renderCall,
506
+ originalRenderResult: registered?.renderResult,
507
+ resolvePresentation,
508
+ resolveNativeRenderer,
509
+ options,
510
+ });
511
+ },
512
+ }),
513
+ copyToClipboard: options?.copyToClipboard,
514
+ stateHolder: context.state,
515
+ resolvePresentation,
516
+ invalidate: () => context.invalidate(),
517
+ });
518
+ }
519
+ return isError && output !== ""
415
520
  ? output
416
521
  .split("\n")
417
522
  .slice(0, 1)
418
523
  .map((line) => theme.fg("error", stripTerminalSequences(line)))
419
- : undefined),
524
+ : undefined;
525
+ })(),
420
526
  sections,
421
527
  copy,
422
528
  outputPad: context.outputPad,
@@ -430,7 +536,7 @@ function renderOwnedPresentation(params) {
430
536
  export function createCollapsibleToolRendererResolver(options) {
431
537
  return createRendererResolver(options);
432
538
  }
433
- function createRendererResolver(options, resolvePresentation) {
539
+ function createRendererResolver(options, resolvePresentation, resolveNativeRenderer) {
434
540
  const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
435
541
  return (toolName, next) => {
436
542
  const upstream = next();
@@ -440,8 +546,9 @@ function createRendererResolver(options, resolvePresentation) {
440
546
  if (provider === undefined && !shouldInterceptTool(toolName, options)) {
441
547
  return upstream;
442
548
  }
443
- const originalRenderCall = upstream?.renderCall;
444
- const originalRenderResult = upstream?.renderResult;
549
+ const registeredNative = resolveNativeRenderer?.(toolName);
550
+ const originalRenderCall = upstream?.renderCall ?? registeredNative?.renderCall;
551
+ const originalRenderResult = upstream?.renderResult ?? registeredNative?.renderResult;
445
552
  return {
446
553
  renderShell: "self",
447
554
  renderCall(args, theme, context) {
@@ -453,6 +560,8 @@ function createRendererResolver(options, resolvePresentation) {
453
560
  theme,
454
561
  context: callContext,
455
562
  defaultLevel,
563
+ resolvePresentation,
564
+ resolveNativeRenderer,
456
565
  options,
457
566
  });
458
567
  rememberCallView(context, view);
@@ -472,14 +581,13 @@ function createRendererResolver(options, resolvePresentation) {
472
581
  summary = tokens > 0 ? `· ${String(tokens)} tokens` : "";
473
582
  }
474
583
  else {
475
- summary = extractSummaryArg(toolName, args);
476
- if (summary === "" && originalRenderCall !== undefined && toolName !== "codemode") {
477
- const upstreamLines = originalRenderCall(args, theme, context).render(120);
478
- const firstLine = stripTerminalSequences(upstreamLines[0] ?? "").trim();
479
- summary = firstLine.startsWith(toolName)
480
- ? firstLine.slice(toolName.length).trim()
481
- : firstLine;
482
- }
584
+ summary = resolveToolCallSummary({
585
+ toolName,
586
+ args,
587
+ originalRenderCall,
588
+ theme,
589
+ context,
590
+ });
483
591
  }
484
592
  const heavyCmd = extractHeavyInputCommand(toolName, args);
485
593
  const sections = [];
@@ -542,6 +650,7 @@ function createRendererResolver(options, resolvePresentation) {
542
650
  theme,
543
651
  context: resultContext,
544
652
  defaultLevel,
653
+ resolvePresentation,
545
654
  options,
546
655
  });
547
656
  }
@@ -555,6 +664,7 @@ function createRendererResolver(options, resolvePresentation) {
555
664
  originalRenderCall,
556
665
  originalRenderResult,
557
666
  resolvePresentation,
667
+ resolveNativeRenderer,
558
668
  options,
559
669
  });
560
670
  },
@@ -563,29 +673,25 @@ function createRendererResolver(options, resolvePresentation) {
563
673
  }
564
674
  /** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
565
675
  export function createOptionalToolRendererFactory(pi, options) {
566
- return (name, native) => createRendererResolver({ ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName))(name, () => native) ?? native;
676
+ return (name, native) => createRendererResolver({ ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName), (toolName) => getRegisteredToolRenderers(pi, toolName))(name, () => native) ?? native;
567
677
  }
568
678
  export function registerCollapsibleToolRenderer(pi, options) {
679
+ const disposers = [];
569
680
  if (typeof pi.on === "function") {
570
- pi.on("tool_execution_start", (event) => {
681
+ const unbindStart = pi.on("tool_execution_start", (event) => {
571
682
  recordToolExecutionStart(event);
572
683
  });
573
- pi.on("tool_execution_end", (event) => {
684
+ if (typeof unbindStart === "function")
685
+ disposers.push(unbindStart);
686
+ const unbindEnd = pi.on("tool_execution_end", (event) => {
574
687
  recordToolExecutionEnd(event);
575
688
  });
576
- pi.on("tool_result", (event) => {
577
- const captured = getCapturedNestedCalls(event.toolCallId);
578
- if (captured && captured.length > 0) {
579
- const currentDetails = isRecord(event.details) ? event.details : {};
580
- return {
581
- details: {
582
- ...currentDetails,
583
- __piExtUiCapturedCalls: captured,
584
- },
585
- };
586
- }
587
- return undefined;
588
- });
689
+ if (typeof unbindEnd === "function")
690
+ disposers.push(unbindEnd);
589
691
  }
590
- pi.registerToolRenderer(createRendererResolver(options, (name) => getRegisteredToolPresentationProvider(pi, name)));
692
+ pi.registerToolRenderer(createRendererResolver(options, (name) => getRegisteredToolPresentationProvider(pi, name), (name) => getRegisteredToolRenderers(pi, name)));
693
+ return () => {
694
+ for (const dispose of disposers)
695
+ dispose();
696
+ };
591
697
  }
@@ -43,6 +43,8 @@ export interface ToolViewSpec {
43
43
  }
44
44
  /** Public composition entry point for extensions that need the shared view behavior. */
45
45
  export declare function createToolView(spec: ToolViewSpec): ToolView;
46
+ export declare const MAX_LEVEL2_SECTION_LINES = 500;
47
+ export declare const MAX_LEVEL2_BYTES: number;
46
48
  export declare function transitionFoldLevel(current: FoldLevel, ctrlKey: boolean): FoldLevel;
47
49
  export declare function formatStatusGlyph(status: ToolStatusKind | undefined, theme: Theme): string;
48
50
  /**
package/dist/tool-view.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { Buffer } from "node:buffer";
1
2
  import { copyToClipboard as copyToSystemClipboard, } from "@earendil-works/pi-coding-agent";
2
- import { truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
3
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
3
4
  export function isCollapsible(value) {
4
5
  return (typeof value === "object" &&
5
6
  value !== null &&
@@ -14,6 +15,8 @@ const FOLD_LEVEL_STATE_KEY = "__piExtUiFoldLevel";
14
15
  const COPIED_UNTIL_STATE_KEY = "__piExtUiCopiedUntil";
15
16
  const COPY_FEEDBACK_TIMER_KEY = "__piExtUiCopyTimer";
16
17
  const DEFAULT_MAX_SECTION_LINES = 9;
18
+ export const MAX_LEVEL2_SECTION_LINES = 500;
19
+ export const MAX_LEVEL2_BYTES = 64 * 1024;
17
20
  const DEFAULT_COPY_FEEDBACK_MS = 1200;
18
21
  function isRecord(value) {
19
22
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -107,20 +110,97 @@ export function toContentRows(text) {
107
110
  }
108
111
  return text.replace(/\r/g, "").replace(/\t/g, " ").split("\n");
109
112
  }
110
- /**
111
- * Resolves a row source into rows that are safe to hand to the host. A string element is one
112
- * caller-provided label, so `normalize` decides whether it stays a single row or becomes several. A
113
- * component returns rows it already laid out, so only its breaks are removed and its indentation is
114
- * preserved.
115
- */
113
+ function truncateUtf8(str, maxBytes) {
114
+ const buf = Buffer.from(str, "utf8");
115
+ if (buf.length <= maxBytes)
116
+ return str;
117
+ let end = maxBytes;
118
+ while (end > 0) {
119
+ const b = buf[end];
120
+ if (b !== undefined && (b & 0xc0) === 0x80) {
121
+ end--;
122
+ }
123
+ else {
124
+ break;
125
+ }
126
+ }
127
+ if (end > 0) {
128
+ const lead = buf[end - 1];
129
+ if (lead !== undefined) {
130
+ let seqLen = 1;
131
+ if ((lead & 0xe0) === 0xc0)
132
+ seqLen = 2;
133
+ else if ((lead & 0xf0) === 0xe0)
134
+ seqLen = 3;
135
+ else if ((lead & 0xf8) === 0xf0)
136
+ seqLen = 4;
137
+ if (seqLen > 1 && end - 1 + seqLen > maxBytes) {
138
+ end = end - 1;
139
+ }
140
+ }
141
+ }
142
+ return buf.subarray(0, end).toString("utf8");
143
+ }
116
144
  function resolveRows(source, width, normalize) {
117
145
  if (typeof source === "string") {
118
- return source === "" ? [] : normalize(source);
146
+ if (source === "")
147
+ return { rows: [], isByteTruncated: false, totalBytes: 0 };
148
+ const totalBytes = Buffer.byteLength(source, "utf8");
149
+ const isByteTruncated = totalBytes > MAX_LEVEL2_BYTES;
150
+ const bounded = isByteTruncated ? truncateUtf8(source, MAX_LEVEL2_BYTES) : source;
151
+ return { rows: normalize(bounded), isByteTruncated, totalBytes };
119
152
  }
120
153
  if (Array.isArray(source)) {
121
- return source.flatMap((item) => (typeof item === "string" ? normalize(item) : []));
154
+ const totalBytes = source.reduce((acc, item) => (typeof item === "string" ? acc + Buffer.byteLength(item, "utf8") : acc), 0);
155
+ const isByteTruncated = totalBytes > MAX_LEVEL2_BYTES;
156
+ const rows = [];
157
+ let currentBytes = 0;
158
+ for (const item of source) {
159
+ if (typeof item !== "string")
160
+ continue;
161
+ if (currentBytes >= MAX_LEVEL2_BYTES)
162
+ continue;
163
+ const itemBytes = Buffer.byteLength(item, "utf8");
164
+ if (currentBytes + itemBytes > MAX_LEVEL2_BYTES) {
165
+ const remaining = MAX_LEVEL2_BYTES - currentBytes;
166
+ if (remaining > 0) {
167
+ for (const row of normalize(truncateUtf8(item, remaining)))
168
+ rows.push(row);
169
+ }
170
+ currentBytes = MAX_LEVEL2_BYTES;
171
+ }
172
+ else {
173
+ currentBytes += itemBytes;
174
+ for (const row of normalize(item))
175
+ rows.push(row);
176
+ }
177
+ }
178
+ return { rows, isByteTruncated, totalBytes };
179
+ }
180
+ const rendered = source.render(width);
181
+ const totalBytes = rendered.reduce((acc, row) => acc + Buffer.byteLength(row, "utf8"), 0);
182
+ const isByteTruncated = totalBytes > MAX_LEVEL2_BYTES;
183
+ const rows = [];
184
+ let currentBytes = 0;
185
+ for (const row of rendered) {
186
+ if (currentBytes >= MAX_LEVEL2_BYTES)
187
+ continue;
188
+ const rowBytes = Buffer.byteLength(row, "utf8");
189
+ if (currentBytes + rowBytes > MAX_LEVEL2_BYTES) {
190
+ const remaining = MAX_LEVEL2_BYTES - currentBytes;
191
+ if (remaining > 0) {
192
+ for (const norm of toContentRows(truncateUtf8(row, remaining)))
193
+ rows.push(norm);
194
+ }
195
+ currentBytes = MAX_LEVEL2_BYTES;
196
+ }
197
+ else {
198
+ currentBytes += rowBytes;
199
+ for (const norm of toContentRows(row))
200
+ rows.push(norm);
201
+ }
122
202
  }
123
- return source.render(width).flatMap((row) => toContentRows(row));
203
+ return { rows, isByteTruncated, totalBytes };
124
204
  }
125
205
  export class ToolView {
126
206
  #spec;
@@ -290,7 +370,7 @@ export class ToolView {
290
370
  if (collapsedRows !== undefined) {
291
371
  const railColor = this.#spec.header.status === "error" ? "error" : "dim";
292
372
  const rail = noRail ? "" : `${theme.fg(railColor, "▎")} `;
293
- const rows = resolveRows(collapsedRows, innerWidth, (row) => [toSingleRow(row)]);
373
+ const { rows } = resolveRows(collapsedRows, innerWidth, (row) => [toSingleRow(row)]);
294
374
  for (const row of rows) {
295
375
  output.push(truncate(rail + row, safeWidth));
296
376
  }
@@ -302,15 +382,34 @@ export class ToolView {
302
382
  }
303
383
  for (const section of sections) {
304
384
  const rail = noRail ? "" : `${theme.fg(section.railToken, "▎")} `;
305
- const rawLines = resolveRows(section.content, innerWidth, toContentRows);
306
- const cap = level === 1 ? (section.maxLines ?? DEFAULT_MAX_SECTION_LINES) : Number.POSITIVE_INFINITY;
307
- const visibleLines = Number.isFinite(cap) && rawLines.length > cap ? rawLines.slice(0, cap) : rawLines;
308
- const omittedCount = rawLines.length - visibleLines.length;
309
- for (const line of visibleLines) {
310
- output.push(truncate(rail + line, safeWidth));
385
+ const { rows: rawLines, isByteTruncated, totalBytes, } = resolveRows(section.content, innerWidth, toContentRows);
386
+ const cap = level === 1 ? (section.maxLines ?? DEFAULT_MAX_SECTION_LINES) : MAX_LEVEL2_SECTION_LINES;
387
+ const sectionRows = [];
388
+ let logicalLinesCount = 0;
389
+ for (const line of rawLines) {
390
+ const wrapped = wrapTextWithAnsi(line, innerWidth);
391
+ const chunks = wrapped.length > 0 ? wrapped : [""];
392
+ if (Number.isFinite(cap) && sectionRows.length + chunks.length > cap) {
393
+ const canTake = Math.max(0, cap - sectionRows.length);
394
+ for (let i = 0; i < canTake; i++) {
395
+ const chunk = chunks[i];
396
+ if (chunk !== undefined) {
397
+ sectionRows.push(chunk);
398
+ }
399
+ }
400
+ break;
401
+ }
402
+ sectionRows.push(...chunks);
403
+ logicalLinesCount++;
404
+ }
405
+ const omittedCount = rawLines.length - logicalLinesCount;
406
+ for (const row of sectionRows) {
407
+ output.push(truncate(rail + row, safeWidth));
311
408
  }
312
- if (omittedCount > 0) {
313
- const hint = theme.fg("dim", `… (${String(omittedCount)} more lines)`);
409
+ if (omittedCount > 0 || isByteTruncated) {
410
+ const hint = isByteTruncated
411
+ ? theme.fg("dim", `… [Output truncated: showing 64KB of ${Math.round(totalBytes / 1024)}KB]`)
412
+ : theme.fg("dim", `… (${String(omittedCount)} more lines)`);
314
413
  output.push(truncate(rail + hint, safeWidth));
315
414
  }
316
415
  }
package/dist/tracker.d.ts CHANGED
@@ -8,6 +8,25 @@ export interface CapturedToolCall {
8
8
  readonly durationMs?: number | undefined;
9
9
  readonly cost?: number | undefined;
10
10
  }
11
+ export declare class ToolExecutionTracker {
12
+ #private;
13
+ recordStart(event: {
14
+ toolCallId: string;
15
+ toolName: string;
16
+ args: unknown;
17
+ parentToolCallId?: string | undefined;
18
+ }): void;
19
+ recordEnd(event: {
20
+ toolCallId: string;
21
+ toolName: string;
22
+ result: unknown;
23
+ isError: boolean;
24
+ durationMs?: number | undefined;
25
+ parentToolCallId?: string | undefined;
26
+ }): void;
27
+ getCaptured(parentToolCallId: string | undefined): readonly CapturedToolCall[] | undefined;
28
+ clear(): void;
29
+ }
11
30
  export declare function recordToolExecutionStart(event: {
12
31
  toolCallId: string;
13
32
  toolName: string;
package/dist/tracker.js CHANGED
@@ -1,4 +1,3 @@
1
- const capturedByParentId = new Map();
2
1
  const MAX_STORED_PARENTS = 200;
3
2
  const MAX_CAPTURED_PAYLOAD_CHARS = 32_768;
4
3
  function boundCapturedValue(val) {
@@ -9,73 +8,89 @@ function boundCapturedValue(val) {
9
8
  }
10
9
  return val;
11
10
  }
12
- export function recordToolExecutionStart(event) {
13
- if (!event.parentToolCallId)
14
- return;
15
- let list = capturedByParentId.get(event.parentToolCallId);
16
- if (!list) {
17
- if (capturedByParentId.size >= MAX_STORED_PARENTS) {
18
- const firstKey = capturedByParentId.keys().next().value;
19
- if (firstKey !== undefined)
20
- capturedByParentId.delete(firstKey);
11
+ export class ToolExecutionTracker {
12
+ #capturedByParentId = new Map();
13
+ recordStart(event) {
14
+ if (!event.parentToolCallId)
15
+ return;
16
+ let list = this.#capturedByParentId.get(event.parentToolCallId);
17
+ if (!list) {
18
+ if (this.#capturedByParentId.size >= MAX_STORED_PARENTS) {
19
+ const firstKey = this.#capturedByParentId.keys().next().value;
20
+ if (firstKey !== undefined)
21
+ this.#capturedByParentId.delete(firstKey);
22
+ }
23
+ list = [];
24
+ this.#capturedByParentId.set(event.parentToolCallId, list);
21
25
  }
22
- list = [];
23
- capturedByParentId.set(event.parentToolCallId, list);
24
- }
25
- const idx = list.findIndex((c) => c.id === event.toolCallId);
26
- const entry = {
27
- id: event.toolCallId,
28
- toolName: event.toolName,
29
- args: boundCapturedValue(event.args),
30
- };
31
- if (idx >= 0) {
32
- list[idx] = { ...list[idx], ...entry };
33
- }
34
- else {
35
- list.push(entry);
36
- }
37
- }
38
- export function recordToolExecutionEnd(event) {
39
- if (!event.parentToolCallId)
40
- return;
41
- let list = capturedByParentId.get(event.parentToolCallId);
42
- if (!list) {
43
- if (capturedByParentId.size >= MAX_STORED_PARENTS) {
44
- const firstKey = capturedByParentId.keys().next().value;
45
- if (firstKey !== undefined)
46
- capturedByParentId.delete(firstKey);
26
+ const idx = list.findIndex((c) => c.id === event.toolCallId);
27
+ const entry = {
28
+ id: event.toolCallId,
29
+ toolName: event.toolName,
30
+ args: boundCapturedValue(event.args),
31
+ };
32
+ if (idx >= 0) {
33
+ list[idx] = { ...list[idx], ...entry };
34
+ }
35
+ else {
36
+ list.push(entry);
47
37
  }
48
- list = [];
49
- capturedByParentId.set(event.parentToolCallId, list);
50
38
  }
51
- const idx = list.findIndex((c) => c.id === event.toolCallId);
52
- const typedResult = event.result;
53
- if (idx >= 0) {
54
- const prev = list[idx];
55
- if (!prev)
39
+ recordEnd(event) {
40
+ if (!event.parentToolCallId)
56
41
  return;
57
- list[idx] = {
58
- ...prev,
59
- result: typedResult,
60
- isError: event.isError,
61
- durationMs: event.durationMs,
62
- };
42
+ let list = this.#capturedByParentId.get(event.parentToolCallId);
43
+ if (!list) {
44
+ if (this.#capturedByParentId.size >= MAX_STORED_PARENTS) {
45
+ const firstKey = this.#capturedByParentId.keys().next().value;
46
+ if (firstKey !== undefined)
47
+ this.#capturedByParentId.delete(firstKey);
48
+ }
49
+ list = [];
50
+ this.#capturedByParentId.set(event.parentToolCallId, list);
51
+ }
52
+ const idx = list.findIndex((c) => c.id === event.toolCallId);
53
+ const typedResult = event.result;
54
+ if (idx >= 0) {
55
+ const prev = list[idx];
56
+ if (!prev)
57
+ return;
58
+ list[idx] = {
59
+ ...prev,
60
+ result: typedResult,
61
+ isError: event.isError,
62
+ durationMs: event.durationMs,
63
+ };
64
+ }
65
+ else {
66
+ list.push({
67
+ id: event.toolCallId,
68
+ toolName: event.toolName,
69
+ result: typedResult,
70
+ isError: event.isError,
71
+ durationMs: event.durationMs,
72
+ });
73
+ }
63
74
  }
64
- else {
65
- list.push({
66
- id: event.toolCallId,
67
- toolName: event.toolName,
68
- result: typedResult,
69
- isError: event.isError,
70
- durationMs: event.durationMs,
71
- });
75
+ getCaptured(parentToolCallId) {
76
+ if (!parentToolCallId)
77
+ return undefined;
78
+ return this.#capturedByParentId.get(parentToolCallId);
72
79
  }
80
+ clear() {
81
+ this.#capturedByParentId.clear();
82
+ }
83
+ }
84
+ const defaultTracker = new ToolExecutionTracker();
85
+ export function recordToolExecutionStart(event) {
86
+ defaultTracker.recordStart(event);
87
+ }
88
+ export function recordToolExecutionEnd(event) {
89
+ defaultTracker.recordEnd(event);
73
90
  }
74
91
  export function getCapturedNestedCalls(parentToolCallId) {
75
- if (!parentToolCallId)
76
- return undefined;
77
- return capturedByParentId.get(parentToolCallId);
92
+ return defaultTracker.getCaptured(parentToolCallId);
78
93
  }
79
94
  export function clearCapturedNestedCalls() {
80
- capturedByParentId.clear();
95
+ defaultTracker.clear();
81
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hheei/pi-ext-ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Collapsible tool rendering frame and tool renderer pilot for Pi.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@hheei/pi-ext-core": "^0.5.0"
39
+ "@hheei/pi-ext-core": "^0.6.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@earendil-works/pi-coding-agent": ">=1.3.0",
@@ -1,11 +0,0 @@
1
- export interface TurnToolCallSummary {
2
- readonly toolName: string;
3
- readonly commandOrArgs?: string | undefined;
4
- readonly summary: string;
5
- }
6
- export interface TurnFlowData {
7
- readonly userPrompt: string;
8
- readonly toolCalls?: readonly TurnToolCallSummary[] | undefined;
9
- readonly assistantReply: string;
10
- }
11
- export declare function formatTurnFlow(data: TurnFlowData): string;
package/dist/turn-copy.js DELETED
@@ -1,20 +0,0 @@
1
- import { stripTerminalSequences } from "@earendil-works/pi-tui";
2
- export function formatTurnFlow(data) {
3
- const blocks = [];
4
- const cleanUser = stripTerminalSequences(data.userPrompt).trim();
5
- blocks.push(`[User]\n${cleanUser}`);
6
- if (data.toolCalls !== undefined && data.toolCalls.length > 0) {
7
- const toolLines = [];
8
- for (const call of data.toolCalls) {
9
- const header = call.commandOrArgs !== undefined && call.commandOrArgs.trim() !== ""
10
- ? `${call.toolName} ${stripTerminalSequences(call.commandOrArgs).trim()}`
11
- : call.toolName;
12
- const detail = stripTerminalSequences(call.summary).trim();
13
- toolLines.push(detail !== "" ? `${header}\n${detail}` : header);
14
- }
15
- blocks.push(`[Tools]\n${toolLines.join("\n\n")}`);
16
- }
17
- const cleanAssistant = stripTerminalSequences(data.assistantReply).trim();
18
- blocks.push(`[Assistant]\n${cleanAssistant}`);
19
- return blocks.join("\n\n");
20
- }