@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 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
 
@@ -9,6 +9,8 @@
9
9
  "bashCollapsedLines": 10,
10
10
  "bashStackConsecutive": true,
11
11
  "bashSemanticDisplay": true,
12
+ "readOnlyToolGrouping": true,
13
+ "readOnlyToolGroupLimit": 5,
12
14
  "diffCollapsedLines": 24,
13
15
  "showTruncationHints": false,
14
16
  "themeAdaptive": true,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlburtoe/pi-cc-tools",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Claude Code-style tool rows for pi with Ctrl+O image previews and consistent built-in, MCP, and custom tool rendering",
5
5
  "keywords": [
6
6
  "pi-package",