@hheei/pi-ext-ui 0.2.0 → 0.2.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,14 +1,8 @@
1
+ import { formatReadLineRange } from "@earendil-works/pi-coding-agent";
1
2
  import { stripTerminalSequences } from "@earendil-works/pi-tui";
2
- import { previewV4aOperations } from "./apply-patch-preview.js";
3
- import { applyPatchFacts } from "./apply-patch-view.js";
4
3
  import { diffStats } from "./diff-view.js";
5
- export const APPLY_PATCH_TOOL_NAME = "apply_patch";
6
- export const PYTHON_EVAL_TOOL_NAME = "python_eval";
7
- export const PYTHON_EVAL_DETAILS_FORMAT = "pi-ext-tools-python-eval";
8
4
  export const EDIT_TOOL_NAME = "edit";
9
5
  export const WRITE_TOOL_NAME = "write";
10
- export const EDIT_VIEW_KEY = "__piExtToolsEditView";
11
- export const WRITE_VIEW_KEY = "__piExtToolsWriteView";
12
6
  export const CODEMODE_OPTIONS_PREFIX = "// @options:";
13
7
  export const CODEMODE_SCRIPT_HEADER = /^Script (?:completed|failed)\r?\nWall time ([\d.]+) seconds\r?\nOutput:\r?\n/;
