gentle-pi 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +52 -15
- package/docs/readme-reference.md +67 -7
- package/docs/review-integration.md +22 -17
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +93 -18
- package/extensions/gentle-ai.ts +180 -37
- package/extensions/gentle-shell.ts +476 -39
- package/extensions/gentle-todo.ts +19 -1
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +297 -0
- package/lib/native-review-cli.ts +50 -10
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +256 -171
- package/lib/review-integration-v2.ts +114 -27
- package/lib/shell-bar.ts +163 -75
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +118 -16
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +103 -12
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +49 -9
- package/runtime/review-integration-v2.mjs +114 -27
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/verify-package-files.mjs +3 -4
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +10 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +599 -7
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +125 -9
- package/tests/gentle-shell.test.ts +1149 -24
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +460 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +297 -299
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +146 -1
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +129 -26
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +200 -124
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +301 -8
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +120 -1
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/lib/opaque-pi-reviewer-adapter.ts +0 -404
- package/skills/release/SKILL.md +0 -137
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
|
@@ -7,15 +7,20 @@ import {
|
|
|
7
7
|
parseAnthropicHeaders,
|
|
8
8
|
parseCodexHeaders,
|
|
9
9
|
parseNanQuota,
|
|
10
|
+
parseProviderUsage,
|
|
10
11
|
parseUsageHeaders,
|
|
11
12
|
parseCodexUsage,
|
|
13
|
+
parseUsageSource,
|
|
12
14
|
providerNote,
|
|
13
15
|
renderUsageBar,
|
|
14
16
|
renderUsagePanel,
|
|
15
17
|
SUPPORTED_USAGE_PROVIDERS,
|
|
18
|
+
UsageSourceRegistry,
|
|
19
|
+
USAGE_SOURCE_SCHEMA,
|
|
16
20
|
UsageStore,
|
|
17
21
|
windowLabel,
|
|
18
22
|
type ProviderUsage,
|
|
23
|
+
type UsageSource,
|
|
19
24
|
type UsageWindow,
|
|
20
25
|
} from "../lib/shell-usage.ts";
|
|
21
26
|
|
|
@@ -243,6 +248,130 @@ test("nan is a supported usage provider with its own pending note", () => {
|
|
|
243
248
|
assert.deepEqual(renderUsagePanel([], plainTheme, 100, NOW, { provider: "nan" }), ["✿ nan · no usage yet · r to fetch"]);
|
|
244
249
|
});
|
|
245
250
|
|
|
251
|
+
// A generic hook: any extension can register a usage source for its own
|
|
252
|
+
// provider at runtime, without gentle-shell knowing anything about it.
|
|
253
|
+
|
|
254
|
+
test("parseUsageSource validates the registration payload and ignores anything malformed", () => {
|
|
255
|
+
const fetchFn = async () => undefined;
|
|
256
|
+
const valid = parseUsageSource({ schema: USAGE_SOURCE_SCHEMA, provider: "acme-cloud", fetch: fetchFn });
|
|
257
|
+
assert.equal(valid?.schema, USAGE_SOURCE_SCHEMA);
|
|
258
|
+
assert.equal(valid?.provider, "acme-cloud");
|
|
259
|
+
assert.equal(valid?.fetch, fetchFn);
|
|
260
|
+
assert.equal(valid?.pendingNote, undefined);
|
|
261
|
+
|
|
262
|
+
const withNote = parseUsageSource({ schema: USAGE_SOURCE_SCHEMA, provider: "acme-cloud", pendingNote: "warming up", fetch: fetchFn });
|
|
263
|
+
assert.equal(withNote?.pendingNote, "warming up");
|
|
264
|
+
|
|
265
|
+
assert.equal(parseUsageSource({ schema: "gentle-pi.usage-source/v0", provider: "acme-cloud", fetch: fetchFn }), undefined, "wrong schema");
|
|
266
|
+
assert.equal(parseUsageSource({ schema: USAGE_SOURCE_SCHEMA, provider: "", fetch: fetchFn }), undefined, "empty provider");
|
|
267
|
+
assert.equal(parseUsageSource({ schema: USAGE_SOURCE_SCHEMA, provider: "acme cloud", fetch: fetchFn }), undefined, "unsafe provider id");
|
|
268
|
+
assert.equal(parseUsageSource({ schema: USAGE_SOURCE_SCHEMA, provider: "acme-cloud", fetch: "nope" }), undefined, "fetch not a function");
|
|
269
|
+
assert.equal(parseUsageSource({ schema: USAGE_SOURCE_SCHEMA, provider: "acme-cloud", pendingNote: 7, fetch: fetchFn }), undefined, "pendingNote must be a string when present");
|
|
270
|
+
assert.equal(parseUsageSource(null), undefined);
|
|
271
|
+
assert.equal(parseUsageSource(undefined), undefined);
|
|
272
|
+
assert.equal(parseUsageSource("acme-cloud"), undefined);
|
|
273
|
+
assert.equal(parseUsageSource({}), undefined);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("UsageSourceRegistry replaces a provider's source on re-registration", () => {
|
|
277
|
+
const registry = new UsageSourceRegistry();
|
|
278
|
+
assert.equal(registry.has("acme-cloud"), false);
|
|
279
|
+
assert.equal(registry.get("acme-cloud"), undefined);
|
|
280
|
+
assert.equal(registry.note("acme-cloud"), undefined);
|
|
281
|
+
|
|
282
|
+
const first: UsageSource = { schema: USAGE_SOURCE_SCHEMA, provider: "acme-cloud", fetch: async () => undefined };
|
|
283
|
+
registry.register(first);
|
|
284
|
+
assert.equal(registry.has("acme-cloud"), true);
|
|
285
|
+
assert.equal(registry.get("acme-cloud"), first);
|
|
286
|
+
assert.equal(registry.note("acme-cloud"), "no usage yet · r to fetch", "default note when the source sets none");
|
|
287
|
+
|
|
288
|
+
const second: UsageSource = { schema: USAGE_SOURCE_SCHEMA, provider: "acme-cloud", pendingNote: "still warming up", fetch: async () => undefined };
|
|
289
|
+
registry.register(second);
|
|
290
|
+
assert.equal(registry.get("acme-cloud"), second, "re-registration replaces, never accumulates");
|
|
291
|
+
assert.equal(registry.note("acme-cloud"), "still warming up");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a registered provider is supported without touching the built-in note map", () => {
|
|
295
|
+
const registry = new UsageSourceRegistry();
|
|
296
|
+
assert.equal(providerNote("acme-cloud"), "no subscription usage for this provider");
|
|
297
|
+
assert.equal(providerNote("acme-cloud", registry), "no subscription usage for this provider", "an empty registry changes nothing");
|
|
298
|
+
registry.register({ schema: USAGE_SOURCE_SCHEMA, provider: "acme-cloud", fetch: async () => undefined });
|
|
299
|
+
assert.equal(providerNote("acme-cloud", registry), "no usage yet · r to fetch");
|
|
300
|
+
assert.equal(providerNote("nan", registry), "no usage yet · r to fetch", "the static built-ins are unaffected");
|
|
301
|
+
assert.deepEqual(renderUsagePanel([], plainTheme, 100, NOW, { provider: "acme-cloud" }, registry), ["✿ acme-cloud · no usage yet · r to fetch"]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// A registered source's resolved value crosses the same trust boundary a
|
|
305
|
+
// parsed HTTP payload does. parseProviderUsage validates it exactly like
|
|
306
|
+
// one, and the object gentle-shell records is always freshly built, never
|
|
307
|
+
// the caller's own reference.
|
|
308
|
+
|
|
309
|
+
const VALID_SOURCE_USAGE = {
|
|
310
|
+
provider: "acme-cloud",
|
|
311
|
+
plan: "Acme · 42 credits",
|
|
312
|
+
fetchedAt: 100,
|
|
313
|
+
limits: [
|
|
314
|
+
{
|
|
315
|
+
name: "acme-cloud",
|
|
316
|
+
limitReached: false,
|
|
317
|
+
windows: [
|
|
318
|
+
{ label: "week", usedPercent: 40, windowSeconds: 604_800, resetAt: null },
|
|
319
|
+
{ label: "day", usedPercent: 12, windowSeconds: 86_400, resetAt: 200, used: 5, budget: 40 },
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
test("parseProviderUsage accepts a matching shape and copies it defensively", () => {
|
|
326
|
+
const source = structuredClone(VALID_SOURCE_USAGE);
|
|
327
|
+
const parsed = parseProviderUsage(source, "acme-cloud");
|
|
328
|
+
assert.deepEqual(parsed, source);
|
|
329
|
+
assert.notEqual(parsed, source, "the recorded object must never be the caller's own reference");
|
|
330
|
+
assert.notEqual(parsed?.limits, source.limits);
|
|
331
|
+
assert.notEqual(parsed?.limits[0], source.limits[0]);
|
|
332
|
+
assert.notEqual(parsed?.limits[0].windows, source.limits[0].windows);
|
|
333
|
+
assert.notEqual(parsed?.limits[0].windows[0], source.limits[0].windows[0]);
|
|
334
|
+
|
|
335
|
+
// Mutating the caller's own object after the fact must never reach the copy.
|
|
336
|
+
source.limits[0].windows[0].usedPercent = 999;
|
|
337
|
+
assert.equal(parsed?.limits[0].windows[0].usedPercent, 40);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("parseProviderUsage rejects a resolution for a different provider", () => {
|
|
341
|
+
assert.equal(parseProviderUsage(structuredClone(VALID_SOURCE_USAGE), "other-provider"), undefined);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("parseProviderUsage rejects a missing or malformed limits array", () => {
|
|
345
|
+
assert.equal(parseProviderUsage({ provider: "acme-cloud", plan: undefined, fetchedAt: 0 }, "acme-cloud"), undefined, "limits missing entirely");
|
|
346
|
+
assert.equal(parseProviderUsage({ provider: "acme-cloud", plan: undefined, fetchedAt: 0, limits: "nope" }, "acme-cloud"), undefined, "limits not an array");
|
|
347
|
+
assert.equal(
|
|
348
|
+
parseProviderUsage({ provider: "acme-cloud", plan: undefined, fetchedAt: 0, limits: [{ name: "x", limitReached: "nope", windows: [] }] }, "acme-cloud"),
|
|
349
|
+
undefined,
|
|
350
|
+
"limitReached must be a boolean",
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("parseProviderUsage rejects a non-numeric usedPercent inside a window", () => {
|
|
355
|
+
const usage = structuredClone(VALID_SOURCE_USAGE);
|
|
356
|
+
(usage.limits[0].windows[0] as unknown as { usedPercent: unknown }).usedPercent = "40";
|
|
357
|
+
assert.equal(parseProviderUsage(usage, "acme-cloud"), undefined);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("parseProviderUsage rejects a limit whose windows is not an array", () => {
|
|
361
|
+
const usage = structuredClone(VALID_SOURCE_USAGE);
|
|
362
|
+
(usage.limits[0] as unknown as { windows: unknown }).windows = "nope";
|
|
363
|
+
assert.equal(parseProviderUsage(usage, "acme-cloud"), undefined);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("parseProviderUsage rejects malformed inputs outright", () => {
|
|
367
|
+
assert.equal(parseProviderUsage(undefined, "acme-cloud"), undefined);
|
|
368
|
+
assert.equal(parseProviderUsage(null, "acme-cloud"), undefined);
|
|
369
|
+
assert.equal(parseProviderUsage("acme-cloud", "acme-cloud"), undefined);
|
|
370
|
+
assert.equal(parseProviderUsage({ provider: "acme-cloud", plan: 7, fetchedAt: 0, limits: [] }, "acme-cloud"), undefined, "plan must be a string when present");
|
|
371
|
+
assert.equal(parseProviderUsage({ provider: "acme-cloud", plan: undefined, fetchedAt: Number.POSITIVE_INFINITY, limits: [] }, "acme-cloud"), undefined, "fetchedAt must be finite");
|
|
372
|
+
assert.deepEqual(parseProviderUsage({ provider: "acme-cloud", plan: undefined, fetchedAt: 0, limits: [] }, "acme-cloud"), { provider: "acme-cloud", plan: undefined, fetchedAt: 0, limits: [] });
|
|
373
|
+
});
|
|
374
|
+
|
|
246
375
|
test("renderUsagePanel lists each NaN model allowance with its reset on the same row", () => {
|
|
247
376
|
const usage = parseNanQuota(NAN_QUOTA, NOW);
|
|
248
377
|
const lines = renderUsagePanel([usage], plainTheme, 80, NOW, { provider: "nan" });
|
|
@@ -25,7 +25,7 @@ const PREFIXED_NAMES: Record<string, string> = {
|
|
|
25
25
|
"work-unit-commits": "gentle-ai-work-unit-commits",
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const UNPREFIXED_DIRS = ["gentle-ai"
|
|
28
|
+
const UNPREFIXED_DIRS = ["gentle-ai"];
|
|
29
29
|
|
|
30
30
|
for (const [dir, expectedName] of Object.entries(PREFIXED_NAMES)) {
|
|
31
31
|
test(`skills/${dir}/SKILL.md frontmatter name is prefixed`, () => {
|
|
@@ -2,7 +2,9 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { syncBuiltinESMExports } from "node:module";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
6
8
|
import startup, { readGitBranch } from "../extensions/startup-banner.ts";
|
|
7
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
10
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -74,6 +76,15 @@ test("startup branch lookup uses direct git argv and hides its Windows child", a
|
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
test("startup banner keeps animating after invalidate and cleans up on dispose", async (t) => {
|
|
79
|
+
const home = mkdtempSync(join(tmpdir(), "gp-banner-quality-"));
|
|
80
|
+
writeFileSync(join(home, "animations.json"), '{"schema":"gentle-pi.animations/v1","policy":"quality"}');
|
|
81
|
+
const previousHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
82
|
+
process.env.GENTLE_PI_CONFIG_HOME = home;
|
|
83
|
+
t.after(() => {
|
|
84
|
+
if (previousHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
85
|
+
else process.env.GENTLE_PI_CONFIG_HOME = previousHome;
|
|
86
|
+
rmSync(home, { recursive: true, force: true });
|
|
87
|
+
});
|
|
77
88
|
t.mock.timers.enable({ apis: ["setTimeout", "setInterval", "Date"] });
|
|
78
89
|
t.mock.method(fs, "readFile", async () => JSON.stringify({ showRose: true, showTextLogo: true, color: "pink" }));
|
|
79
90
|
syncBuiltinESMExports();
|
|
@@ -115,6 +126,85 @@ test("startup banner keeps animating after invalidate and cleans up on dispose",
|
|
|
115
126
|
assert.equal(renders, afterDispose, "session_shutdown cleanup stays idle");
|
|
116
127
|
});
|
|
117
128
|
|
|
129
|
+
test("animation modes retain banner lifetime policy, final artwork and approximate duration", async (t) => {
|
|
130
|
+
const home = mkdtempSync(join(tmpdir(), "gp-banner-animations-"));
|
|
131
|
+
const previousHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
132
|
+
process.env.GENTLE_PI_CONFIG_HOME = home;
|
|
133
|
+
t.after(() => {
|
|
134
|
+
if (previousHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
135
|
+
else process.env.GENTLE_PI_CONFIG_HOME = previousHome;
|
|
136
|
+
rmSync(home, { recursive: true, force: true });
|
|
137
|
+
});
|
|
138
|
+
t.mock.method(fs, "readFile", async () => JSON.stringify({ showRose: true, showTextLogo: true, color: "pink" }));
|
|
139
|
+
syncBuiltinESMExports();
|
|
140
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
141
|
+
const argv = process.argv;
|
|
142
|
+
process.argv = ["node"];
|
|
143
|
+
t.after(() => { process.argv = argv; });
|
|
144
|
+
for (const [key, value] of [["rows", 40], ["columns", 200]] as const) {
|
|
145
|
+
const descriptor = Object.getOwnPropertyDescriptor(process.stdout, key);
|
|
146
|
+
Object.defineProperty(process.stdout, key, { configurable: true, writable: true, value });
|
|
147
|
+
t.after(() => descriptor ? Object.defineProperty(process.stdout, key, descriptor) : Reflect.deleteProperty(process.stdout, key));
|
|
148
|
+
}
|
|
149
|
+
let boot: () => void;
|
|
150
|
+
let pulse: () => void;
|
|
151
|
+
let active = false;
|
|
152
|
+
let delay = 0;
|
|
153
|
+
let clock = 0;
|
|
154
|
+
let paints = 0;
|
|
155
|
+
t.mock.method(Date, "now", () => clock);
|
|
156
|
+
t.mock.method(globalThis, "setTimeout", (callback: () => void, ms: number) => {
|
|
157
|
+
if (ms === 50) boot = callback; // Never run operational stats/home reads.
|
|
158
|
+
return {};
|
|
159
|
+
});
|
|
160
|
+
t.mock.method(globalThis, "setInterval", (callback: () => void, ms: number) => {
|
|
161
|
+
pulse = callback; delay = ms; active = true; return {};
|
|
162
|
+
});
|
|
163
|
+
t.mock.method(globalThis, "clearInterval", () => { active = false; });
|
|
164
|
+
let start: (event: unknown, ctx: unknown) => Promise<void>;
|
|
165
|
+
let shutdown: () => void;
|
|
166
|
+
let header: { render(width: number): string[]; dispose(): void };
|
|
167
|
+
// Stroke warmup is module-global; keep duration driving isolated from cold-start tests.
|
|
168
|
+
const { default: isolatedStartup } = await import(new URL("../extensions/startup-banner.ts?animations", import.meta.url).href) as typeof import("../extensions/startup-banner.ts");
|
|
169
|
+
isolatedStartup({ on: (name: string, fn: typeof start) => {
|
|
170
|
+
if (name === "session_start") start = fn;
|
|
171
|
+
if (name === "session_shutdown") shutdown = fn as unknown as () => void;
|
|
172
|
+
}, registerCommand() {}, getCommands: () => [], getAllTools: () => [] } as unknown as ExtensionAPI);
|
|
173
|
+
const ctx = { hasUI: true, cwd: "/fixture", ui: { setHeader(factory: (tui: unknown, theme: unknown) => typeof header) {
|
|
174
|
+
header = factory({ requestRender() { paints++; } }, { fg: (_role: string, text: string) => text });
|
|
175
|
+
} } };
|
|
176
|
+
const save = (policy: string) => writeFileSync(join(home, "animations.json"), JSON.stringify({ schema: "gentle-pi.animations/v1", policy }));
|
|
177
|
+
try {
|
|
178
|
+
save("potato");
|
|
179
|
+
await start!({}, ctx); boot!();
|
|
180
|
+
assert.equal(active, false, "potato starts no animation interval");
|
|
181
|
+
const staticArt = stripAnsi(header!.render(200).join("\n"));
|
|
182
|
+
assert.match(staticArt, /[▄▀█]/);
|
|
183
|
+
assert.match(staticArt, /[\u2800-\u28ff]/);
|
|
184
|
+
header!.dispose();
|
|
185
|
+
let qualityDuration = 0;
|
|
186
|
+
let qualityPaints = 0;
|
|
187
|
+
for (const policy of ["quality", "performance"]) {
|
|
188
|
+
save(policy);
|
|
189
|
+
await start!({}, ctx); boot!();
|
|
190
|
+
for (let i = 0; i < 15; i++) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
191
|
+
assert.equal(delay, policy === "quality" ? 25 : 250);
|
|
192
|
+
save("potato"); // Already-created animation keeps its policy.
|
|
193
|
+
const began = clock;
|
|
194
|
+
const before = paints;
|
|
195
|
+
while (active && clock - began <= 5500) { clock += delay; pulse!(); }
|
|
196
|
+
assert.equal(active, false, "finishes and clears the interval");
|
|
197
|
+
assert.equal(stripAnsi(header!.render(200).join("\n")), staticArt);
|
|
198
|
+
if (policy === "quality") { qualityDuration = clock - began; qualityPaints = paints - before; }
|
|
199
|
+
else {
|
|
200
|
+
assert.ok(Math.abs(clock - began - qualityDuration) <= 250);
|
|
201
|
+
assert.ok(paints - before <= Math.ceil(qualityPaints / 10));
|
|
202
|
+
}
|
|
203
|
+
header!.dispose();
|
|
204
|
+
}
|
|
205
|
+
} finally { shutdown!(); }
|
|
206
|
+
});
|
|
207
|
+
|
|
118
208
|
// Drive the real header factory; background git/home reads never run.
|
|
119
209
|
for (const showRose of [false, true]) for (const showTextLogo of [false, true]) {
|
|
120
210
|
test(`startup art respects rose=${showRose}, logo=${showTextLogo} and cyan palette`, async (t) => {
|
|
@@ -134,7 +224,8 @@ for (const showRose of [false, true]) for (const showTextLogo of [false, true])
|
|
|
134
224
|
let shutdown: Function;
|
|
135
225
|
let header: { render(width: number): string[]; dispose(): void };
|
|
136
226
|
const writes: string[] = [];
|
|
137
|
-
|
|
227
|
+
const { default: coldStartup } = await import(new URL(`../extensions/startup-banner.ts?art-${showRose}-${showTextLogo}`, import.meta.url).href) as typeof import("../extensions/startup-banner.ts");
|
|
228
|
+
coldStartup({ on: (name: string, fn: Function) => {
|
|
138
229
|
if (name === "session_start") start = fn;
|
|
139
230
|
if (name === "session_shutdown") shutdown = fn;
|
|
140
231
|
}, registerCommand() {}, getCommands: () => [], getAllTools: () => [] } as unknown as ExtensionAPI);
|
|
Binary file
|