@owlburtoe/pi-cc-tools 1.1.1 → 1.1.6

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
@@ -4,15 +4,16 @@ Claude Code inspired tool rendering for Pi — Shiki-powered diffs, status dots,
4
4
 
5
5
  ## Features
6
6
 
7
- - **Compact built-in tool rendering** for `read`, `bash`, `grep`, `find`, `ls`, `edit`, and `write`
7
+ - **Compact Claude Code-like tool rendering** for `read`, `bash`, `grep`, `find`, `ls`, `edit`, and `write`, including `⏺ Tool(args)` headers and `⎿` result rows
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
12
13
  - **Diff stat bar** with colored add/remove summary and hunk metadata
13
14
  - **Progressive collapsed diff hints** that shorten on narrow terminals
14
15
  - **Thinking labels** during streaming and final messages, with context sanitization
15
- - **Claude-style transcript grammar controls** for assistant/thinking prefixes, message spacing, hidden thinking labels, and active working tips
16
+ - **Claude-style transcript grammar controls** for assistant/thinking prefixes, message spacing, and hidden thinking labels
16
17
  - **MCP-aware rendering** with hidden, summary, and preview modes
17
18
  - **Configurable output modes** for read, search, bash, and MCP results
18
19
  - **Transparent tool backgrounds** in `transparent` or `border` mode
@@ -26,7 +27,7 @@ Set in `.pi/settings.json` or `~/.pi/settings.json`:
26
27
 
27
28
  ```json
28
29
  {
29
- "toolBackground": "border",
30
+ "toolBackground": "transparent",
30
31
  "readOutputMode": "preview",
31
32
  "searchOutputMode": "preview",
32
33
  "mcpOutputMode": "preview",
@@ -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",
@@ -45,8 +48,6 @@ Set in `.pi/settings.json` or `~/.pi/settings.json`:
45
48
  "assistantPrefix": "●",
46
49
  "thinkingPrefix": "✻",
47
50
  "messageSpacing": "comfortable",
48
- "workingTipEnabled": true,
49
- "workingTipText": "Run /install-github-app to tag @claude right from your GitHub issues and PRs",
50
51
  "hiddenThinkingLabel": "Pondering..."
51
52
  }
52
53
  ```
@@ -113,13 +114,10 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
113
114
  /cc-message assistant-prefix ● # set assistant paragraph prefix
114
115
  /cc-message thinking-prefix ✻ # set visible thinking prefix
115
116
  /cc-message hidden-thinking-label Pondering...
116
- /cc-message tip on # show active working tip line
117
- /cc-message tip off # hide active working tip line
118
- /cc-message tip text Run /help for tips # set active working tip text
119
117
  /cc-message reset # restore message chrome defaults
