@owlburtoe/pi-cc-tools 1.1.1 → 1.1.2
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/README.md +5 -0
- package/config/config.example.json +2 -0
- package/extensions/index.ts +251 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ Claude Code inspired tool rendering for Pi — Shiki-powered diffs, status dots,
|
|
|
6
6
|
|
|
7
7
|
- **Compact built-in tool rendering** for `read`, `bash`, `grep`, `find`, `ls`, `edit`, and `write`
|
|
8
8
|
- **Semantic bash display** that renders common read-only shell one-liners like `nl -ba file | sed -n '1,200p'` as Claude Code-style `Read file (lines 1-200)` rows
|
|
9
|
+
- **Read-only inspection grouping** that collapses consecutive `read`/`grep`/`find`/`ls` rows into one Claude-style `Inspect` block, capped to five visible entries by default
|
|
9
10
|
- **Claude-style OpenAI tool rendering** for `apply_patch` plus common Pi/OpenAI-style tools like `webfetch`, `web_search`, `fetch_content`, task tools, and context tools
|
|
10
11
|
- **`apply_patch` diff previews** that render parsed file patches in the call phase, similar to `edit`/`write`
|
|
11
12
|
- **Adaptive edit/write diffs** with split or unified layouts, syntax highlighting, and inline word-level emphasis
|
|
@@ -35,6 +36,8 @@ Set in `.pi/settings.json` or `~/.pi/settings.json`:
|
|
|
35
36
|
"bashCollapsedLines": 10,
|
|
36
37
|
"bashStackConsecutive": true,
|
|
37
38
|
"bashSemanticDisplay": true,
|
|
39
|
+
"readOnlyToolGrouping": true,
|
|
40
|
+
"readOnlyToolGroupLimit": 5,
|
|
38
41
|
"diffCollapsedLines": 24,
|
|
39
42
|
"themeAdaptive": true,
|
|
40
43
|
"diffTheme": "github-dark",
|
|
@@ -152,6 +155,7 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
|
|
|
152
155
|
|---------|---------|-------------|
|
|
153
156
|
| `bashStackConsecutive` | `true` | Remove the extra synthetic spacer between adjacent bash tool rows so command bursts render as a tight stack |
|
|
154
157
|
| `bashSemanticDisplay` | `true` | Render common read-only shell file-inspection commands as semantic `Read` rows instead of raw `Bash` rows |
|
|
158
|
+
| `readOnlyToolGrouping` | `true` | Collapse adjacent read-only inspection tools into one `Inspect` block; mutating `write`/`edit`/`apply_patch` rows stay independent |
|
|
155
159
|
|
|
156
160
|
### Numeric settings
|
|
157
161
|
|
|
@@ -161,6 +165,7 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
|
|
|
161
165
|
| `expandedPreviewMaxLines` | `4000` | Max lines when fully expanded |
|
|
162
166
|
| `bashCollapsedLines` | `10` | Lines for collapsed bash output |
|
|
163
167
|
| `diffCollapsedLines` | `24` | Diff lines before collapsing |
|
|
168
|
+
| `readOnlyToolGroupLimit` | `5` | Max inspection entries shown inside a grouped read-only tool block |
|
|
164
169
|
|
|
165
170
|
## Notes
|
|
166
171
|
|
package/extensions/index.ts
CHANGED
|
@@ -88,6 +88,8 @@ interface SettingsFile {
|
|
|
88
88
|
bashStackConsecutive?: boolean;
|
|
89
89
|
/** When true (default), read-only shell file-inspection one-liners render as semantic Read rows instead of raw Bash rows. */
|
|
90
90
|
bashSemanticDisplay?: boolean;
|
|
91
|
+
readOnlyToolGrouping?: boolean;
|
|
92
|
+
readOnlyToolGroupLimit?: number;
|
|
91
93
|
showTruncationHints?: boolean;
|
|
92
94
|
diffCollapsedLines?: number;
|
|
93
95
|
diffTheme?: string;
|
|
@@ -341,6 +343,253 @@ function shouldStackConsecutiveBash(): boolean {
|
|
|
341
343
|
return readSettings().bashStackConsecutive !== false;
|
|
342
344
|
}
|
|
343
345
|
|
|
346
|
+
function readOnlyToolGroupingEnabled(): boolean {
|
|
347
|
+
return readSettings().readOnlyToolGrouping !== false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function readOnlyToolGroupLimit(): number {
|
|
351
|
+
const value = readSettings().readOnlyToolGroupLimit;
|
|
352
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
353
|
+
? Math.max(1, Math.min(20, Math.floor(value)))
|
|
354
|
+
: 5;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function toolComponentRecord(value: unknown): Record<string, any> {
|
|
358
|
+
return value as Record<string, any>;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function componentCwd(value: unknown): string {
|
|
362
|
+
const cwd = toolComponentRecord(value).cwd;
|
|
363
|
+
return typeof cwd === "string" && cwd ? cwd : process.cwd();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function componentTextContent(value: unknown): string {
|
|
367
|
+
return getTextContent(toolComponentRecord(value).result);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function componentHasImageResult(value: unknown): boolean {
|
|
371
|
+
return !!getFirstImageBlock(toolComponentRecord(value).result);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function bashReadDisplayInfo(value: unknown): BashDisplayInfo | null {
|
|
375
|
+
const rec = toolComponentRecord(value);
|
|
376
|
+
if (rec.toolName !== "bash" || !bashSemanticDisplayEnabled()) return null;
|
|
377
|
+
return classifyBashCommandForDisplay(rec.args?.command ?? "");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isReadOnlyInspectionToolExecution(value: unknown): boolean {
|
|
381
|
+
if (!readOnlyToolGroupingEnabled() || !isToolExecutionLike(value)) return false;
|
|
382
|
+
const rec = toolComponentRecord(value);
|
|
383
|
+
if (rec.expanded === true || componentHasImageResult(value)) return false;
|
|
384
|
+
if (rec.toolName === "read" || rec.toolName === "grep" || rec.toolName === "find" || rec.toolName === "ls") return true;
|
|
385
|
+
return !!bashReadDisplayInfo(value);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function hasConsecutiveReadOnlyInspectionToolChildren(children: unknown[]): boolean {
|
|
389
|
+
let previousWasInspect = false;
|
|
390
|
+
for (const child of children) {
|
|
391
|
+
const currentIsInspect = isReadOnlyInspectionToolExecution(child);
|
|
392
|
+
if (currentIsInspect && previousWasInspect) return true;
|
|
393
|
+
previousWasInspect = currentIsInspect;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function plural(count: number, noun: string): string {
|
|
399
|
+
if (count === 1) return `1 ${noun}`;
|
|
400
|
+
const suffix = /(?:s|x|z|ch|sh)$/i.test(noun) ? "es" : "s";
|
|
401
|
+
return `${count} ${noun}${suffix}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function textDataLines(text: string): string[] {
|
|
405
|
+
return text.split("\n").filter((line) => line.trim().length > 0 && !line.trimStart().startsWith("["));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function compactList(values: string[], limit: number): string {
|
|
409
|
+
if (values.length === 0) return "";
|
|
410
|
+
const shown = values.slice(0, limit);
|
|
411
|
+
const suffix = values.length > shown.length ? `, … +${values.length - shown.length}` : "";
|
|
412
|
+
return `${shown.join(", ")}${suffix}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function uniqueLeadingGrepFiles(text: string, limit: number): string[] {
|
|
416
|
+
const seen = new Set<string>();
|
|
417
|
+
const files: string[] = [];
|
|
418
|
+
for (const rawLine of text.split("\n")) {
|
|
419
|
+
const line = rawLine.trim();
|
|
420
|
+
if (!line || line.startsWith("[")) continue;
|
|
421
|
+
const match = /^(.+?)(?::\d+:|-\d+-)/.exec(line);
|
|
422
|
+
if (!match) continue;
|
|
423
|
+
const file = match[1];
|
|
424
|
+
if (seen.has(file)) continue;
|
|
425
|
+
seen.add(file);
|
|
426
|
+
files.push(file);
|
|
427
|
+
if (files.length >= limit + 1) break;
|
|
428
|
+
}
|
|
429
|
+
return files;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function formatOffsetLimit(args: any): string {
|
|
433
|
+
const parts: string[] = [];
|
|
434
|
+
if (args?.offset !== undefined && args?.offset !== null) parts.push(`offset=${args.offset}`);
|
|
435
|
+
if (args?.limit !== undefined && args?.limit !== null) parts.push(`limit=${args.limit}`);
|
|
436
|
+
return parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function readInspectionTarget(value: unknown): string {
|
|
440
|
+
const rec = toolComponentRecord(value);
|
|
441
|
+
return `${shortPath(componentCwd(value), rec.args?.path ?? "")}${formatOffsetLimit(rec.args)}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function grepInspectionTarget(value: unknown): string {
|
|
445
|
+
const rec = toolComponentRecord(value);
|
|
446
|
+
const pattern = summarizeText(rec.args?.pattern ?? "", 40);
|
|
447
|
+
const path = rec.args?.path ? ` in ${shortPath(componentCwd(value), rec.args.path)}` : "";
|
|
448
|
+
return `"${pattern}"${path}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function findInspectionTarget(value: unknown): string {
|
|
452
|
+
const rec = toolComponentRecord(value);
|
|
453
|
+
const pattern = summarizeText(rec.args?.pattern ?? "", 40);
|
|
454
|
+
const path = rec.args?.path ? ` in ${shortPath(componentCwd(value), rec.args.path)}` : "";
|
|
455
|
+
return `"${pattern}"${path}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function listInspectionTarget(value: unknown): string {
|
|
459
|
+
const rec = toolComponentRecord(value);
|
|
460
|
+
return shortPath(componentCwd(value), rec.args?.path ?? ".");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function readInspectionStatus(value: unknown): string {
|
|
464
|
+
const rec = toolComponentRecord(value);
|
|
465
|
+
if (!rec.result) return "Reading...";
|
|
466
|
+
if (rec.result?.isError) return "failed";
|
|
467
|
+
const text = componentTextContent(value);
|
|
468
|
+
const lines = text.length === 0 ? 0 : text.split("\n").length;
|
|
469
|
+
const suffix = rec.result?.details?.truncation?.truncated ? " (truncated)" : "";
|
|
470
|
+
return `${plural(lines, "line")} loaded${suffix}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function grepInspectionStatus(value: unknown): string {
|
|
474
|
+
const rec = toolComponentRecord(value);
|
|
475
|
+
if (!rec.result) return "Searching...";
|
|
476
|
+
if (rec.result?.isError) return "failed";
|
|
477
|
+
const text = componentTextContent(value).trim();
|
|
478
|
+
if (!text || /^No matches found\b/i.test(text)) return "no matches";
|
|
479
|
+
const lines = textDataLines(text);
|
|
480
|
+
let status = plural(lines.length, "match");
|
|
481
|
+
const files = uniqueLeadingGrepFiles(text, readOnlyToolGroupLimit());
|
|
482
|
+
if (files.length > 1) status += ` in ${compactList(files, readOnlyToolGroupLimit())}`;
|
|
483
|
+
if (rec.result?.details?.truncation?.truncated) status += " (truncated)";
|
|
484
|
+
return status;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function findInspectionStatus(value: unknown): string {
|
|
488
|
+
const rec = toolComponentRecord(value);
|
|
489
|
+
if (!rec.result) return "Finding...";
|
|
490
|
+
if (rec.result?.isError) return "failed";
|
|
491
|
+
const text = componentTextContent(value).trim();
|
|
492
|
+
if (!text || /^No files found\b/i.test(text)) return "no files";
|
|
493
|
+
const items = textDataLines(text);
|
|
494
|
+
let status = plural(items.length, "file");
|
|
495
|
+
if (items.length > 0) status += `: ${compactList(items, readOnlyToolGroupLimit())}`;
|
|
496
|
+
if (rec.result?.details?.truncation?.truncated) status += " (truncated)";
|
|
497
|
+
return status;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function listInspectionStatus(value: unknown): string {
|
|
501
|
+
const rec = toolComponentRecord(value);
|
|
502
|
+
if (!rec.result) return "Listing...";
|
|
503
|
+
if (rec.result?.isError) return "failed";
|
|
504
|
+
const text = componentTextContent(value).trim();
|
|
505
|
+
if (!text || /^empty directory\b/i.test(text)) return "empty directory";
|
|
506
|
+
const items = textDataLines(text);
|
|
507
|
+
let status = plural(items.length, "entry");
|
|
508
|
+
if (items.length > 0) status += `: ${compactList(items, readOnlyToolGroupLimit())}`;
|
|
509
|
+
return status;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function bashReadInspectionTarget(value: unknown): string {
|
|
513
|
+
const info = bashReadDisplayInfo(value);
|
|
514
|
+
if (!info) return "";
|
|
515
|
+
const range = info.rangeLabel ? ` (${info.rangeLabel})` : "";
|
|
516
|
+
return `${shortPath(componentCwd(value), info.path)}${range}`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function bashReadInspectionStatus(value: unknown): string {
|
|
520
|
+
const rec = toolComponentRecord(value);
|
|
521
|
+
if (!rec.result) return "Reading...";
|
|
522
|
+
if (rec.result?.isError) return "failed";
|
|
523
|
+
const count = componentTextContent(value).split("\n").filter((line) => line.trim().length > 0).length;
|
|
524
|
+
return `${plural(count, "line")} read`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function summarizeReadOnlyInspectionTool(value: unknown): string {
|
|
528
|
+
const rec = toolComponentRecord(value);
|
|
529
|
+
if (rec.toolName === "read") return `Read ${readInspectionTarget(value)} — ${readInspectionStatus(value)}`;
|
|
530
|
+
if (rec.toolName === "grep") return `Grep ${grepInspectionTarget(value)} — ${grepInspectionStatus(value)}`;
|
|
531
|
+
if (rec.toolName === "find") return `Find ${findInspectionTarget(value)} — ${findInspectionStatus(value)}`;
|
|
532
|
+
if (rec.toolName === "ls") return `List ${listInspectionTarget(value)} — ${listInspectionStatus(value)}`;
|
|
533
|
+
return `Read ${bashReadInspectionTarget(value)} — ${bashReadInspectionStatus(value)}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function renderInspectionGroup(group: unknown[], width: number): string[] {
|
|
537
|
+
syncToolBackgroundMode();
|
|
538
|
+
const limit = readOnlyToolGroupLimit();
|
|
539
|
+
const shown = group.slice(0, limit);
|
|
540
|
+
const remaining = group.length - shown.length;
|
|
541
|
+
const core: string[] = [` ${WRAP_MARK}● Inspect ${plural(group.length, "tool use")}`];
|
|
542
|
+
for (let index = 0; index < shown.length; index++) {
|
|
543
|
+
const isLast = index === shown.length - 1 && remaining === 0;
|
|
544
|
+
const branch = isLast ? "└─" : "├─";
|
|
545
|
+
core.push(` ${TOOL_RULE}${branch}${TRANSPARENT_RESET} ${WRAP_MARK}${summarizeReadOnlyInspectionTool(shown[index])}`);
|
|
546
|
+
}
|
|
547
|
+
if (remaining > 0) {
|
|
548
|
+
core.push(` ${TOOL_RULE}└─${TRANSPARENT_RESET} ${WRAP_MARK}… ${remaining} more inspection${remaining === 1 ? "" : "s"}`);
|
|
549
|
+
}
|
|
550
|
+
const renderedCore = core.flatMap((line) => wrapMarkedLine(line, width)).map((line) => padToWidth(line, width));
|
|
551
|
+
if (toolBackgroundMode === "outlines") return [" ".repeat(width), borderLine(width), ...renderedCore, borderLine(width)];
|
|
552
|
+
if (toolBackgroundMode === "transparent") return [" ".repeat(width), ...renderedCore];
|
|
553
|
+
return renderedCore;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function renderWithGroupedReadOnlyInspectionTools(container: any, width: number): string[] | null {
|
|
557
|
+
const children = Array.isArray(container?.children) ? container.children : null;
|
|
558
|
+
if (!children || !hasConsecutiveReadOnlyInspectionToolChildren(children)) return null;
|
|
559
|
+
|
|
560
|
+
const rendered: string[] = [];
|
|
561
|
+
let group: unknown[] = [];
|
|
562
|
+
let previousWasBash = false;
|
|
563
|
+
const flushGroup = () => {
|
|
564
|
+
if (group.length === 0) return;
|
|
565
|
+
if (group.length === 1) {
|
|
566
|
+
const child = group[0] as any;
|
|
567
|
+
const currentIsBash = isBashToolExecution(child);
|
|
568
|
+
const childLines = typeof child?.render === "function" ? child.render(width) : [];
|
|
569
|
+
rendered.push(...(currentIsBash && previousWasBash ? dropLeadingSpacerLine(childLines) : childLines));
|
|
570
|
+
previousWasBash = currentIsBash;
|
|
571
|
+
} else {
|
|
572
|
+
rendered.push(...renderInspectionGroup(group, width));
|
|
573
|
+
previousWasBash = false;
|
|
574
|
+
}
|
|
575
|
+
group = [];
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
for (const child of children) {
|
|
579
|
+
if (isReadOnlyInspectionToolExecution(child)) {
|
|
580
|
+
group.push(child);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
flushGroup();
|
|
584
|
+
const currentIsBash = isBashToolExecution(child);
|
|
585
|
+
const childLines = typeof child?.render === "function" ? child.render(width) : [];
|
|
586
|
+
rendered.push(...(currentIsBash && previousWasBash ? dropLeadingSpacerLine(childLines) : childLines));
|
|
587
|
+
previousWasBash = currentIsBash;
|
|
588
|
+
}
|
|
589
|
+
flushGroup();
|
|
590
|
+
return rendered;
|
|
591
|
+
}
|
|
592
|
+
|
|
344
593
|
function hasConsecutiveBashToolChildren(children: unknown[]): boolean {
|
|
345
594
|
let previousWasBash = false;
|
|
346
595
|
for (const child of children) {
|
|
@@ -402,6 +651,8 @@ function patchGlobalToolBorders(): void {
|
|
|
402
651
|
const originalRender = proto.render;
|
|
403
652
|
proto.render = function patchedContainerRender(width: number): string[] {
|
|
404
653
|
if (!isToolExecutionLike(this)) {
|
|
654
|
+
const grouped = renderWithGroupedReadOnlyInspectionTools(this, width);
|
|
655
|
+
if (grouped) return grouped;
|
|
405
656
|
const stacked = renderWithStackedConsecutiveBash(this, width);
|
|
406
657
|
if (stacked) return stacked;
|
|
407
658
|
}
|
package/package.json
CHANGED