@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 +44 -11
- package/config/config.example.json +9 -1
- package/extensions/index.ts +341 -39
- package/extensions/message-chrome.ts +142 -0
- package/extensions/spinner.ts +25 -3
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
# pi-
|
|
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
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
GrepToolDetails,
|
|
10
10
|
ReadToolDetails,
|
|
11
11
|
Theme,
|
|
12
|
-
} from "@
|
|
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 "@
|
|
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 "@
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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.
|
|
622
|
+
this.cachedChromeKey = chromeKey;
|
|
623
|
+
this.cachedLines = [` ${prefixGlyph} `];
|
|
499
624
|
return this.cachedLines;
|
|
500
625
|
}
|
|
501
|
-
const lines = sanitizeRenderedTextBlockLines(this.md.render(width -
|
|
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
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
|
|
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.
|
|
702
|
+
this.cachedChromeKey = chromeKey;
|
|
703
|
+
this.cachedLines = [` ${coloredPrefixGlyph} `];
|
|
574
704
|
return this.cachedLines;
|
|
575
705
|
}
|
|
576
|
-
const lines = sanitizeRenderedTextBlockLines(this.md.render(width -
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/extensions/spinner.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import type { ExtensionAPI } from "@
|
|
4
|
-
import { Loader } from "@
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
-
"@
|
|
41
|
-
"@
|
|
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
|
}
|