gentle-pi 2.5.0 → 2.6.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 +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import { sidebarPart, sidebarState } from "../lib/shell-sidebar.ts";
|
|
5
|
+
|
|
6
|
+
const host = (terminal?: object) => ({ terminal }) as TUI;
|
|
7
|
+
const component = () => ({ render: (_width = 80) => ["bottom"], invalidate() {} });
|
|
8
|
+
|
|
9
|
+
test("unsupported hosts retain the original bottom widget and disposal", () => {
|
|
10
|
+
for (const tui of [host(), host(null as unknown as object)]) {
|
|
11
|
+
let disposed = 0;
|
|
12
|
+
const bottom = { ...component(), dispose() { disposed++; } };
|
|
13
|
+
const part = sidebarPart(tui, "todo", bottom);
|
|
14
|
+
assert.equal(part, bottom);
|
|
15
|
+
assert.deepEqual(part.render(), ["bottom"]);
|
|
16
|
+
part.dispose();
|
|
17
|
+
assert.equal(disposed, 1);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("terminal state survives host replacement without leaking across terminals", () => {
|
|
22
|
+
const terminal = {};
|
|
23
|
+
assert.equal(sidebarState(host(terminal)), sidebarState(host(terminal)));
|
|
24
|
+
assert.notEqual(sidebarState(host(terminal)), sidebarState(host({})));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("bottom paint is suppressed only while the sidebar owns the host", () => {
|
|
28
|
+
const tui = host({});
|
|
29
|
+
const state = sidebarState(tui);
|
|
30
|
+
const part = sidebarPart(tui, "todo", component());
|
|
31
|
+
state.active = true;
|
|
32
|
+
assert.deepEqual(part.render(80), ["bottom"]);
|
|
33
|
+
state.ownsHost = () => true;
|
|
34
|
+
assert.deepEqual(part.render(80), []);
|
|
35
|
+
state.active = false;
|
|
36
|
+
assert.deepEqual(part.render(80), ["bottom"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("disposing an old part preserves its replacement and releases its bottom", () => {
|
|
40
|
+
const tui = host({});
|
|
41
|
+
let disposed = 0;
|
|
42
|
+
const first = sidebarPart(tui, "todo", { ...component(), dispose() { disposed++; } });
|
|
43
|
+
const replacement = component();
|
|
44
|
+
const second = sidebarPart(tui, "todo", replacement);
|
|
45
|
+
first.dispose?.();
|
|
46
|
+
assert.equal(disposed, 1);
|
|
47
|
+
assert.equal(sidebarState(tui).parts.get("todo"), replacement);
|
|
48
|
+
(second as typeof second & { dispose?(): void }).dispose?.();
|
|
49
|
+
assert.equal(sidebarState(tui).parts.size, 0);
|
|
50
|
+
});
|
package/tests/shell-todo.test.ts
CHANGED
|
@@ -134,7 +134,7 @@ test("renderTodoCard draws the framed list with status glyphs and keeps every li
|
|
|
134
134
|
const lines = renderTodoCard(seeded(), plainTheme, 60, { collapsed: false, staleTurns: 0, collapseKey: "ctrl+shift+t" });
|
|
135
135
|
for (const line of lines) assert.equal(visibleWidth(line), 60, `"${stripAnsi(line)}" is not 60 wide`);
|
|
136
136
|
const plain = lines.map(stripAnsi);
|
|
137
|
-
assert.match(plain[0], /^╭─ ❀ Todos · 1 of 3
|
|
137
|
+
assert.match(plain[0], /^╭─ ❀ Todos ▾ Collapse · 1 of 3 ─+ ctrl\+shift\+t collapse ╮$/);
|
|
138
138
|
assert.match(plain[1], /^│ ✓ ~Add quiet tool rendering~ +│$/);
|
|
139
139
|
assert.match(plain[2], /^│ ◐ Fix quiet tools conflict · fixing conflict +│$/);
|
|
140
140
|
assert.match(plain[3], /^│ ○ Show git bash tails +│$/);
|
|
@@ -146,7 +146,7 @@ test("renderTodoCard folds a long list: done tasks become one row and the open o
|
|
|
146
146
|
const state = applyTodo(emptyTodo(), { action: "write", tasks }, 1).state;
|
|
147
147
|
const plain = renderTodoCard(state, plainTheme, 60, { collapsed: false, staleTurns: 0 }).map(stripAnsi);
|
|
148
148
|
assert.equal(plain.length, 14, "top rule, twelve rows, bottom rule");
|
|
149
|
-
assert.match(plain[0], /Todos · 25 of 40/);
|
|
149
|
+
assert.match(plain[0], /Todos ▾ Collapse · 25 of 40/);
|
|
150
150
|
assert.match(plain[1], /^│ ✓ 25 done +│$/);
|
|
151
151
|
assert.match(plain[2], /^│ ◐ Task 26 +│$/);
|
|
152
152
|
assert.match(plain[11], /^│ ○ Task 35 +│$/);
|
|
@@ -157,14 +157,103 @@ test("renderTodoCard folds a long list: done tasks become one row and the open o
|
|
|
157
157
|
assert.match(folded[1], /^│ ✓ 40 done +│$/);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
test("renderTodoCard
|
|
161
|
-
const
|
|
162
|
-
assert.match(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
assert.
|
|
166
|
-
assert.match(
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
test("renderTodoCard keeps the configured collapse shortcut in the header while stale state remains visible", () => {
|
|
161
|
+
const freshExpanded = renderTodoCard(seeded(), plainTheme, 70, { collapsed: false, staleTurns: 0, collapseKey: "ctrl+shift+t" }).map(stripAnsi);
|
|
162
|
+
assert.match(freshExpanded[0], /^╭─ ❀ Todos ▾ Collapse · 1 of 3 ─+ ctrl\+shift\+t collapse ╮$/);
|
|
163
|
+
|
|
164
|
+
const freshCollapsed = renderTodoCard(seeded(), plainTheme, 70, { collapsed: true, staleTurns: 0, collapseKey: "ctrl+shift+t" }).map(stripAnsi);
|
|
165
|
+
assert.equal(freshCollapsed.length, 3);
|
|
166
|
+
assert.match(freshCollapsed[0], /^╭─ ❀ Todos ▸ Expand · 1 of 3 ─+ ctrl\+shift\+t expand ╮$/);
|
|
167
|
+
assert.match(freshCollapsed[1], /^│ ◐ Fix quiet tools conflict · fixing conflict +│$/);
|
|
168
|
+
|
|
169
|
+
const staleExpanded = renderTodoCard(seeded(), plainTheme, 70, { collapsed: false, staleTurns: 2, collapseKey: "ctrl+shift+t" }).map(stripAnsi);
|
|
170
|
+
assert.match(staleExpanded[0], /^╭─ ❀ Todos ▾ Collapse · 1 of 3 ─+ ctrl\+shift\+t collapse ╮$/);
|
|
171
|
+
assert.match(staleExpanded[1], /^│ stale · 2 turns +│$/);
|
|
172
|
+
|
|
173
|
+
const staleCollapsed = renderTodoCard(seeded(), plainTheme, 70, { collapsed: true, staleTurns: 2, collapseKey: "ctrl+shift+t" }).map(stripAnsi);
|
|
174
|
+
assert.match(staleCollapsed[0], /^╭─ ❀ Todos ▸ Expand · 1 of 3 ─+ ctrl\+shift\+t expand ╮$/);
|
|
175
|
+
assert.match(staleCollapsed[1], /^│ stale · 2 turns +│$/);
|
|
176
|
+
assert.match(staleCollapsed[2], /^│ ◐ Fix quiet tools conflict · fixing conflict +│$/);
|
|
177
|
+
|
|
178
|
+
const fallback = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "Finished first", status: "done" }, { title: "First pending" }, { title: "Later pending" }] }, 1).state;
|
|
179
|
+
assert.match(renderTodoCard(fallback, plainTheme, 70, { collapsed: true, staleTurns: 0 }).map(stripAnsi)[1], /^│ ○ First pending +│$/);
|
|
180
|
+
|
|
181
|
+
const activeAfterPending = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "Pending first" }, { title: "Active second", status: "in_progress" }, { title: "Pending third" }] }, 1).state;
|
|
182
|
+
assert.match(renderTodoCard(activeAfterPending, plainTheme, 70, { collapsed: true, staleTurns: 0 }).map(stripAnsi)[1], /^│ ◐ Active second +│$/);
|
|
169
183
|
assert.deepEqual(renderTodoCard(emptyTodo(), plainTheme, 70, { collapsed: false, staleTurns: 0 }), []);
|
|
170
184
|
});
|
|
185
|
+
|
|
186
|
+
test("renderTodoCard keeps English controls visible and falls back to their icons at narrow widths", () => {
|
|
187
|
+
const expanded = renderTodoCard(seeded(), plainTheme, 70, { collapsed: false, staleTurns: 0, collapseKey: "ctrl+shift+t" }).map(stripAnsi);
|
|
188
|
+
assert.match(expanded[0], /Todos ▾ Collapse/);
|
|
189
|
+
assert.match(expanded[0], /ctrl\+shift\+t collapse/);
|
|
190
|
+
const collapsed = renderTodoCard(seeded(), plainTheme, 70, { collapsed: true, staleTurns: 0, collapseKey: "ctrl+shift+t" }).map(stripAnsi);
|
|
191
|
+
assert.match(collapsed[0], /Todos ▸ Expand/);
|
|
192
|
+
for (const [collapsedState, icon] of [[false, "▾"], [true, "▸"]] as const) {
|
|
193
|
+
const line = renderTodoCard(seeded(), plainTheme, 16, { collapsed: collapsedState, staleTurns: 0, collapseKey: "ctrl+shift+t" })[0]!;
|
|
194
|
+
assert.match(stripAnsi(line), new RegExp(`Todos ${icon}`));
|
|
195
|
+
assert.equal(visibleWidth(line), 16);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("completed titles strike only title cells across wrapped lines, never padding or rails", () => {
|
|
200
|
+
const theme = {
|
|
201
|
+
fg: (_color: string, text: string) => `\x1b[32m${text}\x1b[39m`,
|
|
202
|
+
strikethrough: (text: string) => `\x1b[9m${text}\x1b[29m`,
|
|
203
|
+
};
|
|
204
|
+
const state = applyTodo(emptyTodo(), { action: "write", tasks: [
|
|
205
|
+
{ title: "Alpha beta gamma delta epsilon", status: "done" },
|
|
206
|
+
{ title: "Pending", status: "pending" },
|
|
207
|
+
] }, 1).state;
|
|
208
|
+
for (const width of [12, 20, 60]) {
|
|
209
|
+
const lines = renderTodoCard(state, theme, width, { collapsed: false, staleTurns: 0 });
|
|
210
|
+
let struckLetters = "";
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
let strike = false, column = 0;
|
|
213
|
+
const plain = stripAnsi(line);
|
|
214
|
+
const titleEnd = plain.slice(0, -1).trimEnd().length;
|
|
215
|
+
for (const token of line.match(/\x1b\[[\d;]*m|[^\x1b]/gu) ?? []) {
|
|
216
|
+
if (token.startsWith("\x1b")) {
|
|
217
|
+
for (const code of token.slice(2, -1).split(";").map(Number)) {
|
|
218
|
+
if (code === 0 || code === 29) strike = false;
|
|
219
|
+
if (code === 9) strike = true;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
if (column < 2 || column >= titleEnd || /[│╭╮╰╯─✓]/u.test(token)) assert.equal(strike, false, `struck frame/padding: ${JSON.stringify(line)}`);
|
|
223
|
+
if (strike && /[A-Za-z]/.test(token)) struckLetters += token;
|
|
224
|
+
column += visibleWidth(token);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
assert.equal(strike, false, "SGR 9 must close before the host appends padding");
|
|
228
|
+
assert.equal(visibleWidth(line), width);
|
|
229
|
+
}
|
|
230
|
+
assert.equal(struckLetters, "Alphabetagammadeltaepsilon");
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("renderTodoCard keeps stale indicators and collapse hints in scrollable lists", () => {
|
|
235
|
+
const tasks = Array.from({ length: 40 }, (_, index) => ({ title: `Task ${index + 1}`, status: index < 25 ? "done" : "pending" }));
|
|
236
|
+
const state = applyTodo(emptyTodo(), { action: "write", tasks }, 1).state;
|
|
237
|
+
const lines = renderTodoCard(state, plainTheme, 70, { scrollable: true, collapsed: false, staleTurns: 2, collapseKey: "alt+t" }).map(stripAnsi);
|
|
238
|
+
assert.equal(lines.length, 43, "top rule, stale row, every task, bottom rule");
|
|
239
|
+
assert.match(lines[0], /^╭─ ❀ Todos ▾ Collapse · 25 of 40 ─+ alt\+t collapse ╮$/);
|
|
240
|
+
assert.match(lines[1], /^│ stale · 2 turns +│$/);
|
|
241
|
+
assert.match(lines[2], /^│ ✓ ~Task 1~ +│$/);
|
|
242
|
+
assert.match(lines[41], /^│ ○ Task 40 +│$/);
|
|
243
|
+
for (const line of lines) assert.equal(visibleWidth(line), 70);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("renderTodoCard uses custom shortcuts, omits disabled shortcuts, and remains width-safe when hints cannot fit", () => {
|
|
247
|
+
const custom = renderTodoCard(seeded(), plainTheme, 70, { collapsed: false, staleTurns: 2, collapseKey: "alt+t" }).map(stripAnsi);
|
|
248
|
+
assert.match(custom[0], /alt\+t collapse/);
|
|
249
|
+
assert.match(custom[1], /stale · 2 turns/);
|
|
250
|
+
|
|
251
|
+
const disabled = renderTodoCard(seeded(), plainTheme, 70, { collapsed: true, staleTurns: 2 }).map(stripAnsi);
|
|
252
|
+
assert.doesNotMatch(disabled[0], /(?:collapse|expand|ctrl\+shift\+t)/);
|
|
253
|
+
assert.match(disabled[1], /stale · 2 turns/);
|
|
254
|
+
|
|
255
|
+
for (const width of [0, 1, 2, 3, 4, 5, 8, 16]) {
|
|
256
|
+
const lines = renderTodoCard(seeded(), plainTheme, width, { collapsed: false, staleTurns: 2, collapseKey: "ctrl+shift+t" });
|
|
257
|
+
for (const line of lines) assert.equal(visibleWidth(line), width, `"${stripAnsi(line)}" is not ${width} wide`);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import startup, { readGitBranch } from "../extensions/startup-banner.ts";
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
9
|
+
|
|
10
|
+
test("startup branch lookup uses direct git argv and hides its Windows child", async () => {
|
|
11
|
+
const calls: Array<{ command: string; args: readonly string[]; options: Record<string, unknown> }> = [];
|
|
12
|
+
const run = ((command: string, args: readonly string[], options: Record<string, unknown>, callback: (error: Error | null, stdout: string) => void) => {
|
|
13
|
+
calls.push({ command, args, options });
|
|
14
|
+
callback(null, "main\n");
|
|
15
|
+
}) as typeof import("node:child_process").execFile;
|
|
16
|
+
assert.equal(await readGitBranch("/repo with spaces & metacharacters", run), "On branch main");
|
|
17
|
+
assert.deepEqual(calls, [{
|
|
18
|
+
command: "git",
|
|
19
|
+
args: ["-C", "/repo with spaces & metacharacters", "branch", "--show-current"],
|
|
20
|
+
options: { encoding: "utf8", shell: false, windowsHide: true },
|
|
21
|
+
}]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("startup banner keeps animating after invalidate and cleans up on dispose", async (t) => {
|
|
25
|
+
t.mock.timers.enable({ apis: ["setTimeout", "setInterval", "Date"] });
|
|
26
|
+
t.mock.method(fs, "readFile", async () => JSON.stringify({ showRose: true, showTextLogo: true, color: "pink" }));
|
|
27
|
+
syncBuiltinESMExports();
|
|
28
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
29
|
+
const argv = process.argv;
|
|
30
|
+
process.argv = ["node"];
|
|
31
|
+
t.after(() => { process.argv = argv; });
|
|
32
|
+
for (const [key, value] of [["rows", 40], ["columns", 160]] as const) {
|
|
33
|
+
const descriptor = Object.getOwnPropertyDescriptor(process.stdout, key);
|
|
34
|
+
Object.defineProperty(process.stdout, key, { configurable: true, writable: true, value });
|
|
35
|
+
t.after(() => descriptor ? Object.defineProperty(process.stdout, key, descriptor) : Reflect.deleteProperty(process.stdout, key));
|
|
36
|
+
}
|
|
37
|
+
let start: Function;
|
|
38
|
+
let shutdown: Function;
|
|
39
|
+
let header: { render(width: number): string[]; invalidate(): void; dispose(): void };
|
|
40
|
+
let renders = 0;
|
|
41
|
+
startup({ on: (name: string, fn: Function) => {
|
|
42
|
+
if (name === "session_start") start = fn;
|
|
43
|
+
if (name === "session_shutdown") shutdown = fn;
|
|
44
|
+
}, registerCommand() {}, getCommands: () => [], getAllTools: () => [] } as unknown as ExtensionAPI);
|
|
45
|
+
await start!({}, { hasUI: true, cwd: "/fixture", ui: { setHeader: (factory: Function) => {
|
|
46
|
+
header = factory({ requestRender() { renders++; } }, { fg: (_role: string, text: string) => text });
|
|
47
|
+
} } });
|
|
48
|
+
t.mock.timers.tick(50);
|
|
49
|
+
const afterBoot = renders;
|
|
50
|
+
t.mock.timers.tick(25);
|
|
51
|
+
assert.ok(renders > afterBoot, "animation timer requests renders");
|
|
52
|
+
header!.invalidate();
|
|
53
|
+
const afterInvalidate = renders;
|
|
54
|
+
t.mock.timers.tick(25);
|
|
55
|
+
assert.ok(renders > afterInvalidate, "invalidate() must not stop the animation timer");
|
|
56
|
+
header!.dispose();
|
|
57
|
+
const afterDispose = renders;
|
|
58
|
+
t.mock.timers.tick(25);
|
|
59
|
+
assert.equal(renders, afterDispose, "dispose() stops the animation timer");
|
|
60
|
+
shutdown!();
|
|
61
|
+
t.mock.timers.tick(25);
|
|
62
|
+
assert.equal(renders, afterDispose, "session_shutdown cleanup stays idle");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Drive the real header factory; background git/home reads never run.
|
|
66
|
+
for (const showRose of [false, true]) for (const showTextLogo of [false, true]) {
|
|
67
|
+
test(`startup art respects rose=${showRose}, logo=${showTextLogo} and cyan palette`, async (t) => {
|
|
68
|
+
t.mock.timers.enable({ apis: ["setTimeout", "setInterval", "Date"] });
|
|
69
|
+
t.mock.method(fs, "readFile", async () => JSON.stringify({ showRose, showTextLogo, color: "cyan" }));
|
|
70
|
+
syncBuiltinESMExports();
|
|
71
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
72
|
+
const argv = process.argv;
|
|
73
|
+
process.argv = ["node"];
|
|
74
|
+
t.after(() => { process.argv = argv; });
|
|
75
|
+
for (const [key, value] of [["rows", 40], ["columns", 160]] as const) {
|
|
76
|
+
const descriptor = Object.getOwnPropertyDescriptor(process.stdout, key);
|
|
77
|
+
Object.defineProperty(process.stdout, key, { configurable: true, writable: true, value });
|
|
78
|
+
t.after(() => descriptor ? Object.defineProperty(process.stdout, key, descriptor) : Reflect.deleteProperty(process.stdout, key));
|
|
79
|
+
}
|
|
80
|
+
let start: Function;
|
|
81
|
+
let shutdown: Function;
|
|
82
|
+
let header: { render(width: number): string[]; dispose(): void };
|
|
83
|
+
const writes: string[] = [];
|
|
84
|
+
startup({ on: (name: string, fn: Function) => {
|
|
85
|
+
if (name === "session_start") start = fn;
|
|
86
|
+
if (name === "session_shutdown") shutdown = fn;
|
|
87
|
+
}, registerCommand() {}, getCommands: () => [], getAllTools: () => [] } as unknown as ExtensionAPI);
|
|
88
|
+
const write = t.mock.method(process.stdout, "write", (text: string) => { writes.push(String(text)); return true; });
|
|
89
|
+
await start!({}, { hasUI: true, cwd: "/fixture", ui: { setHeader: (factory: Function) => {
|
|
90
|
+
header = factory({ requestRender() {} }, { fg: (_role: string, text: string) => text });
|
|
91
|
+
} } });
|
|
92
|
+
t.mock.timers.tick(50);
|
|
93
|
+
try {
|
|
94
|
+
for (const width of [40, 80, 160]) {
|
|
95
|
+
const lines = header!.render(width);
|
|
96
|
+
assert.ok(lines.every((line) => visibleWidth(line) <= width));
|
|
97
|
+
const text = stripAnsi(lines.join("\n"));
|
|
98
|
+
assert.match(text, /GIT:/);
|
|
99
|
+
assert.match(text, /PATH:/);
|
|
100
|
+
if (width === 160) {
|
|
101
|
+
assert.equal(/[\u2800-\u28ff]/.test(text), showRose);
|
|
102
|
+
assert.equal(/[▒▄▀█]/.test(text), showTextLogo);
|
|
103
|
+
}
|
|
104
|
+
assert.match(lines.join("\n"), /\x1b\[38;2;85;170;205m/, "startup labels use the saved cyan palette");
|
|
105
|
+
}
|
|
106
|
+
// Cancel pending context reads before advancing the resize clock.
|
|
107
|
+
t.mock.timers.reset();
|
|
108
|
+
t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: Date.now() + 1000 });
|
|
109
|
+
process.stdout.rows = 10;
|
|
110
|
+
process.stdout.emit("resize");
|
|
111
|
+
t.mock.timers.tick(150);
|
|
112
|
+
assert.deepEqual(header!.render(80), []);
|
|
113
|
+
process.stdout.rows = 25;
|
|
114
|
+
process.stdout.columns = 80;
|
|
115
|
+
process.stdout.emit("resize");
|
|
116
|
+
t.mock.timers.tick(150);
|
|
117
|
+
const minimal = stripAnsi(header!.render(80).join("\n"));
|
|
118
|
+
assert.doesNotMatch(minimal, /[\u2800-\u28ff]/);
|
|
119
|
+
assert.equal(/[▒▄▀█]/.test(minimal), showTextLogo);
|
|
120
|
+
assert.deepEqual(writes, [], "Pi owns stdout during startup and resize");
|
|
121
|
+
} finally {
|
|
122
|
+
shutdown!();
|
|
123
|
+
write.mock.restore();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -73,7 +73,7 @@ test("shouldTriggerTelemetry: pure predicate table", () => {
|
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test("spawnTelemetryTrigger: launches the expected argv, detached and stdio-ignored", () => {
|
|
76
|
+
test("spawnTelemetryTrigger: launches the expected argv, detached, hidden, and stdio-ignored", () => {
|
|
77
77
|
const fake = fakeSpawn();
|
|
78
78
|
const result = spawnTelemetryTrigger({
|
|
79
79
|
executable: "/opt/gentle-ai/gentle-ai",
|
|
@@ -88,6 +88,7 @@ test("spawnTelemetryTrigger: launches the expected argv, detached and stdio-igno
|
|
|
88
88
|
assert.deepEqual(call.args, ["telemetry", "trigger", "--json"]);
|
|
89
89
|
assert.equal(call.options.cwd, "/work/project");
|
|
90
90
|
assert.equal(call.options.detached, true);
|
|
91
|
+
assert.equal(call.options.windowsHide, true);
|
|
91
92
|
assert.equal(call.options.stdio, "ignore");
|
|
92
93
|
});
|
|
93
94
|
|
|
@@ -212,6 +213,7 @@ test("activation: spawns the telemetry trigger exactly once for a primary sessio
|
|
|
212
213
|
assert.deepEqual(call.args, ["telemetry", "trigger", "--json"]);
|
|
213
214
|
assert.equal(call.options.cwd, "/work/project");
|
|
214
215
|
assert.equal(call.options.detached, true);
|
|
216
|
+
assert.equal(call.options.windowsHide, true);
|
|
215
217
|
assert.equal(call.options.stdio, "ignore");
|
|
216
218
|
});
|
|
217
219
|
|