pi-compact-tools 0.13.0 → 0.13.2
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 +4 -0
- package/extensions/compact-tools-activity.ts +10 -2
- package/extensions/compact-tools-color.ts +12 -8
- package/extensions/compact-tools-layout.ts +9 -8
- package/extensions/compact-tools-runtime.ts +16 -0
- package/extensions/compact-tools-types.ts +1 -0
- package/extensions/compact-tools-viewport.ts +5 -7
- package/extensions/compact-tools.ts +18 -6
- package/package.json +7 -5
- package/release-notes.json +10 -0
- package/scripts/check-resources.mjs +0 -82
package/README.md
CHANGED
|
@@ -79,6 +79,10 @@ npm run check
|
|
|
79
79
|
pi -e .
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
Development dependencies are pinned to Pi 1.0.0; the peer minimum remains Pi 0.85.1. CI checks Pi 0.85.1, 0.99.1, and 1.0.0 on Node 22.19.0 and 24. Schema validation and deterministic test clocks are development-only dependencies, not runtime dependencies. Development scripts and tests are not included in the published package. `npm test` runs the rendering tests first and the resource/packaging checks separately so their subprocesses do not disturb animation tests.
|
|
83
|
+
|
|
84
|
+
For a local before/after performance comparison, run `npx tsx tests/benchmark.ts <clean-baseline-directory>` with the same host dependencies in both directories. Measurements are informational, not timing-sensitive test assertions.
|
|
85
|
+
|
|
82
86
|
Renders are pinned in `tests/golden/render.json`. After an intended change to the output, record it with `UPDATE_GOLDEN=1 npm test` and review the diff. Before publishing, add the version's notes to `release-notes.json` (`[]` for none).
|
|
83
87
|
|
|
84
88
|
## License
|
|
@@ -264,8 +264,16 @@ export class SilentActivityAnimator {
|
|
|
264
264
|
else if (type === "thinking_start" || type === "toolcall_start") this.setAnswering(false);
|
|
265
265
|
});
|
|
266
266
|
pi.on("tool_execution_start", () => this.setAnswering(false));
|
|
267
|
-
pi.on("turn_start", () =>
|
|
268
|
-
|
|
267
|
+
pi.on("turn_start", () => {
|
|
268
|
+
if (!this.context) return;
|
|
269
|
+
this.running = true;
|
|
270
|
+
this.answering = false;
|
|
271
|
+
this.refresh();
|
|
272
|
+
});
|
|
273
|
+
// agent_end closes a low-level run, not retries/compaction/continuations.
|
|
274
|
+
// Keep the shared clock alive until Pi declares final settlement, as the
|
|
275
|
+
// working-label controller already does.
|
|
276
|
+
pi.on("agent_settled", () => {
|
|
269
277
|
if (!this.context) return;
|
|
270
278
|
this.running = false;
|
|
271
279
|
this.refresh();
|
|
@@ -6,14 +6,18 @@ import * as tui from "@earendil-works/pi-tui";
|
|
|
6
6
|
export type Rgb = { r: number; g: number; b: number };
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* What Pi 0.99 added to its Theme for extensions
|
|
10
|
-
* piece is read as optional and
|
|
9
|
+
* What Pi 0.99 added to its Theme for extensions, and what Pi 0.85 to 0.87 keep on
|
|
10
|
+
* theirs. Each piece is read as optional, and typed here rather than taken from Pi,
|
|
11
|
+
* so the extension builds and runs against every Pi it supports.
|
|
11
12
|
*/
|
|
12
|
-
type
|
|
13
|
-
|
|
13
|
+
type HostColor = object;
|
|
14
|
+
type HostTheme = {
|
|
15
|
+
colors?: Readonly<Record<string, HostColor | undefined>>;
|
|
14
16
|
appearance?: "dark" | "light";
|
|
17
|
+
fgColors?: unknown;
|
|
15
18
|
};
|
|
16
|
-
const
|
|
19
|
+
const host = (theme: Theme) => theme as unknown as HostTheme;
|
|
20
|
+
const colorToRgb = (tui as unknown as { colorToRgb?: (color: HostColor) => Rgb }).colorToRgb;
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* A theme token's concrete color. Pi 0.99 resolves every token, including one set
|
|
@@ -22,7 +26,7 @@ const colorToRgb = (tui as Partial<typeof tui>).colorToRgb;
|
|
|
22
26
|
* nothing rather than throwing, since callers compute names from host state.
|
|
23
27
|
*/
|
|
24
28
|
export function themeColorRgb(theme: Theme, color: ThemeColor): Rgb | undefined {
|
|
25
|
-
const { colors } = theme
|
|
29
|
+
const { colors } = host(theme);
|
|
26
30
|
if (colors && colorToRgb) {
|
|
27
31
|
const concrete = colors[color];
|
|
28
32
|
if (!concrete) return undefined;
|
|
@@ -119,7 +123,7 @@ export function rgbPainter(theme: Theme, color: Rgb): (text: string) => string {
|
|
|
119
123
|
* neither is told apart by the escape sequences of the colors derived from.
|
|
120
124
|
*/
|
|
121
125
|
export function paletteOf(theme: Theme): object {
|
|
122
|
-
const { colors, fgColors } = theme
|
|
126
|
+
const { colors, fgColors } = host(theme);
|
|
123
127
|
if (colors) return colors;
|
|
124
128
|
if (fgColors instanceof Map) return fgColors;
|
|
125
129
|
let key = theme.getColorMode?.() ?? "";
|
|
@@ -164,7 +168,7 @@ const WHITE: Rgb = { r: 255, g: 255, b: 255 };
|
|
|
164
168
|
|
|
165
169
|
/** The theme's declared appearance; before Pi 0.99, a theme with light text is a dark one. */
|
|
166
170
|
function isDark(theme: Theme): boolean {
|
|
167
|
-
const { appearance } = theme
|
|
171
|
+
const { appearance } = host(theme);
|
|
168
172
|
if (appearance) return appearance === "dark";
|
|
169
173
|
const text = themeColorRgb(theme, "text");
|
|
170
174
|
return !text || luminanceRgb(text) > 128;
|
|
@@ -358,14 +358,11 @@ function highlightCodeLines(lines: CodeDiffLine[], language: string | undefined,
|
|
|
358
358
|
}
|
|
359
359
|
let end = index;
|
|
360
360
|
while (end < lines.length && lines[end]!.kind !== "separator") end++;
|
|
361
|
-
const
|
|
362
|
-
const contents = block.map((line) => line.content);
|
|
361
|
+
const contents = lines.slice(index, end).map((line) => line.content);
|
|
363
362
|
const highlighted = language === "markdown"
|
|
364
363
|
? highlightMarkdown(contents, theme)
|
|
365
364
|
: highlightCode(contents.join("\n"), language);
|
|
366
|
-
|
|
367
|
-
rendered[index + offset] = highlighted[offset] ?? line.content;
|
|
368
|
-
});
|
|
365
|
+
for (let offset = 0; offset < contents.length; offset++) rendered[index + offset] = highlighted[offset] ?? contents[offset]!;
|
|
369
366
|
index = end;
|
|
370
367
|
}
|
|
371
368
|
return rendered;
|
|
@@ -411,14 +408,18 @@ function renderCodeRows(lines: CodeDiffLine[], path: string, theme: Theme, optio
|
|
|
411
408
|
// read at an offset or a diff hunk never reads one out of a comment.
|
|
412
409
|
const first = lines[0];
|
|
413
410
|
const shebangLine = first?.lineNumber === 1 && first.kind !== "separator" ? first.content : undefined;
|
|
414
|
-
|
|
415
|
-
|
|
411
|
+
// ToolExecutionComponent builds result components even when silent mode hides
|
|
412
|
+
// the row. Defer syntax work until the code is actually drawn, and keep it
|
|
413
|
+
// across width changes so resize does not highlight the same source again.
|
|
414
|
+
let highlighted: string[] | undefined;
|
|
415
|
+
const numberWidth = lines.reduce((maximum, line) => Math.max(maximum, String(line.lineNumber ?? "").length), 1);
|
|
416
416
|
const prefix = options.rail ?? paintChrome(theme, " \u2502 ");
|
|
417
417
|
const prefixWidth = visibleWidth(prefix);
|
|
418
418
|
const gutterWidth = numberWidth + (options.signs ? 3 : 2);
|
|
419
419
|
const addedTint = options.signs ? diffTintRgb(theme, "toolDiffAdded") : undefined;
|
|
420
420
|
const removedTint = options.signs ? diffTintRgb(theme, "toolDiffRemoved") : undefined;
|
|
421
421
|
return new CachedComponent((width) => {
|
|
422
|
+
highlighted ??= highlightCodeLines(lines, resolveLanguage(path, shebangLine), theme);
|
|
422
423
|
const bodyWidth = Math.max(1, width - prefixWidth);
|
|
423
424
|
const codeWidth = Math.max(1, bodyWidth - gutterWidth);
|
|
424
425
|
const output: string[] = [];
|
|
@@ -428,7 +429,7 @@ function renderCodeRows(lines: CodeDiffLine[], path: string, theme: Theme, optio
|
|
|
428
429
|
return;
|
|
429
430
|
}
|
|
430
431
|
const tint = line.kind === "add" ? addedTint : line.kind === "remove" ? removedTint : undefined;
|
|
431
|
-
for (const [chunkIndex, chunk] of hardSliceAnsi(highlighted[index] ?? line.content, codeWidth).entries()) {
|
|
432
|
+
for (const [chunkIndex, chunk] of hardSliceAnsi(highlighted![index] ?? line.content, codeWidth).entries()) {
|
|
432
433
|
const gutter = codeGutter(
|
|
433
434
|
line.kind,
|
|
434
435
|
chunkIndex === 0 ? line.lineNumber : undefined,
|
|
@@ -207,12 +207,14 @@ export class ToolRuntime {
|
|
|
207
207
|
*/
|
|
208
208
|
noteExecutionStart(toolCallId: string): void {
|
|
209
209
|
if (!this.executionTimings.has(toolCallId)) this.executionTimings.set(toolCallId, { startedAt: Date.now() });
|
|
210
|
+
this.trimTimings();
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
noteExecutionEnd(toolCallId: string): void {
|
|
213
214
|
const timing = this.executionTimings.get(toolCallId) ?? { startedAt: Date.now() };
|
|
214
215
|
timing.endedAt ??= Date.now();
|
|
215
216
|
this.executionTimings.set(toolCallId, timing);
|
|
217
|
+
this.trimTimings();
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
createTimedExecute(definition: BuiltInDefinition): TimedExecute {
|
|
@@ -222,6 +224,7 @@ export class ToolRuntime {
|
|
|
222
224
|
const startedAt = (toolCallId ? this.executionTimings.get(toolCallId)?.startedAt : undefined) ?? Date.now();
|
|
223
225
|
if (toolCallId && !this.executionTimings.has(toolCallId)) {
|
|
224
226
|
this.executionTimings.set(toolCallId, { startedAt });
|
|
227
|
+
this.trimTimings();
|
|
225
228
|
}
|
|
226
229
|
try {
|
|
227
230
|
return await execute.apply(definition, args);
|
|
@@ -295,7 +298,20 @@ export class ToolRuntime {
|
|
|
295
298
|
timing.startedAt = state.startedAt;
|
|
296
299
|
if (state.endedAt !== undefined) timing.endedAt = state.endedAt;
|
|
297
300
|
this.executionTimings.set(toolCallId, timing);
|
|
301
|
+
this.trimTimings();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Bound every insertion, including calls never drawn (for example in silent mode). */
|
|
305
|
+
private trimTimings(): void {
|
|
298
306
|
if (this.executionTimings.size <= MAX_TRACKED_ROWS) return;
|
|
307
|
+
// Prefer settled calls so a long-running sibling keeps its start time. If all
|
|
308
|
+
// 2,001 calls are active, retain the original FIFO fallback to stay bounded.
|
|
309
|
+
for (const [id, timing] of this.executionTimings) {
|
|
310
|
+
if (timing.endedAt !== undefined) {
|
|
311
|
+
this.executionTimings.delete(id);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
299
315
|
const oldest = this.executionTimings.keys().next().value;
|
|
300
316
|
if (oldest !== undefined) this.executionTimings.delete(oldest);
|
|
301
317
|
}
|
|
@@ -89,17 +89,15 @@ export function findAnchorTop(
|
|
|
89
89
|
else starts.set(line, [index]);
|
|
90
90
|
});
|
|
91
91
|
const bottom = Math.min(before.length, top + Math.max(1, height));
|
|
92
|
-
|
|
93
|
-
for (let
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const window = before.slice(row, row + ANCHOR_LINES);
|
|
97
|
-
if (window.length < ANCHOR_LINES || !window.some(hasText)) continue;
|
|
92
|
+
// Visit the viewport first, then preceding rows, without copying candidate windows.
|
|
93
|
+
for (let candidate = 0; candidate < bottom; candidate++) {
|
|
94
|
+
const row = candidate < bottom - top ? top + candidate : bottom - candidate - 1;
|
|
95
|
+
if (row + ANCHOR_LINES > before.length || (!hasText(before[row]) && !hasText(before[row + 1]))) continue;
|
|
98
96
|
// Several places can match a short window; the one that keeps matching for
|
|
99
97
|
// longest is the same text, and distance only breaks a remaining tie.
|
|
100
98
|
let best: number | undefined;
|
|
101
99
|
let bestRun = 0;
|
|
102
|
-
for (const start of starts.get(
|
|
100
|
+
for (const start of starts.get(before[row]!) ?? []) {
|
|
103
101
|
const run = matchingRun(before, row, after, start, TIE_BREAK_LINES);
|
|
104
102
|
if (run < ANCHOR_LINES) continue;
|
|
105
103
|
if (best === undefined || run > bestRun
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import type { Component } from "@earendil-works/pi-tui";
|
|
20
20
|
import { classifyCallStatus, formatDurationMs, normalizeLineEndings, type RowStatus } from "./compact-tools-core.ts";
|
|
21
21
|
import { loadConfig } from "./compact-tools-config.ts";
|
|
22
|
+
import { paletteOf } from "./compact-tools-color.ts";
|
|
22
23
|
import { installToolRowPatch, setRowResolver, type RowRenderers, type ToolRow } from "./compact-tools-custom.ts";
|
|
23
24
|
import {
|
|
24
25
|
formatResultLineSummary,
|
|
@@ -128,6 +129,7 @@ function canReuseResult<TArgs>(
|
|
|
128
129
|
state: RowState,
|
|
129
130
|
result: AgentToolResult<unknown>,
|
|
130
131
|
options: ToolRenderResultOptions,
|
|
132
|
+
theme: Theme,
|
|
131
133
|
ctx: RenderContext<TArgs>,
|
|
132
134
|
): ctx is RenderContext<TArgs> & { lastComponent: Component } {
|
|
133
135
|
return ctx.lastComponent !== undefined
|
|
@@ -137,10 +139,11 @@ function canReuseResult<TArgs>(
|
|
|
137
139
|
&& state.lastResultExpanded === state.expanded
|
|
138
140
|
&& state.lastResultPreview === state.preview
|
|
139
141
|
&& state.lastResultError === ctx.isError
|
|
140
|
-
&& state.lastResultConfigRevision === state.configRevision
|
|
142
|
+
&& state.lastResultConfigRevision === state.configRevision
|
|
143
|
+
&& state.lastResultPalette === paletteOf(theme);
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
function rememberResult(state: RowState, result: AgentToolResult<unknown>, options: ToolRenderResultOptions, isError: boolean): void {
|
|
146
|
+
function rememberResult(state: RowState, result: AgentToolResult<unknown>, options: ToolRenderResultOptions, isError: boolean, theme: Theme): void {
|
|
144
147
|
state.lastResultContent = result.content;
|
|
145
148
|
state.lastResultDetails = result.details;
|
|
146
149
|
state.lastResultPartial = options.isPartial;
|
|
@@ -148,6 +151,7 @@ function rememberResult(state: RowState, result: AgentToolResult<unknown>, optio
|
|
|
148
151
|
state.lastResultPreview = state.preview;
|
|
149
152
|
state.lastResultError = isError;
|
|
150
153
|
state.lastResultConfigRevision = state.configRevision;
|
|
154
|
+
state.lastResultPalette = paletteOf(theme);
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
function renderControls(
|
|
@@ -376,7 +380,7 @@ function renderRowResult(
|
|
|
376
380
|
): Component {
|
|
377
381
|
const state = runtime.syncRow(ctx, !options.isPartial);
|
|
378
382
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
379
|
-
if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
|
|
383
|
+
if (canReuseResult(state, result, options, theme, ctx)) return ctx.lastComponent;
|
|
380
384
|
const kind = rowKind(name);
|
|
381
385
|
const output = kind === "file" ? getFileOutput(name, ctx.args, result, ctx.isError) : getTextResult(result);
|
|
382
386
|
const hasEditDiff = name === "edit" && getEditDiff(result).length > 0;
|
|
@@ -402,7 +406,7 @@ function renderRowResult(
|
|
|
402
406
|
name === "edit" ? countEditChanges(result) : undefined));
|
|
403
407
|
component = container;
|
|
404
408
|
}
|
|
405
|
-
rememberResult(state, result, options, ctx.isError);
|
|
409
|
+
rememberResult(state, result, options, ctx.isError, theme);
|
|
406
410
|
return component;
|
|
407
411
|
}
|
|
408
412
|
|
|
@@ -503,6 +507,14 @@ function noteCalls(message: unknown): void {
|
|
|
503
507
|
}
|
|
504
508
|
}
|
|
505
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Whether another tool made the call through `ctx.executeTool()`, as codemode scripts
|
|
512
|
+
* do. Pi 0.99 marks such calls with `parentToolCallId`; older Pi never makes them.
|
|
513
|
+
*/
|
|
514
|
+
function madeByTool(event: object): boolean {
|
|
515
|
+
return typeof (event as { parentToolCallId?: unknown }).parentToolCallId === "string";
|
|
516
|
+
}
|
|
517
|
+
|
|
506
518
|
/** Hand the animation Pi's frame request, reached through an invisible widget as the extension API allows. */
|
|
507
519
|
function bindRenderer(ctx: ExtensionContext): void {
|
|
508
520
|
ctx.ui.setWidget(RENDER_WIDGET_KEY, (tui) => {
|
|
@@ -572,12 +584,12 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
572
584
|
// their rows, and told to extensions before Pi makes them.
|
|
573
585
|
pi.on("message_update", (event) => noteCalls(event.message));
|
|
574
586
|
pi.on("tool_execution_start", (event) => {
|
|
575
|
-
if (event
|
|
587
|
+
if (madeByTool(event)) return runtime.noteNestedCall(event.toolCallId, true);
|
|
576
588
|
runtime.noteCall(event.toolCallId);
|
|
577
589
|
runtime.noteExecutionStart(event.toolCallId);
|
|
578
590
|
});
|
|
579
591
|
pi.on("tool_execution_end", (event) => {
|
|
580
|
-
if (event
|
|
592
|
+
if (madeByTool(event)) return runtime.noteNestedCall(event.toolCallId, false);
|
|
581
593
|
runtime.noteExecutionEnd(event.toolCallId);
|
|
582
594
|
});
|
|
583
595
|
pi.on("session_shutdown", (event) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-compact-tools",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Compact, expandable tool rows for Pi in its own compact style or Claude Code's, with a silent mode and a GitHub Dark theme.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,14 +24,13 @@
|
|
|
24
24
|
"extensions",
|
|
25
25
|
"themes",
|
|
26
26
|
"examples",
|
|
27
|
-
"scripts",
|
|
28
27
|
"schemas",
|
|
29
28
|
"release-notes.json",
|
|
30
29
|
"README.md",
|
|
31
30
|
"LICENSE"
|
|
32
31
|
],
|
|
33
32
|
"scripts": {
|
|
34
|
-
"test": "tsx --test tests/*.test.ts",
|
|
33
|
+
"test": "tsx --test tests/*.test.ts && tsx --test tests/resources.validation.ts",
|
|
35
34
|
"check": "tsc --noEmit && npm test && npm run check:resources",
|
|
36
35
|
"check:resources": "node scripts/check-resources.mjs",
|
|
37
36
|
"prepublishOnly": "npm run check"
|
|
@@ -50,9 +49,12 @@
|
|
|
50
49
|
"@earendil-works/pi-tui": ">=0.85.1"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
54
|
-
"@earendil-works/pi-tui": "0.
|
|
52
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
53
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
54
|
+
"@sinonjs/fake-timers": "15.0.0",
|
|
55
55
|
"@types/node": "^24.0.0",
|
|
56
|
+
"@types/sinonjs__fake-timers": "15.0.1",
|
|
57
|
+
"ajv": "8.20.0",
|
|
56
58
|
"tsx": "^4.23.13",
|
|
57
59
|
"typescript": "^5.9.0"
|
|
58
60
|
},
|
package/release-notes.json
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
+
"0.13.2": [
|
|
3
|
+
"Verified on Pi 1.0.0 while keeping compatibility with Pi 0.85.1 and later",
|
|
4
|
+
"Silent activity keeps animating through retries and follow-up turns until final settlement",
|
|
5
|
+
"Hidden file results defer syntax highlighting until their code is actually shown",
|
|
6
|
+
"Completed tool results repaint correctly when the theme changes",
|
|
7
|
+
"Large file views render without exceeding JavaScript argument limits",
|
|
8
|
+
"Fullscreen viewport anchoring and code-row layout allocate less work",
|
|
9
|
+
"Execution timing history stays bounded even when tool rows are not drawn"
|
|
10
|
+
],
|
|
11
|
+
"0.13.1": [],
|
|
2
12
|
"0.13.0": [
|
|
3
13
|
"Follows Pi 0.99's system theme and light terminals: rails, dots, and the working label take their colors from the theme itself",
|
|
4
14
|
"Running dots, rails, and silent mode's activity line are painted once per theme instead of every frame",
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
const resources = [
|
|
4
|
-
"themes/github-dark-pro.json",
|
|
5
|
-
"examples/compact-tools.json",
|
|
6
|
-
"examples/compact-tools-2.json",
|
|
7
|
-
"schemas/compact-tools.schema.json",
|
|
8
|
-
"release-notes.json",
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
for (const path of resources) {
|
|
12
|
-
const value = JSON.parse(await readFile(new URL(`../${path}`, import.meta.url), "utf8"));
|
|
13
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
-
throw new Error(`${path} must contain a JSON object`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const theme = JSON.parse(
|
|
19
|
-
await readFile(new URL("../themes/github-dark-pro.json", import.meta.url), "utf8"),
|
|
20
|
-
);
|
|
21
|
-
if (theme.name !== "github-dark-pro" || !theme.colors || typeof theme.colors !== "object") {
|
|
22
|
-
throw new Error("themes/github-dark-pro.json is not a valid pi theme document");
|
|
23
|
-
}
|
|
24
|
-
if (!theme.colors.border || !theme.colors.dim || !theme.colors.muted) {
|
|
25
|
-
throw new Error("github-dark-pro must define separator colors");
|
|
26
|
-
}
|
|
27
|
-
if (!theme.vars?.thinkingText || !theme.colors.thinkingMax) {
|
|
28
|
-
throw new Error("github-dark-pro must define thinking text and summary colors");
|
|
29
|
-
}
|
|
30
|
-
if (!theme.colors.mdQuote || !theme.colors.mdCodeBlockBorder) {
|
|
31
|
-
throw new Error("github-dark-pro must define thinking detail and code fence colors");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const example = JSON.parse(
|
|
35
|
-
await readFile(new URL("../examples/compact-tools.json", import.meta.url), "utf8"),
|
|
36
|
-
);
|
|
37
|
-
if (!example.auto_compact || example.auto_compact.edit !== false) {
|
|
38
|
-
throw new Error("example auto_compact policy must keep edit diffs visible by default");
|
|
39
|
-
}
|
|
40
|
-
if (Object.values(example.auto_compact).some((enabled) => typeof enabled !== "boolean")) {
|
|
41
|
-
throw new Error("example auto_compact values must be booleans");
|
|
42
|
-
}
|
|
43
|
-
if (!Number.isInteger(example.previewLines) || example.previewLines < 1 || example.previewLines > 100) {
|
|
44
|
-
throw new Error("example previewLines must be an integer from 1 to 100");
|
|
45
|
-
}
|
|
46
|
-
const alternateExample = JSON.parse(
|
|
47
|
-
await readFile(new URL("../examples/compact-tools-2.json", import.meta.url), "utf8"),
|
|
48
|
-
);
|
|
49
|
-
for (const [name, config] of [["compact-tools", example], ["compact-tools-2", alternateExample]]) {
|
|
50
|
-
if ("durationIndicators" in config) {
|
|
51
|
-
throw new Error(`${name} must not contain the removed durationIndicators setting`);
|
|
52
|
-
}
|
|
53
|
-
if ("spinner" in config) {
|
|
54
|
-
throw new Error(`${name} must not contain the removed spinner setting`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const schema = JSON.parse(
|
|
59
|
-
await readFile(new URL("../schemas/compact-tools.schema.json", import.meta.url), "utf8"),
|
|
60
|
-
);
|
|
61
|
-
if (schema.properties?.durationIndicators) {
|
|
62
|
-
throw new Error("schema must not expose the removed durationIndicators setting");
|
|
63
|
-
}
|
|
64
|
-
if (schema.properties?.spinner) {
|
|
65
|
-
throw new Error("schema must not expose the removed spinner setting");
|
|
66
|
-
}
|
|
67
|
-
if (!schema.properties?.auto_compact?.properties?.edit) {
|
|
68
|
-
throw new Error("schema must describe per-tool auto_compact policies");
|
|
69
|
-
}
|
|
70
|
-
if (schema.properties?.previewLines?.default !== 10) {
|
|
71
|
-
throw new Error("schema must expose the bounded previewLines setting");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
75
|
-
const releaseNotes = JSON.parse(await readFile(new URL("../release-notes.json", import.meta.url), "utf8"));
|
|
76
|
-
// Every published version needs an entry. An empty list is a quiet release that shows no notice.
|
|
77
|
-
if (!Array.isArray(releaseNotes[manifest.version]) ||
|
|
78
|
-
releaseNotes[manifest.version].some((note) => typeof note !== "string" || !note.trim())) {
|
|
79
|
-
throw new Error(`release-notes.json must have an entry for v${manifest.version} before publishing; use [] for no notice`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
console.log("Resource checks passed");
|