gentle-pi 3.7.0 → 4.0.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.
Files changed (316) hide show
  1. package/README.md +37 -6
  2. package/assets/agents/gentle-ai-explore.md +4 -4
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -9
  5. package/assets/orchestrator-delegation.md +40 -41
  6. package/assets/orchestrator-memory.md +1 -22
  7. package/assets/orchestrator-skills.md +1 -1
  8. package/assets/orchestrator.md +12 -24
  9. package/assets/support/strict-tdd-verify.md +4 -266
  10. package/assets/support/strict-tdd.md +8 -360
  11. package/bin/gentle-shell.mjs +254 -29
  12. package/docs/delegated-verification.md +26 -1
  13. package/docs/gentle-agents-activity.md +24 -0
  14. package/docs/gentle-shell.md +92 -28
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +112 -206
  18. package/docs/telemetry.md +1 -1
  19. package/docs/yolo-mode.md +86 -0
  20. package/extensions/child-context.ts +26 -0
  21. package/extensions/child-safety.ts +23 -0
  22. package/extensions/gentle-agents.ts +429 -262
  23. package/extensions/gentle-ai.ts +781 -683
  24. package/extensions/gentle-shell.ts +1375 -88
  25. package/extensions/gentle-stats.ts +101 -0
  26. package/extensions/gentle-todo.ts +18 -7
  27. package/extensions/history/atomic-write.ts +38 -0
  28. package/extensions/history/hide-prompts.ts +183 -0
  29. package/extensions/history/index.ts +1419 -0
  30. package/extensions/history/load-shared-history.ts +39 -0
  31. package/extensions/history/selector-helpers.ts +538 -0
  32. package/extensions/history/session-scan.ts +233 -0
  33. package/extensions/history/store.ts +1119 -0
  34. package/extensions/nan-provider.ts +6 -0
  35. package/extensions/quiet-tools.ts +179 -87
  36. package/extensions/resume-hint.ts +60 -0
  37. package/extensions/skill-registry.ts +16 -12
  38. package/extensions/startup-banner.ts +60 -31
  39. package/lib/agent-assets.ts +604 -0
  40. package/lib/agent-profile-pin.ts +12 -0
  41. package/lib/agents-message-delivery.ts +181 -0
  42. package/lib/agents-protocol.ts +60 -0
  43. package/lib/agents-runner.ts +96 -98
  44. package/lib/agents-view.ts +26 -3
  45. package/lib/agents-widget.ts +16 -9
  46. package/lib/append-system-prompt.ts +21 -0
  47. package/lib/bounded-writer-admission.ts +147 -0
  48. package/lib/card-style-policy.ts +60 -0
  49. package/lib/child-context-files.ts +166 -0
  50. package/lib/codemode-renderer.ts +185 -0
  51. package/lib/command-palette-catalog.ts +3 -9
  52. package/lib/command-palette.ts +25 -14
  53. package/lib/destructive-command-guard.ts +144 -0
  54. package/lib/gentle-ai-elapsed-store.ts +87 -0
  55. package/lib/gentle-ai-renderer.ts +196 -39
  56. package/lib/gentle-shell-launcher.ts +24 -13
  57. package/lib/gentle-shell-resume-hint.ts +176 -0
  58. package/lib/history-capture-policy.ts +95 -0
  59. package/lib/model-routing-authority.ts +5 -1
  60. package/lib/nan-provider.ts +227 -0
  61. package/lib/native-review-cli.ts +49 -108
  62. package/lib/odd-phase-inference.ts +231 -0
  63. package/lib/odd-phase.ts +141 -0
  64. package/lib/overlay-repaint.ts +26 -0
  65. package/lib/pi-tui-keys.ts +53 -0
  66. package/lib/review-candidate-view-owner.ts +67 -17
  67. package/lib/review-candidate-view.ts +112 -25
  68. package/lib/review-reminder-receipt.ts +48 -8
  69. package/lib/review-risk-assessment.ts +156 -11
  70. package/lib/review-sidebar-state.ts +223 -0
  71. package/lib/selection-engine.ts +515 -0
  72. package/lib/session-messaging-grants.ts +135 -0
  73. package/lib/session-worktree-registry.ts +14 -2
  74. package/lib/shell-bar.ts +179 -24
  75. package/lib/shell-card.ts +287 -18
  76. package/lib/shell-changes-view.ts +2 -1
  77. package/lib/shell-prompt.ts +102 -6
  78. package/lib/shell-sidebar-layout.ts +78 -14
  79. package/lib/shell-sidebar.ts +15 -1
  80. package/lib/shell-todo.ts +23 -13
  81. package/lib/shell-usage-view.ts +9 -4
  82. package/lib/shell-usage.ts +66 -10
  83. package/lib/stats-collector.ts +381 -0
  84. package/lib/stats-view.ts +431 -0
  85. package/lib/theme-customization.ts +52 -0
  86. package/lib/vim-editor-adapter.ts +379 -0
  87. package/lib/vim-normal-engine.ts +154 -0
  88. package/lib/vim-operator-engine.ts +416 -0
  89. package/lib/vim-policy.ts +49 -0
  90. package/lib/vim-visual-engine.ts +107 -0
  91. package/lib/visual-customization-policy.ts +108 -0
  92. package/lib/visual-customize-view.ts +330 -0
  93. package/lib/visual-profiles.ts +228 -0
  94. package/lib/yolo-session-policy.ts +240 -0
  95. package/package.json +20 -8
  96. package/runtime/gentle-shell-launcher.mjs +23 -12
  97. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  98. package/runtime/native-review-cli.mjs +49 -108
  99. package/runtime/review-risk-assessment.mjs +154 -9
  100. package/scripts/build-runtime-modules.mjs +1 -0
  101. package/scripts/gentle-ai-installer.mjs +14 -13
  102. package/scripts/mirror-odd-routing.mjs +2 -2
  103. package/scripts/run-test-suite.mjs +76 -0
  104. package/scripts/test-packed-runner.mjs +31 -14
  105. package/scripts/verify-package-files.mjs +11 -22
  106. package/skills/branch-pr/SKILL.md +24 -52
  107. package/skills/chained-pr/SKILL.md +31 -15
  108. package/skills/chained-pr/references/chaining-details.md +31 -20
  109. package/skills/gentle-ai/SKILL.md +9 -15
  110. package/skills/issue-creation/SKILL.md +8 -2
  111. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  112. package/skills/work-unit-commits/SKILL.md +4 -3
  113. package/tests/agent-profiles.test.ts +18 -0
  114. package/tests/agents-fake-child.ts +2 -2
  115. package/tests/agents-message-delivery.test.ts +106 -0
  116. package/tests/agents-protocol.test.ts +40 -0
  117. package/tests/agents-runner.test.ts +378 -89
  118. package/tests/agents-view-thread-identity.test.ts +169 -0
  119. package/tests/agents-view.test.ts +8 -2
  120. package/tests/agents-widget.test.ts +154 -15
  121. package/tests/append-system-prompt-route.test.ts +160 -0
  122. package/tests/append-system-prompt.test.ts +46 -0
  123. package/tests/artifact-language.test.ts +19 -213
  124. package/tests/ask-user-question.test.ts +44 -1
  125. package/tests/asset-installation-runtime.test.ts +5 -16
  126. package/tests/autonomous-guard.test.ts +69 -1
  127. package/tests/bounded-writer-admission.test.ts +95 -0
  128. package/tests/branch-pr-skill.test.ts +43 -0
  129. package/tests/card-style-policy.test.ts +55 -0
  130. package/tests/chained-pr-skill.test.ts +124 -0
  131. package/tests/child-context-files.test.ts +255 -0
  132. package/tests/child-safety.test.ts +82 -0
  133. package/tests/codemode-rendering.test.ts +491 -0
  134. package/tests/command-palette.test.ts +39 -3
  135. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  136. package/tests/destructive-command-guard.test.ts +84 -0
  137. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  138. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  139. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  140. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  145. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  146. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  148. package/tests/generic-agent-tools.test.ts +54 -0
  149. package/tests/gentle-agents.test.ts +1047 -252
  150. package/tests/gentle-ai-binary.test.ts +3 -3
  151. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  152. package/tests/gentle-ai-installer.test.ts +68 -54
  153. package/tests/gentle-ai-renderer.test.ts +487 -8
  154. package/tests/gentle-ai.test.ts +288 -65
  155. package/tests/gentle-card-text.ts +2 -1
  156. package/tests/gentle-shell-bin.test.ts +651 -114
  157. package/tests/gentle-shell-launcher.test.ts +99 -52
  158. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  159. package/tests/gentle-shell.test.ts +3839 -187
  160. package/tests/gentle-stats.test.ts +152 -0
  161. package/tests/gentle-theme.test.ts +4 -1
  162. package/tests/gentle-todo.test.ts +80 -8
  163. package/tests/history-atomic-write.test.ts +57 -0
  164. package/tests/history-capture-policy.test.ts +102 -0
  165. package/tests/history-command-registration.test.ts +164 -0
  166. package/tests/history-dedupe-entries.test.ts +123 -0
  167. package/tests/history-delete-backfill.test.ts +190 -0
  168. package/tests/history-delete-confirm.test.ts +460 -0
  169. package/tests/history-dispatch.test.ts +180 -0
  170. package/tests/history-drain-hidden.test.ts +110 -0
  171. package/tests/history-drain-order.test.ts +98 -0
  172. package/tests/history-expanded-globals.test.ts +62 -0
  173. package/tests/history-gc.test.ts +832 -0
  174. package/tests/history-header-layout.test.ts +265 -0
  175. package/tests/history-hide-prompts.test.ts +275 -0
  176. package/tests/history-lazy-windowing.test.ts +508 -0
  177. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  178. package/tests/history-load-shared-history.test.ts +53 -0
  179. package/tests/history-max-results-cap.test.ts +76 -0
  180. package/tests/history-multi-reader.test.ts +203 -0
  181. package/tests/history-off-path.test.ts +170 -0
  182. package/tests/history-openflow-integration.test.ts +173 -0
  183. package/tests/history-overlay-margin.test.ts +326 -0
  184. package/tests/history-preview-layout.test.ts +93 -0
  185. package/tests/history-registry.test.ts +143 -0
  186. package/tests/history-scope-delete.test.ts +411 -0
  187. package/tests/history-search-caret-keys.test.ts +142 -0
  188. package/tests/history-seed-bootstrap.test.ts +170 -0
  189. package/tests/history-seed-regen.test.ts +129 -0
  190. package/tests/history-selector-windowing.test.ts +94 -0
  191. package/tests/history-session-scan-directory.test.ts +87 -0
  192. package/tests/history-session-scan-extract.test.ts +583 -0
  193. package/tests/history-session-writer.test.ts +351 -0
  194. package/tests/history-store-paths.test.ts +79 -0
  195. package/tests/history-tombstone-exact.test.ts +139 -0
  196. package/tests/history-wheel-mouse.test.ts +242 -0
  197. package/tests/inprocess-reviewer.test.ts +29 -19
  198. package/tests/issue-creation-skill.test.ts +61 -0
  199. package/tests/model-routing-authority.test.ts +16 -0
  200. package/tests/nan-provider.test.ts +471 -0
  201. package/tests/native-review-capability-contract.test.ts +7 -1
  202. package/tests/native-review-cli.test.ts +6 -120
  203. package/tests/native-review-parity-runtime.test.ts +100 -3
  204. package/tests/odd-integration.test.ts +67 -0
  205. package/tests/odd-phase-inference.test.ts +213 -0
  206. package/tests/odd-phase-loader.test.ts +253 -0
  207. package/tests/odd-phase.test.ts +307 -0
  208. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  209. package/tests/odd-routing-contract.test.ts +86 -35
  210. package/tests/orchestrator-budget.test.ts +14 -39
  211. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  212. package/tests/overlay-repaint.test.ts +74 -0
  213. package/tests/package-manifest.test.ts +252 -115
  214. package/tests/packed-runner-owned-path.test.ts +46 -0
  215. package/tests/persona-single-channel.test.ts +6 -6
  216. package/tests/provider-defect-handoff.test.ts +3 -11
  217. package/tests/quiet-bash-runtime.test.ts +76 -0
  218. package/tests/quiet-tool-rendering.test.ts +409 -184
  219. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  220. package/tests/rdd-status-line.test.ts +9 -4
  221. package/tests/resume-hint-extension.test.ts +122 -0
  222. package/tests/review-agent-end-preflight.test.ts +176 -12
  223. package/tests/review-candidate-owner-retry.test.ts +22 -1
  224. package/tests/review-candidate-view.test.ts +298 -0
  225. package/tests/review-contract-prompt.test.ts +108 -43
  226. package/tests/review-controller-lock-status.test.ts +0 -1
  227. package/tests/review-controller-native-routing.test.ts +611 -5
  228. package/tests/review-controller-workspace-root.test.ts +163 -4
  229. package/tests/review-host-relay-routing.test.ts +338 -2
  230. package/tests/review-integration-v2-forward.test.ts +200 -0
  231. package/tests/review-ledger-contract.test.ts +10 -34
  232. package/tests/review-reminder-receipt.test.ts +47 -1
  233. package/tests/review-risk-assessment.test.ts +498 -6
  234. package/tests/review-sidebar-state.test.ts +402 -0
  235. package/tests/run-test-suite.test.ts +124 -0
  236. package/tests/runtime-harness.mjs +145 -786
  237. package/tests/runtime-metrics-children.test.ts +16 -23
  238. package/tests/selection-engine.test.ts +421 -0
  239. package/tests/session-messaging-grants.test.ts +255 -0
  240. package/tests/session-worktree-registry.test.ts +77 -0
  241. package/tests/shell-bar.test.ts +382 -1
  242. package/tests/shell-card.test.ts +353 -1
  243. package/tests/shell-changes-view.test.ts +52 -0
  244. package/tests/shell-prompt.test.ts +94 -2
  245. package/tests/shell-sidebar-layout.test.ts +325 -21
  246. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  247. package/tests/shell-todo.test.ts +87 -1
  248. package/tests/shell-usage-view.test.ts +27 -0
  249. package/tests/shell-usage.test.ts +73 -0
  250. package/tests/skill-registry.test.ts +50 -1
  251. package/tests/startup-banner.test.ts +130 -2
  252. package/tests/stats-collector.test.ts +195 -0
  253. package/tests/stats-view.test.ts +202 -0
  254. package/tests/telemetry-trigger.test.ts +81 -20
  255. package/tests/theme-customization.test.ts +72 -0
  256. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  257. package/tests/vim-editor-adapter.test.ts +804 -0
  258. package/tests/vim-normal-engine.test.ts +101 -0
  259. package/tests/vim-operator-engine.test.ts +215 -0
  260. package/tests/vim-policy.test.ts +19 -0
  261. package/tests/vim-visual-engine.test.ts +52 -0
  262. package/tests/visual-customization-policy.test.ts +110 -0
  263. package/tests/visual-customize-view.test.ts +418 -0
  264. package/tests/visual-profiles.test.ts +87 -0
  265. package/tests/yolo-customize.test.ts +256 -0
  266. package/tests/yolo-mode-runtime.test.ts +161 -0
  267. package/tests/yolo-mode.test.ts +261 -0
  268. package/tests/yolo-session-policy.test.ts +59 -0
  269. package/themes/Gentle.json +2 -1
  270. package/themes/Gentleman-Cute.json +2 -1
  271. package/themes/Gentleman-Sexy.json +2 -1
  272. package/assets/agents/sdd-apply.md +0 -159
  273. package/assets/agents/sdd-archive.md +0 -228
  274. package/assets/agents/sdd-design.md +0 -49
  275. package/assets/agents/sdd-explore.md +0 -48
  276. package/assets/agents/sdd-init.md +0 -56
  277. package/assets/agents/sdd-onboard.md +0 -52
  278. package/assets/agents/sdd-proposal.md +0 -64
  279. package/assets/agents/sdd-remediate.md +0 -37
  280. package/assets/agents/sdd-research.md +0 -49
  281. package/assets/agents/sdd-spec.md +0 -192
  282. package/assets/agents/sdd-status.md +0 -54
  283. package/assets/agents/sdd-tasks.md +0 -108
  284. package/assets/agents/sdd-verify.md +0 -124
  285. package/assets/chains/sdd-full.chain.md +0 -83
  286. package/assets/chains/sdd-plan.chain.md +0 -56
  287. package/assets/chains/sdd-verify.chain.md +0 -43
  288. package/assets/sdd-orchestrator-workflow.md +0 -319
  289. package/assets/support/sdd-status-contract.md +0 -77
  290. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  291. package/extensions/sdd-init.ts +0 -816
  292. package/lib/openspec-deltas.ts +0 -156
  293. package/lib/sdd-preflight.ts +0 -1066
  294. package/lib/sdd-research-capabilities.ts +0 -94
  295. package/lib/sdd-status.ts +0 -26
  296. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  297. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  298. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  299. package/tests/openspec-deltas.test.ts +0 -209
  300. package/tests/sdd-agent-tools.test.ts +0 -156
  301. package/tests/sdd-archive-replay.test.ts +0 -82
  302. package/tests/sdd-classical-continuation.test.ts +0 -74
  303. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  304. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  305. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  306. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  307. package/tests/sdd-odd-integration.test.ts +0 -33
  308. package/tests/sdd-optional-research.test.ts +0 -124
  309. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  310. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  311. package/tests/sdd-preflight.test.ts +0 -541
  312. package/tests/sdd-research-capabilities.test.ts +0 -114
  313. package/tests/sdd-research-live.test.ts +0 -241
  314. package/tests/sdd-selection-transport.test.ts +0 -653
  315. package/tests/sdd-status.test.ts +0 -9
  316. package/tests/sdd-task-truth.test.ts +0 -43
