gentle-pi 3.7.0 → 4.1.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 (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  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 +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -27,7 +27,6 @@ export interface ChangesModel {
27
27
  files: ChangedFile[];
28
28
  added: number;
29
29
  deleted: number;
30
- notice?: string;
31
30
  }
32
31
 
33
32
  export interface ChangesSnapshot {
@@ -46,6 +45,11 @@ interface NumstatEntry {
46
45
  }
47
46
 
48
47
  export const CHANGES_COMMAND = "/gentle:changes";
48
+ // One selected file's diff is the only bounded thing in Changes: past either
49
+ // limit the panel shows DIFF_TOO_LARGE instead of buffering and coloring it.
50
+ export const DIFF_MAX_BYTES = 1024 * 1024;
51
+ export const DIFF_MAX_LINES = 20_000;
52
+ export const DIFF_TOO_LARGE = "Diff too large — press o to open in editor.";
49
53
  const WIDGET_GLYPH = "✎";
50
54
  const STATUS_BY_CODE: Record<string, ChangeStatus> = {
51
55
  A: CHANGE_STATUS.ADDED,
@@ -123,7 +127,7 @@ export function changesModel(changed: ChangedFile[]): ChangesModel {
123
127
 
124
128
  export function changesSummary(model: ChangesModel): string {
125
129
  const noun = model.files.length === 1 ? "file" : "files";
126
- return `${model.files.length} ${noun} · +${model.added} −${model.deleted}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}${model.notice ? " · capture limit reached" : ""}`;
130
+ return `${model.files.length} ${noun} · +${model.added} −${model.deleted}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}`;
127
131
  }
128
132
 
129
133
  // One line: summary, the files joined by dots, and the command pushed to
@@ -132,7 +136,7 @@ export function renderChangesWidget(model: ChangesModel, theme: ChangesTheme, wi
132
136
  if (model.files.length === 0) return [];
133
137
  const noun = model.files.length === 1 ? "file" : "files";
134
138
  const dot = theme.fg("muted", "·");
135
- const head = `${theme.fg("accent", WIDGET_GLYPH)} ${theme.fg("text", `${model.files.length} ${noun}`)} ${dot} ${theme.fg("success", `+${model.added}`)} ${theme.fg("error", `−${model.deleted}`)}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}${model.notice ? ` ${dot} ${theme.fg("warning", "capture limit reached")}` : ""}`;
139
+ const head = `${theme.fg("accent", WIDGET_GLYPH)} ${theme.fg("text", `${model.files.length} ${noun}`)} ${dot} ${theme.fg("success", `+${model.added}`)} ${theme.fg("error", `−${model.deleted}`)}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}`;
136
140
  const hint = theme.fg("dim", CHANGES_COMMAND);
137
141
  const list = model.files.map((file) => theme.fg("muted", file.path)).join(` ${dot} `);
138
142
  const left = `${head} ${dot} ${list}`;
@@ -146,10 +150,14 @@ export function renderChangesWidget(model: ChangesModel, theme: ChangesTheme, wi
146
150
  export interface GitResult {
147
151
  stdout: string;
148
152
  code: number;
153
+ /** The runner stopped reading at its output bound; stdout is partial. */
154
+ truncated?: boolean;
149
155
  }
150
156
 
151
157
  export type GitRunner = (args: string[]) => Promise<GitResult>;
152
158
  export type LineCounter = (path: string) => Promise<number>;
159
+ /** A cheap content identity (e.g. size and mtime) so same-count edits refresh their diff. */
160
+ export type FileRevision = (path: string) => Promise<string | undefined>;
153
161
 
154
162
  export interface WorktreeChanges {
155
163
  root: string;
@@ -189,76 +197,41 @@ export async function foreignRootBranch(git: GitRunner): Promise<string | undefi
189
197
  return verified.code === 0 ? branch : "no commits yet";
190
198
  }
191
199
 
192
- // The session-evidence tracker behind /gentle:changes records roots, never
193
- // branches, so the overlay would label every tree "detached". This resolves
194
- // each root's own HEAD state once (branch, "no commits yet", undefined for a
195
- // real detached HEAD, or UNKNOWN_BRANCH when the root could not even be
196
- // asked), decorates the trees from the cache, and tells the caller when a
197
- // fresh answer arrived so the view can repaint. Git is touched only while the
198
- // overlay is open, never at startup or on evidence refresh.
199
- export class RootBranchLabels {
200
- private readonly labels = new Map<string, string | undefined>();
201
- private readonly pending = new Map<string, Promise<void>>();
202
- private readonly gitForRoot: (root: string) => GitRunner;
203
- private readonly onChange: () => void;
204
- constructor(gitForRoot: (root: string) => GitRunner, onChange: () => void = () => {}) {
205
- this.gitForRoot = gitForRoot;
206
- this.onChange = onChange;
207
- }
208
-
209
- decorate(trees: readonly WorktreeChanges[]): WorktreeChanges[] {
210
- return trees.map((tree) => {
211
- if (tree.branch !== undefined) return tree;
212
- if (!this.labels.has(tree.root)) this.resolve(tree.root);
213
- const branch = this.labels.get(tree.root);
214
- return branch === undefined ? tree : { ...tree, branch };
215
- });
216
- }
217
-
218
- /** Every in-flight resolution has finished. */
219
- async settled(): Promise<void> {
220
- while (this.pending.size) await Promise.all(this.pending.values());
221
- }
222
-
223
- private resolve(root: string): void {
224
- if (this.pending.has(root)) return;
225
- // A rejection here means the root could not even be asked -- distinct
226
- // from foreignRootBranch resolving to undefined, which means git
227
- // answered and there really is no branch (a real detached HEAD).
228
- const task = foreignRootBranch(this.gitForRoot(root))
229
- .then(
230
- (branch) => branch,
231
- () => UNKNOWN_BRANCH,
232
- )
233
- .then((branch) => {
234
- this.labels.set(root, branch);
235
- this.pending.delete(root);
236
- this.onChange();
237
- });
238
- this.pending.set(root, task);
239
- }
240
- }
241
-
200
+ // /gentle:changes: one group per registered root holding its working tree
201
+ // against HEAD. A root whose Git fails (timed out, rejected) has no group but
202
+ // is listed in failedRoots: a failed scan is never reported as a clean root.
242
203
  export class WorktreeChangesTracker {
243
204
  worktrees: WorktreeChanges[] = [];
205
+ failedRoots: string[] = [];
206
+ scannedRootCount = 0;
244
207
  private inFlight: Promise<ChangesModel> | undefined;
245
208
  private readonly discover: GitRunner;
246
209
  private readonly gitForRoot: (root: string) => GitRunner;
247
210
  private readonly linesForRoot: (root: string) => LineCounter;
248
211
  private readonly registeredRoots: () => readonly string[];
212
+ private readonly revisionForRoot: (root: string) => FileRevision | undefined;
249
213
 
250
- constructor(discover: GitRunner, gitForRoot: (root: string) => GitRunner, linesForRoot: (root: string) => LineCounter = () => noLines, registeredRoots: () => readonly string[] = () => []) {
214
+ constructor(discover: GitRunner, gitForRoot: (root: string) => GitRunner, linesForRoot: (root: string) => LineCounter = () => noLines, registeredRoots: () => readonly string[] = () => [], revisionForRoot: (root: string) => FileRevision | undefined = () => undefined) {
251
215
  this.discover = discover;
252
216
  this.gitForRoot = gitForRoot;
253
217
  this.linesForRoot = linesForRoot;
254
218
  this.registeredRoots = registeredRoots;
219
+ this.revisionForRoot = revisionForRoot;
255
220
  }
256
221
 
222
+ private modelOf: WorktreeChanges[] | undefined;
223
+ private combined: ChangesModel = emptyChanges();
224
+
225
+ // Footers read this every frame; rebuild only when a scan replaced the trees.
257
226
  get model(): ChangesModel {
258
- return changesModel(this.worktrees.flatMap((tree) => tree.model.files.map((file) => ({
259
- ...file,
260
- path: this.worktrees.length === 1 ? file.path : `${tree.root}/${file.path}`,
261
- }))));
227
+ if (this.modelOf !== this.worktrees) {
228
+ this.modelOf = this.worktrees;
229
+ this.combined = changesModel(this.worktrees.flatMap((tree) => tree.model.files.map((file) => ({
230
+ ...file,
231
+ path: this.worktrees.length === 1 ? file.path : `${tree.root}/${file.path}`,
232
+ }))));
233
+ }
234
+ return this.combined;
262
235
  }
263
236
 
264
237
  async start(): Promise<void> {
@@ -278,17 +251,29 @@ export class WorktreeChangesTracker {
278
251
  }
279
252
 
280
253
  private async capture(): Promise<ChangesModel> {
281
- const result = await this.discover(["worktree", "list", "--porcelain", "-z"]).catch(() => ({ code: 128, stdout: "" }));
282
- const trees: WorktreeChanges[] = [];
283
- const metadata = new Map((result.code === 0 ? parseWorktrees(result.stdout) : []).map((tree) => [tree.root, tree]));
284
254
  // Discovery labels registered roots; it never grants visibility to siblings.
285
255
  const roots = new Set(this.registeredRoots());
286
- for (const root of roots) {
256
+ // No root to scan means no Git at all, not even branch discovery.
257
+ if (roots.size === 0) {
258
+ this.worktrees = [];
259
+ this.failedRoots = [];
260
+ this.scannedRootCount = 0;
261
+ return this.model;
262
+ }
263
+ const result = await this.discover(["worktree", "list", "--porcelain", "-z"]).catch(() => ({ code: 128, stdout: "" }));
264
+ const metadata = new Map((result.code === 0 ? parseWorktrees(result.stdout) : []).map((tree) => [tree.root, tree]));
265
+ // Roots scan in parallel so one slow root does not delay the others.
266
+ const failed: string[] = [];
267
+ const scanned = await Promise.all([...roots].map(async (root): Promise<WorktreeChanges | undefined> => {
287
268
  const known = metadata.get(root);
288
269
  try {
289
- const tracker = new ChangesTracker(this.gitForRoot(root), this.linesForRoot(root));
270
+ const tracker = new ChangesTracker(this.gitForRoot(root), this.linesForRoot(root), this.revisionForRoot(root));
290
271
  await tracker.start();
291
- if (!tracker.model.files.length) continue;
272
+ if (!tracker.statusRead) {
273
+ failed.push(root);
274
+ return undefined;
275
+ }
276
+ if (!tracker.model.files.length) return undefined;
292
277
  // A root missing from this discovery scan belongs to a different Git
293
278
  // clone entirely (e.g. an inner repository nested inside an outer
294
279
  // one) -- worktree list can never describe a foreign clone, so ask
@@ -299,22 +284,40 @@ export class WorktreeChangesTracker {
299
284
  // change scan above failing -- that must label the root
300
285
  // UNKNOWN_BRANCH, not drop real, already-captured changes.
301
286
  const branch = known ? known.branch : await foreignRootBranch(this.gitForRoot(root)).catch(() => UNKNOWN_BRANCH);
302
- trees.push({ root, ...(branch ? { branch } : {}), model: tracker.model });
287
+ return { root, ...(branch ? { branch } : {}), model: tracker.model };
303
288
  } catch {
304
- // Linked roots can disappear between discovery and status.
289
+ // A root the caller stops registering mid-scan (e.g. a removed
290
+ // worktree) triggers the rescan below without it, not a failure.
291
+ failed.push(root);
292
+ return undefined;
305
293
  }
306
- }
294
+ }));
295
+ const trees = scanned.filter((tree): tree is WorktreeChanges => tree !== undefined);
307
296
  const latest = new Set(this.registeredRoots());
308
297
  // A root admitted during status must not wait for a later poll (which may
309
298
  // be disabled). Ordinary overlapping polls still share exactly one scan.
310
299
  if (latest.size !== roots.size || [...latest].some((root) => !roots.has(root))) return this.capture();
311
300
  this.worktrees = trees;
301
+ this.failedRoots = failed;
302
+ this.scannedRootCount = roots.size;
312
303
  return this.model;
313
304
  }
314
305
  }
315
306
 
316
307
  const noLines: LineCounter = async () => 0;
317
308
 
309
+ // Per-file line counts and revision lookups are file IO: a large change set
310
+ // runs this many files at a time instead of one by one or all at once.
311
+ export const FILE_WORK_CONCURRENCY = 8;
312
+
313
+ async function eachLimited<T>(items: readonly T[], limit: number, work: (item: T) => Promise<void>): Promise<void> {
314
+ let next = 0;
315
+ const worker = async () => {
316
+ while (next < items.length) await work(items[next++]);
317
+ };
318
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
319
+ }
320
+
318
321
  const NUMSTAT_ARGS = ["diff", "--numstat", "HEAD"];
319
322
  const PORCELAIN_ARGS = ["status", "--porcelain=v1", "--untracked-files=all", "-z"];
320
323
 
@@ -324,20 +327,27 @@ const PORCELAIN_ARGS = ["status", "--porcelain=v1", "--untracked-files=all", "-z
324
327
  export class ChangesTracker {
325
328
  private readonly git: GitRunner;
326
329
  private readonly countLines: LineCounter;
330
+ private readonly revision: FileRevision | undefined;
327
331
  private current: ChangesModel = emptyChanges();
328
332
  private available = false;
329
333
  private inFlight: Promise<ChangesModel> | undefined;
330
334
  private queued = false;
331
335
 
332
- constructor(git: GitRunner, countLines: LineCounter = noLines) {
336
+ constructor(git: GitRunner, countLines: LineCounter = noLines, revision?: FileRevision) {
333
337
  this.git = git;
334
338
  this.countLines = countLines;
339
+ this.revision = revision;
335
340
  }
336
341
 
337
342
  get model(): ChangesModel {
338
343
  return this.current;
339
344
  }
340
345
 
346
+ /** False when the last start could not read `git status`. */
347
+ get statusRead(): boolean {
348
+ return this.available;
349
+ }
350
+
341
351
  async start(): Promise<void> {
342
352
  const files = await this.capture();
343
353
  this.available = files !== undefined;
@@ -372,9 +382,14 @@ export class ChangesTracker {
372
382
  if (porcelain.code !== 0) return undefined;
373
383
  const files = snapshotChanges({ numstat: numstat.stdout, porcelain: porcelain.stdout });
374
384
  // Untracked files never appear in numstat; count their lines directly.
375
- for (const file of files) {
376
- if (file.status === CHANGE_STATUS.UNTRACKED) file.added = await this.countLines(file.path).catch(() => 0);
377
- }
385
+ await eachLimited(files, FILE_WORK_CONCURRENCY, async (file) => {
386
+ const [added, revision] = await Promise.all([
387
+ file.status === CHANGE_STATUS.UNTRACKED ? this.countLines(file.path).catch(() => 0) : undefined,
388
+ this.revision?.(file.path).catch(() => undefined),
389
+ ]);
390
+ if (added !== undefined) file.added = added;
391
+ if (revision) file.diffRevision = revision;
392
+ });
378
393
  return files;
379
394
  }
380
395
  }
@@ -1,9 +1,9 @@
1
1
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import { stripAnsi } from "./terminal-theme.ts";
3
+ import { CARD_STYLE, type CardStyle } from "./shell-card.ts";
3
4
 
4
- // Gentle Shell prompt frame. pi's editor renders a top rule, padded content
5
- // lines, and a bottom rule; this module wraps those lines in a rounded frame
6
- // with a petal that shows what the agent is doing. Everything here is pure.
5
+ // Pure prompt chrome: neon keeps Pi's rule/content/rule rows in a rounded
6
+ // frame; float retains those indices as painted status/content/padding.
7
7
 
8
8
  export const PROMPT_STATE = {
9
9
  IDLE: "idle",
@@ -43,6 +43,8 @@ export function scanWorkingText(text: string, tick: number, fg: (role: string, t
43
43
  const PETAL_TONE_FRAMES = [PETAL_TONE.DEEP, PETAL_TONE.SOFT, PETAL_TONE.ROSE, PETAL_TONE.BRIGHT, PETAL_TONE.ROSE, PETAL_TONE.SOFT, PETAL_TONE.DEEP, PETAL_TONE.DEEP] as const;
44
44
 
45
45
  export interface PromptFrameOptions {
46
+ style?: CardStyle;
47
+ bg?: (role: string, text: string) => string;
46
48
  state: PromptState;
47
49
  tick: number;
48
50
  borderColor: (text: string) => string;
@@ -50,11 +52,17 @@ export interface PromptFrameOptions {
50
52
  bold?: (text: string) => string;
51
53
  /**
52
54
  * Overrides the editor's own bottom scroll indicator (e.g. "esc again to
53
- * cancel"). Both share the bottom rule's single label slot; an explicit
55
+ * cancel"). Both share the bottom rule / float hint slot; an explicit
54
56
  * hint always wins because it reflects state the editor cannot render on
55
57
  * its own.
56
58
  */
57
59
  escHint?: string;
60
+ /**
61
+ * Overrides the generic "working…" label while state is WORKING with an
62
+ * explicit, orchestrator-reported ODD phase label (e.g. "exploring…").
63
+ * Ignored outside the WORKING state; undefined falls back to "working…".
64
+ */
65
+ workingLabel?: string;
58
66
  }
59
67
 
60
68
  // A terminal cell cannot grow, so the petal earns presence with weight and
@@ -93,8 +101,13 @@ function rule(length: number): string {
93
101
  return "─".repeat(Math.max(0, length));
94
102
  }
95
103
 
104
+ function stateLabel(options: PromptFrameOptions): string | undefined {
105
+ if (options.state === PROMPT_STATE.WORKING) return options.workingLabel ?? STATE_LABEL[PROMPT_STATE.WORKING];
106
+ return STATE_LABEL[options.state];
107
+ }
108
+
96
109
  function topRule(width: number, options: PromptFrameOptions, indicator: string | undefined): string {
97
- const label = indicator ?? STATE_LABEL[options.state];
110
+ const label = indicator ?? stateLabel(options);
98
111
  const scanning = options.state === PROMPT_STATE.WORKING;
99
112
  const glyph = petalGlyph(options.state, options.tick);
100
113
  const petal = options.fg(petalTone(options.state, options.tick), options.bold ? options.bold(glyph) : glyph);
@@ -120,8 +133,91 @@ function sideRules(line: string, innerWidth: number, options: PromptFrameOptions
120
133
  return options.borderColor("│") + content + options.borderColor("│");
121
134
  }
122
135
 
123
- export function framePromptLines(lines: string[], width: number, options: PromptFrameOptions): string[] {
136
+ export interface PromptLayout {
137
+ width: number;
138
+ nativeWidth: number;
139
+ prefixWidth: number;
140
+ background: string;
141
+ }
142
+
143
+ /** Resolve fallback before native wrapping, selection, completion and mouse geometry. */
144
+ export function resolvePromptLayout(width: number, style?: CardStyle, bg?: PromptFrameOptions["bg"]): PromptLayout {
145
+ width = Math.max(0, Math.floor(width));
146
+ let background = "";
147
+ if (style === CARD_STYLE.FLOAT && width >= 10 && bg) {
148
+ try {
149
+ const painted = bg("toolSuccessBg", " ");
150
+ const match = painted.match(/^((?:\x1b\[[\d;]*m)+) (?:\x1b\[(?:49|0)?m)$/);
151
+ if (match) {
152
+ for (const sgr of match[1].matchAll(/\x1b\[([\d;]*)m/g)) {
153
+ for (const command of promptSgrCommands(sgr[1])) {
154
+ const code = command[0];
155
+ if (code === 0 || code === 49) background = "";
156
+ else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107) ||
157
+ (code === 48 && ((command[1] === 5 && command.length === 3) || (command[1] === 2 && command.length === 5)))) {
158
+ background = `\x1b[${command.join(";")}m`;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ } catch { /* Missing theme background: retain the entire neon path. */ }
164
+ }
165
+ return { width, nativeWidth: Math.max(1, width - (background ? 5 : 2)), prefixWidth: background ? 3 : 1, background };
166
+ }
167
+
168
+ // Extended-colour parameters belong to one command: zero RGB channels
169
+ // must never be interpreted as full styling resets.
170
+ function promptSgrCommands(parameters: string): number[][] {
171
+ const codes = parameters.split(";").map(Number);
172
+ const commands: number[][] = [];
173
+ for (let i = 0; i < codes.length;) {
174
+ const extended = codes[i] === 38 || codes[i] === 48 || codes[i] === 58;
175
+ const length = extended && codes[i + 1] === 2 ? 5 : extended && codes[i + 1] === 5 ? 3 : 1;
176
+ commands.push(codes.slice(i, i + length));
177
+ i += length;
178
+ }
179
+ return commands;
180
+ }
181
+
182
+ // Rearm only the panel background, leaving marker/inversion bytes intact.
183
+ function rearmPromptBackground(text: string, background: string): string {
184
+ return text.replace(/\x1b\[([\d;]*)m/g, (sgr, parameters: string) =>
185
+ promptSgrCommands(parameters).some(([code]) => code === 0 || code === 49) ? sgr + background : sgr);
186
+ }
187
+
188
+ /** Same chrome and horizontal prefix for editable and native completion rows. */
189
+ export function floatPromptRow(line: string, layout: PromptLayout, borderColor: PromptFrameOptions["borderColor"]): string {
190
+ const clipped = truncateToWidth(line, layout.nativeWidth, "");
191
+ const content = borderColor("▎") + " " + clipped + " ".repeat(Math.max(0, layout.nativeWidth - visibleWidth(clipped))) + " ";
192
+ return " " + layout.background + rearmPromptBackground(content, layout.background) + "\x1b[49m ";
193
+ }
194
+
195
+ function floatPromptHint(top: string, bottom: string, options: PromptFrameOptions, width: number): string {
196
+ const glyph = petalGlyph(options.state, options.tick);
197
+ const petal = options.fg(petalTone(options.state, options.tick), options.bold ? options.bold(glyph) : glyph);
198
+ const label = stateLabel(options) ?? "waiting for input";
199
+ let paintedLabel = "";
200
+ if (label) {
201
+ paintedLabel = options.state === PROMPT_STATE.WORKING
202
+ ? scanWorkingText(label, options.tick, options.fg)
203
+ : options.fg(LABEL_ROLE, label);
204
+ }
205
+ const info = [scrollIndicator(top), options.escHint ?? scrollIndicator(bottom)]
206
+ .filter((text): text is string => Boolean(text))
207
+ .map((text) => options.fg(LABEL_ROLE, text));
208
+ const hint = truncateToWidth(" " + petal + (paintedLabel ? " " + paintedLabel : "") + (info.length ? " · " + info.join(" · ") : ""), width, "");
209
+ return hint + " ".repeat(Math.max(0, width - visibleWidth(hint)));
210
+ }
211
+
212
+ export function framePromptLines(lines: string[], width: number, options: PromptFrameOptions, layout = resolvePromptLayout(width, options.style, options.bg)): string[] {
124
213
  width = Math.max(0, Math.floor(width));
214
+ if (layout.background && lines.length >= 2) {
215
+ return [
216
+ floatPromptRow(floatPromptHint(lines[0], lines[lines.length - 1], options, layout.nativeWidth).slice(2), layout, options.borderColor),
217
+ ...lines.slice(1, -1).map((line) => floatPromptRow(line, layout, options.borderColor)),
218
+ floatPromptRow("", layout, options.borderColor),
219
+ ];
220
+ }
125
221
  if (lines.length < 2) return lines.map((line) => truncateToWidth(line, width, ""));
126
222
  if (width < 2) return lines.map((_line, index) => width === 0 ? "" : options.borderColor(index === 0 ? "╭" : index === lines.length - 1 ? "╰" : "│"));
127
223
  const innerWidth = width - 2;
@@ -1,7 +1,9 @@
1
1
  import { ScrollView, VStack, visibleWidth, type Component, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
2
2
  import { sidebarState, type SidebarRail } from "./shell-sidebar.ts";
3
3
  import type { ShellBarTheme } from "./shell-bar.ts";
4
+ import { CARD_STYLE, cardStyle, type CardStyle } from "./shell-card.ts";
4
5
  import { renderSidebarBanner } from "./shell-sidebar-banner.ts";
6
+ import type { Density, HeaderPlacement, StatusPlacement } from "./visual-customization-policy.ts";
5
7
 
6
8
  export const SIDEBAR_BREAKPOINT = 140;
7
9
  const RAIL_WIDTH = 50;
@@ -12,6 +14,8 @@ const RAIL_PADDING = 1;
12
14
  // the card instead of touching the terminal edge.
13
15
  const HEADER_RIGHT_INSET = RAIL_PADDING + 1;
14
16
  const GAP = 3;
17
+ /** Right-edge columns the painting rail takes from the editor column; published as `railColumns`. */
18
+ export const SIDEBAR_RAIL_COLUMNS = RAIL_WIDTH + GAP;
15
19
  // Experimental Pi 0.85.1 internals. Only the fullscreen layout tree is adapted;
16
20
  // regular mode keeps native scrollback and the original bottom components.
17
21
  const NODE = Symbol.for("@earendil-works/pi-tui/layout-node");
@@ -27,6 +31,8 @@ type PreparedRail = {
27
31
  revision: number;
28
32
  width: number;
29
33
  mode: string | undefined;
34
+ headerPlacement: HeaderPlacement;
35
+ cardStyle: CardStyle;
30
36
  root: LayoutRoot;
31
37
  theme: ShellBarTheme;
32
38
  parts: Array<[string, SidebarRail]>;
@@ -68,8 +74,28 @@ function railDigest(rail: SidebarRail): string | undefined {
68
74
  }
69
75
  }
70
76
 
77
+ export const STATUS_OWNER = { HEADER: "header", BOTTOM: "bottom" } as const;
78
+ export type StatusOwner = (typeof STATUS_OWNER)[keyof typeof STATUS_OWNER];
79
+ export interface StatusOwnerInput {
80
+ mode: string | undefined;
81
+ columns: number;
82
+ statusPlacement: StatusPlacement;
83
+ headerPlacement: HeaderPlacement;
84
+ }
85
+
86
+ /**
87
+ * Which single status row owns a narrow fullscreen terminal, so the header and
88
+ * the bottom bar never both paint there. A configured top header wins;
89
+ * otherwise the bottom bar does, unless Status is hidden and the below-input
90
+ * header is all that is left. Wide and regular layouts keep their own rules.
91
+ */
92
+ export function narrowStatusOwner(input: StatusOwnerInput): StatusOwner | undefined {
93
+ if (input.mode !== "fullscreen" || input.columns >= SIDEBAR_BREAKPOINT) return undefined;
94
+ return input.headerPlacement === "top" || input.statusPlacement === "hidden" ? STATUS_OWNER.HEADER : STATUS_OWNER.BOTTOM;
95
+ }
96
+
71
97
  /** Installs the fullscreen rail: wraps the host layout root with the [rail, transcript] hstack and returns a disposer restoring the original layout. */
72
- export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
98
+ export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () => StatusPlacement = () => "auto", headerPlacement: () => HeaderPlacement = () => "top", density: () => Density = () => "comfortable"): () => void {
73
99
  if (!tui.terminal) return () => {};
74
100
  const host = tui as Host;
75
101
  const state = sidebarState(tui);
@@ -89,7 +115,17 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
89
115
  // revision counter, exactly like the whole-rail memo already did.
90
116
  const sectionCache = new Map<string, SectionCacheEntry>();
91
117
  state.active = false;
92
- state.ownsHost = () => !stopped && host.mode === "fullscreen" && !!host.layoutRoot && roots.has(host.layoutRoot);
118
+ // Hidden removes Status everywhere, including regular mode where the rail
119
+ // never mounts, so it is published independently of the fullscreen layout.
120
+ state.statusHidden = () => !stopped && placement() === "hidden";
121
+ // A narrow top header is the only status row there. Whether it paints comes
122
+ // from the last layout pass (a blank or failed header never swallows the
123
+ // bottom bar); the geometry is read live so a resize applies before the next pass.
124
+ const headerOwnsStatus = () => !stopped && !failed && headerLines.length > 0 && headerPlacement() === "top" &&
125
+ narrowStatusOwner({ mode: host.mode, columns: tui.terminal.columns, statusPlacement: placement(), headerPlacement: headerPlacement() }) === STATUS_OWNER.HEADER;
126
+ state.headerOwnsStatus = headerOwnsStatus;
127
+ state.railColumns = SIDEBAR_RAIL_COLUMNS;
128
+ state.ownsHost = () => !stopped && host.mode === "fullscreen" && tui.terminal.columns >= SIDEBAR_BREAKPOINT && (placement() === "auto" || placement() === "right") && !!host.layoutRoot && roots.has(host.layoutRoot);
93
129
  const rail: Component = {
94
130
  render: () => railLines,
95
131
  invalidate() {
@@ -143,6 +179,10 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
143
179
  };
144
180
  scroll.handleMouse = (event) => {
145
181
  if (event.type === "wheel") {
182
+ // A bounded section (Todo) owns its inner viewport. Only wheel
183
+ // events it declines should move the surrounding rail.
184
+ const sectionResult = dispatchPartMouse(event);
185
+ if (sectionResult?.handled) return sectionResult;
146
186
  // Consume even at the boundary or over blank rail space: Pi 0.85.1
147
187
  // can send unconsumed delta to the primary transcript despite containment.
148
188
  scroll.scrollBy(event.wheelDelta ?? 0);
@@ -156,14 +196,30 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
156
196
  };
157
197
  const prepare = (width: number, root: LayoutRoot): boolean => {
158
198
  state.active = false;
159
- if (stopped || failed || host.mode !== "fullscreen" || width < SIDEBAR_BREAKPOINT) {
199
+ if (stopped || failed || host.mode !== "fullscreen") {
160
200
  prepared = undefined;
201
+ headerLines = [];
161
202
  return false;
162
203
  }
204
+ const railEligible = width >= SIDEBAR_BREAKPOINT && placement() !== "bottom" && placement() !== "hidden";
205
+ if (!railEligible) {
206
+ prepared = undefined;
207
+ try {
208
+ const headerPart = state.parts.get("header");
209
+ headerLines = headerPlacement() === "top" && headerPart
210
+ ? [...(headerPart.render(Math.max(0, width)) ?? [])] : [];
211
+ if (!headerLines.some((line) => line.trim() !== "")) headerLines = [];
212
+ } catch {
213
+ failed = true;
214
+ headerLines = [];
215
+ }
216
+ return false;
217
+ }
218
+ const style = cardStyle();
163
219
  const parts = [...state.parts.entries()];
164
220
  const digests = parts.map(([, rail]) => railDigest(rail));
165
221
  const unchanged = prepared?.revision === cache.revision &&
166
- prepared.width === width && prepared.mode === host.mode && prepared.root === root && prepared.theme === theme &&
222
+ prepared.width === width && prepared.mode === host.mode && prepared.headerPlacement === headerPlacement() && prepared.cardStyle === style && prepared.root === root && prepared.theme === theme &&
167
223
  prepared.parts.length === parts.length && prepared.parts.every(([key, part], index) => parts[index]?.[0] === key && parts[index]?.[1] === part) &&
168
224
  prepared.digests.length === digests.length && prepared.digests.every((digest, index) => digest === digests[index]);
169
225
  if (unchanged) {
@@ -179,7 +235,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
179
235
  const preparedHeaderLines = [...(headerPart?.render(Math.max(0, width - HEADER_RIGHT_INSET)) ?? [])];
180
236
  const headerActive = headerPart !== undefined && preparedHeaderLines.some((line) => line.trim() !== "");
181
237
  const contentWidth = scroll.getContentWidth(RAIL_WIDTH);
182
- const sections = ["footer", "agents", "todo"].map((key) => {
238
+ const sections = ["footer", "agents", "todo"].filter((key) => key !== "todo" || state.visibility?.todo !== false).map((key) => {
183
239
  const component = state.parts.get(key);
184
240
  if (!component) {
185
241
  sectionCache.delete(key);
@@ -204,23 +260,23 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
204
260
  railLines = [];
205
261
  if (sections.length && branding.length) {
206
262
  railLines.push(...branding.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING)));
207
- } else if (sections.length && headerActive) {
208
- // The banner used to hold the first card off the top; the header
209
- // took its place, so keep one blank row between them.
263
+ } else if (sections.length && headerActive && density() === "comfortable" && style === CARD_STYLE.NEON) {
264
+ // Preserve neon's header gap. Float's painted top padding starts
265
+ // on the same body row as the transcript, without an external gap.
210
266
  railLines.push("");
211
267
  }
212
268
  for (const section of sections) {
213
269
  // One blank row separates a section from the banner or the
214
- // previous section; the header gap above is not a section.
215
- if (hits.length > 0 || branding.length > 0) railLines.push("");
270
+ // previous section; neon's header gap above is not a section.
271
+ if ((hits.length > 0 || branding.length > 0) && density() === "comfortable") railLines.push("");
216
272
  const startY = railLines.length;
217
273
  railLines.push(...section.lines.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING)));
218
274
  hits.push({ key: section.key, component: section.component, startY, height: section.lines.length, width: contentWidth - RAIL_PADDING * 2 });
219
275
  }
220
276
  // Height is owned by the native ScrollView, never by the transcript.
221
277
  const active = railLines.length > 0 && railLines.every((line) => visibleWidth(line) <= contentWidth);
222
- headerLines = active && headerActive ? preparedHeaderLines : [];
223
- prepared = { revision: cache.revision, width, mode: host.mode, root, theme, parts, digests, contentWidth, active, lines: railLines, hits, headerLines, headerActive: headerLines.length > 0 };
278
+ headerLines = headerActive && headerPlacement() === "top" ? preparedHeaderLines : [];
279
+ prepared = { revision: cache.revision, width, mode: host.mode, headerPlacement: headerPlacement(), cardStyle: style, root, theme, parts, digests, contentWidth, active, lines: railLines, hits, headerLines, headerActive: headerLines.length > 0 };
224
280
  state.active = active;
225
281
  return active;
226
282
  } catch {
@@ -264,6 +320,11 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
264
320
  }
265
321
  return { ...node, entries: entries.map((entry, index) => index === entries.length - 1 ? { ...entry, component: wrapped! } : entry) };
266
322
  };
323
+ // Without a rail the native layout stays in place; a hidden Status, or a
324
+ // narrow top header owning it, only frees the footer's reserved dock row,
325
+ // exactly as the rail does.
326
+ const nativeLayout = () => placement() === "hidden" || headerOwnsStatus() ? reclaimFooterRow(original.call(root)) : original.call(root);
327
+ const nativeHost = { render: () => [], invalidate() {}, [NODE]: nativeLayout };
267
328
  const left = { render: () => [], invalidate() {}, [NODE]: () => reclaimFooterRow(original.call(root)) };
268
329
  // Stable component wrapping the [left, scroll] hstack behind its own
269
330
  // NODE, exactly like `left` wraps the native transcript: the header
@@ -278,10 +339,17 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
278
339
  ] }),
279
340
  };
280
341
  const replacement = () => {
281
- if (!prepare(tui.terminal.columns, root)) return original.call(root);
342
+ if (!prepare(tui.terminal.columns, root)) {
343
+ if (failed || stopped || host.mode !== "fullscreen") return original.call(root);
344
+ if (!headerLines.length) return nativeLayout();
345
+ return { type: "vstack", gap: 0, align: "stretch", entries: [
346
+ { component: header, basis: "auto", grow: 0, shrink: 0, minSize: 1 },
347
+ { component: nativeHost, basis: 0, grow: 1, shrink: 1, minSize: 1 },
348
+ ] };
349
+ }
282
350
  const current = prepared!;
283
351
  if (current.presentation?.scrollTop === scroll.scrollTop) return current.presentation.output;
284
- const output: LayoutNode = current.headerActive
352
+ const output: LayoutNode = current.headerActive && headerPlacement() !== "below-input"
285
353
  ? { type: "vstack", gap: 0, align: "stretch", entries: [
286
354
  { component: header, basis: "auto", grow: 0, shrink: 0, minSize: 1 },
287
355
  { component: hstackHost, basis: 0, grow: 1, shrink: 1, minSize: 1 },
@@ -311,7 +379,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
311
379
  stopped = true;
312
380
  state.active = false;
313
381
  clearInterval(timer);
314
- scroll.hideTransientScrollbar();
382
+ scroll.setScrollbar("hidden");
315
383
  for (const cleanup of cleanups.reverse()) cleanup();
316
384
  tui.requestRender();
317
385
  };
@@ -3,9 +3,23 @@ import type { Component, TUI } from "@earendil-works/pi-tui";
3
3
  // Store on the terminal, not a module singleton: extension loaders may isolate
4
4
  // modules, while Pi keeps this terminal across regular/fullscreen transitions.
5
5
  const STATE = Symbol.for("gentle-pi.experimental-sidebar.state");
6
+ /** In-process notification after a visual preference is persisted. */
7
+ export const VISUAL_SETTINGS_CHANGED = "gentle-pi.visual-settings-changed";
6
8
  export interface SidebarState {
7
9
  active: boolean;
10
+ visibility?: { todo?: boolean };
8
11
  ownsHost?: () => boolean;
12
+ /**
13
+ * Columns the fullscreen rail reserves at the right edge (rail plus gap)
14
+ * while `active && ownsHost()`. Overlays read it to stay inside the editor
15
+ * column (the prompt-history picker's right margin) without importing
16
+ * gentle-shell.
17
+ */
18
+ railColumns?: number;
19
+ /** True while Status placement is "hidden": the bottom Status bar paints nothing at any width or mode. */
20
+ statusHidden?: () => boolean;
21
+ /** True while a painting top header is the only status row of a narrow fullscreen terminal: the bottom Status bar steps aside. */
22
+ headerOwnsStatus?: () => boolean;
9
23
  parts: Map<string, SidebarRail>;
10
24
  }
11
25
 
@@ -32,7 +46,7 @@ export function sidebarPart<T extends Component & { dispose?(): void }>(tui: TUI
32
46
  state.parts.set(key, rail);
33
47
  return {
34
48
  ...bottom,
35
- render: (width: number) => state.active && state.ownsHost?.() ? [] : bottom.render(width),
49
+ render: (width: number) => (key === "todo" && state.visibility?.todo === false) || (key === "footer" && (state.statusHidden?.() || state.headerOwnsStatus?.())) || (state.active && state.ownsHost?.()) ? [] : bottom.render(width),
36
50
  dispose() {
37
51
  if (state.parts.get(key) === rail) state.parts.delete(key);
38
52
  bottom.dispose?.();