@hheei/pi-ext-ui 0.1.0 → 0.2.0

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.
@@ -12,28 +12,69 @@ export interface ToolViewHeaderSpec {
12
12
  readonly title: string;
13
13
  readonly status?: ToolStatusKind | undefined;
14
14
  readonly summary?: string | undefined;
15
+ readonly unstyledSummary?: boolean | undefined;
15
16
  }
16
17
  export interface ToolViewCopySpec {
17
18
  readonly primary: string;
18
19
  readonly full?: string | undefined;
19
20
  }
21
+ /** Micro-contract for child views that can reset their internal expansion. */
22
+ export interface Collapsible {
23
+ collapseAll(): void;
24
+ }
25
+ export declare function isCollapsible(value: unknown): value is Collapsible;
20
26
  export interface ToolViewSpec {
21
27
  readonly theme: Theme;
22
28
  readonly header: ToolViewHeaderSpec;
23
- readonly collapsedRows?: readonly string[] | Component | undefined;
29
+ readonly collapsedRows?: readonly string[] | Component | (Component & Collapsible) | undefined;
24
30
  readonly sections?: readonly RailSection[] | undefined;
31
+ readonly collapsibleChildren?: readonly Collapsible[] | undefined;
25
32
  readonly copy?: ToolViewCopySpec | undefined;
26
33
  readonly stateHolder?: unknown;
27
34
  readonly outputPad?: number | undefined;
28
35
  readonly defaultLevel?: FoldLevel | undefined;
36
+ readonly controlledLevel?: FoldLevel | undefined;
29
37
  readonly globalExpanded?: boolean | undefined;
30
38
  readonly invalidate?: (() => void) | undefined;
31
39
  readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
32
40
  readonly copyFeedbackDurationMs?: number | undefined;
41
+ readonly noRail?: boolean | undefined;
42
+ readonly railToken?: RailColorToken | undefined;
33
43
  }
34
44
  /** Public composition entry point for extensions that need the shared view behavior. */
35
45
  export declare function createToolView(spec: ToolViewSpec): ToolView;
36
46
  export declare function transitionFoldLevel(current: FoldLevel, ctrlKey: boolean): FoldLevel;
