gentle-pi 4.0.0 → 4.2.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 (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,201 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
+ import type { TSchema } from "typebox";
4
+
5
+ const MAX_STATUSES = 8;
6
+ const MAX_ACTIVE_OBSERVATIONS = 8;
7
+ type Outcome = { kind: "success" | "cli_error" | "native_failure" | "unknown"; code?: string; phase?: string; mutation_outcome?: string; retry_safe?: boolean; next_action?: string };
8
+ interface StatusSample {
9
+ timeout_ms: number;
10
+ total_ms: number;
11
+ resolution_ms: number;
12
+ adapter_ms: number;
13
+ decode_ms: number;
14
+ outcome: Outcome;
15
+ }
16
+ interface Summary {
17
+ schema: "gentle-pi.status-timing/v1";
18
+ host_total_ms: number;
19
+ dispatch_ms: number;
20
+ sidebar_ms: number;
21
+ completion_ms: number;
22
+ truncated: boolean;
23
+ clock_unavailable?: true;
24
+ statuses: StatusSample[];
25
+ }
26
+ interface HostObservation {
27
+ clock: () => number;
28
+ current: () => boolean;
29
+ claim: () => boolean;
30
+ claimed: boolean;
31
+ started: number;
32
+ lastNativeEnd: number;
33
+ summary: Summary;
34
+ }
35
+ interface NativeObservation { host: HostObservation; sample: StatusSample; }
36
+ const hostScope = new AsyncLocalStorage<HostObservation>();
37
+ const nativeScope = new AsyncLocalStorage<NativeObservation>();
38
+
39
+ // Observer failures must never replace the operation's result or exception.
40
+ function safely<T>(run: () => T): T | undefined {
41
+ try { return run(); } catch { return undefined; }
42
+ }
43
+ function now(host: HostObservation): number {
44
+ const value = safely(host.clock);
45
+ if (value !== undefined && Number.isFinite(value) && value >= 0) return value;
46
+ host.summary.clock_unavailable = true;
47
+ return 0;
48
+ }
49
+ function elapsed(start: number, end: number): number { return Math.max(0, end - start); }
50
+
51
+ // Failure/v2 codes and next_action are extensible strings, not safe diagnostic
52
+ // text. Only recognized values enter this summary; no messages or raw envelopes.
53
+ const CLI_CODES = new Set(["unavailable", "timeout", "non-zero", "signal", "unexpected-stderr", "output-limit", "empty-output", "malformed-json", "schema-incompatible", "identity-mismatch", "version-incompatible", "cancelled", "package-local-binary-missing", "unsupported-transition-operation"]);
54
+ const NATIVE_CODES = new Set(["operation_timeout", "invalid_request", "binding_revision_conflict", "internal_error", "cancelled"]);
55
+ function outcome(error: unknown): Outcome {
56
+ return safely((): Outcome => {
57
+ if (typeof error !== "object" || error === null) return { kind: "unknown" };
58
+ const value = error as { name?: unknown; code?: unknown; failureEnvelope?: Record<string, unknown> };
59
+ if (value.name === "NativeReviewCliError") return { kind: "cli_error", code: typeof value.code === "string" && CLI_CODES.has(value.code) ? value.code : "other" };
60
+ if (value.name !== "NativeReviewIntegrationError" || !value.failureEnvelope) return { kind: "unknown" };
61
+ const failure = value.failureEnvelope;
62
+ const result: Outcome = { kind: "native_failure", code: typeof failure.code === "string" && NATIVE_CODES.has(failure.code) ? failure.code : "other" };
63
+ if (["preflight", "pre_native", "native_running", "native_committed", "reconciliation"].includes(String(failure.phase))) result.phase = String(failure.phase);
64
+ if (["not_started", "committed", "unknown"].includes(String(failure.mutationOutcome))) result.mutation_outcome = String(failure.mutationOutcome);
65
+ if (typeof failure.retrySafe === "boolean") result.retry_safe = failure.retrySafe;
66
+ if (["stop", "review.status"].includes(String(failure.nextAction))) result.next_action = String(failure.nextAction);
67
+ return result;
68
+ }) ?? { kind: "unknown" };
69
+ }
70
+
71
+ /** The next already-authorized STATUS-bearing tool call consumes the arm. */
72
+ export class StatusTimingDiagnostics {
73
+ private armed = false;
74
+ private epoch = 0;
75
+ private manager: ExtensionContext["sessionManager"] | undefined;
76
+ private session: string | undefined;
77
+ private last: Summary | undefined;
78
+ private readonly active = new Set<HostObservation>();
79
+ private readonly clock: () => number;
80
+ constructor(clock = () => performance.now()) { this.clock = clock; }
81
+
82
+ reset(ctx?: ExtensionContext): void {
83
+ this.epoch++;
84
+ this.armed = false;
85
+ this.last = undefined;
86
+ for (const host of this.active) host.summary.statuses.length = 0;
87
+ this.active.clear();
88
+ this.manager = ctx?.sessionManager;
89
+ this.session = safely(() => ctx?.sessionManager.getSessionId());
90
+ }
91
+ private matches(ctx: ExtensionContext): boolean {
92
+ return safely(() => this.manager === ctx.sessionManager && !!this.session && this.session === ctx.sessionManager.getSessionId()) === true;
93
+ }
94
+ command(args: string, ctx: ExtensionContext): void {
95
+ if (!this.matches(ctx)) this.reset(ctx);
96
+ const action = args.trim() || "show";
97
+ if (action === "enable") {
98
+ this.reset(ctx);
99
+ this.armed = !!this.session;
100
+ ctx.ui.notify(this.armed
101
+ ? "STATUS timing armed for the next separately authorized STATUS-bearing tool call. No STATUS is invoked or retried. Use /gentle:status-timing show to consult the completed summary."
102
+ : "STATUS timing requires a live session.", "info");
103
+ } else if (action === "disable") {
104
+ this.reset(ctx);
105
+ ctx.ui.notify("STATUS timing disabled; in-memory observations cleared.", "info");
106
+ } else if (action === "show") {
107
+ ctx.ui.notify(this.last ? JSON.stringify(this.last, null, 2) : `No completed STATUS timing summary. Diagnostic is ${this.armed ? "armed" : "off"}.`, "info");
108
+ } else {
109
+ ctx.ui.notify("Unknown /gentle:status-timing sub-action. Use enable, disable, or show.", "warning");
110
+ }
111
+ }
112
+ tool<TParams extends TSchema, TDetails>(definition: ToolDefinition<TParams, TDetails>): ToolDefinition<TParams, TDetails> {
113
+ return {
114
+ ...definition,
115
+ execute: async (toolCallId, params, signal, onUpdate, ctx) => {
116
+ const run = () => definition.execute(toolCallId, params, signal, onUpdate, ctx);
117
+ if (!this.matches(ctx)) this.reset(ctx);
118
+ if (!this.armed || this.active.size >= MAX_ACTIVE_OBSERVATIONS) return run();
119
+ const epoch = this.epoch;
120
+ const host: HostObservation = {
121
+ clock: this.clock,
122
+ current: () => this.epoch === epoch && this.matches(ctx),
123
+ claim: () => {
124
+ if (!host.current() || !this.armed) return false;
125
+ this.armed = false;
126
+ return true;
127
+ },
128
+ claimed: false, started: 0, lastNativeEnd: 0,
129
+ summary: { schema: "gentle-pi.status-timing/v1", host_total_ms: 0, dispatch_ms: 0, sidebar_ms: 0, completion_ms: 0, truncated: false, statuses: [] },
130
+ };
131
+ this.active.add(host);
132
+ host.started = now(host);
133
+ return hostScope.run(host, async () => {
134
+ try { return await run(); }
135
+ finally {
136
+ this.active.delete(host);
137
+ safely(() => {
138
+ if (!host.claimed || !host.current()) return;
139
+ const ended = now(host);
140
+ host.summary.host_total_ms = elapsed(host.started, ended);
141
+ host.summary.completion_ms = elapsed(host.lastNativeEnd, ended);
142
+ this.last = host.summary;
143
+ });
144
+ }
145
+ });
146
+ },
147
+ };
148
+ }
149
+ }
150
+
151
+ /** Only called by the existing STATUS entry points; never invokes one itself. */
152
+ export async function observeNativeStatus<T>(timeoutMs: number, run: () => Promise<T>): Promise<T> {
153
+ const host = hostScope.getStore();
154
+ if (!host || !safely(host.current)) return run();
155
+ if (!host.claimed) {
156
+ if (!safely(host.claim)) return run();
157
+ host.claimed = true;
158
+ host.summary.dispatch_ms = elapsed(host.started, now(host));
159
+ }
160
+ const sample: StatusSample = { timeout_ms: timeoutMs, total_ms: 0, resolution_ms: 0, adapter_ms: 0, decode_ms: 0, outcome: { kind: "success" } };
161
+ if (host.summary.statuses.length < MAX_STATUSES) host.summary.statuses.push(sample);
162
+ else host.summary.truncated = true;
163
+ const started = now(host);
164
+ return nativeScope.run({ host, sample }, async () => {
165
+ try { return await run(); }
166
+ catch (error) { safely(() => { sample.outcome = outcome(error); }); throw error; }
167
+ finally {
168
+ safely(() => {
169
+ const ended = now(host);
170
+ sample.total_ms = elapsed(started, ended);
171
+ host.lastNativeEnd = ended;
172
+ });
173
+ }
174
+ });
175
+ }
176
+
177
+ function activeNative(): NativeObservation | undefined {
178
+ const observation = nativeScope.getStore();
179
+ return observation && safely(observation.host.current) ? observation : undefined;
180
+ }
181
+ export function measureStatusSync<T>(stage: "resolution" | "decode", run: () => T): T {
182
+ const observation = activeNative();
183
+ if (!observation) return run();
184
+ const started = now(observation.host);
185
+ try { return run(); }
186
+ finally { safely(() => { observation.sample[`${stage}_ms`] += elapsed(started, now(observation.host)); }); }
187
+ }
188
+ export async function measureStatusAsync<T>(stage: "adapter", run: () => Promise<T>): Promise<T> {
189
+ const observation = activeNative();
190
+ if (!observation) return run();
191
+ const started = now(observation.host);
192
+ try { return await run(); }
193
+ finally { safely(() => { observation.sample[`${stage}_ms`] += elapsed(started, now(observation.host)); }); }
194
+ }
195
+ export function measureStatusSidebar<T>(run: () => T): T {
196
+ const host = hostScope.getStore();
197
+ if (!host || !safely(host.current)) return run();
198
+ const started = now(host);
199
+ try { return run(); }
200
+ finally { safely(() => { host.summary.sidebar_ms += elapsed(started, now(host)); }); }
201
+ }
@@ -1,4 +1,5 @@
1
1
  import { spawn as nodeSpawn } from "node:child_process";
2
+ import { gentleAiIsolationEnv } from "./gentle-ai-binary.ts";
2
3
 
3
4
  // gentle-ai#4309 owns anonymous usage telemetry end to end: install and
4
5
  // heartbeat events, the notice, every kill switch, and the wire contract.
@@ -128,7 +129,8 @@ export function spawnTelemetryTrigger(options: SpawnTelemetryTriggerOptions): Te
128
129
  try {
129
130
  const child = spawnImpl(options.executable, ["telemetry", "trigger", "--json"], {
130
131
  cwd: options.cwd,
131
- env: options.env,
132
+ // S9: a bundled install keeps Gentle AI's telemetry state inside its prefix.
133
+ env: gentleAiIsolationEnv(options.env),
132
134
  detached: true,
133
135
  windowsHide: true,
134
136
  stdio: "ignore",
@@ -1,14 +1,22 @@
1
1
  import { CURSOR_MARKER, Editor, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import { createRequire } from "node:module";
3
3
 
4
- // Private shape audited against @earendil-works/pi-tui 0.99.1, 0.99.2 and 1.0.0:
4
+ // Private shape audited against @earendil-works/pi-tui 0.99.1, 0.99.2, 1.0.0, 1.1.0 and 1.2.0:
5
5
  // editor.js state/pastes/history, sticky cursor reset, layoutText/render geometry,
6
6
  // autocomplete cancellation and { state, pastes, pasteCounter } undo restoration;
7
7
  // undo-stack.js clone-on-push/pop snapshots. The two 0.99 releases ship identical
8
8
  // editor/undo-stack sources; 1.0.0 is a touched-contract audit, not byte identity.
9
- // Actual 1.0.0 bundled and unbundled tests exercise identity, edit/undo, paste,
9
+ // The 1.1.0 touched-contract audit retains clone-on-push snapshots with the
10
+ // paste registry, cursor resets, layoutText/render geometry and history exit.
11
+ // cancelAutocomplete aborts/debounces requests before clearing its UI;
12
+ // the adapter still calls that host-owned method, never mutating its request state.
13
+ // Actual installed bundled/unbundled tests exercise identity, edit/undo, paste,
10
14
  // selection, wrapping/scroll and autocomplete; fabricated metadata tests only
11
15
  // prove exact-version admission for the older audited releases.
16
+ // 1.2.0 changes only editor cursor rendering to paired pi:fc/pi:/fc APC
17
+ // markers resolved by the host TUI (including hardware-cursor suppression).
18
+ // Keep these markers intact and rearm selection after their reverse-off;
19
+ // state, undo, paste, layout and autocomplete contracts remain unchanged.
12
20
  // Never silently adapt another build: undo also owns the paste registry.
13
21
  interface Position { line: number; col: number }
14
22
  interface State { lines: string[]; cursorLine: number; cursorCol: number }
@@ -47,7 +55,7 @@ interface PrivateEditor {
47
55
 
48
56
  // The single audited-release source for the adapter and Gentle Shell's runtime
49
57
  // identity gates. Add a release only after re-auditing the files named above.
50
- export const AUDITED_PI_EDITOR_VERSIONS = Object.freeze(["0.99.1", "0.99.2", "1.0.0"] as const);
58
+ export const AUDITED_PI_EDITOR_VERSIONS = Object.freeze(["0.99.1", "0.99.2", "1.0.0", "1.1.0", "1.2.0"] as const);
51
59
  export type AuditedPiEditorVersion = (typeof AUDITED_PI_EDITOR_VERSIONS)[number];
52
60
 
53
61
  export function isAuditedPiEditorVersion(version: unknown): version is AuditedPiEditorVersion {
@@ -79,7 +87,7 @@ function hasEditorIdentity(value: unknown, version: string, editorClass: typeof
79
87
  const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
80
88
  const marker = /\[paste #(\d+)(?: (?:\+\d+ lines|\d+ chars))?\]/g;
81
89
 
82
- // Only CSI SGR and Pi's own zero-width cursor marker are expected in editor
90
+ // Only CSI SGR and Pi's own zero-width cursor markers are expected in editor
83
91
  // text. Unknown escapes leave the unmodified frame intact (fail closed).
84
92
  function highlightRenderedRow(row: string, chunk: LayoutLine, left: number, right: number, padding: number): string | undefined {
85
93
  const cursor = chunk.hasCursor ? chunk.cursorPos : undefined;
@@ -110,8 +118,22 @@ function highlightRenderedRow(row: string, chunk: LayoutLine, left: number, righ
110
118
  result += token;
111
119
  if (selected && !inverse) { result += "\x1b[7m"; inverse = true; }
112
120
  } else if (token.startsWith("\x1b")) {
113
- if (token !== CURSOR_MARKER) return undefined;
121
+ if (token !== CURSOR_MARKER && token !== "\x1b_pi:fc\x07" && token !== "\x1b_pi:/fc\x07") return undefined;
122
+ // Close selection *before* the cursor pair, never inside its cell:
123
+ // otherwise a later host SGR 7 would immediately meet our SGR 27.
124
+ // Closing before CURSOR_MARKER also keeps it adjacent to pi:fc so
125
+ // hardware-cursor suppression can still recognize the focused pair.
126
+ const local = col - padding;
127
+ if ((token === CURSOR_MARKER || token === "\x1b_pi:fc\x07") && selected &&
128
+ !(local >= left && local < right && local < chunk.text.length)) {
129
+ if (!nativeInverse && inverse) result += "\x1b[27m";
130
+ selected = false;
131
+ inverse = nativeInverse;
132
+ }
114
133
  result += token;
134
+ // The host resolves pi:/fc to SGR 27, or drops the pair when the
135
+ // hardware cursor owns this cell. Selection must survive either path.
136
+ if (token === "\x1b_pi:/fc\x07" && selected) result += "\x1b[7m";
115
137
  } else {
116
138
  const local = col - padding;
117
139
  const shouldSelect = local >= left && local < right && local < chunk.text.length;
@@ -18,6 +18,8 @@ export interface VisualVisibility {
18
18
  todo: boolean;
19
19
  usageCost: boolean;
20
20
  modelDetails: boolean;
21
+ /** High-context warning notice; independent from audio notifications. */
22
+ contextWarning: boolean;
21
23
  }
22
24
  export interface VisualSettings {
23
25
  statusPlacement: StatusPlacement;
@@ -29,13 +31,13 @@ export const DEFAULT_VISUAL_SETTINGS: VisualSettings = {
29
31
  statusPlacement: STATUS_PLACEMENT.AUTO,
30
32
  headerPlacement: HEADER_PLACEMENT.TOP,
31
33
  density: DENSITY.COMFORTABLE,
32
- visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true },
34
+ visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true },
33
35
  };
34
36
  /** Sections row order; `rdd` follows `changes` as the RDD group follows Changes in the Status card. */
35
- export const VISUAL_SECTION_KEYS = ["changes", "rdd", "agents", "todo", "usageCost", "modelDetails"] as const satisfies readonly (keyof VisualVisibility)[];
36
- /** Keys added after v1 shipped: stored settings may omit them together, and they default to shown. */
37
- const ADDED_SECTION_KEYS: readonly (keyof VisualVisibility)[] = ["rdd"];
38
- const LEGACY_SECTION_KEYS = VISUAL_SECTION_KEYS.filter((key) => !ADDED_SECTION_KEYS.includes(key));
37
+ export const VISUAL_SECTION_KEYS = ["changes", "rdd", "agents", "todo", "usageCost", "modelDetails", "contextWarning"] as const satisfies readonly (keyof VisualVisibility)[];
38
+ /** Keys added after v1 shipped: stored settings may omit them together or one at a time, and they default to shown. */
39
+ const ADDED_SECTION_KEYS: readonly (keyof VisualVisibility)[] = ["rdd", "contextWarning"];
40
+ const BASE_SECTION_KEYS = VISUAL_SECTION_KEYS.filter((key) => !ADDED_SECTION_KEYS.includes(key));
39
41
  interface VisualOptions { gentlePiConfigHome?: string }
40
42
  export interface VisualResolution {
41
43
  settings: VisualSettings;
@@ -64,13 +66,22 @@ export function isVisualSettings(value: unknown): value is VisualSettings {
64
66
  && keysMatch(visibility, VISUAL_SECTION_KEYS)
65
67
  && Object.values(visibility).every((item) => typeof item === "boolean");
66
68
  }
67
- /** Reader-side validation: accepts the current shape and legacy visibility that predates added sections. Writers stay strict. */
69
+ /**
70
+ * Reader-side validation: accepts the current shape plus every historical one.
71
+ * Every key must be known, every base key present, and every present value a
72
+ * boolean; missing added keys (`rdd`, `contextWarning`) then default to shown,
73
+ * while present values (including `rdd: false`) are preserved. Writers stay strict.
74
+ */
68
75
  export function normalizeVisualSettings(value: unknown): VisualSettings | undefined {
69
76
  if (!record(value) || !record(value.visibility)) return undefined;
70
77
  const visibility = value.visibility;
71
- const candidate = keysMatch(visibility, LEGACY_SECTION_KEYS)
72
- ? { ...value, visibility: Object.fromEntries(VISUAL_SECTION_KEYS.map((key) => [key, visibility[key] ?? true])) }
73
- : value;
78
+ if (!Object.keys(visibility).every((key) => (VISUAL_SECTION_KEYS as readonly string[]).includes(key))) return undefined;
79
+ if (!BASE_SECTION_KEYS.every((key) => Object.hasOwn(visibility, key))) return undefined;
80
+ // A key that is present is authoritative: only a missing added key may default to shown.
81
+ if (!Object.keys(visibility).every((key) => typeof visibility[key] === "boolean")) return undefined;
82
+ const candidate = keysMatch(visibility, VISUAL_SECTION_KEYS)
83
+ ? value
84
+ : { ...value, visibility: Object.fromEntries(VISUAL_SECTION_KEYS.map((key) => [key, visibility[key] ?? true])) };
74
85
  return isVisualSettings(candidate) ? candidate : undefined;
75
86
  }
76
87
  export function parseVisualSettingsFile(raw: string): VisualSettings | undefined {
@@ -29,14 +29,45 @@ function dropLastGrapheme(text: string): string {
29
29
  return segments.length ? text.slice(0, segments[segments.length - 1]!.index) : text;
30
30
  }
31
31
 
32
- export type CustomizeCategory = "Animations" | "Banner" | "Themes" | "Editor" | "History" | "Layout" | "Cards" | "Sections" | "Profiles" | "Reset";
32
+ /** Category-aware footer; the audio card advertises audio keys, never the visual profiles pane. */
33
+ function footerHint(category: string, navOnly: boolean, previewLines: number, keyhint?: string): string {
34
+ if (navOnly) return "↑/↓ categories · → open · Esc close";
35
+ if (category === "Notifications") return keyhint ?? "Tab panes · Enter change · p test · Esc close";
36
+ return previewLines ? "←/→ or Tab panes · ↑/↓ move · Enter apply · p profiles · Esc close" : "Preview omitted · ←/→ panes · ↑/↓ move · Enter apply · Esc close";
37
+ }
38
+
39
+ /** A row is interactive only while its `visible()` check passes; a throwing check fails closed. */
40
+ function rowVisible(row: CustomizeRow): boolean {
41
+ if (!row.visible) return true;
42
+ try { return row.visible() !== false; } catch { return false; }
43
+ }
44
+
45
+ export type CustomizeCategory = "Animations" | "Banner" | "Themes" | "Editor" | "History" | "Notifications" | "Layout" | "Cards" | "Sections" | "Profiles" | "Reset";
33
46
 
34
47
  export interface CustomizeRow {
35
48
  category?: CustomizeCategory;
36
49
  label: string | (() => string);
37
50
  /** Read-only representation of the highlighted choice, never an application. */
38
51
  preview?: () => { title: string; sample: string };
39
- action(): void | Promise<void>;
52
+ /** When present and it returns false the row is hidden from its category; an exception fails closed. */
53
+ visible?: () => boolean;
54
+ /** Audio-footer hint for this row; omitted rows use the category default (never advertising `f WAV`). */
55
+ keyhint?: string;
56
+ /** Runs on Enter/Space. May await the inline bridge for a row-scoped edit or consent. */
57
+ action(inline: CustomizeInline): void | Promise<void>;
58
+ /** Row-scoped shortcuts, checked before panel shortcuts. Return `true` for a synchronous
59
+ * consumption, or a Promise for an asynchronous task (e.g. the inline WAV field) the view
60
+ * must keep busy until it settles so overlapping keys/actions cannot write stale state. */
61
+ key?(data: string, inline: CustomizeInline): boolean | Promise<unknown>;
62
+ }
63
+ /** Minimal in-card bridge so a row never opens a nested native dialog. */
64
+ export interface CustomizeInline {
65
+ /** Opens an inline field on the highlighted row; resolves on submit or undefined when cancelled. */
66
+ input(request: { prompt: string; value: string }): Promise<string | undefined>;
67
+ /** Opens an inline confirmation; resolves true only after an explicit, fully visible yes. */
68
+ confirm(message: string): Promise<boolean>;
69
+ /** Set once the owning card is disposed; a row must never persist after this. */
70
+ disposed: boolean;
40
71
  }
41
72
  export interface ProfileActions {
42
73
  list(): VisualProfile[];
@@ -75,6 +106,98 @@ export class VisualCustomizeView {
75
106
  private confirmation?: { action: "replace" | "apply" | "delete" | "reset"; name?: string; fingerprint?: string };
76
107
  private confirmationVisible = false;
77
108
  private profileBusy = false;
109
+ private inlineInput?: { prompt: string; value: string; resolve: (value: string | undefined) => void; visible: boolean };
110
+ private inlineConfirm?: { message: string; resolve: (value: boolean) => void; visible: boolean };
111
+ private readonly inline: CustomizeInline = {
112
+ input: request => this.openInlineInput(request),
113
+ confirm: message => this.openInlineConfirm(message),
114
+ disposed: false,
115
+ };
116
+ /** Header and diagnostic context for the active pane; Notifications is explicitly audio. */
117
+ title(): string { return (this.categories[this.categoryIndex] ?? "Settings") === "Notifications" ? "Audio notifications" : "Customization"; }
118
+ /** Ends the interaction and resolves any pending inline field/consent as cancelled. */
119
+ dispose(): void { this.closed = true; this.inline.disposed = true; this.cancelInline(); }
120
+ private cancelInline(): void {
121
+ const input = this.inlineInput;
122
+ if (input) { this.inlineInput = undefined; input.resolve(undefined); }
123
+ const confirm = this.inlineConfirm;
124
+ if (confirm) { this.inlineConfirm = undefined; confirm.resolve(false); }
125
+ }
126
+ private openInlineInput(request: { prompt: string; value: string }): Promise<string | undefined> {
127
+ if (this.closed) return Promise.resolve(undefined);
128
+ return new Promise(resolve => { this.inlineInput = { prompt: request.prompt, value: request.value, resolve, visible: false }; });
129
+ }
130
+ private openInlineConfirm(message: string): Promise<boolean> {
131
+ if (this.closed) return Promise.resolve(false);
132
+ return new Promise(resolve => { this.inlineConfirm = { message, resolve, visible: false }; });
133
+ }
134
+ /** Runs one exclusive row task, keeping `busy` set until it settles so no stale write can win. */
135
+ private withBusy(run: () => void | Promise<unknown>): void {
136
+ if (this.busy) return;
137
+ this.busy = true;
138
+ try {
139
+ void Promise.resolve(run()).catch((error: unknown) => {
140
+ this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
141
+ }).finally(() => { this.busy = false; if (!this.closed) this.options.requestRender(); });
142
+ } catch (error) {
143
+ this.busy = false;
144
+ this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
145
+ }
146
+ }
147
+ /** Wraps an inline prompt into at most `capacity` fully visible lines. */
148
+ private wrapInline(text: string, inner: number, capacity: number): string[] | undefined {
149
+ if (capacity < 1 || inner < 1) return undefined;
150
+ const lines: string[] = [];
151
+ let remaining = text;
152
+ while (remaining.length) {
153
+ if (lines.length >= capacity) return undefined;
154
+ if (visibleWidth(remaining) <= inner) { lines.push(remaining); return lines; }
155
+ let chunk = "";
156
+ for (const { segment } of graphemes.segment(remaining)) {
157
+ if (visibleWidth(chunk + segment) > inner) break;
158
+ chunk += segment;
159
+ }
160
+ if (!chunk) return undefined;
161
+ lines.push(chunk);
162
+ remaining = remaining.slice(chunk.length);
163
+ }
164
+ return lines.length ? lines : undefined;
165
+ }
166
+ /** Recomputes the inline area from the live terminal height, never from stale render state. */
167
+ private previewCapacity(): { inner: number; lines: number; navOnly: boolean } | undefined {
168
+ const width = this.mainWidth;
169
+ if (width === undefined || width <= 0) return undefined;
170
+ const available = this.options.rowsAvailable?.() ?? 24;
171
+ const height = Number.isFinite(available) ? Math.max(0, Math.min(24, Math.floor(available))) : 0;
172
+ if (width < CARD_PADDING + 1 || height < 4) return undefined;
173
+ const navOnly = width < 60 && this.pane === "categories" && this.categories.length > 1;
174
+ return { inner: width - CARD_PADDING, lines: height >= 11 ? 3 : height >= 8 ? 1 : 0, navOnly };
175
+ }
176
+ private handleInlineInput(data: string): void {
177
+ const field = this.inlineInput!;
178
+ if (matchesKey(data, Key.escape)) { this.inlineInput = undefined; field.resolve(undefined); }
179
+ else if (data.startsWith("\x1b[200~")) {
180
+ const pasted = pastedText(data);
181
+ if (pasted !== undefined) field.value = appendInput(field.value, pasted);
182
+ } else if (matchesKey(data, Key.enter) || data.endsWith("\r") && safeText(data.slice(0, -1))) {
183
+ if (!matchesKey(data, Key.enter)) field.value = appendInput(field.value, data.slice(0, -1));
184
+ // Guard against a resize that hides the field before this Enter: an unseen value never submits.
185
+ const area = this.previewCapacity();
186
+ field.visible = area !== undefined && !area.navOnly && area.lines > 0 && visibleWidth(`${field.prompt}: ${field.value}▏`) <= area.inner;
187
+ if (field.visible) { this.inlineInput = undefined; field.resolve(field.value); }
188
+ } else if (matchesKey(data, Key.backspace) || data === "\x7f") field.value = dropLastGrapheme(field.value);
189
+ else { const text = decodeKittyPrintable(data) ?? data; if (safeText(text)) field.value = appendInput(field.value, text); }
190
+ this.options.requestRender();
191
+ }
192
+ private handleInlineConfirm(data: string): void {
193
+ const confirm = this.inlineConfirm!;
194
+ const area = this.previewCapacity();
195
+ confirm.visible = area !== undefined && !area.navOnly && area.lines > 0
196
+ && this.wrapInline(`${confirm.message} y yes · any other key cancel`, area.inner, area.lines) !== undefined;
197
+ this.inlineConfirm = undefined;
198
+ confirm.resolve(matchesKey(data, "y") && confirm.visible);
199
+ this.options.requestRender();
200
+ }
78
201
  openProfiles(): void { this.profileOpen = true; this.error = undefined; this.options.requestRender(); }
79
202
  private profileAction(action: () => void | Promise<void>): void {
80
203
  if (this.profileBusy) return;
@@ -176,18 +299,41 @@ export class VisualCustomizeView {
176
299
  }
177
300
 
178
301
  private categoryRows(): CustomizeRow[] {
179
- return this.options.rows.filter(row => (row.category ?? "Settings") === this.categories[this.categoryIndex]);
302
+ return this.options.rows.filter(row => (row.category ?? "Settings") === this.categories[this.categoryIndex] && rowVisible(row));
180
303
  }
181
304
 
182
305
  handleInput(data: string): void {
183
306
  if (this.closed || isKeyRelease(data)) return;
307
+ if (this.inlineInput) { this.handleInlineInput(data); return; }
308
+ if (this.inlineConfirm) { this.handleInlineConfirm(data); return; }
184
309
  if (this.profileOpen && this.options.profiles) { this.handleProfiles(data); return; }
185
- if (matchesKey(data, Key.escape)) { this.closed = true; this.options.onClose(); return; }
186
- if (this.options.profiles && matchesKey(data, "p")) { this.openProfiles(); return; }
310
+ if (matchesKey(data, Key.escape)) { this.dispose(); this.options.onClose(); return; }
187
311
  const rows = this.categoryRows();
188
312
  const length = this.pane === "categories" ? this.categories.length : rows.length;
313
+ if (length) {
314
+ this.selected = Math.min(this.controls.get(this.categories[this.categoryIndex]!) ?? 0, Math.max(0, rows.length - 1));
315
+ // Row shortcuts outrank panel shortcuts (Notifications `p` previews instead of opening profiles).
316
+ if (this.pane === "controls" && !this.busy) {
317
+ let consumed = false;
318
+ try {
319
+ const keyed = rows[this.selected]?.key?.(data, this.inline);
320
+ if (keyed !== undefined && keyed !== false) {
321
+ consumed = true;
322
+ // An async key (local WAV field + validation) owns the card until it settles.
323
+ if (typeof (keyed as Promise<unknown>).then === "function") this.withBusy(() => keyed as Promise<unknown>);
324
+ }
325
+ } catch (error) {
326
+ consumed = true;
327
+ this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
328
+ }
329
+ if (consumed) { this.options.requestRender(); return; }
330
+ }
331
+ }
332
+ // Notifications rows own `p` for a per-event preview. When no event row is
333
+ // selected the panel must consume it instead of opening visual profiles.
334
+ if (matchesKey(data, "p") && this.pane === "controls" && (this.categories[this.categoryIndex] ?? "Settings") === "Notifications") { this.options.requestRender(); return; }
335
+ if (this.options.profiles && matchesKey(data, "p")) { this.openProfiles(); return; }
189
336
  if (!length) return;
190
- this.selected = Math.min(this.controls.get(this.categories[this.categoryIndex]!) ?? 0, Math.max(0, rows.length - 1));
191
337
  if (matchesKey(data, Key.left) || matchesKey(data, Key.tab) && this.pane === "controls") this.pane = "categories";
192
338
  else if (matchesKey(data, Key.right) || matchesKey(data, Key.tab) && this.pane === "categories") this.pane = "controls";
193
339
  else if (matchesKey(data, Key.up) || matchesKey(data, "k")) {
@@ -200,15 +346,7 @@ export class VisualCustomizeView {
200
346
  else if ((matchesKey(data, Key.enter) || matchesKey(data, Key.space)) && !this.busy &&
201
347
  this.mainWidth !== undefined && this.mainWidth >= CARD_PADDING + 3 &&
202
348
  (this.options.rowsAvailable?.() ?? 24) >= 4 && rows[this.selected]) {
203
- this.busy = true;
204
- try {
205
- void Promise.resolve(rows[this.selected]?.action()).catch((error: unknown) => {
206
- this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
207
- }).finally(() => { this.busy = false; if (!this.closed) this.options.requestRender(); });
208
- } catch (error) {
209
- this.busy = false;
210
- this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
211
- }
349
+ this.withBusy(() => rows[this.selected]!.action(this.inline));
212
350
  }
213
351
  this.options.requestRender();
214
352
  }
@@ -225,7 +363,7 @@ export class VisualCustomizeView {
225
363
  if (!height) return [];
226
364
  // A card needs two border rows. At extreme dimensions keep the old
227
365
  // read-only heading rather than letting invisible controls be activated.
228
- if (width < CARD_PADDING + 1 || height < 4) return Array.from({ length: Math.min(height, 2) }, (_, i) => truncateToWidth(i ? "Enlarge terminal" : "Visual customization", width, ""));
366
+ if (width < CARD_PADDING + 1 || height < 4) return Array.from({ length: Math.min(height, 2) }, (_, i) => truncateToWidth(i ? "Enlarge terminal" : this.title(), width, ""));
229
367
  const inner = width - CARD_PADDING;
230
368
  const category = this.categories[this.categoryIndex] ?? "Settings";
231
369
  const rows = this.categoryRows();
@@ -250,7 +388,7 @@ export class VisualCustomizeView {
250
388
  };
251
389
  const join = (left: string, right: string) => leftWidth ? `${left}${" ".repeat(Math.max(0, leftWidth - visibleWidth(stripAnsi(left))))} ${paint("│", 1, "border")} ${right}` : right;
252
390
  const heading = navOnly ? "Categories · choose a category →" : `${category} · ${rows.length ? `${this.selected + 1}/${rows.length}` : "empty"}`;
253
- const lines: string[] = [paint("Visual customization", inner, "accent")];
391
+ const lines: string[] = [paint(this.title(), inner, "accent")];
254
392
  // Commands uses a header, grouped section labels, whitespace and a footer.
255
393
  if (showHeading) lines.push(join(leftWidth ? paint("CATEGORIES", leftWidth, "accent") : "", paint(heading, rightWidth, "accent")));
256
394
  if (showTopGap) lines.push("");
@@ -262,7 +400,19 @@ export class VisualCustomizeView {
262
400
  const right = item ? renderPaletteSelection(item, rightWidth, start + i === position, navOnly || this.pane === "controls" ? this.options.theme : { fg: this.options.theme.fg.bind(this.options.theme) }) : rows.length ? "" : paint("No settings available", rightWidth, "muted");
263
401
  lines.push(join(left, right));
264
402
  }
265
- if (previewLines === 3) {
403
+ if (this.inlineConfirm) {
404
+ const shows = !navOnly && previewLines > 0 ? this.wrapInline(`${this.inlineConfirm.message} y yes · any other key cancel`, inner, previewLines) : undefined;
405
+ this.inlineConfirm.visible = shows !== undefined;
406
+ if (shows) for (const line of shows) lines.push(paint(line, inner, "warning"));
407
+ else if (previewLines > 0) lines.push(paint("Confirmation needs more space · enlarge terminal", inner, "warning"));
408
+ } else if (this.inlineInput) {
409
+ const field = `${this.inlineInput.prompt}: ${this.inlineInput.value}▏`;
410
+ this.inlineInput.visible = !navOnly && previewLines > 0 && visibleWidth(field) <= inner;
411
+ if (previewLines > 0) {
412
+ lines.push(paint(field, inner, "accent"));
413
+ if (previewLines === 3) lines.push(paint(this.inlineInput.visible ? "Enter confirm · Esc cancel" : "Value too wide · enlarge terminal · Esc cancel", inner, "muted"));
414
+ }
415
+ } else if (previewLines === 3) {
266
416
  let preview: ReturnType<NonNullable<CustomizeRow["preview"]>> | undefined;
267
417
  try { if (!navOnly) preview = rows[this.selected]?.preview?.(); } catch { /* Never substitute the active theme for an unreadable source. */ }
268
418
  lines.push("");
@@ -270,9 +420,10 @@ export class VisualCustomizeView {
270
420
  lines.push(paint(navOnly ? "" : preview?.sample ?? "No read-only sample for this choice", inner, "muted"));
271
421
  } else if (previewLines === 1) lines.push(paint(navOnly ? "→ controls · Esc close" : "Preview omitted · enlarge terminal", inner, "muted"));
272
422
  if (showBottomGap) lines.push("");
273
- if (showHeading) lines.push(paint(navOnly ? "↑/↓ categories · → open · Esc close" : previewLines ? "←/→ or Tab panes · ↑/↓ move · Enter apply · p profiles · Esc close" : "Preview omitted · ←/→ panes · ↑/↓ move · Enter apply · Esc close", inner, "muted"));
423
+ if (showHeading) lines.push(paint(footerHint(category, navOnly, previewLines, rows[this.selected]?.keyhint), inner, "muted"));
274
424
  // The top-right close hint mirrors Commands without consuming another row.
275
- lines[0] = paint("Visual customization" + " ".repeat(Math.max(1, inner - visibleWidth("Visual customization") - 3)) + "esc", inner, "accent");
425
+ const title = this.title();
426
+ lines[0] = paint(title + " ".repeat(Math.max(1, inner - visibleWidth(title) - 3)) + "esc", inner, "accent");
276
427
  return renderPaletteCard(lines.slice(0, height - 2), width, this.options.theme);
277
428
  }
278
429
  private detailSize(width: number): { height: number; inner: number; capacity: number } {