pi-claude-code-ui 1.0.84 → 1.0.85
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 +10 -0
- package/README.md +3 -3
- package/extensions/index.ts +160 -79
- package/extensions/settings-path.ts +4 -0
- package/extensions/spinner.ts +63 -18
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,16 @@
|
|
|
39
39
|
|
|
40
40
|
- **Hide transient context tags** — hide transient context tags in streaming prose.
|
|
41
41
|
|
|
42
|
+
## 1.0.85 — 2026-09-07
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Preserve Markdown code literals while formatting inline mathematics.
|
|
47
|
+
- Keep assistant presentation state out of canonical session messages so compacted sessions can replay safely.
|
|
48
|
+
- Respect `bashOutputMode` for completed Bash output.
|
|
49
|
+
- Preserve native prompt metadata in wrapped core tools.
|
|
50
|
+
- Harden spinner cleanup when Pi replaces a stale UI context.
|
|
51
|
+
|
|
42
52
|
## Unreleased
|
|
43
53
|
|
|
44
54
|
### 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
|
|
|
@@ -1963,13 +2003,20 @@ function patchTerminalWriteTagScrubber(): void {
|
|
|
1963
2003
|
|
|
1964
2004
|
function replaceInlineMath(text: string): string {
|
|
1965
2005
|
if (!hasInlineMathMarkers(text)) return text;
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2006
|
+
const transform = (segment: string): string => {
|
|
2007
|
+
const withParens = segment.replace(/\\\(([\s\S]*?)\\\)/g, (_match, body: string) => {
|
|
2008
|
+
return codeSpan(formatMathForDisplay(body, false));
|
|
2009
|
+
});
|
|
2010
|
+
return withParens.replace(/(^|[^\\])\$([^\n$]{1,200})\$/g, (match, prefix: string, body: string) => {
|
|
2011
|
+
if (!looksLikeInlineMath(body)) return match;
|
|
2012
|
+
return `${prefix}${codeSpan(formatMathForDisplay(body, false))}`;
|
|
2013
|
+
});
|
|
2014
|
+
};
|
|
2015
|
+
// Markdown code spans and fenced blocks are literal source. Apply math
|
|
2016
|
+
// formatting only to prose segments so shell variables such as `$base` and
|
|
2017
|
+
// `$scope` remain byte-for-byte unchanged in displayed/copied commands.
|
|
2018
|
+
const parts = text.split(/(`{3,}[\s\S]*?`{3,}|`[^`\n]*`)/g);
|
|
2019
|
+
return parts.map((part, index) => index % 2 === 1 ? part : transform(part)).join("");
|
|
1973
2020
|
}
|
|
1974
2021
|
|
|
1975
2022
|
interface MathBlock {
|
|
@@ -2058,16 +2105,30 @@ function shouldFormatStandaloneMath(text: string): boolean {
|
|
|
2058
2105
|
return !/[.!?]\s/.test(plain) && plain.length <= 240;
|
|
2059
2106
|
}
|
|
2060
2107
|
|
|
2061
|
-
|
|
2108
|
+
// Options Pi hands to the Markdown components it builds, including the markdown
|
|
2109
|
+
// transformers (Pi's own Mermaid renderer, extension transformers, ...). Paragraph
|
|
2110
|
+
// wrappers rebuild those components, so they have to forward the options.
|
|
2111
|
+
type MarkdownOptions = ConstructorParameters<typeof Markdown>[5]
|
|
2112
|
+
|
|
2113
|
+
function appendMarkdownSegment(
|
|
2114
|
+
segments: ParagraphSegment[],
|
|
2115
|
+
text: string,
|
|
2116
|
+
theme: MarkdownThemeLike,
|
|
2117
|
+
options?: MarkdownOptions,
|
|
2118
|
+
): void {
|
|
2062
2119
|
if (!text.trim()) return;
|
|
2063
2120
|
const normalized = shouldFormatStandaloneMath(text) ? formatMathForDisplay(text, false) : replaceInlineMath(text);
|
|
2064
|
-
segments.push({ kind: "markdown", md: new Markdown(normalized, 0, 0, theme) });
|
|
2121
|
+
segments.push({ kind: "markdown", md: new Markdown(normalized, 0, 0, theme, undefined, options) });
|
|
2065
2122
|
}
|
|
2066
2123
|
|
|
2067
|
-
function buildParagraphSegments(
|
|
2124
|
+
function buildParagraphSegments(
|
|
2125
|
+
text: string,
|
|
2126
|
+
theme: MarkdownThemeLike,
|
|
2127
|
+
options?: MarkdownOptions,
|
|
2128
|
+
): ParagraphSegment[] {
|
|
2068
2129
|
const segments: ParagraphSegment[] = [];
|
|
2069
2130
|
if (!hasDisplayMathMarkers(text)) {
|
|
2070
|
-
appendMarkdownSegment(segments, text, theme);
|
|
2131
|
+
appendMarkdownSegment(segments, text, theme, options);
|
|
2071
2132
|
return segments;
|
|
2072
2133
|
}
|
|
2073
2134
|
const scanLoose = shouldScanLooseBracketMath(text);
|
|
@@ -2075,12 +2136,12 @@ function buildParagraphSegments(text: string, theme: MarkdownThemeLike): Paragra
|
|
|
2075
2136
|
while (cursor < text.length) {
|
|
2076
2137
|
const next = findNextDisplayMathBlock(text, cursor, scanLoose);
|
|
2077
2138
|
if (!next) break;
|
|
2078
|
-
appendMarkdownSegment(segments, text.slice(cursor, next.index), theme);
|
|
2139
|
+
appendMarkdownSegment(segments, text.slice(cursor, next.index), theme, options);
|
|
2079
2140
|
const raw = text.slice(next.contentStart, next.contentEnd).trim();
|
|
2080
2141
|
if (raw) segments.push({ kind: "math", raw });
|
|
2081
2142
|
cursor = next.endIndex;
|
|
2082
2143
|
}
|
|
2083
|
-
appendMarkdownSegment(segments, text.slice(cursor), theme);
|
|
2144
|
+
appendMarkdownSegment(segments, text.slice(cursor), theme, options);
|
|
2084
2145
|
return segments;
|
|
2085
2146
|
}
|
|
2086
2147
|
|
|
@@ -2167,9 +2228,9 @@ class DottedParagraph {
|
|
|
2167
2228
|
private cachedWidth?: number;
|
|
2168
2229
|
private cachedLines?: string[];
|
|
2169
2230
|
|
|
2170
|
-
constructor(text: string, markdownTheme: MarkdownThemeLike) {
|
|
2231
|
+
constructor(text: string, markdownTheme: MarkdownThemeLike, options?: MarkdownOptions) {
|
|
2171
2232
|
this.markdownTheme = copySafeMarkdownTheme(markdownTheme);
|
|
2172
|
-
this.segments = buildParagraphSegments(stripTransientMagicContextTags(text), this.markdownTheme);
|
|
2233
|
+
this.segments = buildParagraphSegments(stripTransientMagicContextTags(text), this.markdownTheme, options);
|
|
2173
2234
|
}
|
|
2174
2235
|
|
|
2175
2236
|
invalidate(): void {
|
|
@@ -2263,6 +2324,7 @@ class ThinkingParagraph {
|
|
|
2263
2324
|
private cachedLines?: string[];
|
|
2264
2325
|
private cachedHovered = false;
|
|
2265
2326
|
private chromeEpoch = -1;
|
|
2327
|
+
private options?: MarkdownOptions;
|
|
2266
2328
|
|
|
2267
2329
|
setHovered(hovered: boolean): void {
|
|
2268
2330
|
if (this.hovered === hovered) return;
|
|
@@ -2274,8 +2336,12 @@ class ThinkingParagraph {
|
|
|
2274
2336
|
text: string,
|
|
2275
2337
|
_markdownTheme: ConstructorParameters<typeof Markdown>[3],
|
|
2276
2338
|
_defaultTextStyle?: ConstructorParameters<typeof Markdown>[4],
|
|
2339
|
+
options?: MarkdownOptions,
|
|
2277
2340
|
) {
|
|
2278
|
-
|
|
2341
|
+
// Prefix only the renderer's private copy. The assistant message itself is
|
|
2342
|
+
// canonical session data and must remain untouched for compaction replay.
|
|
2343
|
+
this.text = prefixThinkingLine(stripTransientMagicContextTags(text), undefined);
|
|
2344
|
+
this.options = options;
|
|
2279
2345
|
}
|
|
2280
2346
|
|
|
2281
2347
|
private thinkingMarkdown(): InstanceType<typeof Markdown> {
|
|
@@ -2303,7 +2369,7 @@ class ThinkingParagraph {
|
|
|
2303
2369
|
italic: false,
|
|
2304
2370
|
color: (s: string) => `${DIM_FG}${s}`,
|
|
2305
2371
|
};
|
|
2306
|
-
return new Markdown(this.text, 0, 0, plainTheme, plainStyle);
|
|
2372
|
+
return new Markdown(this.text, 0, 0, plainTheme, plainStyle, this.options);
|
|
2307
2373
|
}
|
|
2308
2374
|
|
|
2309
2375
|
invalidate(): void {
|
|
@@ -2667,17 +2733,18 @@ function patchAssistantMessages(): void {
|
|
|
2667
2733
|
if (isMarkdownComponent(inner)) {
|
|
2668
2734
|
const text = (inner as any).text;
|
|
2669
2735
|
if (!text) continue;
|
|
2736
|
+
const options = (inner as any).options as MarkdownOptions;
|
|
2670
2737
|
const isThinking = !!(inner as any).defaultTextStyle?.italic;
|
|
2671
2738
|
if (isThinking) {
|
|
2672
2739
|
const style = (inner as any).defaultTextStyle;
|
|
2673
|
-
const replacement = new ThinkingParagraph(text, mdTheme, style);
|
|
2740
|
+
const replacement = new ThinkingParagraph(text, mdTheme, style, options);
|
|
2674
2741
|
if ((child as any)?.child !== undefined) {
|
|
2675
2742
|
(child as any).child = replacement;
|
|
2676
2743
|
} else {
|
|
2677
2744
|
container.children[i] = replacement;
|
|
2678
2745
|
}
|
|
2679
2746
|
} else {
|
|
2680
|
-
const replacement = new DottedParagraph(text, mdTheme);
|
|
2747
|
+
const replacement = new DottedParagraph(text, mdTheme, options);
|
|
2681
2748
|
if ((child as any)?.child !== undefined) {
|
|
2682
2749
|
(child as any).child = replacement;
|
|
2683
2750
|
} else {
|
|
@@ -2686,9 +2753,10 @@ function patchAssistantMessages(): void {
|
|
|
2686
2753
|
}
|
|
2687
2754
|
}
|
|
2688
2755
|
}
|
|
2689
|
-
const
|
|
2690
|
-
const
|
|
2691
|
-
const
|
|
2756
|
+
const workedMetadata = workedMetadataForMessage(message);
|
|
2757
|
+
const explicitDuration = workedMetadata?.duration;
|
|
2758
|
+
const explicitSessionTotal = workedMetadata?.sessionTotal;
|
|
2759
|
+
const explicitTurns = workedMetadata?.turns;
|
|
2692
2760
|
// The "Turn took" line must only appear once the stream has truly closed.
|
|
2693
2761
|
// `message.stopReason === "stop"` is not a safe "finished" signal here because
|
|
2694
2762
|
// providers may initialize a live message with that value. The `message_end`
|
|
@@ -3775,6 +3843,10 @@ function bashCollapsedLimit(): number {
|
|
|
3775
3843
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 10;
|
|
3776
3844
|
}
|
|
3777
3845
|
|
|
3846
|
+
function bashOutputMode(): "opencode" | "summary" | "preview" {
|
|
3847
|
+
return getMode(readSettings().bashOutputMode, ["opencode", "summary", "preview"] as const, "opencode");
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3778
3850
|
function bashCommandPreviewLimit(): number {
|
|
3779
3851
|
const value = readSettings().bashCommandPreviewLines;
|
|
3780
3852
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 8;
|
|
@@ -5585,8 +5657,8 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
5585
5657
|
thinkingBlockInFlight = true;
|
|
5586
5658
|
thinkingBlockStartMs = Date.now();
|
|
5587
5659
|
if (message?.role === "assistant") {
|
|
5588
|
-
(message
|
|
5589
|
-
delete
|
|
5660
|
+
thinkingActiveMessages.add(message);
|
|
5661
|
+
thinkingDurationByMessage.delete(message);
|
|
5590
5662
|
}
|
|
5591
5663
|
refreshThinkingChrome();
|
|
5592
5664
|
return;
|
|
@@ -5596,9 +5668,9 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
5596
5668
|
const duration = thinkingBlockStartMs > 0
|
|
5597
5669
|
? Math.max(0, Date.now() - thinkingBlockStartMs)
|
|
5598
5670
|
: undefined;
|
|
5599
|
-
if (message?.role === "assistant") delete
|
|
5600
|
-
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS) {
|
|
5601
|
-
|
|
5671
|
+
if (message?.role === "assistant") thinkingActiveMessages.delete(message);
|
|
5672
|
+
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS && message?.role === "assistant") {
|
|
5673
|
+
thinkingDurationByMessage.set(message, duration);
|
|
5602
5674
|
}
|
|
5603
5675
|
thinkingBlockStartMs = 0;
|
|
5604
5676
|
refreshThinkingChrome();
|
|
@@ -5610,13 +5682,13 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
5610
5682
|
// run normally. Any non-thinking stream event on the same assistant message
|
|
5611
5683
|
// means thinking is no longer the live activity: freeze the elapsed time into
|
|
5612
5684
|
// a "Thought for Xs" duration so the row always resolves.
|
|
5613
|
-
if ((message
|
|
5685
|
+
if (hasThinkingActiveMetadata(message) || thinkingBlockInFlight) {
|
|
5614
5686
|
if (evt.type === "text_start" || evt.type === "text_delta" || evt.type === "toolcall_start" || evt.type === "toolcall_end") {
|
|
5615
5687
|
thinkingBlockInFlight = false;
|
|
5616
5688
|
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
|
-
|
|
5689
|
+
if (message?.role === "assistant") thinkingActiveMessages.delete(message);
|
|
5690
|
+
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS && message?.role === "assistant") {
|
|
5691
|
+
thinkingDurationByMessage.set(message, duration);
|
|
5620
5692
|
}
|
|
5621
5693
|
thinkingBlockStartMs = 0;
|
|
5622
5694
|
refreshThinkingChrome();
|
|
@@ -5631,11 +5703,9 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5631
5703
|
if (theme) applyThemePaletteIfNeeded(theme);
|
|
5632
5704
|
const message = event?.message;
|
|
5633
5705
|
if (!message || message.role !== "assistant" || !Array.isArray(message.content)) return;
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
}
|
|
5638
|
-
}
|
|
5706
|
+
// Thinking labels are applied by ThinkingParagraph while rendering. Never
|
|
5707
|
+
// rewrite the assistant message object: context/compaction consumers need
|
|
5708
|
+
// its canonical thinking content unchanged.
|
|
5639
5709
|
};
|
|
5640
5710
|
pi.on("before_agent_start", async () => {
|
|
5641
5711
|
// Start once per top-level request. Steering/follow-up messages can be
|
|
@@ -5661,14 +5731,14 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5661
5731
|
}
|
|
5662
5732
|
if (message?.role === "assistant") {
|
|
5663
5733
|
currentAssistantMessageStartMs = Date.now();
|
|
5664
|
-
(message
|
|
5734
|
+
workedStartByMessage.set(message, currentAssistantMessageStartMs);
|
|
5665
5735
|
// A new assistant message starts a fresh thinking lifecycle. Without
|
|
5666
5736
|
// this, a missing thinking_end on the previous message leaves the
|
|
5667
5737
|
// global in-flight flag set and the next message renders a stale
|
|
5668
5738
|
// "Thinking..." row until its own thinking events arrive.
|
|
5669
5739
|
thinkingBlockInFlight = false;
|
|
5670
5740
|
thinkingBlockStartMs = 0;
|
|
5671
|
-
delete
|
|
5741
|
+
thinkingActiveMessages.delete(message);
|
|
5672
5742
|
}
|
|
5673
5743
|
});
|
|
5674
5744
|
pi.on("message_update", async (event, ctx) => {
|
|
@@ -5683,27 +5753,22 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5683
5753
|
// into its "Thought for Xs" duration here so the row always resolves
|
|
5684
5754
|
// at the end of the message instead of sticking into later calls.
|
|
5685
5755
|
thinkingBlockInFlight = false;
|
|
5686
|
-
delete
|
|
5687
|
-
if (
|
|
5756
|
+
thinkingActiveMessages.delete(message);
|
|
5757
|
+
if (getThinkingDurationMetadata(message) === undefined && thinkingBlockStartMs > 0) {
|
|
5688
5758
|
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
|
-
}
|
|
5759
|
+
if (duration <= MAX_REASONABLE_THINKING_DURATION_MS) thinkingDurationByMessage.set(message, duration);
|
|
5692
5760
|
}
|
|
5693
5761
|
thinkingBlockStartMs = 0;
|
|
5694
5762
|
const started = typeof currentAgentWorkStartMs === "number"
|
|
5695
5763
|
? currentAgentWorkStartMs
|
|
5696
|
-
:
|
|
5697
|
-
? (message as any)[WORKED_START_KEY]
|
|
5698
|
-
: currentAssistantMessageStartMs;
|
|
5764
|
+
: workedStartByMessage.get(message)
|
|
5765
|
+
?? (typeof (message as any)[WORKED_START_KEY] === "number" ? (message as any)[WORKED_START_KEY] : currentAssistantMessageStartMs);
|
|
5699
5766
|
const isFinalAssistantMessage = message.stopReason === "stop";
|
|
5700
5767
|
if (started !== undefined && isFinalAssistantMessage) {
|
|
5701
5768
|
const durationMs = Date.now() - started;
|
|
5702
5769
|
const sessionTotalMs = typeof sessionStartMs === "number" ? Date.now() - sessionStartMs : undefined;
|
|
5703
5770
|
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;
|
|
5771
|
+
workedDurationByMessage.set(message, { duration: durationMs, sessionTotal: sessionTotalMs, turns });
|
|
5707
5772
|
// Duration metadata drives the assistant component's TUI-only status line.
|
|
5708
5773
|
// Message content stays presentation-neutral for persistence and consumers.
|
|
5709
5774
|
}
|
|
@@ -5751,17 +5816,10 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5751
5816
|
sessionStartMs = sessionStartMs === undefined ? earliest : Math.min(sessionStartMs, earliest);
|
|
5752
5817
|
}
|
|
5753
5818
|
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
|
-
}
|
|
5819
|
+
// Do not normalize or strip message content here. The context event is also
|
|
5820
|
+
// consumed by compaction/replay integrations that compare these objects
|
|
5821
|
+
// against the persisted session projection. Presentation cleanup belongs in
|
|
5822
|
+
// the renderer and must not mutate canonical history.
|
|
5765
5823
|
});
|
|
5766
5824
|
}
|
|
5767
5825
|
|
|
@@ -7184,6 +7242,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7184
7242
|
name: "read",
|
|
7185
7243
|
label: "read",
|
|
7186
7244
|
description: readTool.description,
|
|
7245
|
+
promptSnippet: (readTool as any).promptSnippet,
|
|
7246
|
+
promptGuidelines: (readTool as any).promptGuidelines,
|
|
7187
7247
|
parameters: readTool.parameters,
|
|
7188
7248
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7189
7249
|
return readTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7239,6 +7299,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7239
7299
|
name: "bash",
|
|
7240
7300
|
label: "bash",
|
|
7241
7301
|
description: bashTool.description,
|
|
7302
|
+
promptSnippet: (bashTool as any).promptSnippet,
|
|
7303
|
+
promptGuidelines: (bashTool as any).promptGuidelines,
|
|
7242
7304
|
parameters: bashTool.parameters,
|
|
7243
7305
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7244
7306
|
return bashTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7296,9 +7358,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
7296
7358
|
let text = exitCode === null || exitCode === 0 ? theme.fg("success", "Done") : theme.fg("error", `Exit ${exitCode}`);
|
|
7297
7359
|
text += theme.fg("muted", ` (${nonEmpty.total} lines)`);
|
|
7298
7360
|
if (details?.truncation?.truncated) text += theme.fg("warning", " [truncated]");
|
|
7361
|
+
const mode = bashOutputMode();
|
|
7362
|
+
if (mode === "summary") return makeText(ctx.lastComponent, withBranch(text, theme));
|
|
7299
7363
|
const persistentPreview = shouldPreserveBashPreview(ctx) ? buildPersistentBashPreview(nonEmpty.lines, theme) : "";
|
|
7300
|
-
if (!expanded &&
|
|
7301
|
-
|
|
7364
|
+
if (!expanded && mode === "preview") {
|
|
7365
|
+
const previewLines = nonEmpty.lines.slice(0, bashCollapsedLimit());
|
|
7366
|
+
const preview = previewLines.length > 0
|
|
7367
|
+
? buildPreviewText(previewLines, false, theme, bashCollapsedLimit(), nonEmpty.total, (line) => theme.fg("dim", line || " "))
|
|
7368
|
+
: "";
|
|
7369
|
+
const suffix = toolOutputDetailHint(theme, expanded);
|
|
7370
|
+
return makeText(ctx.lastComponent, withBranch(`${text}${suffix}${preview ? `\n${preview}` : ""}`, theme));
|
|
7371
|
+
}
|
|
7372
|
+
if (!expanded && mode === "opencode" && nonEmpty.total > 0) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}`, theme));
|
|
7302
7373
|
if (!expanded) return makeText(ctx.lastComponent, withBranch(text, theme));
|
|
7303
7374
|
const collapsed = bashCollapsedLimit();
|
|
7304
7375
|
if (rewrite) text += `\n${formatRtkRewriteDetails(rewrite, theme)}`;
|
|
@@ -7312,6 +7383,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7312
7383
|
name: "grep",
|
|
7313
7384
|
label: "grep",
|
|
7314
7385
|
description: grepTool.description,
|
|
7386
|
+
promptSnippet: (grepTool as any).promptSnippet,
|
|
7387
|
+
promptGuidelines: (grepTool as any).promptGuidelines,
|
|
7315
7388
|
parameters: grepTool.parameters,
|
|
7316
7389
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7317
7390
|
return grepTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7352,6 +7425,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7352
7425
|
name: "find",
|
|
7353
7426
|
label: "find",
|
|
7354
7427
|
description: findTool.description,
|
|
7428
|
+
promptSnippet: (findTool as any).promptSnippet,
|
|
7429
|
+
promptGuidelines: (findTool as any).promptGuidelines,
|
|
7355
7430
|
parameters: findTool.parameters,
|
|
7356
7431
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7357
7432
|
return findTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7403,6 +7478,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7403
7478
|
name: "ls",
|
|
7404
7479
|
label: "ls",
|
|
7405
7480
|
description: lsTool.description,
|
|
7481
|
+
promptSnippet: (lsTool as any).promptSnippet,
|
|
7482
|
+
promptGuidelines: (lsTool as any).promptGuidelines,
|
|
7406
7483
|
parameters: lsTool.parameters,
|
|
7407
7484
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7408
7485
|
return lsTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7454,6 +7531,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7454
7531
|
name: "write",
|
|
7455
7532
|
label: "write",
|
|
7456
7533
|
description: writeTool.description,
|
|
7534
|
+
promptSnippet: (writeTool as any).promptSnippet,
|
|
7535
|
+
promptGuidelines: (writeTool as any).promptGuidelines,
|
|
7457
7536
|
parameters: writeTool.parameters,
|
|
7458
7537
|
async execute(toolCallId, params, signal, onUpdate, _ctx) {
|
|
7459
7538
|
const fp = params.path ?? (params as any).file_path ?? "";
|
|
@@ -7569,6 +7648,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7569
7648
|
name: "edit",
|
|
7570
7649
|
label: "edit",
|
|
7571
7650
|
description: editTool.description,
|
|
7651
|
+
promptSnippet: (editTool as any).promptSnippet,
|
|
7652
|
+
promptGuidelines: (editTool as any).promptGuidelines,
|
|
7572
7653
|
parameters: editTool.parameters,
|
|
7573
7654
|
async execute(toolCallId, params, signal, onUpdate, _ctx) {
|
|
7574
7655
|
const fp = params.path ?? (params as any).file_path ?? "";
|
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.85",
|
|
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",
|
|
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",
|