pi-claude-code-ui 1.0.84 → 1.0.86
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/CHANGELOG.md +16 -0
- package/README.md +3 -3
- package/extensions/index.ts +210 -81
- package/extensions/settings-path.ts +4 -0
- package/extensions/spinner.ts +63 -18
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
> [!IMPORTANT]
|
|
4
4
|
> **1.0.69 — package rename (permanent).** Canonical npm name is now [`pi-claude-code-ui`](https://www.npmjs.com/package/pi-claude-code-ui). `pi-claude-style-tools` is legacy and will not receive further releases. Install with `pi install npm:pi-claude-code-ui` or `npm i pi-claude-code-ui`.
|
|
5
5
|
|
|
6
|
+
## 1.0.86 — 2026-10-08
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Display-math scanning no longer splits fenced blocks on `$$` and `\[ … \]`** (Perry Trolard, [#33](https://github.com/FammasMaz/pi-cc-tools/issues/33)) - paragraph rebuilding now respects the same Markdown code regions as inline math formatting, so `$$` (shell PID) and `\[ … \]` (grep/ripgrep char-class regexes) inside ```` ``` ```` fences and inline code stay literal instead of cutting the block into a math segment.
|
|
11
|
+
|
|
6
12
|
## 1.0.83 — 2026-09-07
|
|
7
13
|
|
|
8
14
|
### Fixed
|
|
@@ -39,6 +45,16 @@
|
|
|
39
45
|
|
|
40
46
|
- **Hide transient context tags** — hide transient context tags in streaming prose.
|
|
41
47
|
|
|
48
|
+
## 1.0.85 — 2026-09-07
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Preserve Markdown code literals while formatting inline mathematics.
|
|
53
|
+
- Keep assistant presentation state out of canonical session messages so compacted sessions can replay safely.
|
|
54
|
+
- Respect `bashOutputMode` for completed Bash output.
|
|
55
|
+
- Preserve native prompt metadata in wrapped core tools.
|
|
56
|
+
- Harden spinner cleanup when Pi replaces a stale UI context.
|
|
57
|
+
|
|
42
58
|
## Unreleased
|
|
43
59
|
|
|
44
60
|
### Fixed
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Claude Code inspired tool rendering for Pi — Shiki-powered diffs, status dots,
|
|
|
37
37
|
|
|
38
38
|
## Configuration
|
|
39
39
|
|
|
40
|
-
Set in `.pi/settings.json` or `~/.pi/settings.json
|
|
40
|
+
Set in `.pi/settings.json` or the active Pi agent settings file (`$PI_CODING_AGENT_DIR/settings.json`, defaulting to `~/.pi/settings.json`):
|
|
41
41
|
|
|
42
42
|
```json
|
|
43
43
|
{
|
|
@@ -93,7 +93,7 @@ On `/resume`, `/new`, or `/fork`, tool chrome is rebound from the **current** pi
|
|
|
93
93
|
/cc-theme toggle # flip the current value
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
The selection is persisted to
|
|
96
|
+
The selection is persisted to the active Pi agent settings file and applied to the next rendered tool row. No restart required.
|
|
97
97
|
|
|
98
98
|
#### Repaint the spinner with `/cc-spinner`
|
|
99
99
|
|
|
@@ -106,7 +106,7 @@ The spinner glyph itself is still colored by pi's loader using `accent`, while t
|
|
|
106
106
|
/cc-spinner reset # restore defaults (verb=borderAccent, status=muted)
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
The selection is persisted as `spinnerVerbColor` / `spinnerStatusColor` in
|
|
109
|
+
The selection is persisted as `spinnerVerbColor` / `spinnerStatusColor` in the active Pi agent settings file and applied on the next spinner tick.
|
|
110
110
|
|
|
111
111
|
### Tool background modes
|
|
112
112
|
|
package/extensions/index.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
formatBashDuration,
|
|
71
71
|
getLastBashOutputLine,
|
|
72
72
|
} from "./bash-command";
|
|
73
|
+
import { getUserSettingsPath } from "./settings-path";
|
|
73
74
|
|
|
74
75
|
const RESET = "\x1b[0m";
|
|
75
76
|
const TRANSPARENT_BG = "\x1b[49m";
|
|
@@ -167,7 +168,7 @@ function readSettings(): SettingsFile {
|
|
|
167
168
|
return _settingsCache.value;
|
|
168
169
|
}
|
|
169
170
|
const cwdPath = `${process.cwd()}/.pi/settings.json`;
|
|
170
|
-
const homePath =
|
|
171
|
+
const homePath = getUserSettingsPath();
|
|
171
172
|
const merged: SettingsFile = {};
|
|
172
173
|
for (const path of [cwdPath, homePath]) {
|
|
173
174
|
try {
|
|
@@ -195,9 +196,9 @@ function bustSpinnerSettingsCache(): void {
|
|
|
195
196
|
function writeSettingsKey(key: string, value: unknown): void {
|
|
196
197
|
_settingsCache = null; // invalidate cache on write
|
|
197
198
|
const home = process.env.HOME ?? "";
|
|
198
|
-
if (!home) return;
|
|
199
|
-
const
|
|
200
|
-
const
|
|
199
|
+
if (!home && !process.env.PI_CODING_AGENT_DIR) return;
|
|
200
|
+
const path = getUserSettingsPath();
|
|
201
|
+
const dir = dirname(path);
|
|
201
202
|
let settings: Record<string, unknown> = {};
|
|
202
203
|
try {
|
|
203
204
|
if (existsSync(path)) settings = JSON.parse(readFileSync(path, "utf8")) ?? {};
|
|
@@ -526,8 +527,8 @@ function getThinkingMode(): ThinkingMode {
|
|
|
526
527
|
|
|
527
528
|
function isAssistantThinkingComplete(comp: any, message: any): boolean {
|
|
528
529
|
if (!message || message.role !== "assistant") return false;
|
|
529
|
-
if (
|
|
530
|
-
if (message
|
|
530
|
+
if (getThinkingDurationMetadata(message) !== undefined) return true;
|
|
531
|
+
if (hasThinkingActiveMetadata(message)) return false;
|
|
531
532
|
// Providers keep stopReason "pending" for the whole stream ("deferred" while
|
|
532
533
|
// a deferred call is unresolved); both are in-flight sentinels, never
|
|
533
534
|
// completion signals. Without this, live-thinking detection would depend on
|
|
@@ -554,7 +555,7 @@ function isAssistantThinkingComplete(comp: any, message: any): boolean {
|
|
|
554
555
|
function isLiveThinkingMessage(comp: any, message: any): boolean {
|
|
555
556
|
if (!message || message.role !== "assistant") return false;
|
|
556
557
|
if (isAssistantThinkingComplete(comp, message)) return false;
|
|
557
|
-
if ((message
|
|
558
|
+
if (hasThinkingActiveMetadata(message)) return true;
|
|
558
559
|
if (Array.isArray(message.content)) {
|
|
559
560
|
return message.content.some((b: any) => b?.type === "thinking" && typeof b?.thinking === "string" && b.thinking.trim());
|
|
560
561
|
}
|
|
@@ -1199,7 +1200,7 @@ function isThinkingOnlyAssistantComponent(comp: unknown): comp is InstanceType<t
|
|
|
1199
1200
|
if (hasText) return false;
|
|
1200
1201
|
const hasToolCalls = msg.content.some((c: any) => c?.type === "toolCall");
|
|
1201
1202
|
if (hasToolCalls) return false;
|
|
1202
|
-
if ((comp as any).isStreaming === true || msg
|
|
1203
|
+
if ((comp as any).isStreaming === true || hasThinkingActiveMetadata(msg)) return false;
|
|
1203
1204
|
return true;
|
|
1204
1205
|
}
|
|
1205
1206
|
|
|
@@ -1227,10 +1228,13 @@ function maybeMergeConsecutiveThinkingMessages(parent: any): void {
|
|
|
1227
1228
|
const mergedDuration = durA + durB;
|
|
1228
1229
|
|
|
1229
1230
|
const nextThinkingBlocks = nextMsg.content.filter((c: any) => c?.type === "thinking");
|
|
1230
|
-
|
|
1231
|
-
|
|
1231
|
+
const mergedMessage = {
|
|
1232
|
+
...curMsg,
|
|
1233
|
+
content: [...curMsg.content, ...nextThinkingBlocks],
|
|
1234
|
+
[THINKING_DURATION_KEY]: mergedDuration,
|
|
1235
|
+
};
|
|
1232
1236
|
|
|
1233
|
-
(current as any).updateContent(
|
|
1237
|
+
(current as any).updateContent(mergedMessage);
|
|
1234
1238
|
|
|
1235
1239
|
const removeCount = nextIdx - i;
|
|
1236
1240
|
children.splice(i + 1, removeCount);
|
|
@@ -1614,6 +1618,46 @@ const THINKING_DURATION_KEY = "_piClaudeStyleThinkingDurationMs";
|
|
|
1614
1618
|
const THINKING_ACTIVE_KEY = "_piClaudeStyleThinkingActive";
|
|
1615
1619
|
const MAX_REASONABLE_THINKING_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
1616
1620
|
|
|
1621
|
+
// Presentation metadata belongs to the renderer, not the canonical session
|
|
1622
|
+
// messages. Weak collections keep live state available to the TUI without
|
|
1623
|
+
// changing the objects Pi persists or projects for compaction replay.
|
|
1624
|
+
const thinkingDurationByMessage = new WeakMap<object, number>();
|
|
1625
|
+
const thinkingActiveMessages = new WeakSet<object>();
|
|
1626
|
+
const workedStartByMessage = new WeakMap<object, number>();
|
|
1627
|
+
const workedDurationByMessage = new WeakMap<object, { duration: number; sessionTotal?: number; turns?: number }>();
|
|
1628
|
+
|
|
1629
|
+
function getThinkingDurationMetadata(message: any): number | undefined {
|
|
1630
|
+
if (!message || typeof message !== "object") return undefined;
|
|
1631
|
+
const stored = thinkingDurationByMessage.get(message) ?? message[THINKING_DURATION_KEY];
|
|
1632
|
+
return typeof stored === "number"
|
|
1633
|
+
&& Number.isFinite(stored)
|
|
1634
|
+
&& stored > 0
|
|
1635
|
+
&& stored <= MAX_REASONABLE_THINKING_DURATION_MS
|
|
1636
|
+
? stored
|
|
1637
|
+
: undefined;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function hasThinkingActiveMetadata(message: any): boolean {
|
|
1641
|
+
return !!message
|
|
1642
|
+
&& typeof message === "object"
|
|
1643
|
+
&& (thinkingActiveMessages.has(message) || message[THINKING_ACTIVE_KEY] === true);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function workedMetadataForMessage(message: any): { duration: number; sessionTotal?: number; turns?: number } | undefined {
|
|
1647
|
+
if (!message || typeof message !== "object") return undefined;
|
|
1648
|
+
const metadata = workedDurationByMessage.get(message);
|
|
1649
|
+
if (metadata) return metadata;
|
|
1650
|
+
const duration = message[WORKED_DURATION_KEY];
|
|
1651
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0) return undefined;
|
|
1652
|
+
const sessionTotal = message[WORKED_SESSION_TOTAL_KEY];
|
|
1653
|
+
const turns = message[WORKED_TURNS_KEY];
|
|
1654
|
+
return {
|
|
1655
|
+
duration,
|
|
1656
|
+
sessionTotal: typeof sessionTotal === "number" ? sessionTotal : undefined,
|
|
1657
|
+
turns: typeof turns === "number" ? turns : undefined,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1617
1661
|
let thinkingBlockStartMs = 0;
|
|
1618
1662
|
/** True from thinking_start until thinking_end on the current assistant stream. */
|
|
1619
1663
|
let thinkingBlockInFlight = false;
|
|
@@ -1757,9 +1801,8 @@ class HiddenThinkingSummary {
|
|
|
1757
1801
|
}
|
|
1758
1802
|
|
|
1759
1803
|
function getMessageThinkingDurationMs(message: any): number {
|
|
1760
|
-
const stored = (message
|
|
1761
|
-
if (
|
|
1762
|
-
if (message && typeof message === "object" && stored !== undefined) delete (message as any)[THINKING_DURATION_KEY];
|
|
1804
|
+
const stored = getThinkingDurationMetadata(message);
|
|
1805
|
+
if (stored !== undefined) return stored;
|
|
1763
1806
|
let totalChars = 0;
|
|
1764
1807
|
if (Array.isArray(message?.content)) {
|
|
1765
1808
|
for (const block of message.content) {
|
|
@@ -1780,9 +1823,6 @@ function hiddenThinkingSummaryForMessage(message: any, comp?: any): string {
|
|
|
1780
1823
|
return thinkingActiveSummaryText();
|
|
1781
1824
|
}
|
|
1782
1825
|
const durationMs = getMessageThinkingDurationMs(message);
|
|
1783
|
-
if (message && typeof message === "object") {
|
|
1784
|
-
(message as any)[THINKING_DURATION_KEY] = durationMs;
|
|
1785
|
-
}
|
|
1786
1826
|
return thoughtDurationSummaryText(durationMs);
|
|
1787
1827
|
}
|
|
1788
1828
|
|
|
@@ -1961,15 +2001,45 @@ function patchTerminalWriteTagScrubber(): void {
|
|
|
1961
2001
|
proto[TERMINAL_SCRUB_PATCH_FLAG] = true;
|
|
1962
2002
|
}
|
|
1963
2003
|
|
|
2004
|
+
// Code spans and fenced blocks are literal Markdown source. Math preprocessing
|
|
2005
|
+
// must never touch delimiters inside them: `$$` is the shell PID and `\[ … \]
|
|
2006
|
+
// appears in grep/ripgrep class regexes. Inline formatting splits on this
|
|
2007
|
+
// expression before transforming; display-math block scanning below reuses it
|
|
2008
|
+
// so a math candidate opening inside code is skipped in favor of real prose.
|
|
2009
|
+
const MARKDOWN_CODE_REGION_RE = /(`{3,}[\s\S]*?`{3,}|`[^`\n]*`)/g;
|
|
2010
|
+
|
|
2011
|
+
interface CodeRegion {
|
|
2012
|
+
start: number;
|
|
2013
|
+
end: number;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
function findMarkdownCodeRegions(text: string): CodeRegion[] {
|
|
2017
|
+
const regions: CodeRegion[] = [];
|
|
2018
|
+
MARKDOWN_CODE_REGION_RE.lastIndex = 0;
|
|
2019
|
+
let match: RegExpExecArray | null;
|
|
2020
|
+
while ((match = MARKDOWN_CODE_REGION_RE.exec(text))) {
|
|
2021
|
+
regions.push({ start: match.index, end: match.index + match[0].length });
|
|
2022
|
+
}
|
|
2023
|
+
return regions;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function codeRegionContaining(regions: CodeRegion[], index: number): CodeRegion | undefined {
|
|
2027
|
+
return regions.find((region) => index >= region.start && index < region.end);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1964
2030
|
function replaceInlineMath(text: string): string {
|
|
1965
2031
|
if (!hasInlineMathMarkers(text)) return text;
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2032
|
+
const transform = (segment: string): string => {
|
|
2033
|
+
const withParens = segment.replace(/\\\(([\s\S]*?)\\\)/g, (_match, body: string) => {
|
|
2034
|
+
return codeSpan(formatMathForDisplay(body, false));
|
|
2035
|
+
});
|
|
2036
|
+
return withParens.replace(/(^|[^\\])\$([^\n$]{1,200})\$/g, (match, prefix: string, body: string) => {
|
|
2037
|
+
if (!looksLikeInlineMath(body)) return match;
|
|
2038
|
+
return `${prefix}${codeSpan(formatMathForDisplay(body, false))}`;
|
|
2039
|
+
});
|
|
2040
|
+
};
|
|
2041
|
+
const parts = text.split(MARKDOWN_CODE_REGION_RE);
|
|
2042
|
+
return parts.map((part, index) => index % 2 === 1 ? part : transform(part)).join("");
|
|
1973
2043
|
}
|
|
1974
2044
|
|
|
1975
2045
|
interface MathBlock {
|
|
@@ -2058,29 +2128,60 @@ function shouldFormatStandaloneMath(text: string): boolean {
|
|
|
2058
2128
|
return !/[.!?]\s/.test(plain) && plain.length <= 240;
|
|
2059
2129
|
}
|
|
2060
2130
|
|
|
2061
|
-
|
|
2131
|
+
// Options Pi hands to the Markdown components it builds, including the markdown
|
|
2132
|
+
// transformers (Pi's own Mermaid renderer, extension transformers, ...). Paragraph
|
|
2133
|
+
// wrappers rebuild those components, so they have to forward the options.
|
|
2134
|
+
type MarkdownOptions = ConstructorParameters<typeof Markdown>[5]
|
|
2135
|
+
|
|
2136
|
+
function appendMarkdownSegment(
|
|
2137
|
+
segments: ParagraphSegment[],
|
|
2138
|
+
text: string,
|
|
2139
|
+
theme: MarkdownThemeLike,
|
|
2140
|
+
options?: MarkdownOptions,
|
|
2141
|
+
): void {
|
|
2062
2142
|
if (!text.trim()) return;
|
|
2063
2143
|
const normalized = shouldFormatStandaloneMath(text) ? formatMathForDisplay(text, false) : replaceInlineMath(text);
|
|
2064
|
-
segments.push({ kind: "markdown", md: new Markdown(normalized, 0, 0, theme) });
|
|
2144
|
+
segments.push({ kind: "markdown", md: new Markdown(normalized, 0, 0, theme, undefined, options) });
|
|
2065
2145
|
}
|
|
2066
2146
|
|
|
2067
|
-
function buildParagraphSegments(
|
|
2147
|
+
function buildParagraphSegments(
|
|
2148
|
+
text: string,
|
|
2149
|
+
theme: MarkdownThemeLike,
|
|
2150
|
+
options?: MarkdownOptions,
|
|
2151
|
+
): ParagraphSegment[] {
|
|
2068
2152
|
const segments: ParagraphSegment[] = [];
|
|
2069
2153
|
if (!hasDisplayMathMarkers(text)) {
|
|
2070
|
-
appendMarkdownSegment(segments, text, theme);
|
|
2154
|
+
appendMarkdownSegment(segments, text, theme, options);
|
|
2071
2155
|
return segments;
|
|
2072
2156
|
}
|
|
2073
2157
|
const scanLoose = shouldScanLooseBracketMath(text);
|
|
2158
|
+
// Code regions are computed once for the whole paragraph: a display-math
|
|
2159
|
+
// candidate must be rejected before its fence gets cut into separate
|
|
2160
|
+
// Markdown segments (issue #33, sibling of the inline fix in 2ccd158).
|
|
2161
|
+
const codeRegions = findMarkdownCodeRegions(text);
|
|
2162
|
+
// `cursor` marks the start of prose not yet emitted; `scanFrom` only advances
|
|
2163
|
+
// past rejected code regions so skipped candidates never erase prose.
|
|
2074
2164
|
let cursor = 0;
|
|
2075
|
-
|
|
2076
|
-
|
|
2165
|
+
let scanFrom = 0;
|
|
2166
|
+
while (scanFrom < text.length) {
|
|
2167
|
+
let next = findNextDisplayMathBlock(text, scanFrom, scanLoose);
|
|
2168
|
+
// Math delimiters found inside a fenced block or code span are literal
|
|
2169
|
+
// source, not math. Resume the scan after that code region so a later
|
|
2170
|
+
// genuine `$$ … $$` in prose is still honored.
|
|
2171
|
+
while (next) {
|
|
2172
|
+
const region = codeRegionContaining(codeRegions, next.index);
|
|
2173
|
+
if (!region) break;
|
|
2174
|
+
scanFrom = region.end;
|
|
2175
|
+
next = findNextDisplayMathBlock(text, scanFrom, scanLoose);
|
|
2176
|
+
}
|
|
2077
2177
|
if (!next) break;
|
|
2078
|
-
appendMarkdownSegment(segments, text.slice(cursor, next.index), theme);
|
|
2178
|
+
appendMarkdownSegment(segments, text.slice(cursor, next.index), theme, options);
|
|
2079
2179
|
const raw = text.slice(next.contentStart, next.contentEnd).trim();
|
|
2080
2180
|
if (raw) segments.push({ kind: "math", raw });
|
|
2081
2181
|
cursor = next.endIndex;
|
|
2182
|
+
scanFrom = next.endIndex;
|
|
2082
2183
|
}
|
|
2083
|
-
appendMarkdownSegment(segments, text.slice(cursor), theme);
|
|
2184
|
+
appendMarkdownSegment(segments, text.slice(cursor), theme, options);
|
|
2084
2185
|
return segments;
|
|
2085
2186
|
}
|
|
2086
2187
|
|
|
@@ -2167,9 +2268,9 @@ class DottedParagraph {
|
|
|
2167
2268
|
private cachedWidth?: number;
|
|
2168
2269
|
private cachedLines?: string[];
|
|
2169
2270
|
|
|
2170
|
-
constructor(text: string, markdownTheme: MarkdownThemeLike) {
|
|
2271
|
+
constructor(text: string, markdownTheme: MarkdownThemeLike, options?: MarkdownOptions) {
|
|
2171
2272
|
this.markdownTheme = copySafeMarkdownTheme(markdownTheme);
|
|
2172
|
-
this.segments = buildParagraphSegments(stripTransientMagicContextTags(text), this.markdownTheme);
|
|
2273
|
+
this.segments = buildParagraphSegments(stripTransientMagicContextTags(text), this.markdownTheme, options);
|
|
2173
2274
|
}
|
|
2174
2275
|
|
|
2175
2276
|
invalidate(): void {
|
|
@@ -2263,6 +2364,7 @@ class ThinkingParagraph {
|
|
|
2263
2364
|
private cachedLines?: string[];
|
|
2264
2365
|
private cachedHovered = false;
|
|
2265
2366
|
private chromeEpoch = -1;
|
|
2367
|
+
private options?: MarkdownOptions;
|
|
2266
2368
|
|
|
2267
2369
|
setHovered(hovered: boolean): void {
|
|
2268
2370
|
if (this.hovered === hovered) return;
|
|
@@ -2274,8 +2376,12 @@ class ThinkingParagraph {
|
|
|
2274
2376
|
text: string,
|
|
2275
2377
|
_markdownTheme: ConstructorParameters<typeof Markdown>[3],
|
|
2276
2378
|
_defaultTextStyle?: ConstructorParameters<typeof Markdown>[4],
|
|
2379
|
+
options?: MarkdownOptions,
|
|
2277
2380
|
) {
|
|
2278
|
-
|
|
2381
|
+
// Prefix only the renderer's private copy. The assistant message itself is
|
|
2382
|
+
// canonical session data and must remain untouched for compaction replay.
|
|
2383
|
+
this.text = prefixThinkingLine(stripTransientMagicContextTags(text), undefined);
|
|
2384
|
+
this.options = options;
|
|
2279
2385
|
}
|
|
2280
2386
|
|
|
2281
2387
|
private thinkingMarkdown(): InstanceType<typeof Markdown> {
|
|
@@ -2303,7 +2409,7 @@ class ThinkingParagraph {
|
|
|
2303
2409
|
italic: false,
|
|
2304
2410
|
color: (s: string) => `${DIM_FG}${s}`,
|
|
2305
2411
|
};
|
|
2306
|
-
return new Markdown(this.text, 0, 0, plainTheme, plainStyle);
|
|
2412
|
+
return new Markdown(this.text, 0, 0, plainTheme, plainStyle, this.options);
|
|
2307
2413
|
}
|
|
2308
2414
|
|
|
2309
2415
|
invalidate(): void {
|
|
@@ -2667,17 +2773,18 @@ function patchAssistantMessages(): void {
|
|
|
2667
2773
|
if (isMarkdownComponent(inner)) {
|
|
2668
2774
|
const text = (inner as any).text;
|
|
2669
2775
|
if (!text) continue;
|
|
2776
|
+
const options = (inner as any).options as MarkdownOptions;
|
|
2670
2777
|
const isThinking = !!(inner as any).defaultTextStyle?.italic;
|
|
2671
2778
|
if (isThinking) {
|
|
2672
2779
|
const style = (inner as any).defaultTextStyle;
|
|
2673
|
-
const replacement = new ThinkingParagraph(text, mdTheme, style);
|
|
2780
|
+
const replacement = new ThinkingParagraph(text, mdTheme, style, options);
|
|
2674
2781
|
if ((child as any)?.child !== undefined) {
|
|
2675
2782
|
(child as any).child = replacement;
|
|
2676
2783
|
} else {
|
|
2677
2784
|
container.children[i] = replacement;
|
|
2678
2785
|
}
|
|
2679
2786
|
} else {
|
|
2680
|
-
const replacement = new DottedParagraph(text, mdTheme);
|
|
2787
|
+
const replacement = new DottedParagraph(text, mdTheme, options);
|
|
2681
2788
|
if ((child as any)?.child !== undefined) {
|
|
2682
2789
|
(child as any).child = replacement;
|
|
2683
2790
|
} else {
|
|
@@ -2686,9 +2793,10 @@ function patchAssistantMessages(): void {
|
|
|
2686
2793
|
}
|
|
2687
2794
|
}
|
|
2688
2795
|
}
|
|
2689
|
-
const
|
|
2690
|
-
const
|
|
2691
|
-
const
|
|
2796
|
+
const workedMetadata = workedMetadataForMessage(message);
|
|
2797
|
+
const explicitDuration = workedMetadata?.duration;
|
|
2798
|
+
const explicitSessionTotal = workedMetadata?.sessionTotal;
|
|
2799
|
+
const explicitTurns = workedMetadata?.turns;
|
|
2692
2800
|
// The "Turn took" line must only appear once the stream has truly closed.
|
|
2693
2801
|
// `message.stopReason === "stop"` is not a safe "finished" signal here because
|
|
2694
2802
|
// providers may initialize a live message with that value. The `message_end`
|
|
@@ -3775,6 +3883,10 @@ function bashCollapsedLimit(): number {
|
|
|
3775
3883
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 10;
|
|
3776
3884
|
}
|
|
3777
3885
|
|
|
3886
|
+
function bashOutputMode(): "opencode" | "summary" | "preview" {
|
|
3887
|
+
return getMode(readSettings().bashOutputMode, ["opencode", "summary", "preview"] as const, "opencode");
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3778
3890
|
function bashCommandPreviewLimit(): number {
|
|
3779
3891
|
const value = readSettings().bashCommandPreviewLines;
|
|
3780
3892
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 8;
|
|
@@ -5585,8 +5697,8 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
5585
5697
|
thinkingBlockInFlight = true;
|
|
5586
5698
|
thinkingBlockStartMs = Date.now();
|
|
5587
5699
|
if (message?.role === "assistant") {
|
|
5588
|
-
(message
|
|
5589
|
-
delete
|
|
5700
|
+
thinkingActiveMessages.add(message);
|
|
5701
|
+
thinkingDurationByMessage.delete(message);
|
|
5590
5702
|
}
|
|
5591
5703
|
refreshThinkingChrome();
|
|
5592
5704
|
return;
|
|
@@ -5596,9 +5708,9 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
5596
5708
|
const duration = thinkingBlockStartMs > 0
|
|
5597
5709
|
? Math.max(0, Date.now() - thinkingBlockStartMs)
|
|
5598
5710
|
: undefined;
|
|
5599
|
-
if (message?.role === "assistant") delete
|
|
5600
|
-
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS) {
|
|
5601
|
-
|
|
5711
|
+
if (message?.role === "assistant") thinkingActiveMessages.delete(message);
|
|
5712
|
+
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS && message?.role === "assistant") {
|
|
5713
|
+
thinkingDurationByMessage.set(message, duration);
|
|
5602
5714
|
}
|
|
5603
5715
|
thinkingBlockStartMs = 0;
|
|
5604
5716
|
refreshThinkingChrome();
|
|
@@ -5610,13 +5722,13 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
5610
5722
|
// run normally. Any non-thinking stream event on the same assistant message
|
|
5611
5723
|
// means thinking is no longer the live activity: freeze the elapsed time into
|
|
5612
5724
|
// a "Thought for Xs" duration so the row always resolves.
|
|
5613
|
-
if ((message
|
|
5725
|
+
if (hasThinkingActiveMetadata(message) || thinkingBlockInFlight) {
|
|
5614
5726
|
if (evt.type === "text_start" || evt.type === "text_delta" || evt.type === "toolcall_start" || evt.type === "toolcall_end") {
|
|
5615
5727
|
thinkingBlockInFlight = false;
|
|
5616
5728
|
const duration = thinkingBlockStartMs > 0 ? Math.max(0, Date.now() - thinkingBlockStartMs) : undefined;
|
|
5617
|
-
if (message?.role === "assistant") delete
|
|
5618
|
-
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS) {
|
|
5619
|
-
|
|
5729
|
+
if (message?.role === "assistant") thinkingActiveMessages.delete(message);
|
|
5730
|
+
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS && message?.role === "assistant") {
|
|
5731
|
+
thinkingDurationByMessage.set(message, duration);
|
|
5620
5732
|
}
|
|
5621
5733
|
thinkingBlockStartMs = 0;
|
|
5622
5734
|
refreshThinkingChrome();
|
|
@@ -5631,11 +5743,9 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5631
5743
|
if (theme) applyThemePaletteIfNeeded(theme);
|
|
5632
5744
|
const message = event?.message;
|
|
5633
5745
|
if (!message || message.role !== "assistant" || !Array.isArray(message.content)) return;
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
}
|
|
5638
|
-
}
|
|
5746
|
+
// Thinking labels are applied by ThinkingParagraph while rendering. Never
|
|
5747
|
+
// rewrite the assistant message object: context/compaction consumers need
|
|
5748
|
+
// its canonical thinking content unchanged.
|
|
5639
5749
|
};
|
|
5640
5750
|
pi.on("before_agent_start", async () => {
|
|
5641
5751
|
// Start once per top-level request. Steering/follow-up messages can be
|
|
@@ -5661,14 +5771,14 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5661
5771
|
}
|
|
5662
5772
|
if (message?.role === "assistant") {
|
|
5663
5773
|
currentAssistantMessageStartMs = Date.now();
|
|
5664
|
-
(message
|
|
5774
|
+
workedStartByMessage.set(message, currentAssistantMessageStartMs);
|
|
5665
5775
|
// A new assistant message starts a fresh thinking lifecycle. Without
|
|
5666
5776
|
// this, a missing thinking_end on the previous message leaves the
|
|
5667
5777
|
// global in-flight flag set and the next message renders a stale
|
|
5668
5778
|
// "Thinking..." row until its own thinking events arrive.
|
|
5669
5779
|
thinkingBlockInFlight = false;
|
|
5670
5780
|
thinkingBlockStartMs = 0;
|
|
5671
|
-
delete
|
|
5781
|
+
thinkingActiveMessages.delete(message);
|
|
5672
5782
|
}
|
|
5673
5783
|
});
|
|
5674
5784
|
pi.on("message_update", async (event, ctx) => {
|
|
@@ -5683,27 +5793,22 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5683
5793
|
// into its "Thought for Xs" duration here so the row always resolves
|
|
5684
5794
|
// at the end of the message instead of sticking into later calls.
|
|
5685
5795
|
thinkingBlockInFlight = false;
|
|
5686
|
-
delete
|
|
5687
|
-
if (
|
|
5796
|
+
thinkingActiveMessages.delete(message);
|
|
5797
|
+
if (getThinkingDurationMetadata(message) === undefined && thinkingBlockStartMs > 0) {
|
|
5688
5798
|
const duration = Math.max(0, Date.now() - thinkingBlockStartMs);
|
|
5689
|
-
if (duration <= MAX_REASONABLE_THINKING_DURATION_MS)
|
|
5690
|
-
(message as any)[THINKING_DURATION_KEY] = duration;
|
|
5691
|
-
}
|
|
5799
|
+
if (duration <= MAX_REASONABLE_THINKING_DURATION_MS) thinkingDurationByMessage.set(message, duration);
|
|
5692
5800
|
}
|
|
5693
5801
|
thinkingBlockStartMs = 0;
|
|
5694
5802
|
const started = typeof currentAgentWorkStartMs === "number"
|
|
5695
5803
|
? currentAgentWorkStartMs
|
|
5696
|
-
:
|
|
5697
|
-
? (message as any)[WORKED_START_KEY]
|
|
5698
|
-
: currentAssistantMessageStartMs;
|
|
5804
|
+
: workedStartByMessage.get(message)
|
|
5805
|
+
?? (typeof (message as any)[WORKED_START_KEY] === "number" ? (message as any)[WORKED_START_KEY] : currentAssistantMessageStartMs);
|
|
5699
5806
|
const isFinalAssistantMessage = message.stopReason === "stop";
|
|
5700
5807
|
if (started !== undefined && isFinalAssistantMessage) {
|
|
5701
5808
|
const durationMs = Date.now() - started;
|
|
5702
5809
|
const sessionTotalMs = typeof sessionStartMs === "number" ? Date.now() - sessionStartMs : undefined;
|
|
5703
5810
|
const turns = userTurnCount > 0 ? userTurnCount : undefined;
|
|
5704
|
-
(message
|
|
5705
|
-
if (typeof sessionTotalMs === "number") (message as any)[WORKED_SESSION_TOTAL_KEY] = sessionTotalMs;
|
|
5706
|
-
if (typeof turns === "number") (message as any)[WORKED_TURNS_KEY] = turns;
|
|
5811
|
+
workedDurationByMessage.set(message, { duration: durationMs, sessionTotal: sessionTotalMs, turns });
|
|
5707
5812
|
// Duration metadata drives the assistant component's TUI-only status line.
|
|
5708
5813
|
// Message content stays presentation-neutral for persistence and consumers.
|
|
5709
5814
|
}
|
|
@@ -5751,17 +5856,10 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5751
5856
|
sessionStartMs = sessionStartMs === undefined ? earliest : Math.min(sessionStartMs, earliest);
|
|
5752
5857
|
}
|
|
5753
5858
|
if (userCount > userTurnCount) userTurnCount = userCount;
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
block.thinking = stripThinkingPresentationArtifacts(block.thinking);
|
|
5759
|
-
}
|
|
5760
|
-
if (block && block.type === "text" && typeof block.text === "string") {
|
|
5761
|
-
block.text = stripWorkedDurationLine(block.text);
|
|
5762
|
-
}
|
|
5763
|
-
}
|
|
5764
|
-
}
|
|
5859
|
+
// Do not normalize or strip message content here. The context event is also
|
|
5860
|
+
// consumed by compaction/replay integrations that compare these objects
|
|
5861
|
+
// against the persisted session projection. Presentation cleanup belongs in
|
|
5862
|
+
// the renderer and must not mutate canonical history.
|
|
5765
5863
|
});
|
|
5766
5864
|
}
|
|
5767
5865
|
|
|
@@ -7184,6 +7282,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7184
7282
|
name: "read",
|
|
7185
7283
|
label: "read",
|
|
7186
7284
|
description: readTool.description,
|
|
7285
|
+
promptSnippet: (readTool as any).promptSnippet,
|
|
7286
|
+
promptGuidelines: (readTool as any).promptGuidelines,
|
|
7187
7287
|
parameters: readTool.parameters,
|
|
7188
7288
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7189
7289
|
return readTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7239,6 +7339,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7239
7339
|
name: "bash",
|
|
7240
7340
|
label: "bash",
|
|
7241
7341
|
description: bashTool.description,
|
|
7342
|
+
promptSnippet: (bashTool as any).promptSnippet,
|
|
7343
|
+
promptGuidelines: (bashTool as any).promptGuidelines,
|
|
7242
7344
|
parameters: bashTool.parameters,
|
|
7243
7345
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7244
7346
|
return bashTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7296,9 +7398,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7296
7398
|
let text = exitCode === null || exitCode === 0 ? theme.fg("success", "Done") : theme.fg("error", `Exit ${exitCode}`);
|
|
7297
7399
|
text += theme.fg("muted", ` (${nonEmpty.total} lines)`);
|
|
7298
7400
|
if (details?.truncation?.truncated) text += theme.fg("warning", " [truncated]");
|
|
7401
|
+
const mode = bashOutputMode();
|
|
7402
|
+
if (mode === "summary") return makeText(ctx.lastComponent, withBranch(text, theme));
|
|
7299
7403
|
const persistentPreview = shouldPreserveBashPreview(ctx) ? buildPersistentBashPreview(nonEmpty.lines, theme) : "";
|
|
7300
|
-
if (!expanded &&
|
|
7301
|
-
|
|
7404
|
+
if (!expanded && mode === "preview") {
|
|
7405
|
+
const previewLines = nonEmpty.lines.slice(0, bashCollapsedLimit());
|
|
7406
|
+
const preview = previewLines.length > 0
|
|
7407
|
+
? buildPreviewText(previewLines, false, theme, bashCollapsedLimit(), nonEmpty.total, (line) => theme.fg("dim", line || " "))
|
|
7408
|
+
: "";
|
|
7409
|
+
const suffix = toolOutputDetailHint(theme, expanded);
|
|
7410
|
+
return makeText(ctx.lastComponent, withBranch(`${text}${suffix}${preview ? `\n${preview}` : ""}`, theme));
|
|
7411
|
+
}
|
|
7412
|
+
if (!expanded && mode === "opencode" && nonEmpty.total > 0) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}`, theme));
|
|
7302
7413
|
if (!expanded) return makeText(ctx.lastComponent, withBranch(text, theme));
|
|
7303
7414
|
const collapsed = bashCollapsedLimit();
|
|
7304
7415
|
if (rewrite) text += `\n${formatRtkRewriteDetails(rewrite, theme)}`;
|
|
@@ -7312,6 +7423,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7312
7423
|
name: "grep",
|
|
7313
7424
|
label: "grep",
|
|
7314
7425
|
description: grepTool.description,
|
|
7426
|
+
promptSnippet: (grepTool as any).promptSnippet,
|
|
7427
|
+
promptGuidelines: (grepTool as any).promptGuidelines,
|
|
7315
7428
|
parameters: grepTool.parameters,
|
|
7316
7429
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7317
7430
|
return grepTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7352,6 +7465,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7352
7465
|
name: "find",
|
|
7353
7466
|
label: "find",
|
|
7354
7467
|
description: findTool.description,
|
|
7468
|
+
promptSnippet: (findTool as any).promptSnippet,
|
|
7469
|
+
promptGuidelines: (findTool as any).promptGuidelines,
|
|
7355
7470
|
parameters: findTool.parameters,
|
|
7356
7471
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7357
7472
|
return findTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7403,6 +7518,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7403
7518
|
name: "ls",
|
|
7404
7519
|
label: "ls",
|
|
7405
7520
|
description: lsTool.description,
|
|
7521
|
+
promptSnippet: (lsTool as any).promptSnippet,
|
|
7522
|
+
promptGuidelines: (lsTool as any).promptGuidelines,
|
|
7406
7523
|
parameters: lsTool.parameters,
|
|
7407
7524
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7408
7525
|
return lsTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7454,6 +7571,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7454
7571
|
name: "write",
|
|
7455
7572
|
label: "write",
|
|
7456
7573
|
description: writeTool.description,
|
|
7574
|
+
promptSnippet: (writeTool as any).promptSnippet,
|
|
7575
|
+
promptGuidelines: (writeTool as any).promptGuidelines,
|
|
7457
7576
|
parameters: writeTool.parameters,
|
|
7458
7577
|
async execute(toolCallId, params, signal, onUpdate, _ctx) {
|
|
7459
7578
|
const fp = params.path ?? (params as any).file_path ?? "";
|
|
@@ -7569,6 +7688,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7569
7688
|
name: "edit",
|
|
7570
7689
|
label: "edit",
|
|
7571
7690
|
description: editTool.description,
|
|
7691
|
+
promptSnippet: (editTool as any).promptSnippet,
|
|
7692
|
+
promptGuidelines: (editTool as any).promptGuidelines,
|
|
7572
7693
|
parameters: editTool.parameters,
|
|
7573
7694
|
async execute(toolCallId, params, signal, onUpdate, _ctx) {
|
|
7574
7695
|
const fp = params.path ?? (params as any).file_path ?? "";
|
|
@@ -7795,3 +7916,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
7795
7916
|
bumpToolBranchVisualEpoch();
|
|
7796
7917
|
});
|
|
7797
7918
|
}
|
|
7919
|
+
|
|
7920
|
+
// Test-only escape hatch: this package is a default-export extension and the
|
|
7921
|
+
// export surface Pi consumes must stay minimal. Regression tests for math
|
|
7922
|
+
// preprocessing reach these internals through this namespace instead of
|
|
7923
|
+
// widening the public export (see scripts/math-preprocess.test.ts).
|
|
7924
|
+
export const __mathPreprocessInternals = {
|
|
7925
|
+
buildParagraphSegments,
|
|
7926
|
+
};
|
package/extensions/spinner.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import net from "node:net";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
|
|
4
6
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
7
|
import { Loader } from "@earendil-works/pi-tui";
|
|
6
8
|
|
|
9
|
+
import { getUserSettingsPath } from "./settings-path";
|
|
10
|
+
|
|
7
11
|
// ---------------------------------------------------------------------------
|
|
8
12
|
// Patch built-in Loader with Claude/OpenBrawd-style glyphs.
|
|
9
13
|
// Keep animation cadence constant so the spinner doesn't appear to slow down
|
|
@@ -44,7 +48,7 @@ function readSpinnerSettings(): { adaptive: boolean; verbColor: string; statusCo
|
|
|
44
48
|
// orange as the glyph on themes like openAntigravity-dark.
|
|
45
49
|
let verbColor = "borderAccent";
|
|
46
50
|
let statusColor = "muted";
|
|
47
|
-
const paths = [`${process.cwd()}/.pi/settings.json`,
|
|
51
|
+
const paths = [`${process.cwd()}/.pi/settings.json`, getUserSettingsPath()];
|
|
48
52
|
for (const p of paths) {
|
|
49
53
|
try {
|
|
50
54
|
if (!p || !existsSync(p)) continue;
|
|
@@ -517,7 +521,9 @@ function statusText(text: string): string {
|
|
|
517
521
|
// This extension replaces that text with themed spinner verbs (Cooking…,
|
|
518
522
|
// Syncing…, …), so herdr sidebars go idle/stale while pi is actually busy.
|
|
519
523
|
// When running inside herdr (HERDR_ENV=1), report working/idle over the
|
|
520
|
-
// socket
|
|
524
|
+
// socket only when herdr's official pi integration is not installed. The
|
|
525
|
+
// official integration owns the same lifecycle authority and must remain the
|
|
526
|
+
// sole reporter when present. UI is unchanged.
|
|
521
527
|
// ---------------------------------------------------------------------------
|
|
522
528
|
|
|
523
529
|
type HerdrAgentState = "working" | "idle";
|
|
@@ -532,9 +538,33 @@ const HERDR_PANE_ID = process.env.HERDR_PANE_ID;
|
|
|
532
538
|
// Must be exactly "herdr:pi" — herdr only grants full lifecycle authority
|
|
533
539
|
// (skip screen-manifest fallback) to that official source pair with agent "pi".
|
|
534
540
|
const HERDR_SOURCE = "herdr:pi";
|
|
541
|
+
const HERDR_INTEGRATION_MARKER = "HERDR_INTEGRATION_ID=pi";
|
|
542
|
+
let officialHerdrIntegrationInstalled: boolean | undefined;
|
|
543
|
+
|
|
544
|
+
function hasOfficialHerdrIntegration(): boolean {
|
|
545
|
+
if (officialHerdrIntegrationInstalled !== undefined) {
|
|
546
|
+
return officialHerdrIntegrationInstalled;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR
|
|
550
|
+
|| join(homedir(), ".pi", "agent");
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const integrationPath = join(agentDir, "extensions", "herdr-agent-state.ts");
|
|
554
|
+
officialHerdrIntegrationInstalled =
|
|
555
|
+
existsSync(integrationPath)
|
|
556
|
+
&& readFileSync(integrationPath, "utf8").includes(HERDR_INTEGRATION_MARKER);
|
|
557
|
+
} catch {
|
|
558
|
+
officialHerdrIntegrationInstalled = false;
|
|
559
|
+
}
|
|
560
|
+
return officialHerdrIntegrationInstalled;
|
|
561
|
+
}
|
|
535
562
|
|
|
536
563
|
function herdrEnabled(): boolean {
|
|
537
|
-
return HERDR_ENV === "1"
|
|
564
|
+
return HERDR_ENV === "1"
|
|
565
|
+
&& !!HERDR_SOCKET_PATH
|
|
566
|
+
&& !!HERDR_PANE_ID
|
|
567
|
+
&& !hasOfficialHerdrIntegration();
|
|
538
568
|
}
|
|
539
569
|
|
|
540
570
|
function herdrSendAttempt(request: unknown, timeoutMs: number): Promise<boolean> {
|
|
@@ -754,7 +784,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
754
784
|
}
|
|
755
785
|
|
|
756
786
|
function syncWorkingMessage(force = false): void {
|
|
757
|
-
|
|
787
|
+
try {
|
|
788
|
+
if (!activeCtx?.hasUI) return;
|
|
789
|
+
} catch {
|
|
790
|
+
// Session replacement/reload invalidated the captured context.
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
758
793
|
// Re-derive colors on every tick so /cc-spinner verb/status changes
|
|
759
794
|
// take effect within ~250 ms without waiting for the next pi event.
|
|
760
795
|
// applyThemeColors is identity-cached on (theme, verbKey, statusKey) so
|
|
@@ -770,7 +805,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
770
805
|
|
|
771
806
|
function restoreDefaultWorkingMessage(): void {
|
|
772
807
|
lastWorkingMessage = null;
|
|
773
|
-
|
|
808
|
+
try {
|
|
809
|
+
if (!activeCtx?.hasUI) return;
|
|
810
|
+
} catch {
|
|
811
|
+
// Session replacement/reload invalidated the captured context.
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
774
814
|
try {
|
|
775
815
|
activeCtx.ui.setWorkingMessage();
|
|
776
816
|
} catch { /* noop */ }
|
|
@@ -1002,20 +1042,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
1002
1042
|
clearThoughtStatusTimer();
|
|
1003
1043
|
}
|
|
1004
1044
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
completionTimer =
|
|
1013
|
-
|
|
1045
|
+
try {
|
|
1046
|
+
if (activeCtx?.hasUI) {
|
|
1047
|
+
const message = `${STATUS_DIM}✻ Turn took ${formatDuration(elapsed)}${RESET}`;
|
|
1048
|
+
lastWorkingMessage = message;
|
|
1049
|
+
try {
|
|
1050
|
+
activeCtx.ui.setWorkingMessage(message);
|
|
1051
|
+
} catch { /* noop */ }
|
|
1052
|
+
completionTimer = setTimeout(() => {
|
|
1053
|
+
completionTimer = null;
|
|
1054
|
+
if (activeTurnId !== turnId) return;
|
|
1055
|
+
restoreDefaultWorkingMessage();
|
|
1056
|
+
}, TURN_COMPLETION_MS);
|
|
1057
|
+
unrefTimer(completionTimer);
|
|
1058
|
+
} else if (typeof thinkingStatus !== "number") {
|
|
1014
1059
|
restoreDefaultWorkingMessage();
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1060
|
+
}
|
|
1061
|
+
} catch {
|
|
1062
|
+
// Session replacement/reload invalidated the captured context.
|
|
1063
|
+
return;
|
|
1019
1064
|
}
|
|
1020
1065
|
|
|
1021
1066
|
responseLength = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-claude-code-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.86",
|
|
4
4
|
"description": "Claude Code-style grouped tool rows for pi with Ctrl+O/Ctrl+Shift+O previews, math rendering, and consistent built-in, MCP, and custom tool rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,13 +30,14 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"extensions/index.ts",
|
|
32
32
|
"extensions/spinner.ts",
|
|
33
|
+
"extensions/settings-path.ts",
|
|
33
34
|
"extensions/bash-command.ts",
|
|
34
35
|
"config/config.example.json",
|
|
35
36
|
"README.md",
|
|
36
37
|
"CHANGELOG.md"
|
|
37
38
|
],
|
|
38
39
|
"scripts": {
|
|
39
|
-
"test": "node --experimental-strip-types --test scripts
|
|
40
|
+
"test": "node --experimental-strip-types --test scripts/*.test.ts scripts/*.test.mts",
|
|
40
41
|
"test:fullscreen": "node scripts/fullscreen-mouse.test.mjs",
|
|
41
42
|
"typecheck": "tsc --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/index.ts extensions/spinner.ts",
|
|
42
43
|
"pack:dry-run": "npm pack --dry-run",
|