47
+ export declare function formatStatusGlyph(status: ToolStatusKind | undefined, theme: Theme): string;
48
+ /**
49
+ * The glyph, name, and summary of one tool execution, colored the way every tool row colors them.
50
+ *
51
+ * A container tool draws one of these per child call, and so does the row of a tool that ran on its
52
+ * own. Both must agree, so the token assignment lives here rather than in each board: a board that
53
+ * assembled its own row painted the child's summary as a title, which read as one bright run and
54
+ * made a nested call look unlike the same call at top level. The caller owns the fold pin, the
55
+ * right-aligned action, and the width budget, which are the parts that actually differ.
56
+ */
57
+ export declare function formatToolHeaderText(parts: {
58
+ readonly status?: ToolStatusKind | undefined;
59
+ readonly title: string;
60
+ readonly summary?: string | undefined;
61
+ readonly unstyledSummary?: boolean | undefined;
62
+ }, theme: Theme): string;
63
+ /**
64
+ * Collapses text that must occupy one row: header titles and summaries, board rows, and inline
65
+ * labels. A break inside one of them is a contract violation, because Pi writes one array element
66
+ * as one terminal row: `\n` moves to the next row without clearing it, and a lone `\r` returns to
67
+ * column 0 so later text overwrites the row's start. `visibleWidth` counts both as zero-width, so
68
+ * the host's per-row width guard cannot catch either, and the frame would silently grow taller
69
+ * than the rows it reports.
70
+ */
71
+ export declare function toSingleRow(text: string): string;
72
+ /**
73
+ * Splits multi-line content into rows. A lone `\r` is dropped rather than treated as a break so
74
+ * CRLF and progress-style output stay cell-safe, and tabs expand to two spaces so indentation does
75
+ * not depend on the terminal's tab stops.
76
+ */
77
+ export declare function toContentRows(text: string): readonly string[];
37
78
  export declare class ToolView implements Component {
38
79
  #private;
39
80
  constructor(spec: ToolViewSpec);
@@ -45,9 +86,15 @@ export declare class ToolView implements Component {
45
86
  toggle(ctrlKey?: boolean): void;
46
87
  /** The result renderer of the same tool execution hides this row in place. */
47
88
  hide(): void;
89
+ /**
90
+ * Renders only the interior content of the view without the header row.
91
+ * When `noRail: true` is specified, outer rail glyphs are omitted, allowing
92
+ * clean embedding inside composite parent components without regex text scraping.
93
+ */
94
+ renderBody(width: number, options?: {
95
+ noRail?: boolean;
96
+ }): string[];
48
97
  render(width: number): string[];
49
98
  handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
50
99
  invalidate(): void;
51
100
  }
52
- export type { ToolViewSpec as CollapsibleToolFrameOptions };
53
- export { ToolView as CollapsibleToolFrame };
package/dist/tool-view.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import { copyToClipboard as copyToSystemClipboard, } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
3
+ export function isCollapsible(value) {
4
+ return (typeof value === "object" &&
5
+ value !== null &&
6
+ "collapseAll" in value &&
7
+ typeof value.collapseAll === "function");
8
+ }
3
9
  /** Public composition entry point for extensions that need the shared view behavior. */
4
10
  export function createToolView(spec) {
5
11
  return new ToolView(spec);
@@ -8,7 +14,6 @@ const FOLD_LEVEL_STATE_KEY = "__piExtUiFoldLevel";
8
14
  const COPIED_UNTIL_STATE_KEY = "__piExtUiCopiedUntil";
9
15
  const COPY_FEEDBACK_TIMER_KEY = "__piExtUiCopyTimer";
10
16
  const DEFAULT_MAX_SECTION_LINES = 9;
11
- const DEFAULT_MAX_COLLAPSED_ROWS = 6;
12
17
  const DEFAULT_COPY_FEEDBACK_MS = 1200;
13
18
  function isRecord(value) {
14
19
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -42,31 +47,80 @@ function isCopiedActive(stateHolder, localCopiedUntil) {
42
47
  const until = stateHolder[COPIED_UNTIL_STATE_KEY];
43
48
  return typeof until === "number" && until > now;
44
49
  }
45
- function formatStatusGlyph(status, theme) {
50
+ export function formatStatusGlyph(status, theme) {
46
51
  switch (status ?? "ok") {
47
52
  case "streaming":
48
- return `${theme.fg("muted", "⋯")} `;
53
+ return theme.fg("muted", "⋯");
49
54
  case "running":
50
- return `${theme.fg("accent", "◐")} `;
55
+ return theme.fg("accent", "◐");
51
56
  case "ok":
52
- return `${theme.fg("success", "✓")} `;
57
+ return theme.fg("success", "✓");
53
58
  case "warn":
54
- return `${theme.fg("warning", "!")} `;
59
+ return theme.fg("warning", "!");
55
60
  case "error":
56
- return `${theme.fg("error", "✗")} `;
61
+ return theme.fg("error", "✗");
57
62
  case "none":
58
63
  return "";
59
64
  }
60
65
  }
66
+ /**
67
+ * The glyph, name, and summary of one tool execution, colored the way every tool row colors them.
68
+ *
69
+ * A container tool draws one of these per child call, and so does the row of a tool that ran on its
70
+ * own. Both must agree, so the token assignment lives here rather than in each board: a board that
71
+ * assembled its own row painted the child's summary as a title, which read as one bright run and
72
+ * made a nested call look unlike the same call at top level. The caller owns the fold pin, the
73
+ * right-aligned action, and the width budget, which are the parts that actually differ.
74
+ */
75
+ export function formatToolHeaderText(parts, theme) {
76
+ const statusPart = formatStatusGlyph(parts.status, theme);
77
+ const title = toSingleRow(parts.title);
78
+ const titlePart = theme.fg("toolTitle", theme.bold(title));
79
+ const summaryRaw = parts.summary === undefined ? "" : toSingleRow(parts.summary);
80
+ const isUnstyled = parts.unstyledSummary === true || title === "bash";
81
+ const summaryPart = summaryRaw === "" ? "" : isUnstyled ? ` ${summaryRaw}` : ` ${theme.fg("dim", summaryRaw)}`;
82
+ return `${statusPart === "" ? "" : `${statusPart} `}${titlePart}${summaryPart}`;
83
+ }
61
84
  const truncate = (text, width) => truncateToWidth(text, width, "…");
62
- function resolveLines(source, width) {
85
+ /**
86
+ * Collapses text that must occupy one row: header titles and summaries, board rows, and inline
87
+ * labels. A break inside one of them is a contract violation, because Pi writes one array element
88
+ * as one terminal row: `\n` moves to the next row without clearing it, and a lone `\r` returns to
89
+ * column 0 so later text overwrites the row's start. `visibleWidth` counts both as zero-width, so
90
+ * the host's per-row width guard cannot catch either, and the frame would silently grow taller
91
+ * than the rows it reports.
92
+ */
93
+ export function toSingleRow(text) {
94
+ if (!text.includes("\r") && !text.includes("\n") && !text.includes("\t")) {
95
+ return text.trim();
96
+ }
97
+ return text.replace(/[\r\n\t]+/g, " ").trim();
98
+ }
99
+ /**
100
+ * Splits multi-line content into rows. A lone `\r` is dropped rather than treated as a break so
101
+ * CRLF and progress-style output stay cell-safe, and tabs expand to two spaces so indentation does
102
+ * not depend on the terminal's tab stops.
103
+ */
104
+ export function toContentRows(text) {
105
+ if (!text.includes("\r") && !text.includes("\t") && !text.includes("\n")) {
106
+ return [text];
107
+ }
108
+ return text.replace(/\r/g, "").replace(/\t/g, " ").split("\n");
109
+ }
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
+ */
116
+ function resolveRows(source, width, normalize) {
63
117
  if (typeof source === "string") {
64
- return source === "" ? [] : source.replace(/\r/g, "").replace(/\t/g, " ").split("\n");
118
+ return source === "" ? [] : normalize(source);
65
119
  }
66
120
  if (Array.isArray(source)) {
67
- return source.flatMap((item) => typeof item === "string" ? item.replace(/\r/g, "").replace(/\t/g, " ").split("\n") : []);
121
+ return source.flatMap((item) => (typeof item === "string" ? normalize(item) : []));
68
122
  }
69
- return source.render(width);
123
+ return source.render(width).flatMap((row) => toContentRows(row));
70
124
  }
71
125
  export class ToolView {
72
126
  #spec;
@@ -74,12 +128,42 @@ export class ToolView {
74
128
  #localCopiedUntil = 0;
75
129
  #localTimer;
76
130
  #lastWidth = 80;
131
+ #lastHeight = 0;
132
+ #cache;
77
133
  constructor(spec) {
78
134
  this.#spec = spec;
79
- const initial = spec.defaultLevel ?? 0;
80
- this.#localLevel = readFoldLevel(spec.stateHolder, initial);
135
+ const initial = spec.controlledLevel ?? spec.defaultLevel ?? 0;
136
+ this.#localLevel =
137
+ spec.controlledLevel !== undefined
138
+ ? spec.controlledLevel
139
+ : readFoldLevel(spec.stateHolder, initial);
140
+ if (isRecord(spec.stateHolder)) {
141
+ const until = spec.stateHolder[COPIED_UNTIL_STATE_KEY];
142
+ if (typeof until === "number" && until > Date.now()) {
143
+ this.#localCopiedUntil = until;
144
+ const remaining = until - Date.now();
145
+ if (!spec.stateHolder[COPY_FEEDBACK_TIMER_KEY]) {
146
+ const timer = setTimeout(() => {
147
+ this.#localCopiedUntil = 0;
148
+ if (isRecord(this.#spec.stateHolder)) {
149
+ this.#spec.stateHolder[COPIED_UNTIL_STATE_KEY] = 0;
150
+ this.#spec.stateHolder[COPY_FEEDBACK_TIMER_KEY] = undefined;
151
+ }
152
+ this.#localTimer = undefined;
153
+ this.#cache = undefined;
154
+ this.#spec.invalidate?.();
155
+ }, remaining);
156
+ timer.unref?.();
157
+ this.#localTimer = timer;
158
+ spec.stateHolder[COPY_FEEDBACK_TIMER_KEY] = timer;
159
+ }
160
+ }
161
+ }
81
162
  }
82
163
  get foldLevel() {
164
+ if (this.#spec.controlledLevel !== undefined) {
165
+ return this.#spec.controlledLevel;
166
+ }
83
167
  return readFoldLevel(this.#spec.stateHolder, this.#localLevel);
84
168
  }
85
169
  get effectiveLevel() {
@@ -93,11 +177,26 @@ export class ToolView {
93
177
  get collapsed() {
94
178
  return this.foldLevel === 0;
95
179
  }
180
+ #collapseChildren() {
181
+ const { collapsedRows, collapsibleChildren } = this.#spec;
182
+ if (collapsibleChildren !== undefined) {
183
+ for (const child of collapsibleChildren) {
184
+ child.collapseAll();
185
+ }
186
+ }
187
+ if (isCollapsible(collapsedRows)) {
188
+ collapsedRows.collapseAll();
189
+ }
190
+ }
96
191
  setFoldLevel(next) {
97
192
  const prev = this.foldLevel;
98
193
  this.#localLevel = next;
99
- writeFoldLevel(this.#spec.stateHolder, next);
194
+ if (this.#spec.controlledLevel === undefined) {
195
+ writeFoldLevel(this.#spec.stateHolder, next);
196
+ }
100
197
  if (prev !== next) {
198
+ this.#cache = undefined;
199
+ this.#collapseChildren();
101
200
  this.#spec.invalidate?.();
102
201
  }
103
202
  }
@@ -137,6 +236,7 @@ export class ToolView {
137
236
  this.#spec.stateHolder[COPY_FEEDBACK_TIMER_KEY] = undefined;
138
237
  }
139
238
  this.#localTimer = undefined;
239
+ this.#cache = undefined;
140
240
  this.#spec.invalidate?.();
141
241
  }, durationMs);
142
242
  timer.unref?.();
@@ -150,11 +250,7 @@ export class ToolView {
150
250
  const safeWidth = Math.max(1, width);
151
251
  const pinGlyph = level === 0 ? "▸" : "▾";
152
252
  const pin = theme.fg("dim", pinGlyph);
153
- const statusPart = formatStatusGlyph(header.status, theme);
154
- const titlePart = theme.fg("toolTitle", theme.bold(header.title));
155
- const summaryRaw = header.summary?.trim() ?? "";
156
- const summaryPart = summaryRaw === "" ? "" : ` ${theme.fg("dim", summaryRaw)}`;
157
- const leftContent = `${pin} ${statusPart}${titlePart}${summaryPart}`;
253
+ const leftContent = `${pin} ${formatToolHeaderText(header, theme)}`;
158
254
  if (copy === undefined || safeWidth < 6) {
159
255
  return truncate(leftContent, safeWidth);
160
256
  }
@@ -173,47 +269,40 @@ export class ToolView {
173
269
  /** The result renderer of the same tool execution hides this row in place. */
174
270
  hide() {
175
271
  this.#hidden = true;
272
+ this.#cache = undefined;
176
273
  }
177
- render(width) {
274
+ /**
275
+ * Renders only the interior content of the view without the header row.
276
+ * When `noRail: true` is specified, outer rail glyphs are omitted, allowing
277
+ * clean embedding inside composite parent components without regex text scraping.
278
+ */
279
+ renderBody(width, options) {
178
280
  if (this.#hidden)
179
281
  return [];
180
282
  const pad = Math.max(0, this.#spec.outputPad ?? 0);
181
283
  const safeWidth = Math.max(1, width - pad * 2);
182
- this.#lastWidth = width;
183
284
  const level = this.effectiveLevel;
184
285
  const { theme, collapsedRows, sections } = this.#spec;
185
- const output = [this.#renderHeader(safeWidth, level)];
186
- const finalize = (lines) => {
187
- if (pad > 0) {
188
- const leftPad = " ".repeat(pad);
189
- return lines.map((line) => `${leftPad}${line}`);
190
- }
191
- return lines;
192
- };
193
- const innerWidth = Math.max(1, safeWidth - 2);
286
+ const noRail = options?.noRail ?? this.#spec.noRail ?? false;
287
+ const innerWidth = noRail ? safeWidth : Math.max(1, safeWidth - 2);
288
+ const output = [];
194
289
  if (level === 0) {
195
290
  if (collapsedRows !== undefined) {
196
- const rail = `${theme.fg("dim", "▎")} `;
197
- const rows = resolveLines(collapsedRows, innerWidth);
198
- const visibleRows = rows.length > DEFAULT_MAX_COLLAPSED_ROWS
199
- ? rows.slice(0, DEFAULT_MAX_COLLAPSED_ROWS)
200
- : rows;
201
- for (const row of visibleRows) {
291
+ const railColor = this.#spec.header.status === "error" ? "error" : "dim";
292
+ const rail = noRail ? "" : `${theme.fg(railColor, "▎")} `;
293
+ const rows = resolveRows(collapsedRows, innerWidth, (row) => [toSingleRow(row)]);
294
+ for (const row of rows) {
202
295
  output.push(truncate(rail + row, safeWidth));
203
296
  }
204
- if (rows.length > visibleRows.length) {
205
- const hint = theme.fg("dim", `… (${String(rows.length - visibleRows.length)} more)`);
206
- output.push(truncate(rail + hint, safeWidth));
207
- }
208
297
  }
209
- return finalize(output);
298
+ return output;
210
299
  }
211
300
  if (sections === undefined || sections.length === 0) {
212
- return finalize(output);
301
+ return output;
213
302
  }
214
303
  for (const section of sections) {
215
- const rail = `${theme.fg(section.railToken, "▎")} `;
216
- const rawLines = resolveLines(section.content, innerWidth);
304
+ const rail = noRail ? "" : `${theme.fg(section.railToken, "▎")} `;
305
+ const rawLines = resolveRows(section.content, innerWidth, toContentRows);
217
306
  const cap = level === 1 ? (section.maxLines ?? DEFAULT_MAX_SECTION_LINES) : Number.POSITIVE_INFINITY;
218
307
  const visibleLines = Number.isFinite(cap) && rawLines.length > cap ? rawLines.slice(0, cap) : rawLines;
219
308
  const omittedCount = rawLines.length - visibleLines.length;
@@ -225,13 +314,48 @@ export class ToolView {
225
314
  output.push(truncate(rail + hint, safeWidth));
226
315
  }
227
316
  }
228
- return finalize(output);
317
+ return output;
318
+ }
319
+ render(width) {
320
+ if (this.#hidden)
321
+ return [];
322
+ const level = this.effectiveLevel;
323
+ const copied = isCopiedActive(this.#spec.stateHolder, this.#localCopiedUntil);
324
+ if (this.#cache !== undefined &&
325
+ this.#cache.width === width &&
326
+ this.#cache.level === level &&
327
+ this.#cache.copied === copied) {
328
+ return this.#cache.lines;
329
+ }
330
+ const pad = Math.max(0, this.#spec.outputPad ?? 0);
331
+ const safeWidth = Math.max(1, width - pad * 2);
332
+ this.#lastWidth = width;
333
+ if (level !== 0) {
334
+ this.#collapseChildren();
335
+ }
336
+ const rawLines = [this.#renderHeader(safeWidth, level), ...this.renderBody(width)];
337
+ const safeLines = [];
338
+ const leftPad = pad > 0 ? " ".repeat(pad) : "";
339
+ for (const rawLine of rawLines) {
340
+ if (rawLine.includes("\r") || rawLine.includes("\n")) {
341
+ for (const line of toContentRows(rawLine)) {
342
+ const truncated = truncate(line, safeWidth);
343
+ safeLines.push(leftPad !== "" ? `${leftPad}${truncated}` : truncated);
344
+ }
345
+ }
346
+ else {
347
+ safeLines.push(leftPad !== "" ? `${leftPad}${rawLine}` : rawLine);
348
+ }
349
+ }
350
+ this.#lastHeight = safeLines.length;
351
+ this.#cache = { width, level, copied, lines: safeLines };
352
+ return safeLines;
229
353
  }
230
354
  handleMouse(event) {
231
- if (event.type === "wheel") {
355
+ if (event.type === "wheel" || this.#hidden) {
232
356
  return undefined;
233
357
  }
234
- if (event.type !== "click" || event.button !== "left" || event.y !== 0) {
358
+ if (event.type !== "click" || event.button !== "left") {
235
359
  return undefined;
236
360
  }
237
361
  const effectiveWidth = event.width > 0 ? event.width : this.#lastWidth;
@@ -241,17 +365,48 @@ export class ToolView {
241
365
  if (innerX < 0 || innerX >= innerWidth) {
242
366
  return undefined;
243
367
  }
244
- if (this.#spec.copy !== undefined &&
245
- innerWidth >= 6 &&
246
- innerX >= innerWidth - 3 &&
247
- innerX < innerWidth - 1) {
248
- this.#triggerCopy(event.shift === true);
368
+ // The last render's row count bounds the clickable area; a view that has not rendered yet has no
369
+ // extent to test against, and Pi always renders before it dispatches a mouse event.
370
+ if (this.#lastHeight > 0 && (event.y < 0 || event.y >= this.#lastHeight)) {
371
+ return undefined;
372
+ }
373
+ if (event.y === 0) {
374
+ if (this.#spec.copy !== undefined &&
375
+ innerWidth >= 6 &&
376
+ innerX >= innerWidth - 3 &&
377
+ innerX < innerWidth) {
378
+ this.#triggerCopy(event.shift === true);
379
+ return { handled: true, render: true };
380
+ }
381
+ this.toggle(event.ctrl === true);
249
382
  return { handled: true, render: true };
250
383
  }
251
- this.toggle(event.ctrl === true);
252
- return { handled: true, render: true };
384
+ const level = this.effectiveLevel;
385
+ if (level === 0 && this.#spec.collapsedRows !== undefined) {
386
+ const rows = this.#spec.collapsedRows;
387
+ if (typeof rows === "object" &&
388
+ rows !== null &&
389
+ "handleMouse" in rows &&
390
+ typeof rows.handleMouse === "function") {
391
+ const handler = rows.handleMouse;
392
+ const railOffset = this.#spec.noRail ? 0 : 2;
393
+ const subEvent = {
394
+ ...event,
395
+ y: event.y - 1,
396
+ x: Math.max(0, innerX - railOffset),
397
+ width: Math.max(1, innerWidth - railOffset),
398
+ };
399
+ const res = handler.call(rows, subEvent);
400
+ if (res?.handled) {
401
+ this.#cache = undefined;
402
+ return res;
403
+ }
404
+ }
405
+ }
406
+ return { handled: true };
253
407
  }
254
408
  invalidate() {
409
+ this.#cache = undefined;
255
410
  const { collapsedRows, sections } = this.#spec;
256
411
  if (collapsedRows !== undefined &&
257
412
  typeof collapsedRows === "object" &&
@@ -267,4 +422,3 @@ export class ToolView {
267
422
  }
268
423
  }
269
424
  }
270
- export { ToolView as CollapsibleToolFrame };
@@ -0,0 +1,26 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
+ export interface CapturedToolCall {
3
+ readonly id: string;
4
+ readonly toolName: string;
5
+ readonly args?: unknown;
6
+ readonly result?: AgentToolResult<unknown> | undefined;
7
+ readonly isError?: boolean | undefined;
8
+ readonly durationMs?: number | undefined;
9
+ readonly cost?: number | undefined;
10
+ }
11
+ export declare function recordToolExecutionStart(event: {
12
+ toolCallId: string;
13
+ toolName: string;
14
+ args: unknown;
15
+ parentToolCallId?: string | undefined;
16
+ }): void;
17
+ export declare function recordToolExecutionEnd(event: {
18
+ toolCallId: string;
19
+ toolName: string;
20
+ result: unknown;
21
+ isError: boolean;
22
+ durationMs?: number | undefined;
23
+ parentToolCallId?: string | undefined;
24
+ }): void;
25
+ export declare function getCapturedNestedCalls(parentToolCallId: string | undefined): readonly CapturedToolCall[] | undefined;
26
+ export declare function clearCapturedNestedCalls(): void;
@@ -0,0 +1,81 @@
1
+ const capturedByParentId = new Map();
2
+ const MAX_STORED_PARENTS = 200;
3
+ const MAX_CAPTURED_PAYLOAD_CHARS = 32_768;
4
+ function boundCapturedValue(val) {
5
+ if (typeof val === "string") {
6
+ return val.length > MAX_CAPTURED_PAYLOAD_CHARS
7
+ ? `${val.slice(0, MAX_CAPTURED_PAYLOAD_CHARS)}… (truncated)`
8
+ : val;
9
+ }
10
+ return val;
11
+ }
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);
21
+ }
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);
47
+ }
48
+ list = [];
49
+ capturedByParentId.set(event.parentToolCallId, list);
50
+ }
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)
56
+ return;
57
+ list[idx] = {
58
+ ...prev,
59
+ result: typedResult,
60
+ isError: event.isError,
61
+ durationMs: event.durationMs,
62
+ };
63
+ }
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
+ });
72
+ }
73
+ }
74
+ export function getCapturedNestedCalls(parentToolCallId) {
75
+ if (!parentToolCallId)
76
+ return undefined;
77
+ return capturedByParentId.get(parentToolCallId);
78
+ }
79
+ export function clearCapturedNestedCalls() {
80
+ capturedByParentId.clear();
81
+ }