@owlburtoe/pi-cc-tools 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -1,13 +1,4 @@
1
- # pi-cc-tools
2
-
3
- ## Install
4
-
5
- ```bash
6
- pi install npm:@owlburtoe/pi-cc-tools
7
- ```
8
-
9
- Run `/reload` or restart Pi after installation.
10
-
1
+ # pi-claude-style-tools
11
2
 
12
3
  Claude Code inspired tool rendering for Pi — Shiki-powered diffs, status dots, branch connectors, file icons, and configurable output modes.
13
4
 
@@ -20,6 +11,7 @@ Claude Code inspired tool rendering for Pi — Shiki-powered diffs, status dots,
20
11
  - **Diff stat bar** with colored add/remove summary and hunk metadata
21
12
  - **Progressive collapsed diff hints** that shorten on narrow terminals
22
13
  - **Thinking labels** during streaming and final messages, with context sanitization
14
+ - **Claude-style transcript grammar controls** for assistant/thinking prefixes, message spacing, hidden thinking labels, and active working tips
23
15
  - **MCP-aware rendering** with hidden, summary, and preview modes
24
16
  - **Configurable output modes** for read, search, bash, and MCP results
25
17
  - **Transparent tool backgrounds** in `transparent` or `border` mode
@@ -40,12 +32,20 @@ Set in `.pi/settings.json` or `~/.pi/settings.json`:
40
32
  "previewLines": 8,
41
33
  "bashOutputMode": "opencode",
42
34
  "bashCollapsedLines": 10,
35
+ "bashStackConsecutive": true,
43
36
  "diffCollapsedLines": 24,
44
37
  "themeAdaptive": true,
45
38
  "diffTheme": "github-dark",
46
39
  "spinnerColor": "borderAccent",
47
40
  "spinnerVerbs": ["Reviewing", "Polishing"],
48
- "spinnerVerbMode": "append"
41
+ "spinnerVerbMode": "append",
42
+ "messageStyle": "claude",
43
+ "assistantPrefix": "●",
44
+ "thinkingPrefix": "✻",
45
+ "messageSpacing": "comfortable",
46
+ "workingTipEnabled": true,
47
+ "workingTipText": "Run /install-github-app to tag @claude right from your GitHub issues and PRs",
48
+ "hiddenThinkingLabel": "Pondering..."
49
49
  }
50
50
  ```
51
51
 
@@ -100,6 +100,25 @@ The spinner glyph and verb text (e.g. `✻ Cooking…`) share `borderAccent` by
100
100
 
101
101
  Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.pi/settings.json` and applied on the next spinner tick. Older `spinnerVerbColor` settings still work as a backward-compatible alias. Custom verbs are persisted as `spinnerVerbs` and `spinnerVerbMode`; they are picked at the next turn start. `/cc-spinner verbs ...` writes user settings in `~/.pi/settings.json`; project-level custom verbs can be set manually in `.pi/settings.json`. When both project and user spinner settings exist, the spinner reader applies project settings first and user settings second.
102
102
 
103
+ #### Tune assistant/thinking transcript chrome with `/cc-message`
104
+
105
+ ```text
106
+ /cc-message # show current message chrome settings
107
+ /cc-message style claude # screenshot-style transcript rhythm
108
+ /cc-message style classic # older package spacing/prefix behavior
109
+ /cc-message spacing comfortable # keep one blank line between paragraphs
110
+ /cc-message spacing compact # remove blank lines inside assistant/thinking blocks
111
+ /cc-message assistant-prefix ● # set assistant paragraph prefix
112
+ /cc-message thinking-prefix ✻ # set visible thinking prefix
113
+ /cc-message hidden-thinking-label Pondering...
114
+ /cc-message tip on # show active working tip line
115
+ /cc-message tip off # hide active working tip line
116
+ /cc-message tip text Run /help for tips # set active working tip text
117
+ /cc-message reset # restore message chrome defaults
118
+ ```
119
+
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. The active working tip is rendered as a subordinate `└─ Tip:` line under the spinner when supported by the Pi loader.
121
+
103
122
  ### Tool background modes
104
123
 
105
124
  | Value | Behavior |
