@hheei/pi-ext-ui 0.6.0 → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  import { copyToClipboard as copyToSystemClipboard, getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
2
2
  import { stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
3
- import { inlinePresentation } from "./renderer.js";
3
+ import { extractChildToolCopyText, inlinePresentation, nativeRequestCopy } from "./native-tools.js";
4
4
  import { formatToolHeaderText, ToolView, toContentRows, transitionFoldLevel, } from "./tool-view.js";
5
5
  function truncate(text, width) {
6
6
  return truncateToWidth(text, Math.max(1, width));
@@ -8,53 +8,15 @@ function truncate(text, width) {
8
8
  function isRecord(value) {
9
9
  return typeof value === "object" && value !== null && !Array.isArray(value);
10
10
  }
11
- export function extractChildToolCopyText(call, full) {
12
- const toolName = "toolName" in call ? call.toolName : call.name;
13
- const result = "result" in call ? call.result : undefined;
14
- const args = "args" in call ? call.args : undefined;
15
- if (toolName === "write" && isRecord(args) && typeof args.content === "string") {
16
- return args.content;
11
+ /**
12
+ * A copy keeps the request half byte-exact and strips only what follows it: the result is display
13
+ * text, while the request is what the model sent and may legitimately contain escapes.
14
+ */
15
+ function copyText(text, request) {
16
+ if (request === undefined || request === "" || !text.startsWith(request)) {
17
+ return stripTerminalSequences(text);
17
18
  }
18
- const heavyCommand = toolName === "bash" && isRecord(args) && typeof args.command === "string"
19
- ? args.command
20
- : toolName === "codemode" && isRecord(args) && typeof args.code === "string"
21
- ? args.code
22
- : undefined;
23
- if (heavyCommand !== undefined && heavyCommand.trim() !== "") {
24
- if (full !== true)
25
- return heavyCommand;
26
- const output = resultText(result);
27
- return output === undefined ? heavyCommand : `${heavyCommand}\n---\n${output}`;
28
- }
29
- if (result && Array.isArray(result.content)) {
30
- const texts = result.content
31
- .filter((c) => isRecord(c) && c.type === "text" && typeof c.text === "string")
32
- .map((c) => c.text);
33
- if (texts.length > 0)
34
- return texts.join("\n");
35
- }
36
- if (args !== undefined) {
37
- if (typeof args === "string")
38
- return args;
39
- try {
40
- return JSON.stringify(args, null, 2);
41
- }
42
- catch {
43
- return String(args);
44
- }
45
- }
46
- if ("summary" in call && typeof call.summary === "string") {
47
- return call.summary;
48
- }
49
- return "";
50
- }
51
- function resultText(result) {
52
- if (result === undefined || !Array.isArray(result.content))
53
- return undefined;
54
- const texts = result.content
55
- .filter((c) => isRecord(c) && c.type === "text" && typeof c.text === "string")
56
- .map((c) => c.text);
57
- return texts.length === 0 ? undefined : texts.join("\n");
19
+ return request + stripTerminalSequences(text.slice(request.length));
58
20
  }
59
21
  function formatDuration(ms) {
60
22
  if (ms === undefined || !Number.isFinite(ms))
@@ -162,7 +124,7 @@ export class NestedCallsView {
162
124
  const { context } = this.#props;
163
125
  // A captured call carries its own id. A persisted record has none, so it falls back to the
164
126
  // parent's id — the only identity the host offers, and never a child's guess.
165
- const toolCallId = "id" in call ? call.id : context.toolCallId;
127
+ const toolCallId = ("id" in call ? call.id : undefined) ?? context.toolCallId;
166
128
  return {
167
129
  ...context,
168
130
  lastComponent: undefined,
@@ -180,12 +142,15 @@ export class NestedCallsView {
180
142
  return;
181
143
  const provider = this.#props.resolvePresentation?.(call.toolName);
182
144
  const presentation = provider?.("args" in call ? call.args : undefined, "result" in call ? call.result : undefined, this.#props.theme, this.#callContext(call));
183
- const text = stripTerminalSequences((full ? presentation?.copy?.full : presentation?.copy?.primary) ??
184
- extractChildToolCopyText(call, full));
185
- if (!text)
145
+ const chosen = full ? presentation?.copy?.full : presentation?.copy?.primary;
146
+ const text = chosen ?? copyText(extractChildToolCopyText(call, full), nativeRequestCopy(call));
147
+ const durationMs = 1500;
148
+ if (text === "") {
149
+ // Nothing was recorded to copy. A summary is not the request, so the copy reports failure.
150
+ this.#showCopyFeedback(callIndex, durationMs, true);
186
151
  return;
152
+ }
187
153
  const writeClip = this.#props.copyToClipboard ?? copyToSystemClipboard;
188
- const durationMs = 1500;
189
154
  // Feedback starts when the write settles: a rejected clipboard write is not a copy.
190
155
  void writeClip(text).then(() => this.#showCopyFeedback(callIndex, durationMs, false), () => this.#showCopyFeedback(callIndex, durationMs, true));
191
156
  }
@@ -253,18 +218,25 @@ export class NestedCallsView {
253
218
  const isExpanded = expansionLevel !== 0;
254
219
  const rawSummary = "summary" in call ? call.summary : undefined;
255
220
  const rawArgs = "args" in call ? call.args : undefined;
256
- const inline = inlinePresentation(toolName, rawArgs ?? rawSummary, "result" in call ? call.result?.details : undefined, isError);
221
+ // A record's summary is the fact its owner stated; the arguments only fill in for a capture.
222
+ const inline = inlinePresentation(toolName, rawSummary ?? rawArgs, "result" in call ? call.result?.details : undefined, isError);
257
223
  const argText = typeof rawArgs === "string" ? rawArgs : typeof rawSummary === "string" ? rawSummary : "";
258
224
  const provider = this.#props.resolvePresentation?.(toolName);
259
225
  const presentation = provider?.(rawArgs, "result" in call ? call.result : undefined, theme, this.#callContext(call));
260
226
  // The child row is the child's own header minus the fold pin, so it carries the same token
261
227
  // assignment as the row the same call would draw at top level.
262
- const childSummary = stripTerminalSequences(presentation?.summary ?? inline?.summary ?? argText);
263
- const duration = formatDuration(call.durationMs);
228
+ const ownSummary = presentation?.summary === undefined
229
+ ? this.#props.headerSummary?.(toolName, rawArgs ?? rawSummary, theme)
230
+ : undefined;
231
+ const childSummary = ownSummary === undefined || ownSummary === ""
232
+ ? stripTerminalSequences(presentation?.summary ?? inline?.summary ?? argText)
233
+ : ownSummary;
264
234
  const leftContent = formatToolHeaderText({
265
235
  status: isRunning ? "running" : isError ? "error" : "ok",
266
236
  title: toolName,
267
- summary: [childSummary, duration].filter(Boolean).join(" · "),
237
+ summary: childSummary,
238
+ ...(ownSummary === undefined || ownSummary === "" ? {} : { summaryStyled: true }),
239
+ meta: formatDuration(call.durationMs),
268
240
  }, theme);
269
241
  const isCopied = this.#copiedIndex === i;
270
242
  const copyIcon = this.#failedIndex === i
@@ -322,22 +294,17 @@ export class NestedCallsView {
322
294
  if (result && resolveChildRenderer) {
323
295
  const renderer = resolveChildRenderer(toolName);
324
296
  if (renderer?.renderResult) {
325
- try {
326
- const childContext = {
327
- ...this.#callContext(call),
328
- state: undefined,
329
- };
330
- const comp = renderer.renderResult(result, { expanded: expansionLevel === 2, isPartial: false }, theme, childContext);
331
- if (comp instanceof ToolView) {
332
- comp.setFoldLevel(expansionLevel);
333
- // Render body cleanly via deep headless interface without outer rail or header row
334
- return comp.renderBody(width, { noRail: true });
335
- }
336
- return comp.render(width);
337
- }
338
- catch {
339
- // Fall through to plain text extraction on renderer failure
297
+ const childContext = {
298
+ ...this.#callContext(call),
299
+ state: undefined,
300
+ };
301
+ const comp = renderer.renderResult(result, { expanded: expansionLevel === 2, isPartial: false }, theme, childContext);
302
+ if (comp instanceof ToolView) {
303
+ comp.setFoldLevel(expansionLevel);
304
+ // Render body cleanly via deep headless interface without outer rail or header row
305
+ return comp.renderBody(width, { noRail: true });
340
306
  }
307
+ return comp.render(width);
341
308
  }
342
309
  }
343
310
  // Fallback rendering
@@ -381,11 +348,29 @@ export class NestedCallsView {
381
348
  if ("error" in call && typeof call.error === "string" && call.error) {
382
349
  return call.error.split("\n").map((l) => theme.fg("error", l));
383
350
  }
351
+ if ("argsTruncated" in call && call.argsTruncated === true) {
352
+ const bytes = "argsBytes" in call && typeof call.argsBytes === "number" ? call.argsBytes : 0;
353
+ return [
354
+ theme.fg("dim", bytes > 0 ? `(request not recorded: ${String(bytes)} bytes)` : "(request not recorded)"),
355
+ ];
356
+ }
357
+ // The request as the model sent it: the command or source, not a JSON wrapper around it.
358
+ const request = nativeRequestCopy(call);
359
+ if (request !== undefined) {
360
+ return stripTerminalSequences(request)
361
+ .split("\n")
362
+ .map((line) => theme.fg("dim", line));
363
+ }
384
364
  if (args !== undefined) {
385
- const text = typeof args === "string" ? args : JSON.stringify(args, null, 2);
365
+ const text = typeof args === "string" ? stripTerminalSequences(args) : JSON.stringify(args, null, 2);
386
366
  return text.split("\n").map((l) => theme.fg("dim", l));
387
367
  }
388
- return [theme.fg("dim", "(no output)")];
368
+ // A persisted record keeps a summary, not its request: say so instead of implying an empty call.
369
+ return [
370
+ theme.fg("dim", "summary" in call && typeof call.summary === "string" && call.summary !== ""
371
+ ? "(request not recorded; summary only)"
372
+ : "(no output)"),
373
+ ];
389
374
  }
390
375
  handleMouse(event) {
391
376
  if (event.type !== "click" || event.button !== "left")
@@ -1,12 +1,17 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
- import { type DiffSource } from "./diff-view.js";
3
2
  import type { ToolStatusKind } from "./tool-view.js";
4
- export declare const EDIT_TOOL_NAME = "edit";
5
- export declare const WRITE_TOOL_NAME = "write";
6
- export declare const CODEMODE_OPTIONS_PREFIX = "// @options:";
7
- export declare const CODEMODE_SCRIPT_HEADER: RegExp;
3
+ export declare const RESOLVED_CALLS_KEY = "__piExtUiResolvedCalls";
8
4
  export interface NestedCallRecord {
9
5
  readonly toolName: string;
6
+ /** The host's call id (`<parent>/<n>`); the key its saved arguments are indexed under. */
7
+ readonly id?: string | undefined;
8
+ /** The request bytes when this process or the session branch still has them. */
9
+ readonly args?: unknown;
10
+ /** The session saved the size of the request but not the request itself. */
11
+ readonly argsTruncated?: boolean | undefined;
12
+ readonly argsBytes?: number | undefined;
13
+ /** Positive fact only: the host proved this call resolved to a real tool. */
14
+ readonly resolved?: boolean | undefined;
10
15
  readonly summary?: string | undefined;
11
16
  readonly status?: "running" | "ok" | "error" | "cancelled" | undefined;
12
17
  readonly isError?: boolean | undefined;
@@ -21,15 +26,8 @@ export declare function formatDurationMs(ms: number | undefined): string;
21
26
  export declare function formatCostUsd(cost: number | undefined): string;
22
27
  /** One header fact for a deadline, in the same shape codemode's `timeout_ms` renders as. */
23
28
  export declare function formatTimeoutFact(timeoutMs: number): string;
24
- export declare function extractCodemodeHeaderFacts(args: unknown): string;
25
29
  export declare function parseCallArgs(raw: unknown): Record<string, unknown> | undefined;
26
- export declare function extractSummaryArg(toolName: string, args: unknown): string;
27
- export declare function extractHeavyInputCommand(toolName: string, args: unknown): string | undefined;
28
30
  export declare function extractNestedCalls(result: AgentToolResult<unknown> | undefined): readonly NestedCallRecord[];
29
- export declare function extractResultText(toolName: string, result: AgentToolResult<unknown>): {
30
- readonly outputText: string;
31
- readonly wallTimeSeconds: number | undefined;
32
- };
33
31
  export declare function callStatsParts(total: number, failCount: number): string[];
34
32
  export declare function resolveContainerStatus(params: {
35
33
  readonly isError: boolean;
@@ -37,19 +35,3 @@ export declare function resolveContainerStatus(params: {
37
35
  readonly hasChildren: boolean;
38
36
  readonly selfFailed: boolean;
39
37
  }): ToolStatusKind;
40
- export declare function toolPath(args: unknown): string;
41
- /** Compact nested-row result: a one-line summary plus the status it reports. */
42
- export interface InlinePresentation {
43
- readonly summary: string;
44
- readonly status: ToolStatusKind;
45
- }
46
- export declare function fileToolPresentation(toolName: string, args: unknown, _details: unknown, isError: boolean): InlinePresentation | undefined;
47
- /** Pi native edit persists a unified patch plus the same change as a numbered display diff. */
48
- export declare function editDiffSource(details: unknown): DiffSource | undefined;
49
- export declare function displayDiffSource(diff: string): DiffSource | undefined;
50
- /**
51
- * One compact nested summary for a tool call. Adopts the tool's standard collapsed
52
- * single-line format when recognized, degrading to undefined so callers can use
53
- * the default argument rendering.
54
- */
55
- export declare function inlinePresentation(toolName: string, args: unknown, details: unknown, isError: boolean): InlinePresentation | undefined;
@@ -1,18 +1,4 @@
1
- import { stripTerminalSequences } from "@earendil-works/pi-tui";
2
- import { diffStats } from "./diff-view.js";
3
- export const EDIT_TOOL_NAME = "edit";
4
- export const WRITE_TOOL_NAME = "write";
5
- export const CODEMODE_OPTIONS_PREFIX = "// @options:";
6
- export const CODEMODE_SCRIPT_HEADER = /^Script (?:completed|failed)\r?\nWall time ([\d.]+) seconds\r?\nOutput:\r?\n/;
7
- // Pi <1.3 has no exported read-range formatter. Keep this UI summary independent of that export.
8
- function readLineNumber(value) {
9
- const parsed = typeof value === "number"
10
- ? value
11
- : typeof value === "string" && value.trim() !== ""
12
- ? Number(value)
13
- : Number.NaN;
14
- return Number.isFinite(parsed) ? parsed : undefined;
15
- }
1
+ export const RESOLVED_CALLS_KEY = "__piExtUiResolvedCalls";
16
2
  export function isRecord(value) {
17
3
  return typeof value === "object" && value !== null && !Array.isArray(value);
18
4
  }
@@ -44,39 +30,6 @@ export function formatTimeoutFact(timeoutMs) {
44
30
  const label = timeoutMs >= 1000 ? `${String(Math.round(timeoutMs / 1000))}s` : `${String(timeoutMs)}ms`;
45
31
  return `(timeout ${label})`;
46
32
  }
47
- export function extractCodemodeHeaderFacts(args) {
48
- if (!isRecord(args))
49
- return "";
50
- const options = {};
51
- for (const [key, value] of Object.entries(args)) {
52
- if (key !== "code" && value !== undefined && value !== null) {
53
- options[key] = value;
54
- }
55
- }
56
- if (typeof args.code === "string") {
57
- const firstLine = args.code.split(/\r?\n/, 1)[0]?.trim() ?? "";
58
- if (firstLine.startsWith(CODEMODE_OPTIONS_PREFIX)) {
59
- try {
60
- const parsed = JSON.parse(firstLine.slice(CODEMODE_OPTIONS_PREFIX.length).trim());
61
- if (isRecord(parsed)) {
62
- Object.assign(options, parsed);
63
- }
64
- }
65
- catch { }
66
- }
67
- }
68
- const facts = [];
69
- const timeoutMs = typeof options.timeout_ms === "number" ? options.timeout_ms : undefined;
70
- if (timeoutMs !== undefined)
71
- facts.push(formatTimeoutFact(timeoutMs));
72
- if (typeof options.max_output_tokens === "number") {
73
- facts.push(`(tokens ${String(options.max_output_tokens)})`);
74
- }
75
- if (typeof options.target === "string" && options.target !== "local") {
76
- facts.push(`(${options.target})`);
77
- }
78
- return facts.join(" ");
79
- }
80
33
  export function parseCallArgs(raw) {
81
34
  if (isRecord(raw))
82
35
  return raw;
@@ -126,77 +79,6 @@ export function parseCallArgs(raw) {
126
79
  }
127
80
  return undefined;
128
81
  }
129
- export function extractSummaryArg(toolName, args) {
130
- if (toolName === "codemode") {
131
- return extractCodemodeHeaderFacts(args);
132
- }
133
- const parsed = parseCallArgs(args);
134
- if (!parsed)
135
- return "";
136
- const firstLine = (val) => {
137
- if (typeof val !== "string" || val.trim() === "")
138
- return "";
139
- const clean = stripTerminalSequences(val.trim());
140
- return clean.split("\n")[0]?.trim() ?? clean;
141
- };
142
- switch (toolName) {
143
- case "bash":
144
- case "bg_bash_spawn":
145
- return firstLine(parsed.command);
146
- case "read":
147
- case "edit":
148
- case "write": {
149
- const path = typeof parsed.path === "string"
150
- ? parsed.path.trim()
151
- : typeof parsed.file_path === "string"
152
- ? parsed.file_path.trim()
153
- : "";
154
- return stripTerminalSequences(path);
155
- }
156
- case "grep":
157
- case "find": {
158
- const pattern = typeof parsed.pattern === "string" ? parsed.pattern.trim() : "";
159
- const path = typeof parsed.path === "string" ? parsed.path.trim() : "";
160
- if (!pattern)
161
- return path ? stripTerminalSequences(path) : "";
162
- const target = path && path !== "." ? ` ${path}` : "";
163
- return `${pattern}${target}`;
164
- }
165
- case "python_eval":
166
- return firstLine(parsed.code);
167
- case "bg_bash_get":
168
- case "bg_bash_stop":
169
- case "agents_get":
170
- case "agents_stop":
171
- return typeof parsed.id === "string" ? parsed.id.trim() : "";
172
- case "agents_delegate": {
173
- const agent = typeof parsed.agent === "string" ? parsed.agent.trim() : "";
174
- const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
175
- const task = firstLine(parsed.task);
176
- if (agent && task)
177
- return `${agent}: ${task}`;
178
- return agent || id || task;
179
- }
180
- case "tool_search":
181
- return typeof parsed.query === "string" ? parsed.query.trim() : "";
182
- default:
183
- return "";
184
- }
185
- }
186
- export function extractHeavyInputCommand(toolName, args) {
187
- if (!isRecord(args))
188
- return undefined;
189
- if (toolName === "bash" && typeof args.command === "string") {
190
- return args.command.trim();
191
- }
192
- if (toolName === "python_eval" && typeof args.code === "string") {
193
- return args.code.trim();
194
- }
195
- if (toolName === "codemode" && typeof args.code === "string") {
196
- return args.code.trim();
197
- }
198
- return undefined;
199
- }
200
82
  export function extractNestedCalls(result) {
201
83
  if (result === undefined || !isRecord(result.details))
202
84
  return [];
@@ -207,6 +89,10 @@ export function extractNestedCalls(result) {
207
89
  : undefined;
208
90
  if (rawList === undefined)
209
91
  return [];
92
+ const rawResolved = result.details[RESOLVED_CALLS_KEY];
93
+ const resolvedIds = Array.isArray(rawResolved)
94
+ ? new Set(rawResolved.filter((id) => typeof id === "string"))
95
+ : undefined;
210
96
  const items = [];
211
97
  for (const entry of rawList) {
212
98
  if (!isRecord(entry))
@@ -230,8 +116,12 @@ export function extractNestedCalls(result) {
230
116
  : typeof entry.summary === "string"
231
117
  ? entry.summary
232
118
  : undefined;
119
+ const id = typeof entry.id === "string" ? entry.id : undefined;
233
120
  items.push({
234
121
  toolName: name,
122
+ ...(id === undefined ? {} : { id }),
123
+ // Absent facts stay absent: a legacy record and an unresolved call are both unknown.
124
+ ...(id !== undefined && resolvedIds?.has(id) === true ? { resolved: true } : {}),
235
125
  summary,
236
126
  status,
237
127
  isError: entry.isError === true || status === "error",
@@ -243,23 +133,6 @@ export function extractNestedCalls(result) {
243
133
  }
244
134
  return items;
245
135
  }
246
- export function extractResultText(toolName, result) {
247
- const parts = [];
248
- for (const block of result.content) {
249
- if (block.type === "text" && block.text.trim() !== "") {
250
- parts.push(block.text);
251
- }
252
- }
253
- const joined = parts.join("\n");
254
- if (toolName === "codemode") {
255
- const stripped = stripTerminalSequences(joined);
256
- const match = CODEMODE_SCRIPT_HEADER.exec(stripped);
257
- const wallTimeSeconds = match?.[1] !== undefined ? Number.parseFloat(match[1]) : undefined;
258
- const cleaned = joined.replace(CODEMODE_SCRIPT_HEADER, "").trim();
259
- return { outputText: cleaned, wallTimeSeconds };
260
- }
261
- return { outputText: joined, wallTimeSeconds: undefined };
262
- }
263
136
  export function callStatsParts(total, failCount) {
264
137
  if (failCount > 0) {
265
138
  return [`${String(total - failCount)} ok`, `${String(failCount)} failed`];
@@ -275,136 +148,3 @@ export function resolveContainerStatus(params) {
275
148
  return "running";
276
149
  return params.isError ? "error" : "ok";
277
150
  }
278
- export function toolPath(args) {
279
- const parsed = parseCallArgs(args);
280
- if (!parsed)
281
- return "";
282
- const val = parsed.path ?? parsed.file_path;
283
- return typeof val === "string" ? val.trim() : "";
284
- }
285
- export function fileToolPresentation(toolName, args, _details, isError) {
286
- const path = toolPath(args);
287
- if (toolName === "read") {
288
- if (path === "")
289
- return undefined;
290
- const parsed = parseCallArgs(args);
291
- const offset = parsed?.offset;
292
- const limit = parsed?.limit;
293
- const start = offset == null ? 1 : readLineNumber(offset);
294
- const count = limit == null ? undefined : readLineNumber(limit);
295
- const end = start !== undefined && count !== undefined ? start + count - 1 : undefined;
296
- const range = (offset == null && limit == null) ||
297
- start === undefined ||
298
- (limit != null && count === undefined)
299
- ? ""
300
- : `:${start}${end ? `-${end}` : ""}`;
301
- return { summary: `${path}${range}`, status: isError ? "error" : "ok" };
302
- }
303
- if (toolName === WRITE_TOOL_NAME) {
304
- return path === "" ? undefined : { summary: path, status: isError ? "error" : "ok" };
305
- }
306
- return undefined;
307
- }
308
- /** Pi native edit persists a unified patch plus the same change as a numbered display diff. */
309
- export function editDiffSource(details) {
310
- if (!isRecord(details))
311
- return undefined;
312
- if (typeof details.patch === "string" && details.patch.trim() !== "") {
313
- return { kind: "unifiedPatch", text: details.patch };
314
- }
315
- if (typeof details.diff === "string" && details.diff.trim() !== "") {
316
- return displayDiffSource(details.diff);
317
- }
318
- return undefined;
319
- }
320
- export function displayDiffSource(diff) {
321
- const hunks = [];
322
- let current = [];
323
- let pendingBreak = false;
324
- for (const raw of diff.split(/\r?\n/)) {
325
- if (raw === "")
326
- continue;
327
- const sign = raw.charAt(0);
328
- const match = /^ *(\d+)? ?(.*)$/.exec(raw.slice(1));
329
- const lineNumber = match?.[1];
330
- if (lineNumber === undefined) {
331
- pendingBreak = true;
332
- continue;
333
- }
334
- if (pendingBreak && current.length > 0) {
335
- hunks.push(current);
336
- current = [];
337
- }
338
- pendingBreak = false;
339
- const kind = sign === "+" ? "add" : sign === "-" ? "del" : "ctx";
340
- current.push({ kind, line: Number(lineNumber), text: match?.[2] ?? "" });
341
- }
342
- if (current.length === 0)
343
- return undefined;
344
- hunks.push(current);
345
- return { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
346
- }
347
- /**
348
- * The name-free part of a summary for tools whose call arguments read well on their own. The tool
349
- * name is the row's title, so repeating it here would print it twice.
350
- */
351
- function formatArgumentSummary(toolName, parsed) {
352
- const path = typeof parsed.path === "string"
353
- ? parsed.path.trim()
354
- : typeof parsed.file_path === "string"
355
- ? parsed.file_path.trim()
356
- : "";
357
- switch (toolName) {
358
- case "bash": {
359
- const cmd = typeof parsed.command === "string" ? parsed.command.trim() : "";
360
- if (!cmd)
361
- return undefined;
362
- return cmd.length > 70 ? `${cmd.slice(0, 67)}…` : cmd;
363
- }
364
- case "grep":
365
- case "find": {
366
- const pattern = typeof parsed.pattern === "string" ? parsed.pattern.trim() : "";
367
- if (!pattern)
368
- return undefined;
369
- const target = path && path !== "." ? ` ${path}` : "";
370
- return `${pattern}${target}`;
371
- }
372
- default: {
373
- const keyArg = extractSummaryArg(toolName, parsed);
374
- return keyArg ? keyArg : undefined;
375
- }
376
- }
377
- }
378
- /**
379
- * One compact nested summary for a tool call. Adopts the tool's standard collapsed
380
- * single-line format when recognized, degrading to undefined so callers can use
381
- * the default argument rendering.
382
- */
383
- export function inlinePresentation(toolName, args, details, isError) {
384
- const fileTool = fileToolPresentation(toolName, args, details, isError);
385
- if (fileTool !== undefined)
386
- return fileTool;
387
- const parsed = parseCallArgs(args);
388
- if (parsed === undefined)
389
- return undefined;
390
- if (toolName === EDIT_TOOL_NAME) {
391
- const path = toolPath(parsed);
392
- const source = isError ? undefined : editDiffSource(details);
393
- const stats = source === undefined ? undefined : diffStats(source);
394
- if (path === "" && stats === undefined)
395
- return undefined;
396
- return {
397
- summary: stats === undefined
398
- ? path
399
- : `${path} (+${String(stats.added)} -${String(stats.removed)})`.trim(),
400
- status: isError ? "error" : "ok",
401
- };
402
- }
403
- const baseSummary = formatArgumentSummary(toolName, parsed);
404
- if (baseSummary === undefined)
405
- return undefined;
406
- return {
407
- summary: baseSummary,
408
- status: isError ? "error" : "ok",
409
- };
410
- }
@@ -1,4 +1,5 @@
1
1
  import { type ExtensionAPI, type ToolRendererResolver, type ToolRenderers } from "@earendil-works/pi-coding-agent";
2
+ import type { ToolUiRuntime } from "./runtime.js";
2
3
  /** Optional host metadata is absent on early supported Pi versions. */
3
4
  export type ToolRenderContext = Parameters<NonNullable<ToolRenderers["renderCall"]>>[2] & {
4
5
  readonly durationMs?: number | undefined;
@@ -11,7 +12,8 @@ export type ToolRenderContext = Parameters<NonNullable<ToolRenderers["renderCall
11
12
  */
12
13
  export declare const SPECIALIZED_TOOL_RENDERERS: readonly ["codemode"];
13
14
  export type SpecializedToolRendererName = (typeof SPECIALIZED_TOOL_RENDERERS)[number];
14
- export { estimateStreamingTokens, inlinePresentation, type NestedCallRecord, } from "./payload-parser.js";
15
+ export { extractChildToolCopyText, inlinePresentation } from "./native-tools.js";
16
+ export { estimateStreamingTokens, type NestedCallRecord, } from "./payload-parser.js";
15
17
  export interface CollapsibleToolRendererOptions {
16
18
  readonly tools?: readonly string[] | undefined;
17
19
  readonly match?: ((toolName: string) => boolean) | undefined;
@@ -20,7 +22,7 @@ export interface CollapsibleToolRendererOptions {
20
22
  readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
21
23
  }
22
24
  export declare function isMcpTool(toolName: string): boolean;
23
- export declare function createCollapsibleToolRendererResolver(options?: CollapsibleToolRendererOptions): ToolRendererResolver;
25
+ export declare function createCollapsibleToolRendererResolver(runtime: ToolUiRuntime, options?: CollapsibleToolRendererOptions): ToolRendererResolver;
24
26
  /** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
25
- export declare function createOptionalToolRendererFactory(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): import("@hheei/pi-ext-core").ToolRendererFactory;
26
- export declare function registerCollapsibleToolRenderer(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): () => void;
27
+ export declare function createOptionalToolRendererFactory(pi: ExtensionAPI, runtime: ToolUiRuntime, options?: CollapsibleToolRendererOptions): import("@hheei/pi-ext-core").ToolRendererFactory;
28
+ export declare function registerCollapsibleToolRenderer(pi: ExtensionAPI, runtime: ToolUiRuntime, options?: CollapsibleToolRendererOptions): () => void;