14
8
  export function isRecord(value) {
@@ -257,108 +251,17 @@ export function toolPath(args) {
257
251
  const val = parsed.path ?? parsed.file_path;
258
252
  return typeof val === "string" ? val.trim() : "";
259
253
  }
260
- export function editPath(args) {
261
- return toolPath(args);
262
- }
263
- export function numericField(value, key) {
264
- return isRecord(value) && typeof value[key] === "number" ? value[key] : undefined;
265
- }
266
- export function writeStoredDiff(lines) {
267
- const hunks = [];
268
- let hunk = [];
269
- const finish = () => {
270
- if (hunk.length > 0)
271
- hunks.push(hunk);
272
- hunk = [];
273
- };
274
- for (const entry of lines) {
275
- if (!isRecord(entry))
276
- return undefined;
277
- if (entry.type === "sep") {
278
- finish();
279
- continue;
280
- }
281
- if (entry.type !== "add" && entry.type !== "del" && entry.type !== "ctx")
282
- return undefined;
283
- if (typeof entry.content !== "string")
284
- return undefined;
285
- const line = entry.type === "del" ? entry.oldNum : entry.newNum;
286
- if (typeof line !== "number")
287
- return undefined;
288
- hunk.push({ kind: entry.type, line, text: entry.content });
289
- }
290
- finish();
291
- return hunks.length === 0
292
- ? undefined
293
- : { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
294
- }
295
- /** Uses only the baseline and content already stored by the write tool. */
296
- export function writePresentation(args, details) {
297
- const view = isRecord(details) ? details[WRITE_VIEW_KEY] : undefined;
298
- if (!isRecord(view))
299
- return undefined;
300
- if (view.kind === "noChange")
301
- return { kind: "noChange" };
302
- if (view.kind === "diff") {
303
- const stored = Array.isArray(view.lines) ? writeStoredDiff(view.lines) : undefined;
304
- if (stored === undefined)
305
- return undefined;
306
- const source = stored;
307
- const stats = diffStats(source);
308
- return stats === undefined
309
- ? undefined
310
- : {
311
- kind: "diff",
312
- source,
313
- added: numericField(view, "added") ?? stats.added,
314
- removed: numericField(view, "removed") ?? stats.removed,
315
- };
316
- }
317
- if (view.kind !== "new" && view.kind !== "replace")
318
- return undefined;
319
- const content = typeof view.content === "string"
320
- ? view.content
321
- : isRecord(args) && typeof args.content === "string"
322
- ? args.content
323
- : undefined;
324
- if (content === undefined)
325
- return undefined;
326
- const lines = numericField(view, "lines") ?? content.split("\n").length;
327
- return {
328
- kind: "content",
329
- label: view.kind === "new" ? `new file (${String(lines)} lines)` : `wrote (${String(lines)} lines)`,
330
- content,
331
- };
332
- }
333
- export function fileToolPresentation(toolName, args, details, isError) {
254
+ export function fileToolPresentation(toolName, args, _details, isError) {
334
255
  const path = toolPath(args);
335
256
  if (toolName === "read") {
336
257
  if (path === "")
337
258
  return undefined;
338
259
  const parsed = parseCallArgs(args);
339
- let range = "";
340
- if (typeof parsed?.offset === "number" || typeof parsed?.limit === "number") {
341
- const start = typeof parsed.offset === "number" ? parsed.offset : 1;
342
- const end = typeof parsed.limit === "number" ? start + parsed.limit - 1 : "";
343
- range = `:${String(start)}${end ? `-${String(end)}` : ""}`;
344
- }
260
+ const range = formatReadLineRange(parsed?.offset, parsed?.limit);
345
261
  return { summary: `${path}${range}`, status: isError ? "error" : "ok" };
346
262
  }
347
263
  if (toolName === WRITE_TOOL_NAME) {
348
- const presentation = isError ? undefined : writePresentation(args, details);
349
- const delta = presentation?.kind === "diff"
350
- ? ` (+${String(presentation.added)} -${String(presentation.removed)})`
351
- : presentation?.kind === "content"
352
- ? ` (${presentation.label})`
353
- : presentation?.kind === "noChange"
354
- ? " (no changes)"
355
- : "";
356
- if (path === "" && delta === "")
357
- return undefined;
358
- return {
359
- summary: `${path}${delta}`.trim(),
360
- status: isError ? "error" : presentation?.kind === "noChange" ? "none" : "ok",
361
- };
264
+ return path === "" ? undefined : { summary: path, status: isError ? "error" : "ok" };
362
265
  }
363
266
  return undefined;
364
267
  }
@@ -372,7 +275,7 @@ export function editDiffSource(details) {
372
275
  if (typeof details.diff === "string" && details.diff.trim() !== "") {
373
276
  return displayDiffSource(details.diff);
374
277
  }
375
- return editViewSource(details[EDIT_VIEW_KEY]);
278
+ return undefined;
376
279
  }
377
280
  export function displayDiffSource(diff) {
378
281
  const hunks = [];
@@ -401,128 +304,6 @@ export function displayDiffSource(diff) {
401
304
  hunks.push(current);
402
305
  return { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
403
306
  }
404
- export function editViewSource(view) {
405
- const hunks = editViewHunks(view);
406
- return hunks.length === 0 ? undefined : { kind: "hunks", hunks };
407
- }
408
- export function editViewHunks(view) {
409
- if (!isRecord(view))
410
- return [];
411
- const ops = view.kind === "single" && isRecord(view.op)
412
- ? [view.op]
413
- : view.kind === "multi" && Array.isArray(view.ops)
414
- ? view.ops.filter(isRecord)
415
- : [];
416
- return ops.flatMap((op) => hunksForEditOp(op));
417
- }
418
- export function hunksForEditOp(op) {
419
- if (typeof op.oldContent !== "string" || typeof op.newContent !== "string")
420
- return [];
421
- const startLine = typeof op.startLine === "number" && op.startLine > 0 ? op.startLine : 1;
422
- return lineHunks(op.oldContent.replace(/\r\n/g, "\n"), op.newContent.replace(/\r\n/g, "\n"), startLine);
423
- }
424
- export function splitFragment(text) {
425
- if (text === "")
426
- return [];
427
- const lines = text.split("\n");
428
- if (lines.at(-1) === "")
429
- lines.pop();
430
- return lines;
431
- }
432
- /** Line LCS of one stored before/after fragment. No file context is invented. */
433
- export function lineHunks(before, after, startLine) {
434
- const oldLines = splitFragment(before);
435
- const newLines = splitFragment(after);
436
- const oldCount = oldLines.length;
437
- const newCount = newLines.length;
438
- const lengths = Array.from({ length: oldCount + 1 }, () => Array(newCount + 1).fill(0));
439
- for (let oldIndex = oldCount - 1; oldIndex >= 0; oldIndex -= 1) {
440
- const row = lengths[oldIndex];
441
- const next = lengths[oldIndex + 1];
442
- if (row === undefined || next === undefined)
443
- continue;
444
- for (let newIndex = newCount - 1; newIndex >= 0; newIndex -= 1) {
445
- row[newIndex] =
446
- oldLines[oldIndex] === newLines[newIndex]
447
- ? (next[newIndex + 1] ?? 0) + 1
448
- : Math.max(next[newIndex] ?? 0, row[newIndex + 1] ?? 0);
449
- }
450
- }
451
- const lines = [];
452
- let oldIndex = 0;
453
- let newIndex = 0;
454
- let oldLine = startLine;
455
- let newLine = startLine;
456
- while (oldIndex < oldCount || newIndex < newCount) {
457
- const row = lengths[oldIndex];
458
- const next = lengths[oldIndex + 1];
459
- if (oldIndex < oldCount && newIndex < newCount && oldLines[oldIndex] === newLines[newIndex]) {
460
- lines.push({ kind: "ctx", line: newLine, text: oldLines[oldIndex] ?? "" });
461
- oldIndex += 1;
462
- newIndex += 1;
463
- oldLine += 1;
464
- newLine += 1;
465
- }
466
- else if (newIndex < newCount &&
467
- (oldIndex === oldCount || (row?.[newIndex + 1] ?? 0) > (next?.[newIndex] ?? 0))) {
468
- lines.push({ kind: "add", line: newLine, text: newLines[newIndex] ?? "" });
469
- newIndex += 1;
470
- newLine += 1;
471
- }
472
- else {
473
- lines.push({ kind: "del", line: oldLine, text: oldLines[oldIndex] ?? "" });
474
- oldIndex += 1;
475
- oldLine += 1;
476
- }
477
- }
478
- const changed = lines.some((line) => line.kind !== "ctx");
479
- return changed ? [{ lines }] : [];
480
- }
481
- export function patchTextForCopy(details) {
482
- if (isRecord(details) && typeof details.patch === "string")
483
- return details.patch;
484
- const hunks = editViewHunks(isRecord(details) ? details[EDIT_VIEW_KEY] : undefined);
485
- if (hunks.length === 0)
486
- return undefined;
487
- return (hunks
488
- .map((hunk) => {
489
- let oldStart = 0;
490
- let newStart = 0;
491
- let oldCount = 0;
492
- let newCount = 0;
493
- const body = hunk.lines.map((line) => {
494
- if (line.kind !== "add") {
495
- oldCount += 1;
496
- if (oldStart === 0)
497
- oldStart = line.line;
498
- }
499
- if (line.kind !== "del") {
500
- newCount += 1;
501
- if (newStart === 0)
502
- newStart = line.line;
503
- }
504
- const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
505
- return sign + line.text;
506
- });
507
- if (body.length === 0)
508
- return "";
509
- const header = "@@ -" +
510
- String(oldStart || 1) +
511
- "," +
512
- String(oldCount) +
513
- " +" +
514
- String(newStart || 1) +
515
- "," +
516
- String(newCount) +
517
- " @@";
518
- return [header, ...body].join("\n");
519
- })
520
- .filter((hunk) => hunk !== "")
521
- .join("\n") || undefined);
522
- }
523
- export function patchSummary(files, added, removed) {
524
- return `modify ${files}${files === 1 ? " file" : " files"} (+${added} -${removed})`;
525
- }
526
307
  /**
527
308
  * The name-free part of a summary for tools whose call arguments read well on their own. The tool
528
309
  * name is the row's title, so repeating it here would print it twice.
@@ -564,22 +345,6 @@ export function inlinePresentation(toolName, args, details, isError) {
564
345
  if (fileTool !== undefined)
565
346
  return fileTool;
566
347
  const parsed = parseCallArgs(args);
567
- if (toolName === APPLY_PATCH_TOOL_NAME) {
568
- const facts = applyPatchFacts(details);
569
- if (facts !== undefined) {
570
- return {
571
- summary: patchSummary(new Set(facts.changedPaths).size, facts.addedLines, facts.removedLines),
572
- status: isError ? "error" : "ok",
573
- };
574
- }
575
- if (parsed === undefined || typeof parsed.patch !== "string")
576
- return undefined;
577
- const operations = previewV4aOperations(parsed.patch, true, undefined);
578
- return {
579
- summary: patchSummary(new Set(operations.map((operation) => operation.path)).size, operations.reduce((sum, operation) => sum + operation.addedLines, 0), operations.reduce((sum, operation) => sum + operation.removedLines, 0)),
580
- status: isError ? "error" : "ok",
581
- };
582
- }
583
348
  if (parsed === undefined)
584
349
  return undefined;
585
350
  if (toolName === EDIT_TOOL_NAME) {
@@ -603,42 +368,3 @@ export function inlinePresentation(toolName, args, details, isError) {
603
368
  status: isError ? "error" : "ok",
604
369
  };
605
370
  }
606
- /** Reads only the persisted `pi-ext-tools-eval` details; other shapes degrade to plain output. */
607
- export function pythonEvalFacts(details) {
608
- if (!isRecord(details) || details.format !== PYTHON_EVAL_DETAILS_FORMAT)
609
- return undefined;
610
- const rawRows = Array.isArray(details.rows) ? details.rows : [];
611
- const rows = [];
612
- for (const entry of rawRows) {
613
- if (!isRecord(entry))
614
- continue;
615
- if (entry.kind === "text" && typeof entry.text === "string") {
616
- rows.push({ kind: "text", text: entry.text });
617
- }
618
- else if (entry.kind === "display" && typeof entry.text === "string") {
619
- rows.push({ kind: "display", text: entry.text });
620
- }
621
- else if (entry.kind === "result" && typeof entry.text === "string") {
622
- rows.push({ kind: "result", text: entry.text });
623
- }
624
- else if (entry.kind === "state" && typeof entry.text === "string") {
625
- rows.push({ kind: "state", text: entry.text });
626
- }
627
- else if (entry.kind === "tool" && isRecord(entry.trace)) {
628
- const trace = entry.trace;
629
- if (typeof trace.name !== "string")
630
- continue;
631
- rows.push({
632
- kind: "tool",
633
- name: trace.name,
634
- args: trace.args,
635
- durationMs: typeof trace.durationMs === "number" ? trace.durationMs : undefined,
636
- error: typeof trace.error === "string" ? trace.error : undefined,
637
- });
638
- }
639
- }
640
- return {
641
- rows,
642
- error: typeof details.error === "string" ? details.error : undefined,
643
- };
644
- }
@@ -1,7 +1,11 @@
1
1
  import { type ExtensionAPI, type ToolRendererResolver, type ToolRenderers } from "@earendil-works/pi-coding-agent";
2
2
  export type ToolRenderContext = Parameters<NonNullable<ToolRenderers["renderCall"]>>[2];
3
- /** Tools with a pi-ext-ui-owned renderer. Other tools stay on Pi's renderer. */
4
- export declare const SPECIALIZED_TOOL_RENDERERS: readonly ["edit", "write", "apply_patch", "python_eval", "codemode"];
3
+ /**
4
+ * Upstream tools whose call and result views pi-ext-ui still frames itself. Concrete tool packages
5
+ * own their own presentation through ext-core's optional renderer point, so their names never
6
+ * appear here.
7
+ */
8
+ export declare const SPECIALIZED_TOOL_RENDERERS: readonly ["codemode"];
5
9
  export type SpecializedToolRendererName = (typeof SPECIALIZED_TOOL_RENDERERS)[number];
6
10
  /** Web-access tools use the shared folding shell while retaining their native body renderer. */
7
11
  export declare const WEB_ACCESS_TOOLS: readonly ["web_search", "web_fetch", "web_open"];
@@ -15,4 +19,6 @@ export interface CollapsibleToolRendererOptions {
15
19
  readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
16
20
  }
17
21
  export declare function createCollapsibleToolRendererResolver(options?: CollapsibleToolRendererOptions): ToolRendererResolver;
22
+ /** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
23
+ export declare function createOptionalToolRendererFactory(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): import("@hheei/pi-ext-core").ToolRendererFactory;
18
24
  export declare function registerCollapsibleToolRenderer(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): void;