@@ -0,0 +1,202 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { fileURLToPath } from "node:url";
4
+ import { visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
5
+ import { stripAnsi } from "../lib/terminal-theme.ts";
6
+ import { createStatsLoader, type CurrentSessionStats, type SessionRecord } from "../lib/stats-collector.ts";
7
+ import { StatsView, type StatsViewDeps } from "../lib/stats-view.ts";
8
+
9
+ // The /gentle:stats panel: Overview / Models / Session tabs over the
10
+ // collector, with range and scope toggles, q/esc close, and a clickable
11
+ // footer. Rendered against the collector fixtures with a fixed clock.
12
+
13
+ const SESSIONS = fileURLToPath(new URL("./fixtures/stats/sessions", import.meta.url));
14
+ const NOW = Date.parse("2026-10-01T12:00:00.000Z");
15
+ const ROWS = 40;
16
+ const plainTheme = { fg: (_role: string, text: string) => text };
17
+ // Zero-width role markers: one 256-color SGR per role, so widths stay honest.
18
+ const ROLES = ["accent", "borderMuted"];
19
+ const roleCode = (role: string) => `\x1b[38;5;${ROLES.indexOf(role) + 1}m`;
20
+ const roleTheme = { fg: (role: string, text: string) => `${roleCode(role)}${text}\x1b[39m` };
21
+ const CURRENT: CurrentSessionStats = {
22
+ totals: { input: 10, output: 5, cacheRead: 100, cacheWrite: 20, total: 135, cost: 0.125 },
23
+ messages: 1,
24
+ model: "claude-opus-5-5",
25
+ durationMs: 30 * 60 * 1000,
26
+ lines: { added: 2, removed: 1 },
27
+ };
28
+
29
+ function makeView(overrides: Partial<StatsViewDeps> = {}) {
30
+ const events: string[] = [];
31
+ const view = new StatsView({
32
+ theme: plainTheme,
33
+ rows: ROWS,
34
+ cwd: "/work/alpha",
35
+ now: () => NOW,
36
+ dayKey: (ms) => new Date(ms).toISOString().slice(0, 10),
37
+ load: () => createStatsLoader().load(SESSIONS),
38
+ current: () => CURRENT,
39
+ onClose: () => events.push("close"),
40
+ requestRender: () => events.push("render"),
41
+ ...overrides,
42
+ });
43
+ return { view, events };
44
+ }
45
+
46
+ async function ready(overrides: Partial<StatsViewDeps> = {}) {
47
+ const made = makeView(overrides);
48
+ await made.view.load();
49
+ return made;
50
+ }
51
+
52
+ const plain = (view: StatsView, width = 100) => view.render(width).map(stripAnsi);
53
+ const joined = (view: StatsView, width = 100) => plain(view, width).join("\n");
54
+
55
+ function assertFits(view: StatsView, width: number) {
56
+ const lines = view.render(width);
57
+ assert.equal(lines.length, ROWS);
58
+ for (const line of lines) assert.equal(visibleWidth(line), width, `"${stripAnsi(line)}" is not ${width} wide`);
59
+ }
60
+
61
+ function click(view: StatsView, width: number, y: number, label: string) {
62
+ const lines = plain(view, width);
63
+ const x = lines[y].indexOf(label);
64
+ assert.ok(x >= 0, `"${label}" not on row ${y}: ${lines[y]}`);
65
+ const event: TuiMouseEvent = { type: "click", button: "left", x, y, screenX: x, screenY: y, width, height: ROWS, shift: false, alt: false, ctrl: false };
66
+ return view.handleMouse(event);
67
+ }
68
+
69
+ test("StatsView shows a loading state, then the overview once sessions load", async () => {
70
+ let resolve!: (sessions: SessionRecord[]) => void;
71
+ const { view, events } = makeView({ load: () => new Promise((done) => { resolve = done; }) });
72
+ const loading = view.load();
73
+ assert.match(joined(view), /Loading local sessions…/);
74
+ resolve(await createStatsLoader().load(SESSIONS));
75
+ await loading;
76
+ assert.ok(events.includes("render"));
77
+ const text = joined(view);
78
+ assert.doesNotMatch(text, /Loading/);
79
+ assert.match(text, /✿ Stats/);
80
+ assert.match(text, /\[× Close\]/);
81
+ assert.match(text, /Overview/);
82
+ assert.match(text, /All time · All projects/);
83
+ });
84
+
85
+ test("Overview pins every headline figure from the fixtures", async () => {
86
+ const { view } = await ready();
87
+ const text = joined(view);
88
+ assert.match(text, /Favorite model +claude-opus-5-5/);
89
+ assert.match(text, /Total tokens +2\.8k/);
90
+ assert.match(text, /Sessions +3/);
91
+ assert.match(text, /Longest session +2h00m/);
92
+ assert.match(text, /Active days +4\/31/);
93
+ assert.match(text, /Longest streak +3 days/);
94
+ assert.match(text, /Most active day +Sep 28/);
95
+ assert.match(text, /Current streak +3 days/);
96
+ assert.match(text, /in 810 · out 740 · cache read 1\.0k · cache write 200/);
97
+ assert.match(text, /Cost +\$2\.05/);
98
+ assert.match(text, /Don Quixote/);
99
+ assert.match(text, /Less .* More/);
100
+ assert.match(text, /Mon/);
101
+ assert.match(text, /Local Pi sessions on this machine · subagent runs not included/);
102
+ assertFits(view, 100);
103
+ });
104
+
105
+ test("the heatmap shades days with theme roles and leaves future days blank", async () => {
106
+ const { view } = await ready({ theme: roleTheme });
107
+ const text = view.render(100).join("\n");
108
+ const heat = text.split("\n").slice(4, 11).join("\n");
109
+ assert.ok(heat.includes(`${roleCode("accent")}█`), "the busiest day takes the strongest shade");
110
+ assert.ok(heat.includes(`${roleCode("accent")}░`), "a light day takes the faintest shade");
111
+ assert.ok(heat.includes(`${roleCode("borderMuted")}·`), "idle days stay visible as dots");
112
+ // 96 content cells hold 46 two-cell weeks after the 4-cell weekday label.
113
+ const x = 2 + 4 + (Math.floor((96 - 4) / 2) - 1) * 2;
114
+ const thisWeek = stripAnsi(text).split("\n").slice(4, 11).map((line) => line[x]);
115
+ assert.deepEqual(thisWeek, ["█", "░", "░", "·", " ", " ", " "], "Mon–Thu of this week are drawn, Fri–Sun are still blank");
116
+ assert.doesNotMatch(text, /#[0-9a-f]{6}|\x1b\[38;2/i, "no hardcoded colors");
117
+ });
118
+
119
+ test("range and scope toggles re-aggregate and relabel the panel", async () => {
120
+ const { view, events } = await ready();
121
+ view.handleInput("r");
122
+ assert.match(joined(view), /Last 7 days · All projects/);
123
+ assert.match(joined(view), /Total tokens +1\.8k/);
124
+ assert.match(joined(view), /Active days +3\/7/);
125
+ view.handleInput("r");
126
+ assert.match(joined(view), /Last 30 days/);
127
+ view.handleInput("r");
128
+ assert.match(joined(view), /All time/);
129
+ view.handleInput("s");
130
+ assert.match(joined(view), /All time · This project/);
131
+ assert.match(joined(view), /Sessions +2/);
132
+ assert.ok(events.filter((event) => event === "render").length >= 4);
133
+ });
134
+
135
+ test("tabs switch with Tab, shift+Tab, arrows, and digits", async () => {
136
+ const { view } = await ready();
137
+ view.handleInput("\t");
138
+ let text = joined(view);
139
+ assert.match(text, /claude-opus-5-5 .*53% .*1\.4k .*\$0\.800 .*3/);
140
+ assert.match(text, /gpt-5\.5 .*36%/);
141
+ assert.match(text, /gpt-6\.1-sol .*11%/);
142
+ view.handleInput("\x1b[C");
143
+ text = joined(view);
144
+ assert.match(text, /Model +claude-opus-5-5/);
145
+ assert.match(text, /Cost +\$0\.125/);
146
+ assert.match(text, /Duration +30m00s/);
147
+ assert.match(text, /Lines +\+2 −1/);
148
+ assert.match(text, /135 total · in 10 · out 5 · cache read 100 · cache write 20/);
149
+ view.handleInput("\x1b[Z");
150
+ assert.match(joined(view), /Share/);
151
+ view.handleInput("1");
152
+ assert.match(joined(view), /Favorite model/);
153
+ view.handleInput("\x1b[D");
154
+ assert.match(joined(view), /Duration/);
155
+ view.handleInput("2");
156
+ assert.match(joined(view), /Share/);
157
+ });
158
+
159
+ test("q and esc close exactly once", async () => {
160
+ const { view, events } = await ready();
161
+ view.handleInput("q");
162
+ view.handleInput("q");
163
+ view.handleInput("\x1b");
164
+ assert.equal(events.filter((event) => event === "close").length, 1);
165
+ const escaped = await ready();
166
+ escaped.view.handleInput("\x1b");
167
+ assert.equal(escaped.events.filter((event) => event === "close").length, 1);
168
+ });
169
+
170
+ test("empty and failing loads render a calm state without throwing", async () => {
171
+ const empty = await ready({ load: async () => [] });
172
+ assert.match(joined(empty.view), /No usage recorded yet/);
173
+ empty.view.handleInput("2");
174
+ assert.match(joined(empty.view), /No usage recorded yet/);
175
+ assertFits(empty.view, 100);
176
+ const failed = await ready({ load: async () => { throw new Error("disk on fire"); } });
177
+ assert.match(joined(failed.view), /Could not read local sessions: disk on fire/);
178
+ assertFits(failed.view, 100);
179
+ });
180
+
181
+ test("narrow terminals keep every line inside the frame", async () => {
182
+ const { view } = await ready();
183
+ for (const tab of ["1", "2", "3"]) {
184
+ view.handleInput(tab);
185
+ assertFits(view, 48);
186
+ assertFits(view, 120);
187
+ }
188
+ });
189
+
190
+ test("clickable header and footer controls switch tabs, toggle filters, and close", async () => {
191
+ const { view, events } = await ready();
192
+ const width = 100;
193
+ assert.deepEqual(click(view, width, 1, "Models"), { handled: true, render: true });
194
+ assert.match(joined(view), /Share/);
195
+ const footer = ROWS - 2;
196
+ click(view, width, footer, "r range");
197
+ assert.match(joined(view), /Last 7 days/);
198
+ click(view, width, footer, "s scope");
199
+ assert.match(joined(view), /This project/);
200
+ click(view, width, 0, "[× Close]");
201
+ assert.equal(events.filter((event) => event === "close").length, 1);
202
+ });
@@ -1,5 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
- import test from "node:test";
2
+ import test, { after, before } from "node:test";
3
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
3
6
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
7
  import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
5
8
  import type { ExecFileAdapter, ExecFileResult } from "../lib/native-review-cli.ts";
@@ -22,6 +25,36 @@ import {
22
25
  // and the foreground /gentle:telemetry slash command relay.
23
26
  // ---------------------------------------------------------------------------
24
27
 
28
+ let fixtureRoot: string | undefined;
29
+ let fixtureCwd: string;
30
+ const fixtureEnvironment: NodeJS.ProcessEnv = {};
31
+ const previousEnvironment = new Map<string, string | undefined>();
32
+ before(() => {
33
+ fixtureRoot = mkdtempSync(join(tmpdir(), "gentle-pi-telemetry-fixture-"));
34
+ fixtureCwd = join(fixtureRoot, "project");
35
+ const home = join(fixtureRoot, "home");
36
+ mkdirSync(fixtureCwd);
37
+ mkdirSync(home);
38
+ Object.assign(fixtureEnvironment, {
39
+ HOME: home, USERPROFILE: home,
40
+ GENTLE_PI_CONFIG_HOME: join(home, "config"),
41
+ GENTLE_PI_AGENT_HOME: join(home, "agents"),
42
+ PI_CODING_AGENT_DIR: join(home, "pi"),
43
+ XDG_CONFIG_HOME: join(home, "xdg"),
44
+ });
45
+ for (const [key, value] of Object.entries(fixtureEnvironment)) {
46
+ previousEnvironment.set(key, process.env[key]);
47
+ process.env[key] = value;
48
+ }
49
+ });
50
+ after(() => {
51
+ for (const [key, value] of previousEnvironment) {
52
+ if (value === undefined) delete process.env[key];
53
+ else process.env[key] = value;
54
+ }
55
+ if (fixtureRoot !== undefined) rmSync(fixtureRoot, { recursive: true, force: true });
56
+ });
57
+
25
58
  interface FakeSpawnRecord {
26
59
  command: string;
27
60
  args: string[];
@@ -172,13 +205,20 @@ function buildExtensionHarness(overrides: Parameters<typeof createGentleAiExtens
172
205
  registerTool() {},
173
206
  events: { emit() {} },
174
207
  } as unknown as ExtensionAPI;
175
- createGentleAiExtension({ nativeReviewCli: null, ...overrides })(pi);
208
+ createGentleAiExtension({
209
+ nativeReviewCli: null,
210
+ resolveTelemetryTriggerBinary: () => assert.fail("Telemetry fixtures must inject a binary resolver"),
211
+ telemetryTriggerSpawn: () => assert.fail("Telemetry fixtures must inject a spawn mock"),
212
+ telemetryExecFileAdapter: async () => assert.fail("Telemetry fixtures must inject an exec adapter"),
213
+ ...overrides,
214
+ processEnv: { ...fixtureEnvironment, GENTLE_PI_AGENTS_CHILD: "0", ...overrides.processEnv },
215
+ })(pi);
176
216
  return { handlers, commands };
177
217
  }
178
218
 
179
- function fakeContext(cwd: string, notifications: Array<{ message: string; severity: string }>): ExtensionContext {
219
+ function fakeContext(notifications: Array<{ message: string; severity: string }>): ExtensionContext {
180
220
  return {
181
- cwd,
221
+ cwd: fixtureCwd,
182
222
  hasUI: true,
183
223
  ui: {
184
224
  notify(message: string, severity: string) {
@@ -202,7 +242,7 @@ test("activation: spawns the telemetry trigger exactly once for a primary sessio
202
242
  const beforeAgentStart = handlers.get("before_agent_start");
203
243
  assert.equal(typeof beforeAgentStart, "function");
204
244
  const notifications: Array<{ message: string; severity: string }> = [];
205
- const ctx = fakeContext("/work/project", notifications);
245
+ const ctx = fakeContext(notifications);
206
246
 
207
247
  // A primary-session event carries no agent name at all.
208
248
  await beforeAgentStart!({ systemPrompt: "" }, ctx);
@@ -210,14 +250,17 @@ test("activation: spawns the telemetry trigger exactly once for a primary sessio
210
250
 
211
251
  assert.equal(fake.calls.length, 1, "the trigger must be attempted at most once per process");
212
252
  const [call] = fake.calls;
253
+ assert.equal(call.options.env.GENTLE_PI_AGENTS_CHILD, "0");
254
+ assert.equal(call.options.env.HOME, fixtureEnvironment.HOME);
255
+ assert.equal(call.options.env.GENTLE_PI_AGENTS_PARENT_PERMISSION_FD, undefined);
213
256
  assert.deepEqual(call.args, ["telemetry", "trigger", "--json"]);
214
- assert.equal(call.options.cwd, "/work/project");
257
+ assert.equal(call.options.cwd, fixtureCwd);
215
258
  assert.equal(call.options.detached, true);
216
259
  assert.equal(call.options.windowsHide, true);
217
260
  assert.equal(call.options.stdio, "ignore");
218
261
  });
219
262
 
220
- test("activation: never spawns for a named or SDD agent event", async (t) => {
263
+ test("activation: never spawns for named agents, even with legacy prompt text", async (t) => {
221
264
  t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
222
265
  __testing.resetTelemetryTriggerGuardForTesting();
223
266
  const fake = fakeSpawn();
@@ -227,12 +270,27 @@ test("activation: never spawns for a named or SDD agent event", async (t) => {
227
270
  });
228
271
  const beforeAgentStart = handlers.get("before_agent_start");
229
272
  const notifications: Array<{ message: string; severity: string }> = [];
230
- const ctx = fakeContext("/work/project", notifications);
273
+ const ctx = fakeContext(notifications);
231
274
 
232
275
  await beforeAgentStart!({ agentName: "review-risk", systemPrompt: "" }, ctx);
233
- await beforeAgentStart!({ systemPrompt: "SDD apply executor" }, ctx);
276
+ await beforeAgentStart!({ agentName: "gentle-ai-worker", systemPrompt: "SDD apply executor" }, ctx);
234
277
 
235
- assert.equal(fake.calls.length, 0, "named/SDD agents must never trigger the nudge");
278
+ assert.equal(fake.calls.length, 0, "named agents must never trigger the nudge");
279
+ });
280
+
281
+ test("activation: a child session neither spawns telemetry nor appends the primary harness", async (t) => {
282
+ t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
283
+ __testing.resetTelemetryTriggerGuardForTesting();
284
+ const fake = fakeSpawn();
285
+ const { handlers } = buildExtensionHarness({
286
+ resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
287
+ telemetryTriggerSpawn: fake.spawn,
288
+ processEnv: { GENTLE_PI_AGENTS_CHILD: "1" },
289
+ });
290
+ const event = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "Child instructions" } };
291
+ assert.equal(await handlers.get("before_agent_start")!(event, fakeContext([])), undefined);
292
+ assert.equal(fake.calls.length, 0);
293
+ assert.equal(event.systemPromptOptions.appendSystemPrompt, "Child instructions");
236
294
  });
237
295
 
238
296
  test("activation: a missing binary or spawn error never affects activation", async (t) => {
@@ -245,12 +303,15 @@ test("activation: a missing binary or spawn error never affects activation", asy
245
303
  });
246
304
  const beforeAgentStart = handlers.get("before_agent_start");
247
305
  const notifications: Array<{ message: string; severity: string }> = [];
248
- const ctx = fakeContext("/work/project", notifications);
249
-
250
- // Must resolve cleanly and produce the ordinary orchestrator prompt fields,
251
- // never throw or notify about the missing binary.
252
- const outcome = await beforeAgentStart!({ systemPrompt: "base" }, ctx);
253
- assert.equal(typeof outcome, "object");
306
+ const ctx = fakeContext(notifications);
307
+
308
+ // Must resolve cleanly and produce the ordinary orchestrator prompt fields
309
+ // in appendSystemPrompt (never a returned systemPrompt), and never throw
310
+ // or notify about the missing binary.
311
+ const event = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
312
+ const outcome = await beforeAgentStart!(event, ctx);
313
+ assert.equal(outcome, undefined, "the handler must not return a replacement systemPrompt");
314
+ assert.match(event.systemPromptOptions.appendSystemPrompt, /el Gentleman/);
254
315
  assert.equal(notifications.length, 0);
255
316
  });
256
317
 
@@ -284,7 +345,7 @@ test("/gentle:telemetry relays a status --json payload", async () => {
284
345
  const command = commands.get("gentle:telemetry");
285
346
  assert.ok(command, "gentle:telemetry must be registered");
286
347
  const notifications: Array<{ message: string; severity: string }> = [];
287
- await command!.handler("", fakeContext("/work/project", notifications));
348
+ await command!.handler("", fakeContext(notifications));
288
349
 
289
350
  assert.equal(requests.length, 1);
290
351
  assert.deepEqual(requests[0].arguments, ["telemetry", "status", "--json"]);
@@ -308,7 +369,7 @@ test("/gentle:telemetry disable prints a one-line confirmation", async () => {
308
369
  });
309
370
  const command = commands.get("gentle:telemetry");
310
371
  const notifications: Array<{ message: string; severity: string }> = [];
311
- await command!.handler("disable", fakeContext("/work/project", notifications));
372
+ await command!.handler("disable", fakeContext(notifications));
312
373
 
313
374
  assert.deepEqual(notifications, [{ message: "Gentle AI telemetry disabled.", severity: "info" }]);
314
375
  });
@@ -328,7 +389,7 @@ test("/gentle:telemetry relays a typed non-zero failure", async () => {
328
389
  });
329
390
  const command = commands.get("gentle:telemetry");
330
391
  const notifications: Array<{ message: string; severity: string }> = [];
331
- await command!.handler("preview", fakeContext("/work/project", notifications));
392
+ await command!.handler("preview", fakeContext(notifications));
332
393
 
333
394
  assert.equal(notifications.length, 1);
334
395
  assert.equal(notifications[0].severity, "error");
@@ -343,7 +404,7 @@ test("/gentle:telemetry rejects an unknown sub-action without calling the binary
343
404
  });
344
405
  const command = commands.get("gentle:telemetry");
345
406
  const notifications: Array<{ message: string; severity: string }> = [];
346
- await command!.handler("frobnicate", fakeContext("/work/project", notifications));
407
+ await command!.handler("frobnicate", fakeContext(notifications));
347
408
 
348
409
  assert.equal(requests.length, 0);
349
410
  assert.equal(notifications.length, 1);
@@ -0,0 +1,72 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { sourcePalettePreview } from "../lib/theme-customization.ts";
7
+ import { colorToRgb, parseColor } from "@earendil-works/pi-tui";
8
+
9
+ test("Pi 0.99.1 source palette matches the public host OKHSL conversion", () => {
10
+ const source = new URL("../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json", import.meta.url);
11
+ const document = JSON.parse(readFileSync(source, "utf8"));
12
+ const accent = document.vars[document.colors.accent];
13
+ const text = document.vars[document.colors.text];
14
+ assert.match(accent, /^okhsl\(/);
15
+ assert.match(text, /^okhsl\(/);
16
+ const a = colorToRgb(parseColor(accent));
17
+ const b = colorToRgb(parseColor(text));
18
+ assert.equal(sourcePalettePreview("dark", source.pathname).sample,
19
+ `\x1b[48;2;${a.r};${a.g};${a.b}m \x1b[0m \x1b[38;2;${b.r};${b.g};${b.b}mAa sample text\x1b[0m`);
20
+ });
21
+
22
+ test("OKHSL extremes have exact RGB values and unsupported colors fail closed", (t) => {
23
+ const root = mkdtempSync(join(tmpdir(), "theme-preview-"));
24
+ t.after(() => rmSync(root, { recursive: true, force: true }));
25
+ const source = join(root, "theme.json");
26
+ const write = (accent: unknown) => writeFileSync(source, JSON.stringify({ name: "dark", colors: { accent, text: "okhsl(120 0% 100%)" } }));
27
+ write("okhsl(0 0% 0%)");
28
+ assert.equal(sourcePalettePreview("dark", source).sample,
29
+ "\x1b[48;2;0;0;0m \x1b[0m \x1b[38;2;255;255;255mAa sample text\x1b[0m");
30
+ for (const invalid of ["okhsl(0 101% 50%)", "okhsl(0 10% -1%)", "okhsl(NaN 10% 50%)", "okhsl(0 10% 50%)suffix", "rgb(1 2 3)", "red", "", {}, 256]) {
31
+ write(invalid);
32
+ assert.throws(() => sourcePalettePreview("dark", source));
33
+ }
34
+ });
35
+
36
+ test("installed source palette resolves vars without activating a theme", (t) => {
37
+ const root = mkdtempSync(join(tmpdir(), "theme-preview-"));
38
+ t.after(() => rmSync(root, { recursive: true, force: true }));
39
+ const source = join(root, "theme.json");
40
+ const document = JSON.parse(readFileSync(new URL("../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json", import.meta.url), "utf8"));
41
+ writeFileSync(source, JSON.stringify({ ...document, name: "dark", vars: { ...document.vars, previewAccent: "#123456" }, colors: { ...document.colors, accent: "previewAccent" } }));
42
+ const alias = join(root, "alias.json");
43
+ symlinkSync(source, alias);
44
+ const preview = sourcePalettePreview("dark", alias);
45
+ assert.match(preview.title, /dark.*source palette/);
46
+ assert.match(preview.sample, /48;2;18;52;86m/); // The accent swatch is a background; text is foreground.
47
+ assert.match(preview.sample, /38;2;/);
48
+ assert.match(preview.sample, /sample text/);
49
+ });
50
+
51
+ test("preview fails closed for missing, mismatched, malformed and oversized sources", (t) => {
52
+ const root = mkdtempSync(join(tmpdir(), "theme-preview-"));
53
+ t.after(() => rmSync(root, { recursive: true, force: true }));
54
+ const path = join(root, "theme.json");
55
+ assert.throws(() => sourcePalettePreview("dark", undefined));
56
+ writeFileSync(path, JSON.stringify({ name: "wrong", colors: { accent: "#abcdef", text: "#000000" } }));
57
+ assert.throws(() => sourcePalettePreview("dark", path));
58
+ writeFileSync(path, "not json");
59
+ assert.throws(() => sourcePalettePreview("dark", path));
60
+ writeFileSync(path, " ".repeat(256_001));
61
+ assert.throws(() => sourcePalettePreview("dark", path));
62
+ });
63
+
64
+ test("source palette supports indexed colors and rejects unresolved references", (t) => {
65
+ const root = mkdtempSync(join(tmpdir(), "theme-preview-"));
66
+ t.after(() => rmSync(root, { recursive: true, force: true }));
67
+ const path = join(root, "theme.json");
68
+ writeFileSync(path, JSON.stringify({ name: "dark", colors: { accent: 42, text: 255 } }));
69
+ assert.match(sourcePalettePreview("dark", path).sample, /48;5;42m.*38;5;255m/);
70
+ writeFileSync(path, JSON.stringify({ name: "dark", vars: { a: "b", b: "a" }, colors: { accent: "a", text: "#ffffff" } }));
71
+ assert.throws(() => sourcePalettePreview("dark", path));
72
+ });
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { spawnSync } from "node:child_process";
4
+ import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ // A git install (`pi install git:...`) has no node_modules next to the
10
+ // extension: Pi aliases ES imports of host packages, but nothing else
11
+ // resolves them (#1586). Reproduce that layout with an ESM-only alias.
12
+ test("vim adapter loads without an extension-local pi-tui and fails closed without a verified version", (t) => {
13
+ const root = mkdtempSync(join(tmpdir(), "gentle-vim-host-resolution-"));
14
+ t.after(() => rmSync(root, { recursive: true, force: true }));
15
+ mkdirSync(join(root, "lib"));
16
+ copyFileSync(fileURLToPath(new URL("../lib/vim-editor-adapter.ts", import.meta.url)), join(root, "lib", "vim-editor-adapter.ts"));
17
+ const tuiUrl = import.meta.resolve("@earendil-works/pi-tui");
18
+ writeFileSync(join(root, "alias-hooks.mjs"), `export async function resolve(specifier, context, next) {
19
+ if (specifier === "@earendil-works/pi-tui") return { url: ${JSON.stringify(tuiUrl)}, shortCircuit: true };
20
+ return next(specifier, context);
21
+ }
22
+ `);
23
+ writeFileSync(join(root, "register.mjs"), `import { register } from "node:module";
24
+ register("./alias-hooks.mjs", import.meta.url);
25
+ `);
26
+ writeFileSync(join(root, "probe.mjs"), `import { Editor } from "@earendil-works/pi-tui";
27
+ const { createVimEditorAdapter } = await import("./lib/vim-editor-adapter.ts");
28
+ const editor = new Editor({ terminal: { rows: 24 }, requestRender() {} }, { borderColor: (s) => s });
29
+ let unverified = "admitted";
30
+ try { createVimEditorAdapter(editor, "0.99.1"); } catch (error) { unverified = error.message; }
31
+ const verified = typeof createVimEditorAdapter(editor, "0.99.1", Editor, "0.99.1").move;
32
+ process.stdout.write(JSON.stringify({ unverified, verified }));
33
+ `);
34
+ const result = spawnSync(process.execPath, ["--experimental-strip-types", "--import", pathToFileURL(join(root, "register.mjs")).href, join(root, "probe.mjs")], { cwd: root, encoding: "utf8" });
35
+ assert.equal(result.status, 0, result.stderr);
36
+ assert.deepEqual(JSON.parse(result.stdout), { unverified: "Unsupported Pi editor layout/version", verified: "function" });
37
+ });