@@ -117,6 +136,20 @@ Color selections are persisted as `spinnerColor` / `spinnerStatusColor` in `~/.p
117
136
  | `mcpOutputMode` | `hidden`, `summary`, `preview` | `preview` |
118
137
  | `bashOutputMode` | `opencode`, `summary`, `preview` | `opencode` |
119
138
 
139
+ `bashOutputMode` behavior:
140
+
141
+ | Value | Behavior |
142
+ |-------|----------|
143
+ | `opencode` | Compact status while collapsed, with `Ctrl+O` hint for output preview |
144
+ | `summary` | Status only; no output preview or expansion hint |
145
+ | `preview` | Show a small output preview even while collapsed |
146
+
147
+ ### Boolean settings
148
+
149
+ | Setting | Default | Description |
150
+ |---------|---------|-------------|
151
+ | `bashStackConsecutive` | `true` | Remove the extra synthetic spacer between adjacent bash tool rows so command bursts render as a tight stack |
152
+
120
153
  ### Numeric settings
121
154
 
122
155
  | Setting | Default | Description |
@@ -7,6 +7,7 @@
7
7
  "expandedPreviewMaxLines": 4000,
8
8
  "bashOutputMode": "opencode",
9
9
  "bashCollapsedLines": 10,
10
+ "bashStackConsecutive": true,
10
11
  "diffCollapsedLines": 24,
11
12
  "showTruncationHints": false,
12
13
  "themeAdaptive": true,
@@ -14,5 +15,12 @@
14
15
  "diffColors": {},
15
16
  "spinnerColor": "borderAccent",
16
17
  "spinnerVerbs": ["Reviewing", "Polishing"],
17
- "spinnerVerbMode": "append"
18
+ "spinnerVerbMode": "append",
19
+ "messageStyle": "claude",
20
+ "assistantPrefix": "●",
21
+ "thinkingPrefix": "✻",
22
+ "messageSpacing": "comfortable",
23
+ "workingTipEnabled": true,
24
+ "workingTipText": "Run /install-github-app to tag @claude right from your GitHub issues and PRs",
25
+ "hiddenThinkingLabel": "Pondering..."
18
26
  }
@@ -9,7 +9,7 @@ import type {
9
9
  GrepToolDetails,
10
10
  ReadToolDetails,
11
11
  Theme,
12
- } from "@earendil-works/pi-coding-agent";
12
+ } from "@mariozechner/pi-coding-agent";
13
13
  import {
14
14
  AssistantMessageComponent,
15
15
  CustomMessageComponent,
@@ -22,7 +22,7 @@ import {
22
22
  createLsTool,
23
23
  createReadTool,
24
24
  createWriteTool,
25
- } from "@earendil-works/pi-coding-agent";
25
+ } from "@mariozechner/pi-coding-agent";
26
26
  import {
27
27
  Box,
28
28
  Container,
@@ -36,11 +36,21 @@ import {
36
36
  truncateToWidth,
37
37
  visibleWidth,
38
38
  wrapTextWithAnsi,
39
- } from "@earendil-works/pi-tui";
39
+ } from "@mariozechner/pi-tui";
40
40
 
41
41
  import * as Diff from "diff";
42
42
  import type { BundledLanguage, BundledTheme } from "shiki";
43
43
 
44
+ import {
45
+ DEFAULT_HIDDEN_THINKING_LABEL,
46
+ formatTranscriptLines,
47
+ resolveMessageChromeSettings,
48
+ sanitizeMessagePrefix,
49
+ type MessageChromeSettings,
50
+ type MessageSpacing,
51
+ type MessageStyle,
52
+ } from "./message-chrome.ts";
53
+
44
54
  const RESET = "\x1b[0m";
45
55
  const TRANSPARENT_BG = "\x1b[49m";
46
56
  const TRANSPARENT_RESET = `${RESET}${TRANSPARENT_BG}`;
@@ -74,6 +84,8 @@ interface SettingsFile {
74
84
  expandedPreviewMaxLines?: number;
75
85
  bashOutputMode?: "opencode" | "summary" | "preview";
76
86
  bashCollapsedLines?: number;
87
+ /** When true (default), consecutive bash tool rows render without the extra inter-tool spacer. */
88
+ bashStackConsecutive?: boolean;
77
89
  showTruncationHints?: boolean;
78
90
  diffCollapsedLines?: number;
79
91
  diffTheme?: string;
@@ -105,6 +117,20 @@ interface SettingsFile {
105
117
  spinnerVerbs?: string[];
106
118
  /** Whether custom spinnerVerbs append to or replace the default verb list. */
107
119
  spinnerVerbMode?: SpinnerVerbMode;
120
+ /** Assistant/thinking transcript chrome style. "claude" trims spacing like Claude Code; "classic" keeps the older package rhythm. */
121
+ messageStyle?: MessageStyle;
122
+ /** Prefix glyph/text for assistant paragraphs. Defaults to Claude Code-style "●". */
123
+ assistantPrefix?: string;
124
+ /** Prefix glyph/text for visible thinking paragraphs. Defaults to "✻". */
125
+ thinkingPrefix?: string;
126
+ /** Blank-line normalization in assistant/thinking transcript blocks. */
127
+ messageSpacing?: MessageSpacing;
128
+ /** Whether the active working spinner may show a subordinate Claude-style tip line. */
129
+ workingTipEnabled?: boolean;
130
+ /** Custom subordinate tip line text for the active working spinner. */
131
+ workingTipText?: string;
132
+ /** Label shown when thinking blocks are hidden by the Pi UI. */
133
+ hiddenThinkingLabel?: string;
108
134
  }
109
135
 
110
136
  let _settingsCache: { value: SettingsFile; timestamp: number } | null = null;
@@ -144,7 +170,13 @@ function bustSpinnerSettingsCache(): void {
144
170
  (globalThis as any)[SPINNER_BUST_KEY] = current + 1;
145
171
  }
146
172
 
173
+ let _mergedSettingsCache: { value: SettingsFile; timestamp: number } | null = null;
174
+
147
175
  function readMergedSettings(): SettingsFile {
176
+ const now = Date.now();
177
+ if (_mergedSettingsCache && now - _mergedSettingsCache.timestamp < SETTINGS_CACHE_TTL_MS) {
178
+ return _mergedSettingsCache.value;
179
+ }
148
180
  const paths = [`${process.cwd()}/.pi/settings.json`, `${process.env.HOME ?? ""}/.pi/settings.json`];
149
181
  const merged: SettingsFile = {};
150
182
  for (const path of paths) {
@@ -156,9 +188,20 @@ function readMergedSettings(): SettingsFile {
156
188
  // ignore invalid settings files
157
189
  }
158
190
  }
191
+ _mergedSettingsCache = { value: merged, timestamp: now };
159
192
  return merged;
160
193
  }
161
194
 
195
+ function getMessageChromeSettings(): MessageChromeSettings {
196
+ return resolveMessageChromeSettings(readMergedSettings());
197
+ }
198
+
199
+ function applyHiddenThinkingLabel(ctx: any): void {
200
+ if (!ctx?.hasUI || typeof ctx.ui?.setHiddenThinkingLabel !== "function") return;
201
+ const label = getMessageChromeSettings().hiddenThinkingLabel;
202
+ ctx.ui.setHiddenThinkingLabel(label || DEFAULT_HIDDEN_THINKING_LABEL);
203
+ }
204
+
162
205
  const MAX_CUSTOM_SPINNER_VERBS = 200;
163
206
  const MAX_SPINNER_VERB_LENGTH = 48;
164
207
  const ANSI_ESCAPE_SEQUENCE_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
@@ -196,6 +239,7 @@ function getSpinnerVerbMode(settings: SettingsFile): SpinnerVerbMode {
196
239
 
197
240
  function writeSettingsKey(key: string, value: unknown): void {
198
241
  _settingsCache = null; // invalidate cache on write
242
+ _mergedSettingsCache = null;
199
243
  const home = process.env.HOME ?? "";
200
244
  if (!home) return;
201
245
  const dir = `${home}/.pi`;
@@ -287,6 +331,44 @@ function isToolExecutionLike(value: unknown): value is { toolName: string; toolC
287
331
  return typeof candidate.toolName === "string" && typeof candidate.toolCallId === "string";
288
332
  }
289
333
 
334
+ function isBashToolExecution(value: unknown): boolean {
335
+ return isToolExecutionLike(value) && (value as any).toolName === "bash";
336
+ }
337
+
338
+ function shouldStackConsecutiveBash(): boolean {
339
+ return readSettings().bashStackConsecutive !== false;
340
+ }
341
+
342
+ function hasConsecutiveBashToolChildren(children: unknown[]): boolean {
343
+ let previousWasBash = false;
344
+ for (const child of children) {
345
+ const currentIsBash = isBashToolExecution(child);
346
+ if (currentIsBash && previousWasBash) return true;
347
+ previousWasBash = currentIsBash;
348
+ }
349
+ return false;
350
+ }
351
+
352
+ function dropLeadingSpacerLine(lines: string[]): string[] {
353
+ return lines.length > 0 && isBlankLine(lines[0]) ? lines.slice(1) : lines;
354
+ }
355
+
356
+ function renderWithStackedConsecutiveBash(container: any, width: number): string[] | null {
357
+ if (!shouldStackConsecutiveBash()) return null;
358
+ const children = Array.isArray(container?.children) ? container.children : null;
359
+ if (!children || !hasConsecutiveBashToolChildren(children)) return null;
360
+
361
+ const rendered: string[] = [];
362
+ let previousWasBash = false;
363
+ for (const child of children) {
364
+ const currentIsBash = isBashToolExecution(child);
365
+ const childLines = typeof child?.render === "function" ? child.render(width) : [];
366
+ rendered.push(...(currentIsBash && previousWasBash ? dropLeadingSpacerLine(childLines) : childLines));
367
+ previousWasBash = currentIsBash;
368
+ }
369
+ return rendered;
370
+ }
371
+
290
372
  function isTerminalImageLine(line: string): boolean {
291
373
  return line.includes(KITTY_IMAGE_PREFIX) || line.includes(ITERM2_IMAGE_PREFIX);
292
374
  }
@@ -317,6 +399,11 @@ function patchGlobalToolBorders(): void {
317
399
 
318
400
  const originalRender = proto.render;
319
401
  proto.render = function patchedContainerRender(width: number): string[] {
402
+ if (!isToolExecutionLike(this)) {
403
+ const stacked = renderWithStackedConsecutiveBash(this, width);
404
+ if (stacked) return stacked;
405
+ }
406
+
320
407
  if (isToolExecutionLike(this)) {
321
408
  const cached = (this as any)[TOOL_RENDER_CACHE];
322
409
  if (cached?.width === width && cached?.mode === toolBackgroundMode) {
@@ -474,9 +561,43 @@ function appendWorkedDurationLine(message: any, durationMs: number): void {
474
561
  lastText.text = `${text.trimEnd()}\n\n${inlineWorkedDurationText(durationMs)}`;
475
562
  }
476
563
 
564
+ function messageChromeCacheKey(settings: MessageChromeSettings, kind: "assistant" | "thinking"): string {
565
+ return [
566
+ settings.messageStyle,
567
+ settings.messageSpacing,
568
+ kind === "assistant" ? settings.assistantPrefix : settings.thinkingPrefix,
569
+ ].join(":");
570
+ }
571
+
572
+ function renderClassicPrefixedLines(lines: string[], prefix: string, normalizeChecks = true): string[] {
573
+ let prefixPlaced = false;
574
+ return lines.map((line: string) => {
575
+ const displayLine = normalizeChecks ? normalizeLeadingCheckGlyph(line) : line;
576
+ if (!prefixPlaced && stripAnsi(displayLine).trim()) {
577
+ prefixPlaced = true;
578
+ return ` ${prefix} ${displayLine}`;
579
+ }
580
+ return ` ${displayLine}`;
581
+ });
582
+ }
583
+
584
+ function colorFirstTranscriptPrefix(lines: string[], prefixGlyph: string, coloredPrefixGlyph: string): string[] {
585
+ const plainPrefix = ` ${prefixGlyph} `;
586
+ const coloredPrefix = ` ${coloredPrefixGlyph} `;
587
+ let replaced = false;
588
+ return lines.map((line) => {
589
+ if (!replaced && line.startsWith(plainPrefix)) {
590
+ replaced = true;
591
+ return `${coloredPrefix}${line.slice(plainPrefix.length)}`;
592
+ }
593
+ return line;
594
+ });
595
+ }
596
+
477
597
  class DottedParagraph {
478
598
  private md: InstanceType<typeof Markdown>;
479
599
  private cachedWidth?: number;
600
+ private cachedChromeKey?: string;
480
601
  private cachedLines?: string[];
481
602
 
482
603
  constructor(text: string, markdownTheme: ConstructorParameters<typeof Markdown>[3]) {
@@ -485,31 +606,35 @@ class DottedParagraph {
485
606
 
486
607
  invalidate(): void {
487
608
  this.cachedWidth = undefined;
609
+ this.cachedChromeKey = undefined;
488
610
  this.cachedLines = undefined;
489
611
  this.md.invalidate();
490
612
  }
491
613
 
492
614
  render(width: number): string[] {
493
- if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
494
- // " ● " = 1 margin + dot + space = 3 visible chars
495
- const PREFIX_W = 3;
496
- if (width <= PREFIX_W) {
615
+ const settings = getMessageChromeSettings();
616
+ const chromeKey = messageChromeCacheKey(settings, "assistant");
617
+ if (this.cachedLines && this.cachedWidth === width && this.cachedChromeKey === chromeKey) return this.cachedLines;
618
+ const prefixGlyph = settings.messageStyle === "classic" ? "●" : settings.assistantPrefix;
619
+ const prefixWidth = Math.max(1, visibleWidth(` ${prefixGlyph} `));
620
+ if (width <= prefixWidth) {
497
621
  this.cachedWidth = width;
498
- this.cachedLines = [" ● "];
622
+ this.cachedChromeKey = chromeKey;
623
+ this.cachedLines = [` ${prefixGlyph} `];
499
624
  return this.cachedLines;
500
625
  }
501
- const lines = sanitizeRenderedTextBlockLines(this.md.render(width - PREFIX_W));
626
+ const lines = sanitizeRenderedTextBlockLines(this.md.render(width - prefixWidth));
502
627
  const looksLikeTaskStatus = lines.some((line) => /\b(?:transcript:|No output\.|Wrapped up)/.test(stripAnsi(line)));
503
- const displayLines = looksLikeTaskStatus ? lines.map(normalizeLeadingCheckGlyph) : lines;
504
- let dotPlaced = false;
505
- const rendered = displayLines.map((line: string) => {
506
- if (!dotPlaced && stripAnsi(line).trim()) {
507
- dotPlaced = true;
508
- return ` ● ${line}`;
509
- }
510
- return ` ${line}`;
511
- });
628
+ const rendered = settings.messageStyle === "classic"
629
+ ? renderClassicPrefixedLines(lines, "●", looksLikeTaskStatus)
630
+ : formatTranscriptLines(lines, {
631
+ prefix: settings.assistantPrefix,
632
+ spacing: settings.messageSpacing,
633
+ normalizeChecks: looksLikeTaskStatus,
634
+ visibleWidth,
635
+ });
512
636
  this.cachedWidth = width;
637
+ this.cachedChromeKey = chromeKey;
513
638
  this.cachedLines = rendered;
514
639
  return rendered;
515
640
  }
@@ -518,6 +643,7 @@ class DottedParagraph {
518
643
  class ThinkingParagraph {
519
644
  private md: InstanceType<typeof Markdown>;
520
645
  private cachedWidth?: number;
646
+ private cachedChromeKey?: string;
521
647
  private cachedLines?: string[];
522
648
 
523
649
  constructor(
@@ -559,30 +685,39 @@ class ThinkingParagraph {
559
685
 
560
686
  invalidate(): void {
561
687
  this.cachedWidth = undefined;
688
+ this.cachedChromeKey = undefined;
562
689
  this.cachedLines = undefined;
563
690
  this.md.invalidate();
564
691
  }
565
692
 
566
693
  render(width: number): string[] {
567
- if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
568
- // " ✻ " = 1 margin + symbol + space = 3 visible chars
569
- const PREFIX_W = 3;
570
- const prefix = `${WORKED_LINE_FG}✻${RESET}`;
571
- if (width <= PREFIX_W) {
694
+ const settings = getMessageChromeSettings();
695
+ const chromeKey = messageChromeCacheKey(settings, "thinking");
696
+ if (this.cachedLines && this.cachedWidth === width && this.cachedChromeKey === chromeKey) return this.cachedLines;
697
+ const prefixGlyph = settings.messageStyle === "classic" ? "✻" : settings.thinkingPrefix;
698
+ const coloredPrefixGlyph = `${WORKED_LINE_FG}${prefixGlyph}${RESET}`;
699
+ const prefixWidth = Math.max(1, visibleWidth(` ${prefixGlyph} `));
700
+ if (width <= prefixWidth) {
572
701
  this.cachedWidth = width;
573
- this.cachedLines = [` ${prefix} `];
702
+ this.cachedChromeKey = chromeKey;
703
+ this.cachedLines = [` ${coloredPrefixGlyph} `];
574
704
  return this.cachedLines;
575
705
  }
576
- const lines = sanitizeRenderedTextBlockLines(this.md.render(width - PREFIX_W));
577
- let symbolPlaced = false;
578
- const rendered = lines.map((line: string) => {
579
- if (!symbolPlaced && stripAnsi(line).trim()) {
580
- symbolPlaced = true;
581
- return ` ${prefix} ${line}`;
582
- }
583
- return ` ${line}`;
584
- });
706
+ const lines = sanitizeRenderedTextBlockLines(this.md.render(width - prefixWidth));
707
+ const rendered = settings.messageStyle === "classic"
708
+ ? renderClassicPrefixedLines(lines, coloredPrefixGlyph, false)
709
+ : colorFirstTranscriptPrefix(
710
+ formatTranscriptLines(lines, {
711
+ prefix: prefixGlyph,
712
+ spacing: settings.messageSpacing,
713
+ normalizeChecks: false,
714
+ visibleWidth,
715
+ }),
716
+ prefixGlyph,
717
+ coloredPrefixGlyph,
718
+ );
585
719
  this.cachedWidth = width;
720
+ this.cachedChromeKey = chromeKey;
586
721
  this.cachedLines = rendered;
587
722
  return rendered;
588
723
  }
@@ -1234,6 +1369,10 @@ function bashCollapsedLimit(): number {
1234
1369
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 10;
1235
1370
  }
1236
1371
 
1372
+ function bashOutputMode(): "opencode" | "summary" | "preview" {
1373
+ return getMode(readSettings().bashOutputMode, ["opencode", "summary", "preview"] as const, "opencode");
1374
+ }
1375
+
1237
1376
  function diffCollapsedLimit(): number {
1238
1377
  const value = readSettings().diffCollapsedLines;
1239
1378
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 24;
@@ -2641,13 +2780,14 @@ function stripThinkingPresentationArtifacts(text: string): string {
2641
2780
  }
2642
2781
 
2643
2782
  function prefixThinkingLine(text: string, _theme: Theme | undefined): string {
2644
- if (!ANSI_PRESENT_RE.test(text) && text.startsWith("Thinking: ") && !/^Thinking:\s*thinking:\s*/i.test(text)) {
2783
+ const settings = getMessageChromeSettings();
2784
+ if (settings.messageStyle === "classic" && !ANSI_PRESENT_RE.test(text) && text.startsWith("Thinking: ") && !/^Thinking:\s*thinking:\s*/i.test(text)) {
2645
2785
  return text;
2646
2786
  }
2647
2787
  const normalized = stripThinkingPresentationArtifacts(text).trim();
2648
2788
  if (!normalized) return text;
2649
2789
  // Plain text — no ANSI colors, no theme. The ThinkingParagraph handles styling.
2650
- return `Thinking: ${normalized}`;
2790
+ return settings.messageStyle === "classic" ? `Thinking: ${normalized}` : normalized;
2651
2791
  }
2652
2792
 
2653
2793
  function registerThinkingLabels(pi: ExtensionAPI): void {
@@ -3920,16 +4060,167 @@ export default function (pi: ExtensionAPI) {
3920
4060
  },
3921
4061
  });
3922
4062
 
4063
+ const MESSAGE_COMMANDS = ["style", "spacing", "assistant-prefix", "thinking-prefix", "tip", "hidden-thinking-label", "reset", "status"] as const;
4064
+ pi.registerCommand("cc-message", {
4065
+ description: "Configure Claude-style assistant/thinking transcript chrome",
4066
+ getArgumentCompletions(prefix: string) {
4067
+ const parts = prefix.split(/\s+/);
4068
+ if (parts.length <= 1) {
4069
+ const first = (parts[0] ?? "").toLowerCase();
4070
+ return MESSAGE_COMMANDS
4071
+ .filter((c) => c.startsWith(first))
4072
+ .map((c) => ({
4073
+ value: c,
4074
+ label: c,
4075
+ description:
4076
+ c === "style" ? "Set transcript style: claude or classic"
4077
+ : c === "spacing" ? "Set blank-line rhythm: compact or comfortable"
4078
+ : c === "assistant-prefix" ? "Set assistant paragraph prefix glyph/text"
4079
+ : c === "thinking-prefix" ? "Set visible thinking prefix glyph/text"
4080
+ : c === "tip" ? "Enable, disable, or customize active working tip line"
4081
+ : c === "hidden-thinking-label" ? "Set label shown when thinking is hidden"
4082
+ : c === "reset" ? "Reset message chrome settings"
4083
+ : "Show current message chrome settings",
4084
+ }));
4085
+ }
4086
+ if (parts[0] === "style") {
4087
+ const valuePrefix = (parts[1] ?? "").toLowerCase();
4088
+ return ["claude", "classic"]
4089
+ .filter((v) => v.startsWith(valuePrefix))
4090
+ .map((v) => ({ value: `style ${v}`, label: v, description: v === "claude" ? "Screenshot-style transcript rhythm" : "Older package spacing/prefix behavior" }));
4091
+ }
4092
+ if (parts[0] === "spacing") {
4093
+ const valuePrefix = (parts[1] ?? "").toLowerCase();
4094
+ return ["comfortable", "compact"]
4095
+ .filter((v) => v.startsWith(valuePrefix))
4096
+ .map((v) => ({ value: `spacing ${v}`, label: v, description: v === "comfortable" ? "Preserve one blank line between paragraphs" : "Remove blank lines inside assistant/thinking blocks" }));
4097
+ }
4098
+ if (parts[0] === "tip") {
4099
+ const valuePrefix = (parts[1] ?? "").toLowerCase();
4100
+ return ["on", "off", "text", "reset"]
4101
+ .filter((v) => v.startsWith(valuePrefix))
4102
+ .map((v) => ({ value: `tip ${v}`, label: v, description: v === "text" ? "Set custom active working tip text" : `${v} working tip line` }));
4103
+ }
4104
+ return [];
4105
+ },
4106
+ async handler(args: string, ctx: any) {
4107
+ const rawArgs = args.trim();
4108
+ const parts = rawArgs.split(/\s+/).filter((p: string) => p.length > 0);
4109
+ const sub = (parts[0] ?? "status").toLowerCase();
4110
+ const current = getMessageChromeSettings();
4111
+ const notifyStatus = () => {
4112
+ if (!ctx.hasUI) return;
4113
+ ctx.ui.notify([
4114
+ `Message style: ${current.messageStyle}`,
4115
+ `Assistant prefix: ${current.assistantPrefix}`,
4116
+ `Thinking prefix: ${current.thinkingPrefix}`,
4117
+ `Spacing: ${current.messageSpacing}`,
4118
+ `Working tip: ${current.workingTipEnabled ? "on" : "off"}`,
4119
+ `Tip text: ${current.workingTipText}`,
4120
+ `Hidden thinking label: ${current.hiddenThinkingLabel}`,
4121
+ ].join("\n"), "info");
4122
+ };
4123
+
4124
+ if (!rawArgs || sub === "status") {
4125
+ notifyStatus();
4126
+ return;
4127
+ }
4128
+
4129
+ if (sub === "reset") {
4130
+ for (const key of ["messageStyle", "assistantPrefix", "thinkingPrefix", "messageSpacing", "workingTipEnabled", "workingTipText", "hiddenThinkingLabel"]) {
4131
+ writeSettingsKey(key, undefined);
4132
+ }
4133
+ bustSpinnerSettingsCache();
4134
+ applyHiddenThinkingLabel(ctx);
4135
+ if (ctx.hasUI) ctx.ui.notify("Message chrome reset to Claude-style defaults", "info");
4136
+ return;
4137
+ }
4138
+
4139
+ if (sub === "style") {
4140
+ const value = (parts[1] ?? "").toLowerCase();
4141
+ if (value !== "claude" && value !== "classic") {
4142
+ if (ctx.hasUI) ctx.ui.notify("Usage: /cc-message style claude|classic", "error");
4143
+ return;
4144
+ }
4145
+ writeSettingsKey("messageStyle", value);
4146
+ writeSettingsKey("workingTipEnabled", value === "classic" ? false : undefined);
4147
+ bustSpinnerSettingsCache();
4148
+ if (ctx.hasUI) ctx.ui.notify(`Message style → ${value}`, "info");
4149
+ return;
4150
+ }
4151
+
4152
+ if (sub === "spacing") {
4153
+ const value = (parts[1] ?? "").toLowerCase();
4154
+ if (value !== "compact" && value !== "comfortable") {
4155
+ if (ctx.hasUI) ctx.ui.notify("Usage: /cc-message spacing compact|comfortable", "error");
4156
+ return;
4157
+ }
4158
+ writeSettingsKey("messageSpacing", value);
4159
+ if (ctx.hasUI) ctx.ui.notify(`Message spacing → ${value}`, "info");
4160
+ return;
4161
+ }
4162
+
4163
+ if (sub === "assistant-prefix" || sub === "thinking-prefix") {
4164
+ const rawValue = rawArgs.replace(new RegExp(`^${sub}\\s*`, "i"), "");
4165
+ const fallback = sub === "assistant-prefix" ? "●" : "✻";
4166
+ const value = sanitizeMessagePrefix(rawValue, fallback);
4167
+ writeSettingsKey(sub === "assistant-prefix" ? "assistantPrefix" : "thinkingPrefix", value);
4168
+ if (ctx.hasUI) ctx.ui.notify(`${sub === "assistant-prefix" ? "Assistant" : "Thinking"} prefix → ${value}`, "info");
4169
+ return;
4170
+ }
4171
+
4172
+ if (sub === "tip") {
4173
+ const action = (parts[1] ?? "").toLowerCase();
4174
+ if (action === "on" || action === "off") {
4175
+ writeSettingsKey("workingTipEnabled", action === "on");
4176
+ bustSpinnerSettingsCache();
4177
+ if (ctx.hasUI) ctx.ui.notify(`Working tip → ${action}`, "info");
4178
+ return;
4179
+ }
4180
+ if (action === "reset") {
4181
+ writeSettingsKey("workingTipEnabled", undefined);
4182
+ writeSettingsKey("workingTipText", undefined);
4183
+ bustSpinnerSettingsCache();
4184
+ if (ctx.hasUI) ctx.ui.notify("Working tip reset", "info");
4185
+ return;
4186
+ }
4187
+ if (action === "text") {
4188
+ const textArg = rawArgs.match(/^tip\s+text(?:\s+(.+))?$/i)?.[1] ?? "";
4189
+ const value = resolveMessageChromeSettings({ workingTipText: textArg }).workingTipText;
4190
+ writeSettingsKey("workingTipText", value);
4191
+ bustSpinnerSettingsCache();
4192
+ if (ctx.hasUI) ctx.ui.notify(`Working tip text → ${value}`, "info");
4193
+ return;
4194
+ }
4195
+ if (ctx.hasUI) ctx.ui.notify("Usage: /cc-message tip on|off|reset|text <text>", "error");
4196
+ return;
4197
+ }
4198
+
4199
+ if (sub === "hidden-thinking-label") {
4200
+ const textArg = rawArgs.match(/^hidden-thinking-label(?:\s+(.+))?$/i)?.[1] ?? "";
4201
+ const value = resolveMessageChromeSettings({ hiddenThinkingLabel: textArg }).hiddenThinkingLabel;
4202
+ writeSettingsKey("hiddenThinkingLabel", value);
4203
+ applyHiddenThinkingLabel(ctx);
4204
+ if (ctx.hasUI) ctx.ui.notify(`Hidden thinking label → ${value}`, "info");
4205
+ return;
4206
+ }
4207
+
4208
+ if (ctx.hasUI) ctx.ui.notify("Usage: /cc-message style|spacing|assistant-prefix|thinking-prefix|tip|hidden-thinking-label|reset|status", "error");
4209
+ },
4210
+ });
4211
+
3923
4212
  pi.on("session_start", async (_event, ctx) => {
3924
4213
  if (!ctx.hasUI) return;
3925
4214
  applyToolBackgroundMode(ctx.ui.theme);
3926
4215
  applyThemePaletteIfNeeded(ctx.ui.theme);
4216
+ applyHiddenThinkingLabel(ctx);
3927
4217
  });
3928
4218
 
3929
4219
  pi.on("turn_start", async (_event, ctx) => {
3930
4220
  if (!ctx.hasUI) return;
3931
4221
  applyToolBackgroundMode(ctx.ui.theme);
3932
4222
  applyThemePaletteIfNeeded(ctx.ui.theme);
4223
+ applyHiddenThinkingLabel(ctx);
3933
4224
  });
3934
4225
 
3935
4226
  const cwd = process.cwd();
@@ -4003,15 +4294,26 @@ export default function (pi: ExtensionAPI) {
4003
4294
  }
4004
4295
  clearBlinkTimer(ctx);
4005
4296
  setToolStatus(ctx, ctx.isError ? "error" : "success");
4006
- const exitMatch = output.match(/exit code: (\d+)/);
4297
+ const exitMatch = output.match(/(?:exit code:|exited with code)\s+(\d+)/i);
4007
4298
  const exitCode = exitMatch ? Number.parseInt(exitMatch[1], 10) : null;
4008
- let text = exitCode === null || exitCode === 0 ? theme.fg("success", "Done") : theme.fg("error", `Exit ${exitCode}`);
4299
+ const isError = ctx.isError || (exitCode !== null && exitCode !== 0);
4300
+ let text = isError
4301
+ ? theme.fg("error", exitCode !== null ? `Exit ${exitCode}` : "Failed")
4302
+ : theme.fg("success", "Done");
4009
4303
  text += theme.fg("muted", ` (${nonEmpty.length} lines)`);
4010
4304
  if (details?.truncation?.truncated) text += theme.fg("warning", " [truncated]");
4305
+
4306
+ const mode = bashOutputMode();
4307
+ if (mode === "summary") return makeText(ctx.lastComponent, withBranch(text, theme));
4308
+ if (mode === "preview") {
4309
+ if (nonEmpty.length === 0) return makeText(ctx.lastComponent, withBranch(text, theme));
4310
+ const preview = buildPreviewText(nonEmpty.map((line) => theme.fg(isError ? "error" : "dim", line)), expanded, theme, bashCollapsedLimit());
4311
+ return makeText(ctx.lastComponent, withBranch(`${text}\n${preview}`, theme));
4312
+ }
4313
+
4011
4314
  if (!expanded && nonEmpty.length > 0) return makeText(ctx.lastComponent, withBranch(`${text}${theme.fg("muted", " • Ctrl+O to expand")}`, theme));
4012
4315
  if (!expanded) return makeText(ctx.lastComponent, withBranch(text, theme));
4013
- const collapsed = bashCollapsedLimit();
4014
- text += `\n${buildPreviewText(nonEmpty.map((line) => theme.fg("dim", line)), false, theme, collapsed)}`;
4316
+ text += `\n${buildPreviewText(nonEmpty.map((line) => theme.fg(isError ? "error" : "dim", line)), true, theme, bashCollapsedLimit())}`;
4015
4317
  return makeText(ctx.lastComponent, withBranch(text, theme));
4016
4318
  },
4017
4319
  });
@@ -0,0 +1,142 @@
1
+ export type MessageStyle = "classic" | "claude";
2
+ export type MessageSpacing = "compact" | "comfortable";
3
+
4
+ export interface MessageChromeInput {
5
+ messageStyle?: unknown;
6
+ assistantPrefix?: unknown;
7
+ thinkingPrefix?: unknown;
8
+ messageSpacing?: unknown;
9
+ workingTipEnabled?: unknown;
10
+ workingTipText?: unknown;
11
+ hiddenThinkingLabel?: unknown;
12
+ }
13
+
14
+ export interface MessageChromeSettings {
15
+ messageStyle: MessageStyle;
16
+ assistantPrefix: string;
17
+ thinkingPrefix: string;
18
+ messageSpacing: MessageSpacing;
19
+ workingTipEnabled: boolean;
20
+ workingTipText: string;
21
+ hiddenThinkingLabel: string;
22
+ }
23
+
24
+ export interface TranscriptLineOptions {
25
+ prefix: string;
26
+ spacing?: MessageSpacing;
27
+ normalizeChecks?: boolean;
28
+ visibleWidth?: (text: string) => number;
29
+ }
30
+
31
+ export const DEFAULT_WORKING_TIP_TEXT = "Run /install-github-app to tag @claude right from your GitHub issues and PRs";
32
+ export const DEFAULT_HIDDEN_THINKING_LABEL = "Pondering...";
33
+
34
+ const DEFAULT_MESSAGE_CHROME: MessageChromeSettings = {
35
+ messageStyle: "claude",
36
+ assistantPrefix: "●",
37
+ thinkingPrefix: "✻",
38
+ messageSpacing: "comfortable",
39
+ workingTipEnabled: true,
40
+ workingTipText: DEFAULT_WORKING_TIP_TEXT,
41
+ hiddenThinkingLabel: DEFAULT_HIDDEN_THINKING_LABEL,
42
+ };
43
+
44
+ const ANSI_ESCAPE_SEQUENCE_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
45
+ const ANSI_SGR_RE = /\x1b\[[0-9;]*m/g;
46
+ const CONTROL_CHARS_RE = /[\u0000-\u001F\u007F-\u009F]/g;
47
+ const MAX_PREFIX_GRAPHEMES = 8;
48
+ const MAX_TIP_GRAPHEMES = 160;
49
+ const MAX_LABEL_GRAPHEMES = 80;
50
+
51
+ function stripAnsi(text: string): string {
52
+ return text.replace(ANSI_ESCAPE_SEQUENCE_RE, "").replace(ANSI_SGR_RE, "");
53
+ }
54
+
55
+ function stripControlChars(text: string): string {
56
+ return text.replace(CONTROL_CHARS_RE, "");
57
+ }
58
+
59
+ function takeGraphemes(text: string, max: number): string {
60
+ return Array.from(text).slice(0, max).join("");
61
+ }
62
+
63
+ function fallbackVisibleWidth(text: string): number {
64
+ return Array.from(stripAnsi(text)).length;
65
+ }
66
+
67
+ function isBlankLine(text: string): boolean {
68
+ return stripAnsi(text).trim().length === 0;
69
+ }
70
+
71
+ function sanitizeTextSetting(value: unknown, fallback: string, maxGraphemes: number): string {
72
+ if (typeof value !== "string") return fallback;
73
+ const cleaned = takeGraphemes(stripControlChars(stripAnsi(value)).trim(), maxGraphemes);
74
+ return cleaned || fallback;
75
+ }
76
+
77
+ export function sanitizeMessagePrefix(value: unknown, fallback: string): string {
78
+ return sanitizeTextSetting(value, fallback, MAX_PREFIX_GRAPHEMES);
79
+ }
80
+
81
+ export function resolveMessageChromeSettings(input: MessageChromeInput = {}): MessageChromeSettings {
82
+ const messageStyle: MessageStyle = input.messageStyle === "classic" ? "classic" : "claude";
83
+ const messageSpacing: MessageSpacing = input.messageSpacing === "compact" ? "compact" : "comfortable";
84
+ const defaultWorkingTipEnabled = messageStyle === "classic" ? false : DEFAULT_MESSAGE_CHROME.workingTipEnabled;
85
+ return {
86
+ messageStyle,
87
+ assistantPrefix: sanitizeMessagePrefix(input.assistantPrefix, DEFAULT_MESSAGE_CHROME.assistantPrefix),
88
+ thinkingPrefix: sanitizeMessagePrefix(input.thinkingPrefix, DEFAULT_MESSAGE_CHROME.thinkingPrefix),
89
+ messageSpacing,
90
+ workingTipEnabled: typeof input.workingTipEnabled === "boolean" ? input.workingTipEnabled : defaultWorkingTipEnabled,
91
+ workingTipText: sanitizeTextSetting(input.workingTipText, DEFAULT_MESSAGE_CHROME.workingTipText, MAX_TIP_GRAPHEMES),
92
+ hiddenThinkingLabel: sanitizeTextSetting(input.hiddenThinkingLabel, DEFAULT_MESSAGE_CHROME.hiddenThinkingLabel, MAX_LABEL_GRAPHEMES),
93
+ };
94
+ }
95
+
96
+ function normalizeTranscriptRenderedLines(lines: string[], spacing: MessageSpacing): string[] {
97
+ let start = 0;
98
+ while (start < lines.length && isBlankLine(lines[start])) start++;
99
+ let end = lines.length - 1;
100
+ while (end >= start && isBlankLine(lines[end])) end--;
101
+ if (start > end) return [];
102
+
103
+ const normalized: string[] = [];
104
+ let pendingBlank = false;
105
+ for (const line of lines.slice(start, end + 1)) {
106
+ if (isBlankLine(line)) {
107
+ if (spacing === "comfortable" && normalized.length > 0) pendingBlank = true;
108
+ continue;
109
+ }
110
+ if (pendingBlank) {
111
+ normalized.push("");
112
+ pendingBlank = false;
113
+ }
114
+ normalized.push(line);
115
+ }
116
+ return normalized;
117
+ }
118
+
119
+ function normalizeLeadingCheckGlyph(line: string): string {
120
+ return line.replace(/^((?:\x1b\[[0-9;]*m|[ \t])*)[✓✔](?=\s)/, "$1●");
121
+ }
122
+
123
+ export function formatTranscriptLines(lines: string[], options: TranscriptLineOptions): string[] {
124
+ const prefixGlyph = sanitizeMessagePrefix(options.prefix, DEFAULT_MESSAGE_CHROME.assistantPrefix);
125
+ const spacing = options.spacing ?? DEFAULT_MESSAGE_CHROME.messageSpacing;
126
+ const normalized = normalizeTranscriptRenderedLines(lines, spacing);
127
+ const prefix = ` ${prefixGlyph} `;
128
+ const measureWidth = options.visibleWidth ?? fallbackVisibleWidth;
129
+ const continuation = " ".repeat(Math.max(1, measureWidth(prefix)));
130
+
131
+ if (normalized.length === 0) return [prefix];
132
+
133
+ let prefixPlaced = false;
134
+ return normalized.map((line) => {
135
+ const displayLine = options.normalizeChecks === false ? line : normalizeLeadingCheckGlyph(line);
136
+ if (!prefixPlaced && !isBlankLine(displayLine)) {
137
+ prefixPlaced = true;
138
+ return `${prefix}${displayLine}`;
139
+ }
140
+ return `${continuation}${displayLine}`;
141
+ });
142
+ }
@@ -1,7 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
 
3
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import { Loader } from "@earendil-works/pi-tui";
3
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
+ import { Loader } from "@mariozechner/pi-tui";
5
+
6
+ import { resolveMessageChromeSettings } from "./message-chrome.ts";
5
7
 
6
8
  // ---------------------------------------------------------------------------
7
9
  // Patch built-in Loader with Claude/OpenBrawd-style glyphs.
@@ -26,6 +28,8 @@ interface SpinnerSettings {
26
28
  verbColor: string;
27
29
  statusColor: string;
28
30
  verbs: readonly string[];
31
+ workingTipEnabled: boolean;
32
+ workingTipText: string;
29
33
  }
30
34
 
31
35
  let _spinnerSettingsCache: { value: SpinnerSettings; expires: number } | null = null;
@@ -96,6 +100,9 @@ function readSpinnerSettings(): SpinnerSettings {
96
100
  let statusColor = "muted";
97
101
  let customVerbs: string[] | null = null;
98
102
  let verbMode: SpinnerVerbMode = "append";
103
+ let messageStyle: string | undefined;
104
+ let workingTipEnabled: boolean | undefined;
105
+ let workingTipText: string | undefined;
99
106
  const paths = [`${process.cwd()}/.pi/settings.json`, `${process.env.HOME ?? ""}/.pi/settings.json`];
100
107
  for (const p of paths) {
101
108
  try {
@@ -108,10 +115,21 @@ function readSpinnerSettings(): SpinnerSettings {
108
115
  if (typeof raw.spinnerStatusColor === "string" && raw.spinnerStatusColor.length > 0) statusColor = raw.spinnerStatusColor;
109
116
  if (raw.spinnerVerbMode === "append" || raw.spinnerVerbMode === "replace") verbMode = raw.spinnerVerbMode;
110
117
  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;
111
121
  }
112
122
  } catch { /* ignore */ }
113
123
  }
114
- const value: SpinnerSettings = { adaptive, verbColor, statusColor, verbs: resolveSpinnerVerbs(customVerbs, verbMode) };
124
+ const messageChrome = resolveMessageChromeSettings({ messageStyle, workingTipEnabled, workingTipText });
125
+ const value: SpinnerSettings = {
126
+ adaptive,
127
+ verbColor,
128
+ statusColor,
129
+ verbs: resolveSpinnerVerbs(customVerbs, verbMode),
130
+ workingTipEnabled: messageChrome.workingTipEnabled,
131
+ workingTipText: messageChrome.workingTipText,
132
+ };
115
133
  _spinnerSettingsCache = { value, expires: now + SPINNER_SETTINGS_TTL_MS };
116
134
  return value;
117
135
  }
@@ -549,6 +567,10 @@ export default function (pi: ExtensionAPI) {
549
567
  if (statusParts.length > 0) {
550
568
  message += statusText(` (${statusParts.join(" · ")})`);
551
569
  }
570
+ const { workingTipEnabled, workingTipText } = readSpinnerSettings();
571
+ if (workingTipEnabled && workingTipText) {
572
+ message += `\n${STATUS_DIM} └─ Tip: ${workingTipText}${RESET}`;
573
+ }
552
574
  return message;
553
575
  }
554
576
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlburtoe/pi-cc-tools",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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",
@@ -14,7 +14,7 @@
14
14
  "diff"
15
15
  ],
16
16
  "license": "MIT",
17
- "author": "owlburtoe",
17
+ "author": "FammasMaz",
18
18
  "repository": {
19
19
  "type": "git",
20
20
  "url": "git+https://forgejo.owlburtoe.dev/owlburtoe/pi-claude-style-tools.git"
@@ -26,19 +26,22 @@
26
26
  "files": [
27
27
  "extensions/index.ts",
28
28
  "extensions/spinner.ts",
29
+ "extensions/message-chrome.ts",
29
30
  "config/config.example.json",
30
31
  "README.md"
31
32
  ],
32
33
  "scripts": {
33
- "bench:tools": "bun scripts/benchmark-tools.ts"
34
+ "bench:tools": "bun scripts/benchmark-tools.ts",
35
+ "test:message-chrome": "bun scripts/test-message-chrome.ts"
34
36
  },
35
37
  "dependencies": {
36
38
  "@shikijs/cli": "^4.0.2",
37
39
  "diff": "^7.0.0"
38
40
  },
39
41
  "peerDependencies": {
40
- "@earendil-works/pi-coding-agent": "*",
41
- "@earendil-works/pi-tui": "*"
42
+ "@mariozechner/pi-coding-agent": "*",
43
+ "@mariozechner/pi-tui": "*",
44
+ "@sinclair/typebox": "*"
42
45
  },
43
46
  "pi": {
44
47
  "extensions": [
@@ -47,10 +50,11 @@
47
50
  ]
48
51
  },
49
52
  "devDependencies": {
53
+ "@mariozechner/pi-coding-agent": "latest",
54
+ "@mariozechner/pi-tui": "latest",
55
+ "@sinclair/typebox": "latest",
50
56
  "@types/diff": "^7.0.2",
51
57
  "@types/node": "^25.6.0",
52
- "typescript": "^6.0.2",
53
- "@earendil-works/pi-coding-agent": "latest",
54
- "@earendil-works/pi-tui": "latest"
58
+ "typescript": "^6.0.2"
55
59
  }
56
60
  }