120
118
  ```
121
119
 
122
- `messageStyle: "claude"` trims leading/trailing blank render lines, collapses paragraph gaps, and aligns wrapped assistant/thinking lines under the message body, matching Claude Code's sparse transcript grammar. `messageStyle: "classic"` keeps the previous package behavior. The active working tip is rendered as a subordinate `└─ Tip:` line under the spinner when supported by the Pi loader.
120
+ `messageStyle: "claude"` trims leading/trailing blank render lines, collapses paragraph gaps, and aligns wrapped assistant/thinking lines under the message body, matching Claude Code's sparse transcript grammar. `messageStyle: "classic"` keeps the previous package behavior.
123
121
 
124
122
  ### Tool background modes
125
123
 
@@ -127,7 +125,8 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
127
125
  |-------|----------|
128
126
  | `default` | Standard Pi tool backgrounds |
129
127
  | `transparent` | Transparent tool backgrounds |
130
- | `border` | Transparent backgrounds with top/bottom border lines |
128
+ | `border` | Transparent backgrounds with top/bottom border lines (alias for `outlines`) |
129
+ | `outlines` | Transparent backgrounds with top/bottom border lines |
131
130
 
132
131
  ### Output modes
133
132
 
@@ -152,6 +151,7 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
152
151
  |---------|---------|-------------|
153
152
  | `bashStackConsecutive` | `true` | Remove the extra synthetic spacer between adjacent bash tool rows so command bursts render as a tight stack |
154
153
  | `bashSemanticDisplay` | `true` | Render common read-only shell file-inspection commands as semantic `Read` rows instead of raw `Bash` rows |
154
+ | `readOnlyToolGrouping` | `true` | Collapse adjacent read-only inspection tools into one `Inspect` block; mutating `write`/`edit`/`apply_patch` rows stay independent |
155
155
 
156
156
  ### Numeric settings
157
157
 
@@ -161,6 +161,7 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
161
161
  | `expandedPreviewMaxLines` | `4000` | Max lines when fully expanded |
162
162
  | `bashCollapsedLines` | `10` | Lines for collapsed bash output |
163
163
  | `diffCollapsedLines` | `24` | Diff lines before collapsing |
164
+ | `readOnlyToolGroupLimit` | `5` | Max inspection entries shown inside a grouped read-only tool block |
164
165
 
165
166
  ## Notes
166
167
 
@@ -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,
@@ -21,7 +23,5 @@
21
23
  "assistantPrefix": "●",
22
24
  "thinkingPrefix": "✻",
23
25
  "messageSpacing": "comfortable",
24
- "workingTipEnabled": true,
25
- "workingTipText": "Run /install-github-app to tag @claude right from your GitHub issues and PRs",
26
26
  "hiddenThinkingLabel": "Pondering..."
27
27
  }
@@ -71,7 +71,12 @@ const WRAP_MARK = "\uE000";
71
71
  const KITTY_IMAGE_PREFIX = "\x1b_G";
72
72
  const ITERM2_IMAGE_PREFIX = "\x1b]1337;File=";
73
73
 
74
- let toolBackgroundMode: "default" | "transparent" | "outlines" = "outlines";
74
+ let toolBackgroundMode: "default" | "transparent" | "outlines" = "transparent";
75
+
76
+ const CLAUDE_TOOL_GLYPH = "⏺";
77
+ const CLAUDE_RESULT_GLYPH = "⎿";
78
+ const CLAUDE_RESULT_PREFIX = ` ${CLAUDE_RESULT_GLYPH} `;
79
+ const CLAUDE_RESULT_CONTINUATION = " ".repeat(CLAUDE_RESULT_PREFIX.length);
75
80
 
76
81
  type SpinnerVerbMode = "append" | "replace";
77
82
 
@@ -88,6 +93,8 @@ interface SettingsFile {
88
93
  bashStackConsecutive?: boolean;
89
94
  /** When true (default), read-only shell file-inspection one-liners render as semantic Read rows instead of raw Bash rows. */
90
95
  bashSemanticDisplay?: boolean;
96
+ readOnlyToolGrouping?: boolean;
97
+ readOnlyToolGroupLimit?: number;
91
98
  showTruncationHints?: boolean;
92
99
  diffCollapsedLines?: number;
93
100
  diffTheme?: string;
@@ -127,10 +134,6 @@ interface SettingsFile {
127
134
  thinkingPrefix?: string;
128
135
  /** Blank-line normalization in assistant/thinking transcript blocks. */
129
136
  messageSpacing?: MessageSpacing;
130
- /** Whether the active working spinner may show a subordinate Claude-style tip line. */
131
- workingTipEnabled?: boolean;
132
- /** Custom subordinate tip line text for the active working spinner. */
133
- workingTipText?: string;
134
137
  /** Label shown when thinking blocks are hidden by the Pi UI. */
135
138
  hiddenThinkingLabel?: string;
136
139
  }
@@ -271,7 +274,7 @@ function syncToolBackgroundMode(): void {
271
274
  const settings = readSettings();
272
275
  // Backward compat: "border" was renamed to "outlines"
273
276
  const raw = settings.toolBackground === "border" ? "outlines" : settings.toolBackground;
274
- toolBackgroundMode = raw ?? "outlines";
277
+ toolBackgroundMode = raw ?? "transparent";
275
278
  }
276
279
 
277
280
  function setThemeBg(theme: unknown, key: string, value: string): void {
@@ -341,6 +344,253 @@ function shouldStackConsecutiveBash(): boolean {
341
344
  return readSettings().bashStackConsecutive !== false;
342
345
  }
343
346
 
347
+ function readOnlyToolGroupingEnabled(): boolean {
348
+ return readSettings().readOnlyToolGrouping !== false;
349
+ }
350
+
351
+ function readOnlyToolGroupLimit(): number {
352
+ const value = readSettings().readOnlyToolGroupLimit;
353
+ return typeof value === "number" && Number.isFinite(value) && value > 0
354
+ ? Math.max(1, Math.min(20, Math.floor(value)))
355
+ : 5;
356
+ }
357
+
358
+ function toolComponentRecord(value: unknown): Record<string, any> {
359
+ return value as Record<string, any>;
360
+ }
361
+
362
+ function componentCwd(value: unknown): string {
363
+ const cwd = toolComponentRecord(value).cwd;
364
+ return typeof cwd === "string" && cwd ? cwd : process.cwd();
365
+ }
366
+
367
+ function componentTextContent(value: unknown): string {
368
+ return getTextContent(toolComponentRecord(value).result);
369
+ }
370
+
371
+ function componentHasImageResult(value: unknown): boolean {
372
+ return !!getFirstImageBlock(toolComponentRecord(value).result);
373
+ }
374
+
375
+ function bashReadDisplayInfo(value: unknown): BashDisplayInfo | null {
376
+ const rec = toolComponentRecord(value);
377
+ if (rec.toolName !== "bash" || !bashSemanticDisplayEnabled()) return null;
378
+ return classifyBashCommandForDisplay(rec.args?.command ?? "");
379
+ }
380
+
381
+ function isReadOnlyInspectionToolExecution(value: unknown): boolean {
382
+ if (!readOnlyToolGroupingEnabled() || !isToolExecutionLike(value)) return false;
383
+ const rec = toolComponentRecord(value);
384
+ if (rec.expanded === true || componentHasImageResult(value)) return false;
385
+ if (rec.toolName === "read" || rec.toolName === "grep" || rec.toolName === "find" || rec.toolName === "ls") return true;
386
+ return !!bashReadDisplayInfo(value);
387
+ }
388
+
389
+ function hasConsecutiveReadOnlyInspectionToolChildren(children: unknown[]): boolean {
390
+ let previousWasInspect = false;
391
+ for (const child of children) {
392
+ const currentIsInspect = isReadOnlyInspectionToolExecution(child);
393
+ if (currentIsInspect && previousWasInspect) return true;
394
+ previousWasInspect = currentIsInspect;
395
+ }
396
+ return false;
397
+ }
398
+
399
+ function plural(count: number, noun: string): string {
400
+ if (count === 1) return `1 ${noun}`;
401
+ const suffix = /(?:s|x|z|ch|sh)$/i.test(noun) ? "es" : "s";
402
+ return `${count} ${noun}${suffix}`;
403
+ }
404
+
405
+ function textDataLines(text: string): string[] {
406
+ return text.split("\n").filter((line) => line.trim().length > 0 && !line.trimStart().startsWith("["));
407
+ }
408
+
409
+ function compactList(values: string[], limit: number): string {
410
+ if (values.length === 0) return "";
411
+ const shown = values.slice(0, limit);
412
+ const suffix = values.length > shown.length ? `, … +${values.length - shown.length}` : "";
413
+ return `${shown.join(", ")}${suffix}`;
414
+ }
415
+
416
+ function uniqueLeadingGrepFiles(text: string, limit: number): string[] {
417
+ const seen = new Set<string>();
418
+ const files: string[] = [];
419
+ for (const rawLine of text.split("\n")) {
420
+ const line = rawLine.trim();
421
+ if (!line || line.startsWith("[")) continue;
422
+ const match = /^(.+?)(?::\d+:|-\d+-)/.exec(line);
423
+ if (!match) continue;
424
+ const file = match[1];
425
+ if (seen.has(file)) continue;
426
+ seen.add(file);
427
+ files.push(file);
428
+ if (files.length >= limit + 1) break;
429
+ }
430
+ return files;
431
+ }
432
+
433
+ function formatOffsetLimit(args: any): string {
434
+ const parts: string[] = [];
435
+ if (args?.offset !== undefined && args?.offset !== null) parts.push(`offset=${args.offset}`);
436
+ if (args?.limit !== undefined && args?.limit !== null) parts.push(`limit=${args.limit}`);
437
+ return parts.length > 0 ? ` (${parts.join(", ")})` : "";
438
+ }
439
+
440
+ function readInspectionTarget(value: unknown): string {
441
+ const rec = toolComponentRecord(value);
442
+ return `${shortPath(componentCwd(value), rec.args?.path ?? "")}${formatOffsetLimit(rec.args)}`;
443
+ }
444
+
445
+ function grepInspectionTarget(value: unknown): string {
446
+ const rec = toolComponentRecord(value);
447
+ const pattern = summarizeText(rec.args?.pattern ?? "", 40);
448
+ const path = rec.args?.path ? ` in ${shortPath(componentCwd(value), rec.args.path)}` : "";
449
+ return `"${pattern}"${path}`;
450
+ }
451
+
452
+ function findInspectionTarget(value: unknown): string {
453
+ const rec = toolComponentRecord(value);
454
+ const pattern = summarizeText(rec.args?.pattern ?? "", 40);
455
+ const path = rec.args?.path ? ` in ${shortPath(componentCwd(value), rec.args.path)}` : "";
456
+ return `"${pattern}"${path}`;
457
+ }
458
+
459
+ function listInspectionTarget(value: unknown): string {
460
+ const rec = toolComponentRecord(value);
461
+ return shortPath(componentCwd(value), rec.args?.path ?? ".");
462
+ }
463
+
464
+ function readInspectionStatus(value: unknown): string {
465
+ const rec = toolComponentRecord(value);
466
+ if (!rec.result) return "Reading...";
467
+ if (rec.result?.isError) return "failed";
468
+ const text = componentTextContent(value);
469
+ const lines = text.length === 0 ? 0 : text.split("\n").length;
470
+ const suffix = rec.result?.details?.truncation?.truncated ? " (truncated)" : "";
471
+ return `${plural(lines, "line")} loaded${suffix}`;
472
+ }
473
+
474
+ function grepInspectionStatus(value: unknown): string {
475
+ const rec = toolComponentRecord(value);
476
+ if (!rec.result) return "Searching...";
477
+ if (rec.result?.isError) return "failed";
478
+ const text = componentTextContent(value).trim();
479
+ if (!text || /^No matches found\b/i.test(text)) return "no matches";
480
+ const lines = textDataLines(text);
481
+ let status = plural(lines.length, "match");
482
+ const files = uniqueLeadingGrepFiles(text, readOnlyToolGroupLimit());
483
+ if (files.length > 1) status += ` in ${compactList(files, readOnlyToolGroupLimit())}`;
484
+ if (rec.result?.details?.truncation?.truncated) status += " (truncated)";
485
+ return status;
486
+ }
487
+
488
+ function findInspectionStatus(value: unknown): string {
489
+ const rec = toolComponentRecord(value);
490
+ if (!rec.result) return "Finding...";
491
+ if (rec.result?.isError) return "failed";
492
+ const text = componentTextContent(value).trim();
493
+ if (!text || /^No files found\b/i.test(text)) return "no files";
494
+ const items = textDataLines(text);
495
+ let status = plural(items.length, "file");
496
+ if (items.length > 0) status += `: ${compactList(items, readOnlyToolGroupLimit())}`;
497
+ if (rec.result?.details?.truncation?.truncated) status += " (truncated)";
498
+ return status;
499
+ }
500
+
501
+ function listInspectionStatus(value: unknown): string {
502
+ const rec = toolComponentRecord(value);
503
+ if (!rec.result) return "Listing...";
504
+ if (rec.result?.isError) return "failed";
505
+ const text = componentTextContent(value).trim();
506
+ if (!text || /^empty directory\b/i.test(text)) return "empty directory";
507
+ const items = textDataLines(text);
508
+ let status = plural(items.length, "entry");
509
+ if (items.length > 0) status += `: ${compactList(items, readOnlyToolGroupLimit())}`;
510
+ return status;
511
+ }
512
+
513
+ function bashReadInspectionTarget(value: unknown): string {
514
+ const info = bashReadDisplayInfo(value);
515
+ if (!info) return "";
516
+ const range = info.rangeLabel ? ` (${info.rangeLabel})` : "";
517
+ return `${shortPath(componentCwd(value), info.path)}${range}`;
518
+ }
519
+
520
+ function bashReadInspectionStatus(value: unknown): string {
521
+ const rec = toolComponentRecord(value);
522
+ if (!rec.result) return "Reading...";
523
+ if (rec.result?.isError) return "failed";
524
+ const count = componentTextContent(value).split("\n").filter((line) => line.trim().length > 0).length;
525
+ return `${plural(count, "line")} read`;
526
+ }
527
+
528
+ function summarizeReadOnlyInspectionTool(value: unknown): string {
529
+ const rec = toolComponentRecord(value);
530
+ if (rec.toolName === "read") return `Read ${readInspectionTarget(value)} — ${readInspectionStatus(value)}`;
531
+ if (rec.toolName === "grep") return `Grep ${grepInspectionTarget(value)} — ${grepInspectionStatus(value)}`;
532
+ if (rec.toolName === "find") return `Find ${findInspectionTarget(value)} — ${findInspectionStatus(value)}`;
533
+ if (rec.toolName === "ls") return `List ${listInspectionTarget(value)} — ${listInspectionStatus(value)}`;
534
+ return `Read ${bashReadInspectionTarget(value)} — ${bashReadInspectionStatus(value)}`;
535
+ }
536
+
537
+ function renderInspectionGroup(group: unknown[], width: number): string[] {
538
+ syncToolBackgroundMode();
539
+ const limit = readOnlyToolGroupLimit();
540
+ const shown = group.slice(0, limit);
541
+ const remaining = group.length - shown.length;
542
+ const core: string[] = [` ${WRAP_MARK}${CLAUDE_TOOL_GLYPH} Inspect(${plural(group.length, "tool use")})`];
543
+ for (let index = 0; index < shown.length; index++) {
544
+ const prefix = index === 0 ? `${TOOL_RULE}${CLAUDE_RESULT_PREFIX}${TRANSPARENT_RESET}` : CLAUDE_RESULT_CONTINUATION;
545
+ core.push(`${prefix}${WRAP_MARK}${summarizeReadOnlyInspectionTool(shown[index])}`);
546
+ }
547
+ if (remaining > 0) {
548
+ const prefix = shown.length === 0 ? `${TOOL_RULE}${CLAUDE_RESULT_PREFIX}${TRANSPARENT_RESET}` : CLAUDE_RESULT_CONTINUATION;
549
+ core.push(`${prefix}${WRAP_MARK}… ${remaining} more inspection${remaining === 1 ? "" : "s"}`);
550
+ }
551
+ const renderedCore = core.flatMap((line) => wrapMarkedLine(line, width)).map((line) => padToWidth(line, width));
552
+ if (toolBackgroundMode === "outlines") return [" ".repeat(width), borderLine(width), ...renderedCore, borderLine(width)];
553
+ if (toolBackgroundMode === "transparent") return [" ".repeat(width), ...renderedCore];
554
+ return renderedCore;
555
+ }
556
+
557
+ function renderWithGroupedReadOnlyInspectionTools(container: any, width: number): string[] | null {
558
+ const children = Array.isArray(container?.children) ? container.children : null;
559
+ if (!children || !hasConsecutiveReadOnlyInspectionToolChildren(children)) return null;
560
+
561
+ const rendered: string[] = [];
562
+ let group: unknown[] = [];
563
+ let previousWasBash = false;
564
+ const flushGroup = () => {
565
+ if (group.length === 0) return;
566
+ if (group.length === 1) {
567
+ const child = group[0] as any;
568
+ const currentIsBash = isBashToolExecution(child);
569
+ const childLines = typeof child?.render === "function" ? child.render(width) : [];
570
+ rendered.push(...(currentIsBash && previousWasBash ? dropLeadingSpacerLine(childLines) : childLines));
571
+ previousWasBash = currentIsBash;
572
+ } else {
573
+ rendered.push(...renderInspectionGroup(group, width));
574
+ previousWasBash = false;
575
+ }
576
+ group = [];
577
+ };
578
+
579
+ for (const child of children) {
580
+ if (isReadOnlyInspectionToolExecution(child)) {
581
+ group.push(child);
582
+ continue;
583
+ }
584
+ flushGroup();
585
+ const currentIsBash = isBashToolExecution(child);
586
+ const childLines = typeof child?.render === "function" ? child.render(width) : [];
587
+ rendered.push(...(currentIsBash && previousWasBash ? dropLeadingSpacerLine(childLines) : childLines));
588
+ previousWasBash = currentIsBash;
589
+ }
590
+ flushGroup();
591
+ return rendered;
592
+ }
593
+
344
594
  function hasConsecutiveBashToolChildren(children: unknown[]): boolean {
345
595
  let previousWasBash = false;
346
596
  for (const child of children) {
@@ -402,6 +652,8 @@ function patchGlobalToolBorders(): void {
402
652
  const originalRender = proto.render;
403
653
  proto.render = function patchedContainerRender(width: number): string[] {
404
654
  if (!isToolExecutionLike(this)) {
655
+ const grouped = renderWithGroupedReadOnlyInspectionTools(this, width);
656
+ if (grouped) return grouped;
405
657
  const stacked = renderWithStackedConsecutiveBash(this, width);
406
658
  if (stacked) return stacked;
407
659
  }
@@ -1000,7 +1252,7 @@ function toolHeader(tool: string, summary: string, theme: Theme, prefix = ""): s
1000
1252
  applyThemePaletteIfNeeded(theme);
1001
1253
  const label = theme.fg("toolTitle", theme.bold(tool));
1002
1254
  if (!summary) return `${prefix}${label}`;
1003
- return `${prefix}${label} ${WRAP_MARK}${theme.fg("accent", summary)}`;
1255
+ return `${prefix}${label}${theme.fg("muted", "(")}${WRAP_MARK}${theme.fg("accent", summary)}${theme.fg("muted", ")")}`;
1004
1256
  }
1005
1257
 
1006
1258
  function setToolStatus(ctx: any, status: "pending" | "success" | "error"): void {
@@ -1064,8 +1316,8 @@ function getWriteWasNewFile(ctx: any, cwd: string, filePath: string, reveal = sh
1064
1316
 
1065
1317
  function toolStatusDot(ctx: any, theme: Theme): string {
1066
1318
  const status = ctx.state?._toolStatus as "pending" | "success" | "error" | undefined;
1067
- if (status === "success") return `${theme.fg("success", "●")} `;
1068
- if (status === "error") return `${theme.fg("error", "●")} `;
1319
+ if (status === "error") return `${theme.fg("error", CLAUDE_TOOL_GLYPH)} `;
1320
+ if (status === "success") return `${theme.fg("accent", CLAUDE_TOOL_GLYPH)} `;
1069
1321
  return `${blinkDot(ctx, theme)} `;
1070
1322
  }
1071
1323
 
@@ -1073,13 +1325,12 @@ function toolStatusDot(ctx: any, theme: Theme): string {
1073
1325
  // Branch connector — visual tree from header to output
1074
1326
  // ---------------------------------------------------------------------------
1075
1327
 
1076
- function branchIndent(text: string, continued = false): string {
1077
- const prefix = continued ? `${TOOL_RULE}│${TRANSPARENT_RESET} ` : " ";
1078
- return `${prefix}${WRAP_MARK}${text}`;
1328
+ function branchIndent(text: string, _continued = false): string {
1329
+ return `${CLAUDE_RESULT_CONTINUATION}${WRAP_MARK}${text}`;
1079
1330
  }
1080
1331
 
1081
- function branchLead(text: string, continued = false): string {
1082
- return `${TOOL_RULE}${continued ? "├─" : "└─"}${TRANSPARENT_RESET} ${WRAP_MARK}${text}`;
1332
+ function branchLead(text: string, _continued = false): string {
1333
+ return `${TOOL_RULE}${CLAUDE_RESULT_PREFIX}${TRANSPARENT_RESET}${WRAP_MARK}${text}`;
1083
1334
  }
1084
1335
 
1085
1336
  function withBranch(content: string, _theme: Theme, _isError = false, continued = false): string {
@@ -1092,20 +1343,11 @@ function withBranch(content: string, _theme: Theme, _isError = false, continued
1092
1343
  }
1093
1344
 
1094
1345
  function withFinalBranchBlock(content: string, theme: Theme, isError = false): string {
1095
- if (!content || !content.trim()) return "";
1096
- const lines = content.split("\n");
1097
- const first = lines[0] ?? "";
1098
- if (lines.length === 1) return branchLead(first, false);
1099
- const middle = lines.slice(1, -1).map((line) => branchIndent(line, true));
1100
- const last = lines[lines.length - 1] ?? "";
1101
- return [branchLead(first, true), ...middle, branchLead(last, false)].join("\n");
1346
+ return withBranch(content, theme, isError);
1102
1347
  }
1103
1348
 
1104
1349
  function indentBranchBlock(block: string): string {
1105
- return block
1106
- .split("\n")
1107
- .map((line) => (line ? ` ${line}` : line))
1108
- .join("\n");
1350
+ return block;
1109
1351
  }
1110
1352
 
1111
1353
  // ---------------------------------------------------------------------------
@@ -1207,8 +1449,8 @@ function clearBlinkTimer(ctx: any): void {
1207
1449
  function blinkDot(ctx: any, theme: Theme): string {
1208
1450
  setupBlinkTimer(ctx);
1209
1451
  const key = getBlinkKey(ctx);
1210
- if (key?._blinkActive !== true) return theme.fg("muted", "○");
1211
- return _globalBlinkPhase ? theme.fg("success", "●") : theme.fg("muted", "○");
1452
+ if (key?._blinkActive !== true) return theme.fg("muted", CLAUDE_TOOL_GLYPH);
1453
+ return _globalBlinkPhase ? theme.fg("accent", CLAUDE_TOOL_GLYPH) : theme.fg("muted", CLAUDE_TOOL_GLYPH);
1212
1454
  }
1213
1455
 
1214
1456
  // ---------------------------------------------------------------------------
@@ -1294,6 +1536,8 @@ function markedContinuationPrefix(prefix: string): string {
1294
1536
  if (branchMatch) {
1295
1537
  return `${branchMatch[1]}${TOOL_RULE}│${TRANSPARENT_RESET} `;
1296
1538
  }
1539
+ const resultMatch = /^(\s*)⎿\s+/.exec(plain);
1540
+ if (resultMatch) return " ".repeat(visibleWidth(prefix));
1297
1541
  return " ".repeat(visibleWidth(prefix));
1298
1542
  }
1299
1543
 
@@ -4247,7 +4491,7 @@ export default function (pi: ExtensionAPI) {
4247
4491
  },
4248
4492
  });
4249
4493
 
4250
- const MESSAGE_COMMANDS = ["style", "spacing", "assistant-prefix", "thinking-prefix", "tip", "hidden-thinking-label", "reset", "status"] as const;
4494
+ const MESSAGE_COMMANDS = ["style", "spacing", "assistant-prefix", "thinking-prefix", "hidden-thinking-label", "reset", "status"] as const;
4251
4495
  pi.registerCommand("cc-message", {
4252
4496
  description: "Configure Claude-style assistant/thinking transcript chrome",
4253
4497
  getArgumentCompletions(prefix: string) {
@@ -4264,7 +4508,6 @@ export default function (pi: ExtensionAPI) {
4264
4508
  : c === "spacing" ? "Set blank-line rhythm: compact or comfortable"
4265
4509
  : c === "assistant-prefix" ? "Set assistant paragraph prefix glyph/text"
4266
4510
  : c === "thinking-prefix" ? "Set visible thinking prefix glyph/text"
4267
- : c === "tip" ? "Enable, disable, or customize active working tip line"
4268
4511
  : c === "hidden-thinking-label" ? "Set label shown when thinking is hidden"
4269
4512
  : c === "reset" ? "Reset message chrome settings"
4270
4513
  : "Show current message chrome settings",
@@ -4282,12 +4525,6 @@ export default function (pi: ExtensionAPI) {
4282
4525
  .filter((v) => v.startsWith(valuePrefix))
4283
4526
  .map((v) => ({ value: `spacing ${v}`, label: v, description: v === "comfortable" ? "Preserve one blank line between paragraphs" : "Remove blank lines inside assistant/thinking blocks" }));
4284
4527
  }
4285
- if (parts[0] === "tip") {
4286
- const valuePrefix = (parts[1] ?? "").toLowerCase();
4287
- return ["on", "off", "text", "reset"]
4288
- .filter((v) => v.startsWith(valuePrefix))
4289
- .map((v) => ({ value: `tip ${v}`, label: v, description: v === "text" ? "Set custom active working tip text" : `${v} working tip line` }));
4290
- }
4291
4528
  return [];
4292
4529
  },
4293
4530
  async handler(args: string, ctx: any) {
@@ -4302,8 +4539,6 @@ export default function (pi: ExtensionAPI) {
4302
4539
  `Assistant prefix: ${current.assistantPrefix}`,
4303
4540
  `Thinking prefix: ${current.thinkingPrefix}`,
4304
4541
  `Spacing: ${current.messageSpacing}`,
4305
- `Working tip: ${current.workingTipEnabled ? "on" : "off"}`,
4306
- `Tip text: ${current.workingTipText}`,
4307
4542
  `Hidden thinking label: ${current.hiddenThinkingLabel}`,
4308
4543
  ].join("\n"), "info");
4309
4544
  };
@@ -4314,7 +4549,7 @@ export default function (pi: ExtensionAPI) {
4314
4549
  }
4315
4550
 
4316
4551
  if (sub === "reset") {
4317
- for (const key of ["messageStyle", "assistantPrefix", "thinkingPrefix", "messageSpacing", "workingTipEnabled", "workingTipText", "hiddenThinkingLabel"]) {
4552
+ for (const key of ["messageStyle", "assistantPrefix", "thinkingPrefix", "messageSpacing", "hiddenThinkingLabel"]) {
4318
4553
  writeSettingsKey(key, undefined);
4319
4554
  }
4320
4555
  bustSpinnerSettingsCache();
@@ -4330,8 +4565,6 @@ export default function (pi: ExtensionAPI) {
4330
4565
  return;
4331
4566
  }
4332
4567
  writeSettingsKey("messageStyle", value);
4333
- writeSettingsKey("workingTipEnabled", value === "classic" ? false : undefined);
4334
- bustSpinnerSettingsCache();
4335
4568
  if (ctx.hasUI) ctx.ui.notify(`Message style → ${value}`, "info");
4336
4569
  return;
4337
4570
  }
@@ -4356,33 +4589,6 @@ export default function (pi: ExtensionAPI) {
4356
4589
  return;
4357
4590
  }
4358
4591
 
4359
- if (sub === "tip") {
4360
- const action = (parts[1] ?? "").toLowerCase();
4361
- if (action === "on" || action === "off") {
4362
- writeSettingsKey("workingTipEnabled", action === "on");
4363
- bustSpinnerSettingsCache();
4364
- if (ctx.hasUI) ctx.ui.notify(`Working tip → ${action}`, "info");
4365
- return;
4366
- }
4367
- if (action === "reset") {
4368
- writeSettingsKey("workingTipEnabled", undefined);
4369
- writeSettingsKey("workingTipText", undefined);
4370
- bustSpinnerSettingsCache();
4371
- if (ctx.hasUI) ctx.ui.notify("Working tip reset", "info");
4372
- return;
4373
- }
4374
- if (action === "text") {
4375
- const textArg = rawArgs.match(/^tip\s+text(?:\s+(.+))?$/i)?.[1] ?? "";
4376
- const value = resolveMessageChromeSettings({ workingTipText: textArg }).workingTipText;
4377
- writeSettingsKey("workingTipText", value);
4378
- bustSpinnerSettingsCache();
4379
- if (ctx.hasUI) ctx.ui.notify(`Working tip text → ${value}`, "info");
4380
- return;
4381
- }
4382
- if (ctx.hasUI) ctx.ui.notify("Usage: /cc-message tip on|off|reset|text <text>", "error");
4383
- return;
4384
- }
4385
-
4386
4592
  if (sub === "hidden-thinking-label") {
4387
4593
  const textArg = rawArgs.match(/^hidden-thinking-label(?:\s+(.+))?$/i)?.[1] ?? "";
4388
4594
  const value = resolveMessageChromeSettings({ hiddenThinkingLabel: textArg }).hiddenThinkingLabel;
@@ -4392,7 +4598,7 @@ export default function (pi: ExtensionAPI) {
4392
4598
  return;
4393
4599
  }
4394
4600
 
4395
- if (ctx.hasUI) ctx.ui.notify("Usage: /cc-message style|spacing|assistant-prefix|thinking-prefix|tip|hidden-thinking-label|reset|status", "error");
4601
+ if (ctx.hasUI) ctx.ui.notify("Usage: /cc-message style|spacing|assistant-prefix|thinking-prefix|hidden-thinking-label|reset|status", "error");
4396
4602
  },
4397
4603
  });
4398
4604
 
@@ -4815,7 +5021,7 @@ export default function (pi: ExtensionAPI) {
4815
5021
  const revealSummary = shouldRevealCallArgs(ctx) || (!!fp && hasOwnArg(args, "edits"));
4816
5022
  const summary = stableCallSummary(ctx, "_callSummary", () => shouldRevealCallArgs(ctx) && operations.length > 1 ? `${sp(fp)} ${theme.fg("muted", `(${operations.length} edits)`)}` : sp(fp), revealSummary);
4817
5023
  syncToolCallStatus(ctx);
4818
- const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}`);
5024
+ const hdr = toolHeader("Edit", summary, theme, toolStatusDot(ctx, theme));
4819
5025
  if (!(ctx.argsComplete && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
4820
5026
  const diffWidth = branchDiffWidth();
4821
5027
  const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
@@ -6,8 +6,6 @@ export interface MessageChromeInput {
6
6
  assistantPrefix?: unknown;
7
7
  thinkingPrefix?: unknown;
8
8
  messageSpacing?: unknown;
9
- workingTipEnabled?: unknown;
10
- workingTipText?: unknown;
11
9
  hiddenThinkingLabel?: unknown;
12
10
  }
13
11
 
@@ -16,8 +14,6 @@ export interface MessageChromeSettings {
16
14
  assistantPrefix: string;
17
15
  thinkingPrefix: string;
18
16
  messageSpacing: MessageSpacing;
19
- workingTipEnabled: boolean;
20
- workingTipText: string;
21
17
  hiddenThinkingLabel: string;
22
18
  }
23
19
 
@@ -28,7 +24,6 @@ export interface TranscriptLineOptions {
28
24
  visibleWidth?: (text: string) => number;
29
25
  }
30
26
 
31
- export const DEFAULT_WORKING_TIP_TEXT = "Run /install-github-app to tag @claude right from your GitHub issues and PRs";
32
27
  export const DEFAULT_HIDDEN_THINKING_LABEL = "Pondering...";
33
28
 
34
29
  const DEFAULT_MESSAGE_CHROME: MessageChromeSettings = {
@@ -36,8 +31,6 @@ const DEFAULT_MESSAGE_CHROME: MessageChromeSettings = {
36
31
  assistantPrefix: "●",
37
32
  thinkingPrefix: "✻",
38
33
  messageSpacing: "comfortable",
39
- workingTipEnabled: true,
40
- workingTipText: DEFAULT_WORKING_TIP_TEXT,
41
34
  hiddenThinkingLabel: DEFAULT_HIDDEN_THINKING_LABEL,
42
35
  };
43
36
 
@@ -45,7 +38,6 @@ const ANSI_ESCAPE_SEQUENCE_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*
45
38
  const ANSI_SGR_RE = /\x1b\[[0-9;]*m/g;
46
39
  const CONTROL_CHARS_RE = /[\u0000-\u001F\u007F-\u009F]/g;
47
40
  const MAX_PREFIX_GRAPHEMES = 8;
48
- const MAX_TIP_GRAPHEMES = 160;
49
41
  const MAX_LABEL_GRAPHEMES = 80;
50
42
 
51
43
  function stripAnsi(text: string): string {
@@ -81,14 +73,11 @@ export function sanitizeMessagePrefix(value: unknown, fallback: string): string
81
73
  export function resolveMessageChromeSettings(input: MessageChromeInput = {}): MessageChromeSettings {
82
74
  const messageStyle: MessageStyle = input.messageStyle === "classic" ? "classic" : "claude";
83
75
  const messageSpacing: MessageSpacing = input.messageSpacing === "compact" ? "compact" : "comfortable";
84
- const defaultWorkingTipEnabled = messageStyle === "classic" ? false : DEFAULT_MESSAGE_CHROME.workingTipEnabled;
85
76
  return {
86
77
  messageStyle,
87
78
  assistantPrefix: sanitizeMessagePrefix(input.assistantPrefix, DEFAULT_MESSAGE_CHROME.assistantPrefix),
88
79
  thinkingPrefix: sanitizeMessagePrefix(input.thinkingPrefix, DEFAULT_MESSAGE_CHROME.thinkingPrefix),
89
80
  messageSpacing,
90
- workingTipEnabled: typeof input.workingTipEnabled === "boolean" ? input.workingTipEnabled : defaultWorkingTipEnabled,
91
- workingTipText: sanitizeTextSetting(input.workingTipText, DEFAULT_MESSAGE_CHROME.workingTipText, MAX_TIP_GRAPHEMES),
92
81
  hiddenThinkingLabel: sanitizeTextSetting(input.hiddenThinkingLabel, DEFAULT_MESSAGE_CHROME.hiddenThinkingLabel, MAX_LABEL_GRAPHEMES),
93
82
  };
94
83
  }
@@ -3,8 +3,6 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
4
  import { Loader } from "@mariozechner/pi-tui";
5
5
 
6
- import { resolveMessageChromeSettings } from "./message-chrome.ts";
7
-
8
6
  // ---------------------------------------------------------------------------
9
7
  // Patch built-in Loader with Claude/OpenBrawd-style glyphs.
10
8
  // Keep animation cadence constant so the spinner doesn't appear to slow down
@@ -28,8 +26,6 @@ interface SpinnerSettings {
28
26
  verbColor: string;
29
27
  statusColor: string;
30
28
  verbs: readonly string[];
31
- workingTipEnabled: boolean;
32
- workingTipText: string;
33
29
  }
34
30
 
35
31
  let _spinnerSettingsCache: { value: SpinnerSettings; expires: number } | null = null;
@@ -100,9 +96,6 @@ function readSpinnerSettings(): SpinnerSettings {
100
96
  let statusColor = "muted";
101
97
  let customVerbs: string[] | null = null;
102
98
  let verbMode: SpinnerVerbMode = "append";
103
- let messageStyle: string | undefined;
104
- let workingTipEnabled: boolean | undefined;
105
- let workingTipText: string | undefined;
106
99
  const paths = [`${process.cwd()}/.pi/settings.json`, `${process.env.HOME ?? ""}/.pi/settings.json`];
107
100
  for (const p of paths) {
108
101
  try {
@@ -115,20 +108,14 @@ function readSpinnerSettings(): SpinnerSettings {
115
108
  if (typeof raw.spinnerStatusColor === "string" && raw.spinnerStatusColor.length > 0) statusColor = raw.spinnerStatusColor;
116
109
  if (raw.spinnerVerbMode === "append" || raw.spinnerVerbMode === "replace") verbMode = raw.spinnerVerbMode;
117
110
  if (Array.isArray(raw.spinnerVerbs)) customVerbs = sanitizeSpinnerVerbs(raw.spinnerVerbs);
118
- if (typeof raw.messageStyle === "string") messageStyle = raw.messageStyle;
119
- if (typeof raw.workingTipEnabled === "boolean") workingTipEnabled = raw.workingTipEnabled;
120
- if (typeof raw.workingTipText === "string") workingTipText = raw.workingTipText;
121
111
  }
122
112
  } catch { /* ignore */ }
123
113
  }
124
- const messageChrome = resolveMessageChromeSettings({ messageStyle, workingTipEnabled, workingTipText });
125
114
  const value: SpinnerSettings = {
126
115
  adaptive,
127
116
  verbColor,
128
117
  statusColor,
129
118
  verbs: resolveSpinnerVerbs(customVerbs, verbMode),
130
- workingTipEnabled: messageChrome.workingTipEnabled,
131
- workingTipText: messageChrome.workingTipText,
132
119
  };
133
120
  _spinnerSettingsCache = { value, expires: now + SPINNER_SETTINGS_TTL_MS };
134
121
  return value;
@@ -567,10 +554,6 @@ export default function (pi: ExtensionAPI) {
567
554
  if (statusParts.length > 0) {
568
555
  message += statusText(` (${statusParts.join(" · ")})`);
569
556
  }
570
- const { workingTipEnabled, workingTipText } = readSpinnerSettings();
571
- if (workingTipEnabled && workingTipText) {
572
- message += `\n${STATUS_DIM} └─ Tip: ${workingTipText}${RESET}`;
573
- }
574
557
  return message;
575
558
  }
576
559
 
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.6",
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",