@hheei/pi-ext-ui 0.5.2 → 0.7.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.
@@ -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 "agent_send":
170
- case "agent_get":
171
- case "agent_stop":
172
- return typeof parsed.id === "string" ? parsed.id.trim() : "";
173
- case "agent_spawn": {
174
- const agent = typeof parsed.agent === "string" ? parsed.agent.trim() : "";
175
- const task = firstLine(parsed.task);
176
- if (agent && task)
177
- return `${agent}: ${task}`;
178
- return agent || 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;