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
@@ -1,18 +1,21 @@
1
1
  import { CustomEditor, keyHint, type ExtensionAPI, type ExtensionContext, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
2
+ import { isHomeOrBroadUserDirectory } from "../lib/bounded-writer-admission.ts";
2
3
  import { Editor, decodeKittyPrintable, isKeyRelease, matchesKey, parseKey, truncateToWidth, visibleWidth, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
3
4
  import { execFile, spawnSync } from "node:child_process";
4
- import { realpathSync, statSync } from "node:fs";
5
+ import { existsSync, realpathSync, statSync } from "node:fs";
6
+ import { lstat, readFile } from "node:fs/promises";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import { profilesFilePath, profileRoleEntries, readProfilesFileResult } from "../lib/agent-profiles.ts";
7
- import { resolveProfilePin } from "../lib/agent-profile-pin.ts";
9
+ import { resolveSessionProfile, sessionProfileLabel, sessionProfileModelProfiles } from "../lib/session-profile-freeze.ts";
8
10
  import * as os from "node:os";
9
11
  import { dirname, join, resolve } from "node:path";
10
- import { buildShellHeaderModel, renderShellBar, renderShellBelowInputFloat, renderShellBottomOnlyBar, renderShellHeaderChrome, renderShellSidebarBar, shellEnabled, shellHeaderUsageHit, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
11
- import { CHANGE_STATUS, RootBranchLabels, renderChangesWidget, type ChangedFile, type ChangesModel, type GitRunner, type WorktreeChanges } from "../lib/shell-changes.ts";
12
+ import { buildShellHeaderModel, renderShellBar, renderShellBelowInputFloat, renderShellBottomOnlyBar, renderShellHeaderChrome, renderShellSidebarBar, keepNativeWorkingRow, shellEnabled, shellHeaderUsageHit, shellJobsCount, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
13
+ import { CHANGE_STATUS, DIFF_MAX_BYTES, DIFF_MAX_LINES, DIFF_TOO_LARGE, WorktreeChangesTracker, renderChangesWidget, type ChangedFile, type ChangesModel, type FileRevision, type GitRunner, type LineCounter, type WorktreeChanges } from "../lib/shell-changes.ts";
12
14
  import { WorktreeChangesView } from "../lib/shell-changes-view.ts";
13
15
  import { SessionWorktreeRegistry, resolveSessionWorktree, worktreeGitEnvironment, type WorktreeResolver, type WorktreeIdentity } from "../lib/session-worktree-registry.ts";
14
16
  import { CARD_STYLE, CARD_TONE, cardStyle, renderCard, setCardStyle, type Card, type CardTheme } from "../lib/shell-card.ts";
15
17
  import { resolveCardStyle, writeCardStyle } from "../lib/card-style-policy.ts";
18
+ import { CARD_CONTENT, resolveCardContent, setCardContent, writeCardContent } from "../lib/card-content-policy.ts";
16
19
  import { CommandPalette, commandsKey, type CommandPaletteResult } from "../lib/command-palette.ts";
17
20
  import { buildCommandPaletteGroups } from "../lib/command-palette-catalog.ts";
18
21
  import { discoverYoloUiAdapter, YOLO_DISPLAY, type YoloDisplay, type YoloUiAdapter } from "../lib/yolo-session-policy.ts";
@@ -28,6 +31,8 @@ import { oddPhaseRegistry } from "../lib/odd-phase.ts";
28
31
  import { inferOddPhase } from "../lib/odd-phase-inference.ts";
29
32
  import { isInteractiveMode } from "../lib/rpc-host.ts";
30
33
  import { gentlePiConfigHome } from "../lib/agent-home.ts";
34
+ import { bundledGentlePiConfigHome } from "../lib/gentle-shell-bundled.ts";
35
+ import { isNanAdaptiveReasoningModel } from "../lib/nan-provider.ts";
31
36
  import { resolveAnimationPolicy, writeAnimationPolicy, type AnimationPolicy } from "../lib/animation-policy.ts";
32
37
  import { resolveVimPolicy, writeVimPolicy, type VimPolicy } from "../lib/vim-policy.ts";
33
38
  import { resolveHistoryCapture, writeHistoryCapturePolicy } from "../lib/history-capture-policy.ts";
@@ -112,12 +117,14 @@ import {
112
117
  import { accountIdFromToken, CODEX_PROVIDER, CODEX_USAGE_URL, NAN_PROVIDER, NAN_QUOTA_URL, parseCodexUsage, parseNanQuota, parseProviderUsage, parseUsageHeaders, parseUsageSource, usageScopeProviders, UsageSourceRegistry, UsageStore, USAGE_SOURCE_EVENT, type ProviderUsage, type UsageSource } from "../lib/shell-usage.ts";
113
118
  import { UsageView } from "../lib/shell-usage-view.ts";
114
119
  import { sidebarHeader, sidebarPart, sidebarState, VISUAL_SETTINGS_CHANGED, type SidebarRail } from "../lib/shell-sidebar.ts";
115
- import { installSidebar, invalidateSidebar, narrowStatusOwner, STATUS_OWNER } from "../lib/shell-sidebar-layout.ts";
116
- import { SessionChanges, SESSION_CHANGE_EVENT } from "../lib/session-changes.ts";
120
+ import { installSidebar, invalidateSidebar, narrowStatusOwner, SIDEBAR_BREAKPOINT, STATUS_OWNER } from "../lib/shell-sidebar-layout.ts";
121
+ import { sessionChangeRoots } from "../lib/session-changes.ts";
117
122
  import { REVIEW_SIDEBAR_EVENT, isReviewSidebarSnapshot, type ReviewSidebarSnapshot } from "../lib/review-sidebar-state.ts";
123
+ import { JOBS_SIDEBAR_EVENT, JOBS_STATUS_KEY, isJobsSidebarSnapshot, type RunningJobDisplay } from "../lib/jobs-sidebar-state.ts";
118
124
  import { installSessionChangeCapture } from "../lib/session-change-capture.ts";
119
125
  import { SelectionEngine } from "../lib/selection-engine.ts";
120
126
  import { withOverlayRepaint } from "../lib/overlay-repaint.ts";
127
+ import { buildNotificationRows } from "../lib/notification-customize.ts";
121
128
 
122
129
  // Gentle Shell: the visual layer gentle-pi puts on top of pi. It installs the
123
130
  // status bar, the petal prompt, the working-tree changes widget and overlay,
@@ -142,6 +149,7 @@ interface ShellBarComponent {
142
149
  }
143
150
 
144
151
  interface BuildOptions {
152
+ jobs?: RunningJobDisplay[];
145
153
  profile?: string;
146
154
  home?: string;
147
155
  dirty?: number;
@@ -156,12 +164,15 @@ export interface ShellDeps {
156
164
  now(): number;
157
165
  devBinary(): DevBinaryNotice | undefined;
158
166
  resolveWorktree: WorktreeResolver;
159
- gitRunner(cwd: string): GitRunner;
167
+ /** maxBuffer bounds one diff; status and discovery stay unbounded. */
168
+ gitRunner(cwd: string, maxBuffer?: number): GitRunner;
169
+ /** A root Changes may scan: removed worktrees and leftover directories are skipped. */
170
+ rootExists(root: string): boolean;
160
171
  vimRuntimeVersion?(): string | undefined;
161
172
  }
162
173
 
163
174
  export type ActiveProfileReader = (() => string | undefined) & {
164
- bind(cwd: string, resolveWorktree: WorktreeResolver): boolean;
175
+ bind(cwd: string, resolveWorktree: WorktreeResolver, sessionId?: string): boolean;
165
176
  refresh(): boolean;
166
177
  reset(): void;
167
178
  };
@@ -169,12 +180,13 @@ export type ActiveProfileReader = (() => string | undefined) & {
169
180
  // Unbound reads retain the global file-identity cache, including atomic replacements.
170
181
  // Bound reads are snapshots: no filesystem or Git work is done during a frame.
171
182
  export function createActiveProfileReader(env: NodeJS.ProcessEnv = process.env): ActiveProfileReader {
172
- const path = profilesFilePath(env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"));
183
+ const path = profilesFilePath(bundledGentlePiConfigHome(env) ?? env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"));
173
184
  let fingerprint: string | undefined;
174
185
  let name: string | undefined;
175
186
  let bound = false;
176
187
  let cwd: string | undefined;
177
188
  let identity: WorktreeIdentity | undefined;
189
+ let sessionId: string | undefined;
178
190
  let effective: string | undefined;
179
191
  const global = () => {
180
192
  try {
@@ -195,24 +207,31 @@ export function createActiveProfileReader(env: NodeJS.ProcessEnv = process.env):
195
207
  const reader = (() => bound ? effective : global()) as ActiveProfileReader;
196
208
  reader.refresh = () => {
197
209
  if (!bound) return false;
198
- const pin = identity && cwd ? resolveProfilePin({
210
+ // gentle-shell#1064: the status shows the profile launches use, through
211
+ // the same single rule (explicit Enter binding → profile frozen at
212
+ // startup → live p → P → G only in follow mode), with the existing
213
+ // spelling: "name (session)", "name (local)", "name (repo)", or the bare
214
+ // global name.
215
+ const next = cwd === undefined ? global() : sessionProfileLabel(resolveSessionProfile({
216
+ sessionId,
199
217
  cwd,
200
- configHome: env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"),
201
- resolveWorktree: () => identity!,
202
- }) : undefined;
203
- const next = pin ? `${pin.profile} (${pin.source})` : global();
218
+ configHome: bundledGentlePiConfigHome(env) ?? env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"),
219
+ resolveWorktree: () => identity,
220
+ env,
221
+ }));
204
222
  const changed = next !== effective;
205
223
  effective = next;
206
224
  return changed;
207
225
  };
208
- reader.bind = (nextCwd, resolveWorktree) => {
226
+ reader.bind = (nextCwd, resolveWorktree, nextSessionId) => {
209
227
  reader.reset();
210
228
  cwd = nextCwd;
229
+ sessionId = nextSessionId;
211
230
  try { identity = resolveWorktree(nextCwd, nextCwd); } catch { identity = undefined; }
212
231
  bound = true;
213
232
  return reader.refresh();
214
233
  };
215
- reader.reset = () => { bound = false; cwd = undefined; identity = undefined; effective = undefined; };
234
+ reader.reset = () => { bound = false; cwd = undefined; identity = undefined; sessionId = undefined; effective = undefined; };
216
235
  return reader;
217
236
  }
218
237
 
@@ -226,7 +245,7 @@ function ambientDevBinary(): DevBinaryNotice | undefined {
226
245
  }
227
246
  }
228
247
 
229
- const defaultShellDeps: Omit<ShellDeps, "activeProfile"> = { fetch: (input, init) => globalThis.fetch(input, init), now: () => Date.now(), devBinary: ambientDevBinary, resolveWorktree: resolveSessionWorktree, gitRunner: shellGitRunner };
248
+ const defaultShellDeps: Omit<ShellDeps, "activeProfile"> = { fetch: (input, init) => globalThis.fetch(input, init), now: () => Date.now(), devBinary: ambientDevBinary, resolveWorktree: resolveSessionWorktree, gitRunner: (cwd, maxBuffer) => shellGitRunner(cwd, process.env, execFile, maxBuffer), rootExists: (root) => existsSync(join(root, ".git")) };
230
249
 
231
250
  interface AssistantUsageEntry {
232
251
  type: string;
@@ -252,6 +271,37 @@ function sessionCost(ctx: ExtensionContext): number {
252
271
  return total;
253
272
  }
254
273
 
274
+ type ShellSessionStats = { costTotal: number; usage: ReturnType<ExtensionContext["getContextUsage"]> };
275
+ const shellSessionStats = new WeakMap<object, {
276
+ sessionId: string | undefined;
277
+ leafId: string | null;
278
+ entryCount: number | undefined;
279
+ model: ExtensionContext["model"];
280
+ contextWindow: number | undefined;
281
+ stats: ShellSessionStats;
282
+ }>();
283
+
284
+ function sessionStats(ctx: ExtensionContext): ShellSessionStats {
285
+ const manager = ctx.sessionManager as ExtensionContext["sessionManager"] & { getEntryCount?(): number };
286
+ const compute = (): ShellSessionStats => ({ costTotal: sessionCost(ctx), usage: ctx.getContextUsage() });
287
+ // Append-only session entries move the leaf, including compaction and branch
288
+ // switches. A virtual model's routed physical model comes from the latest
289
+ // response, which is appended as an entry, so the leaf covers it as well.
290
+ // Older/test hosts without that identity must stay uncached.
291
+ if (typeof manager.getLeafId !== "function") return compute();
292
+ const sessionId = manager.getSessionId();
293
+ const leafId = manager.getLeafId();
294
+ const entryCount = manager.getEntryCount?.();
295
+ const model = ctx.model;
296
+ const contextWindow = model?.contextWindow;
297
+ const cached = shellSessionStats.get(manager);
298
+ if (cached && cached.sessionId === sessionId && cached.leafId === leafId &&
299
+ cached.entryCount === entryCount && cached.model === model && cached.contextWindow === contextWindow) return cached.stats;
300
+ const stats = compute();
301
+ shellSessionStats.set(manager, { sessionId, leafId, entryCount, model, contextWindow, stats });
302
+ return stats;
303
+ }
304
+
255
305
  export function buildShellBarModel(
256
306
  pi: ExtensionAPI,
257
307
  ctx: ExtensionContext,
@@ -259,9 +309,11 @@ export function buildShellBarModel(
259
309
  options: BuildOptions = {},
260
310
  ): ShellBarModel {
261
311
  const home = options.home ?? os.homedir();
262
- const usage = ctx.getContextUsage();
312
+ const { usage, costTotal } = sessionStats(ctx);
263
313
  const model = ctx.model;
314
+ const adaptiveReasoning = isNanAdaptiveReasoningModel(model);
264
315
  const statuses = Array.from(footerData.getExtensionStatuses().entries())
316
+ .filter(([key]) => options.jobs === undefined || key !== JOBS_STATUS_KEY)
265
317
  .sort(([a], [b]) => a.localeCompare(b))
266
318
  .map(([, text]) => text);
267
319
  return {
@@ -271,12 +323,13 @@ export function buildShellBarModel(
271
323
  dirty: options.dirty,
272
324
  sessionName: ctx.sessionManager.getSessionName(),
273
325
  modelId: model?.id ?? "no-model",
274
- effort: model?.reasoning ? pi.getThinkingLevel() : undefined,
326
+ effort: model?.reasoning ? (adaptiveReasoning ? "auto" : pi.getThinkingLevel()) : undefined,
275
327
  contextPercent: usage?.percent ?? null,
276
328
  contextWindow: usage?.contextWindow ?? model?.contextWindow ?? 0,
277
- costTotal: sessionCost(ctx),
329
+ costTotal,
278
330
  subscription: model ? ctx.modelRegistry.isUsingOAuth(model) : false,
279
331
  usage: options.usage,
332
+ ...(options.jobs !== undefined ? { jobs: options.jobs } : {}),
280
333
  statuses,
281
334
  };
282
335
  }
@@ -539,9 +592,10 @@ export class GentlePromptEditor extends CustomEditor {
539
592
  this.pendingIdleClearText = undefined;
540
593
  }
541
594
  // Vim owns its modal keys and paste frames. Ordinary editing retains
542
- // native selection before the prompt's existing input chain.
595
+ // native selection before the prompt's existing input chain, but a
596
+ // registered extension shortcut still owns its chord (gentle-shell#1565).
543
597
  if (this.vimPolicy === "on") this.handleInputNative(data);
544
- else this.selectionEngine.handleInput(data, (d) => this.handleInputNative(d));
598
+ else this.selectionEngine.handleInput(data, (d) => this.handleInputNative(d), (d) => this.onExtensionShortcut?.(d) === true);
545
599
  }
546
600
 
547
601
  private handleInputNative(data: string): void {
@@ -1018,6 +1072,8 @@ export class GentlePromptEditor extends CustomEditor {
1018
1072
  } catch { /* Unknown layout: keep the original rendered prompt. */ }
1019
1073
  }
1020
1074
  if (this.vimPolicy !== "on") editorLines = this.selectionEngine.decorateRows(editorLines, inner, 0);
1075
+ // SAFETY: this optional host hint is accepted only as an integer within the
1076
+ // rendered row bounds below; absent/invalid hints fall back to all rows.
1021
1077
  const visibleCount = (this as unknown as { renderedVisibleLineCount?: number }).renderedVisibleLineCount;
1022
1078
  const borderEnd = Number.isInteger(visibleCount) && visibleCount! >= 1 && visibleCount! + 2 <= editorLines.length
1023
1079
  ? visibleCount! + 2 : editorLines.length;
@@ -1170,7 +1226,7 @@ const COMMANDS_COMMAND_NAME = "gentle:commands";
1170
1226
  const OVERLAY_HEIGHT_RATIO = 0.8;
1171
1227
  const OVERLAY_MIN_ROWS = 8;
1172
1228
 
1173
- export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env, run: typeof execFile = execFile): GitRunner {
1229
+ export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env, run: typeof execFile = execFile, maxBuffer = Infinity): GitRunner {
1174
1230
  // Pi exec cannot replace the inherited environment. Use argv directly and
1175
1231
  // a complete sanitized environment for discovery, status, and lazy diffs.
1176
1232
  const childEnv = worktreeGitEnvironment(env);
@@ -1183,8 +1239,10 @@ export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env
1183
1239
  timeout: GIT_TIMEOUT_MS,
1184
1240
  // Pi exec accumulates output without a maxBuffer cap. In particular,
1185
1241
  // large porcelain inventories must not become partial successful scans.
1186
- maxBuffer: Infinity,
1242
+ // Only a single diff passes a bound, and is told when it hit it.
1243
+ maxBuffer,
1187
1244
  }, (error, stdout) => {
1245
+ if ((error as { code?: unknown } | null)?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return resolve({ stdout, code: 1, truncated: true });
1188
1246
  resolve({ stdout, code: error ? typeof error.code === "number" ? error.code : 1 : 0 });
1189
1247
  });
1190
1248
  });
@@ -1193,9 +1251,24 @@ export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env
1193
1251
  export async function loadFileDiff(git: GitRunner, file: ChangedFile): Promise<string> {
1194
1252
  const args = file.status === CHANGE_STATUS.UNTRACKED ? ["diff", "--no-index", "--", "/dev/null", file.path] : ["diff", "HEAD", "--", file.path];
1195
1253
  const result = await git(args);
1254
+ if (result.truncated || result.stdout.length > DIFF_MAX_BYTES || result.stdout.split("\n", DIFF_MAX_LINES + 2).length > DIFF_MAX_LINES + 1) return DIFF_TOO_LARGE;
1196
1255
  return result.code === 0 || result.code === 1 ? result.stdout : "";
1197
1256
  }
1198
1257
 
1258
+ // Untracked files have no numstat; count their lines without reading past the diff bound.
1259
+ const untrackedLines = (root: string): LineCounter => async (path) => {
1260
+ const target = join(root, path);
1261
+ const info = await lstat(target);
1262
+ if (!info.isFile() || info.size > DIFF_MAX_BYTES) return 0;
1263
+ const text = await readFile(target, "utf8");
1264
+ return text === "" ? 0 : text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
1265
+ };
1266
+ // Same-count edits still change size or mtime, so an open diff reloads.
1267
+ const fileRevision = (root: string): FileRevision => async (path) => {
1268
+ const info = await lstat(join(root, path));
1269
+ return `${info.size}:${info.mtimeMs}`;
1270
+ };
1271
+
1199
1272
  export interface ExternalEditorHost {
1200
1273
  stop(): void;
1201
1274
  start(): void;
@@ -1244,48 +1317,39 @@ const USAGE_FETCH_TIMEOUT_DEFAULT_MS = 10_000;
1244
1317
  const usageFetchTimeoutMs = (env: NodeJS.ProcessEnv): number => positiveMs(env.GENTLE_PI_SHELL_USAGE_TIMEOUT_MS, USAGE_FETCH_TIMEOUT_DEFAULT_MS);
1245
1318
 
1246
1319
  function changesFingerprint(model: ChangesModel): string {
1247
- return [model.notice ?? "", ...model.files.map((file) => `${file.path}:${file.status}:${file.added}:${file.deleted}:${file.diffRevision ?? ""}:${file.countsUnavailable ?? ""}`)].join("|");
1320
+ return model.files.map((file) => `${file.path}:${file.status}:${file.added}:${file.deleted}:${file.diffRevision ?? ""}:${file.countsUnavailable ?? ""}`).join("|");
1248
1321
  }
1249
1322
 
1250
1323
  interface OverlayDeps {
1251
- loadDiff(root: string, file: ChangedFile): string;
1324
+ loadDiff(root: string, file: ChangedFile): Promise<string>;
1252
1325
  worktrees(): WorktreeChanges[];
1253
1326
  refresh(): Promise<ChangesModel>;
1254
1327
  apply(ctx: ExtensionContext, model: ChangesModel): void;
1255
1328
  pollMs: number;
1256
- gitForRoot(root: string): GitRunner;
1257
1329
  }
1258
1330
 
1259
- // Refresh only the captured session model. Never read live files here; the
1260
- // only Git the overlay touches is each root's HEAD, to label its tree.
1331
+ // The overlay polls each root's Git state while it is open; diffs load lazily.
1261
1332
  async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Promise<void> {
1262
1333
  let host: ExternalEditorHost | undefined;
1263
1334
  let view: WorktreeChangesView | undefined;
1264
- // Session evidence knows roots, not branches; label them while the overlay
1265
- // is open and repaint when Git answers.
1266
- const labels = new RootBranchLabels(deps.gitForRoot, () => {
1267
- view?.update(labels.decorate(deps.worktrees()));
1268
- host?.requestRender();
1269
- });
1270
- const worktrees = () => labels.decorate(deps.worktrees());
1271
1335
  const refresh = async () => {
1272
1336
  const latest = await deps.refresh();
1273
- view?.update(worktrees());
1337
+ view?.update(deps.worktrees());
1274
1338
  deps.apply(ctx, latest);
1275
1339
  };
1276
- const poll = setInterval(() => void refresh(), deps.pollMs);
1340
+ const poll = setInterval(() => void refresh().catch(() => {}), deps.pollMs);
1277
1341
  poll.unref();
1278
1342
  try {
1279
1343
  const chosen = await ctx.ui.custom<{ root: string; file: ChangedFile } | null>(
1280
1344
  (tui, theme, _keybindings, done) => {
1281
1345
  const close = withOverlayRepaint(tui, done);
1282
1346
  host = tui;
1283
- view = new WorktreeChangesView(worktrees(), {
1347
+ view = new WorktreeChangesView(deps.worktrees(), {
1284
1348
  theme,
1285
1349
  rows: () => Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
1286
- loadDiff: (root, file) => Promise.resolve(deps.loadDiff(root, file)),
1350
+ loadDiff: deps.loadDiff,
1287
1351
  onOpen: (root, file) => close({ root, file }),
1288
- onRefresh: () => void refresh(),
1352
+ onRefresh: () => void refresh().catch(() => {}),
1289
1353
  onClose: () => close(null),
1290
1354
  requestRender: () => tui.requestRender(),
1291
1355
  });
@@ -1446,6 +1510,23 @@ async function fetchFromSource(source: UsageSource, apiKey: string | undefined,
1446
1510
  }
1447
1511
  }
1448
1512
 
1513
+ // Changes refreshes started without awaiting (turn end, worktree registration)
1514
+ // are tracked so tests can wait for them instead of guessing a delay.
1515
+ const backgroundChangeRefreshes = new Set<Promise<void>>();
1516
+ function trackChangeRefresh(refresh: Promise<void>): void {
1517
+ const tracked = refresh.catch(() => {}).finally(() => { backgroundChangeRefreshes.delete(tracked); });
1518
+ backgroundChangeRefreshes.add(tracked);
1519
+ }
1520
+
1521
+ /** Resolves once every background Changes refresh has finished, or after `timeoutMs` for one that never settles. */
1522
+ export async function backgroundChangeRefreshesSettled(timeoutMs = 2000): Promise<void> {
1523
+ let timer: ReturnType<typeof setTimeout> | undefined;
1524
+ const deadline = new Promise<void>((resolve) => { timer = setTimeout(resolve, timeoutMs); });
1525
+ const settled = (async () => { while (backgroundChangeRefreshes.size > 0) await Promise.all([...backgroundChangeRefreshes]); })();
1526
+ await Promise.race([settled, deadline]);
1527
+ clearTimeout(timer);
1528
+ }
1529
+
1449
1530
  export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env, overrides: Partial<ShellDeps> = {}): void {
1450
1531
  installSessionChangeCapture(pi, env, overrides.resolveWorktree ?? resolveSessionWorktree);
1451
1532
  if (!shellEnabled(env)) return;
@@ -1476,20 +1557,35 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1476
1557
  // overlapping refreshes of the SAME source (or of builtins) can otherwise
1477
1558
  // let an older failure settle after a newer success and poison it.
1478
1559
  const usageGenerations = new Map<string, number>();
1560
+ // NaN accepts a thinking level for DeepSeek V4 Flash, Qwen 3.8 Flash, and MiMo
1561
+ // but manages their depth itself, so the header reports `auto` and the shell says
1562
+ // why once per model instead of repeating it on every level change.
1563
+ const adaptiveReasoningNotices = new Set<string>();
1564
+ const announceAdaptiveReasoning = (ctx: ExtensionContext, model: { provider: string; id: string; name?: string } | undefined) => {
1565
+ if (!ctx.hasUI || !isNanAdaptiveReasoningModel(model)) return;
1566
+ const key = `${model.provider}/${model.id}`;
1567
+ if (adaptiveReasoningNotices.has(key)) return;
1568
+ adaptiveReasoningNotices.add(key);
1569
+ ctx.ui.notify(`${model.name || model.id} chooses its reasoning depth automatically; the selected thinking level is accepted but does not change it.`, "info");
1570
+ };
1479
1571
  // One spelling of the config home, so the pin resolver, the global profiles
1480
1572
  // store and the profile reader cannot drift onto two different stores.
1481
1573
  const usageConfigHome = gentlePiConfigHome(env);
1482
1574
  const usageFetchTimeout = usageFetchTimeoutMs(env);
1483
- // The subagent routing in force for this session: a repository pin first,
1484
- // then the global active profile. Only the profile's own role entries count;
1485
- // the reserved orchestrator key is not a route.
1575
+ // The subagent routing in force for this session, through the same single
1576
+ // rule launches use (gentle-shell#1064): explicit Enter binding, then the
1577
+ // profile frozen at startup, then the live p → P → G only in follow mode.
1578
+ // Only the profile's own role entries count; the reserved orchestrator key
1579
+ // is not a route.
1486
1580
  const activeRoutingModels = (ctx: ExtensionContext): Array<string | undefined> => {
1487
- const pin = resolveProfilePin({ cwd: ctx.cwd, configHome: usageConfigHome, resolveWorktree: deps.resolveWorktree });
1488
- const config = pin ? pin.modelProfiles : undefined;
1489
- if (config) return [...profileRoleEntries(config).map(([, entry]) => entry.model)];
1490
- const store = readProfilesFileResult(profilesFilePath(usageConfigHome));
1491
- const active = store.status === "valid" && store.file.active !== undefined ? store.file.profiles[store.file.active] : undefined;
1492
- return active ? profileRoleEntries(active).map(([, entry]) => entry.model) : [];
1581
+ const config = sessionProfileModelProfiles(resolveSessionProfile({
1582
+ sessionId: ctx.sessionManager?.getSessionId?.(),
1583
+ cwd: ctx.cwd,
1584
+ configHome: usageConfigHome,
1585
+ resolveWorktree: deps.resolveWorktree,
1586
+ env,
1587
+ }));
1588
+ return config ? profileRoleEntries(config).map(([, entry]) => entry.model) : [];
1493
1589
  };
1494
1590
  // A bare model id names a provider only when exactly one provider in the
1495
1591
  // registry carries that id; anything else stays untargeted rather than guessed.
@@ -1684,6 +1780,10 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1684
1780
  // preference fills it at startup and again on every session start.
1685
1781
  const applyCardStyle = () => setCardStyle(resolveCardStyle(animationOptions).style);
1686
1782
  applyCardStyle();
1783
+ // The Card content preference rides the same kind of slot: the quiet tool
1784
+ // renderers read it live, so the saved level applies from the first render.
1785
+ const applyCardContent = () => setCardContent(resolveCardContent(animationOptions).content);
1786
+ applyCardContent();
1687
1787
  const reportVim = (ctx: ExtensionContext, result: ReturnType<typeof resolveVimPolicy>) => {
1688
1788
  const source = result.source === "default" ? "built-in default" : `global file ${result.globalFile}`;
1689
1789
  const effective = prompt?.effectiveVimPolicy;
@@ -1706,10 +1806,11 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1706
1806
  env,
1707
1807
  gentlePiConfigHome: doubleEscCancelConfigHome,
1708
1808
  }).policy;
1709
- let changes: SessionChanges | undefined;
1809
+ let changes: WorktreeChangesTracker | undefined;
1710
1810
  let registry: SessionWorktreeRegistry | undefined;
1711
1811
  let currentContext: ExtensionContext | undefined;
1712
1812
  let review: ReviewSidebarSnapshot | undefined;
1813
+ let runningJobs: RunningJobDisplay[] | undefined;
1713
1814
  const redrawReview = () => {
1714
1815
  renderHost?.invalidateSidebar?.();
1715
1816
  renderHost?.requestRender();
@@ -1721,6 +1822,13 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1721
1822
  review = { state: event.snapshot.state, scope: event.snapshot.scope };
1722
1823
  redrawReview();
1723
1824
  });
1825
+ const unsubscribeJobs = pi.events.on(JOBS_SIDEBAR_EVENT, (value) => {
1826
+ const event = value as { sessionId?: unknown; snapshot?: unknown } | undefined;
1827
+ if (!currentContext || event?.sessionId !== currentContext.sessionManager.getSessionId()) return;
1828
+ if (!isJobsSidebarSnapshot(event.snapshot)) return;
1829
+ runningJobs = event.snapshot.jobs.map(({ id, label, startedAt }) => ({ id, label, startedAt }));
1830
+ redrawReview();
1831
+ });
1724
1832
  pi.on("session_tree", () => {
1725
1833
  review = undefined;
1726
1834
  redrawReview();
@@ -1732,34 +1840,43 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1732
1840
  shown = fingerprint;
1733
1841
  showChanges(ctx, model, visualSettings.visibility.changes, (width) => groupedChangesOwner(width));
1734
1842
  };
1843
+ const gitForRoot = (root: string) => deps.gitRunner(root);
1844
+ // Changes is the Git state of the session's roots that still exist: read
1845
+ // at turn end, on panel open and by the panel's poll, never at startup.
1846
+ const changesTracker = (ctx: ExtensionContext) => {
1847
+ const sessionId = ctx.sessionManager.getSessionId();
1848
+ const roots = () => {
1849
+ try { return sessionChangeRoots(sessionId, ctx.sessionManager.getEntries()).filter((root) => deps.rootExists(root) && !isHomeOrBroadUserDirectory(root)); }
1850
+ catch { return []; }
1851
+ };
1852
+ return new WorktreeChangesTracker(gitForRoot(ctx.cwd), gitForRoot, untrackedLines, roots, fileRevision);
1853
+ };
1735
1854
  const refreshChanges = async (ctx: ExtensionContext) => {
1736
1855
  const tracker = changes;
1737
1856
  if (!tracker || !ctx.hasUI || registry?.sessionId !== ctx.sessionManager.getSessionId()) return;
1738
- tracker.restore(ctx.sessionManager.getEntries());
1739
1857
  const model = await tracker.refresh();
1740
1858
  if (changes === tracker) applyChanges(ctx, model);
1741
1859
  };
1742
- const unsubscribeWorktrees = pi.events.on(SESSION_CHANGE_EVENT, (data) => {
1743
- if (!currentContext || !registry || (data as { sessionId?: string } | undefined)?.sessionId !== registry.sessionId) return;
1744
- const notice = (data as { notice?: string } | undefined)?.notice;
1745
- if (notice) { if (changes) changes.notice = notice; currentContext.ui.notify(notice, "warning"); }
1746
- void refreshChanges(currentContext);
1747
- });
1748
1860
  pi.registerTool({
1749
1861
  name: "session_worktree_register",
1750
1862
  renderShell: "self",
1751
1863
  label: "Register session worktree",
1752
- description: "Register a worktree in the same Git clone for session coordination. Registration does not attribute file changes; Changes shows captured write/edit operations only.",
1864
+ description: "Register a worktree in the same Git clone for session coordination. /gentle:changes then also lists that worktree's uncommitted Git changes.",
1753
1865
  parameters: { type: "object", required: ["path"], additionalProperties: false, properties: { path: { type: "string", description: "Worktree path to include in this session." } } } as never,
1754
1866
  async execute(_id, params, _signal, _onUpdate, ctx) {
1755
1867
  if (!registry || registry.sessionId !== ctx.sessionManager.getSessionId()) throw new Error("No active session worktree registry.");
1756
1868
  const root = registry.register((params as { path: string }).path, "explicit");
1757
- await refreshChanges(ctx);
1869
+ trackChangeRefresh(refreshChanges(ctx));
1758
1870
  return { content: [{ type: "text", text: `Registered session worktree: ${root}` }], details: { root, sessionId: registry.sessionId } };
1759
1871
  },
1760
1872
  });
1761
1873
  pi.on("session_start", async (_event, ctx) => {
1762
1874
  closeCustomize?.();
1875
+ runningJobs = undefined;
1876
+ adaptiveReasoningNotices.clear();
1877
+ // A restored or default session never emits model_select, so the startup model
1878
+ // announces itself here when it manages its own depth.
1879
+ announceAdaptiveReasoning(ctx, ctx.model);
1763
1880
  if (review) {
1764
1881
  review = undefined;
1765
1882
  redrawReview();
@@ -1771,10 +1888,11 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1771
1888
  registry = new SessionWorktreeRegistry(pi, ctx.sessionManager, ctx.cwd, deps.resolveWorktree);
1772
1889
  registry.start();
1773
1890
  applyCardStyle();
1891
+ applyCardContent();
1774
1892
  if (!ctx.hasUI) return;
1775
1893
  visualSettings = resolveVisualSettings(animationOptions).settings;
1776
1894
  if (!overrides.activeProfile) {
1777
- profileReader.bind(ctx.cwd, deps.resolveWorktree);
1895
+ profileReader.bind(ctx.cwd, deps.resolveWorktree, ctx.sessionManager.getSessionId());
1778
1896
  const sessionId = ctx.sessionManager.getSessionId();
1779
1897
  profilePoll = setInterval(() => {
1780
1898
  if (currentContext !== ctx || ctx.sessionManager.getSessionId() !== sessionId) return;
@@ -1784,7 +1902,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1784
1902
  }, 2_000);
1785
1903
  profilePoll.unref?.();
1786
1904
  }
1787
- changes = new SessionChanges(ctx.sessionManager.getSessionId(), ctx.sessionManager.getEntries());
1905
+ changes = changesTracker(ctx);
1788
1906
  const tracker = changes;
1789
1907
  ctx.ui.setFooter((tui, theme, footerData) => {
1790
1908
  sidebarTui = tui;
@@ -1796,16 +1914,17 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1796
1914
  // statuses. The digest is what keeps the fullscreen memo honest, and it
1797
1915
  // rebuilds the model exactly as the narrow bottom bar does every frame.
1798
1916
  const footerModel = (): ShellBarModel => ({
1799
- ...buildShellBarModel(pi, ctx, footerData, { dirty: tracker.model.files.length, usage: usage.get(ctx.model?.provider ?? ""), profile: deps.activeProfile() }),
1800
- changes: { files: tracker.model.files.length, added: tracker.model.added, deleted: tracker.model.deleted, notice: tracker.model.notice },
1917
+ ...buildShellBarModel(pi, ctx, footerData, { dirty: tracker.model.files.length, usage: usage.get(ctx.model?.provider ?? ""), profile: deps.activeProfile(), jobs: runningJobs }),
1918
+ changes: { files: tracker.model.files.length, added: tracker.model.added, deleted: tracker.model.deleted },
1801
1919
  review,
1802
1920
  });
1803
1921
  // At narrow fullscreen widths only one status row paints: a top header
1804
1922
  // suppresses the bottom bar in the layout, and otherwise the bottom bar
1805
1923
  // takes over the header's data while the below-input header steps aside.
1806
1924
  const statusOwner = () => narrowStatusOwner({ mode: (tui as TUI & { mode?: string }).mode, columns: tui.terminal?.columns ?? 0, statusPlacement: visualSettings.statusPlacement, headerPlacement: visualSettings.headerPlacement });
1925
+ const statusCardVisible = () => Boolean(tui.terminal && tui.terminal.columns >= SIDEBAR_BREAKPOINT && sidebarState(tui).ownsHost?.() && visualSettings.statusPlacement !== "hidden");
1807
1926
  const belowFloat = (width: number, statuses: boolean) => (tui as TUI & { mode?: string }).mode === "fullscreen"
1808
- ? renderShellBelowInputFloat({ ...footerModel(), ...(statuses ? {} : { statuses: [] }) }, theme, width, usageShortcutKey, visualSettings, tracker.model)
1927
+ ? renderShellBelowInputFloat({ ...footerModel(), ...(statuses ? {} : { statuses: [] }), ...(statusCardVisible() ? { jobs: undefined } : {}) }, theme, width, usageShortcutKey, visualSettings, tracker.model)
1809
1928
  : undefined;
1810
1929
  // Match sidebarPart's live paint ownership, including an unavailable rail.
1811
1930
  const footerSuppressed = () => {
@@ -1831,9 +1950,9 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1831
1950
  invalidate() {},
1832
1951
  });
1833
1952
  // The header row carries everything that ticks every frame (model,
1834
- // effort, context, cost, usage) plus session identity; it never sees
1835
- // extension statuses or the working/thinking state.
1836
- const headerBar = (width: number) => renderShellHeaderChrome(buildShellHeaderModel(footerModel()), theme, width, usageShortcutKey, visualSettings);
1953
+ // effort, context, cost, usage) plus session identity and the jobs count
1954
+ // when Status is absent. Opaque statuses and working state stay excluded.
1955
+ const headerBar = (width: number) => renderShellHeaderChrome(buildShellHeaderModel(footerModel(), !statusCardVisible() && (statusOwner() === STATUS_OWNER.HEADER || visualSettings.statusPlacement === "hidden")), theme, width, usageShortcutKey, visualSettings);
1837
1956
  const disposeHeader = sidebarHeader(tui, {
1838
1957
  digest: () => JSON.stringify([footerModel(), tracker.model, visualSettings, cardStyle()]),
1839
1958
  render: (width) => headerBar(width).rows,
@@ -1865,6 +1984,9 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1865
1984
  }), { placement: "belowEditor" });
1866
1985
  return { ...part,
1867
1986
  render(width: number) {
1987
+ if (visualSettings.statusPlacement === "hidden" && (tui as TUI & { mode?: string }).mode !== "fullscreen" && runningJobs?.length) {
1988
+ return [truncateToWidth(theme.fg("muted", shellJobsCount(runningJobs.length)), width, "…")];
1989
+ }
1868
1990
  const rows = part.render(width);
1869
1991
  // Preserve a shrinkable exterior dock row, outside prompt/completion geometry.
1870
1992
  return rows.length === 0 && (tui as TUI & { mode?: string }).mode === "fullscreen"
@@ -1885,7 +2007,8 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1885
2007
  );
1886
2008
  // Hide native feedback only when our petal replaces it. Native transcript
1887
2009
  // thinking blocks remain Pi-owned; this changes only the supported loader UI.
1888
- if (ownsPrompt) ctx.ui.setWorkingVisible(false);
2010
+ // Inside Herdr the row stays: its native Pi detection reads it.
2011
+ if (ownsPrompt && !keepNativeWorkingRow(env)) ctx.ui.setWorkingVisible(false);
1889
2012
  const notice = deps.devBinary();
1890
2013
  ctx.ui.setWidget(
1891
2014
  DEV_BINARY_WIDGET_KEY,
@@ -1906,6 +2029,8 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1906
2029
  // Pi rebuilds the extension runtime after every shutdown (reload, replacement,
1907
2030
  // fork, quit), so the factory-level subscription never needs to be restored.
1908
2031
  unsubscribeReview();
2032
+ unsubscribeJobs();
2033
+ runningJobs = undefined;
1909
2034
  stopProfilePoll();
1910
2035
  oddPhaseRegistry.clear(ctx.sessionManager.getSessionId());
1911
2036
  oddPhaseRegistry.clearRenderRequest(ctx.sessionManager.getSessionId());
@@ -1920,26 +2045,33 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1920
2045
  registry = undefined;
1921
2046
  changes = undefined;
1922
2047
  currentContext = undefined;
1923
- unsubscribeWorktrees();
1924
2048
  });
1925
2049
  const openChanges = async (ctx: ExtensionContext) => {
1926
2050
  if (!changes) return;
1927
2051
  const tracker = changes;
1928
2052
  const model = await tracker.refresh();
2053
+ if (changes !== tracker) return;
2054
+ applyChanges(ctx, model);
2055
+ // A failed root scan is unknown, never clean: say so whenever one failed.
2056
+ const failed = tracker.failedRoots.length;
2057
+ const roots = tracker.scannedRootCount;
1929
2058
  if (model.files.length === 0) {
1930
- ctx.ui.notify("No captured agent changes. Only successful write/edit operations from this session and its subagents are shown; shell changes are not attributed.", "info");
2059
+ if (failed > 0 && failed === roots) ctx.ui.notify(`Could not read Git status for ${failed} ${failed === 1 ? "root" : "roots"}; uncommitted changes are unknown.`, "error");
2060
+ else if (failed > 0) ctx.ui.notify(`No uncommitted changes in the roots Git could read; Git status failed for ${failed} of ${roots} roots.`, "warning");
2061
+ else ctx.ui.notify("No uncommitted changes in this session's repository or its subagents' worktrees.", "info");
1931
2062
  return;
1932
2063
  }
1933
- await showChangesOverlay(ctx, { loadDiff: (root, file) => tracker.loadDiff(root, file), worktrees: () => tracker.worktrees, refresh: () => tracker.refresh(), apply: applyChanges, pollMs: changesPollMs(env), gitForRoot: (root) => deps.gitRunner(root) });
2064
+ if (failed > 0) ctx.ui.notify(`Git status failed for ${failed} ${failed === 1 ? "root" : "roots"}; ${failed === 1 ? "its" : "their"} changes are not listed.`, "warning");
2065
+ await showChangesOverlay(ctx, { loadDiff: (root, file) => loadFileDiff(deps.gitRunner(root, DIFF_MAX_BYTES), file), worktrees: () => tracker.worktrees, refresh: () => tracker.refresh(), apply: applyChanges, pollMs: changesPollMs(env) });
1934
2066
  };
1935
2067
  pi.registerCommand(CHANGES_COMMAND_NAME, {
1936
- description: "Browse captured write/edit changes from this agent session and its subagents, excluding preexisting and external edits. Shell changes are not attributed. Press o to open $EDITOR.",
2068
+ description: "Browse uncommitted Git changes (working tree vs HEAD, untracked included) in this session's repository and its subagents' worktrees. Press o to open $EDITOR.",
1937
2069
  handler: async (_args, ctx) => openChanges(ctx),
1938
2070
  });
1939
2071
  const shortcut = changesShortcut(env);
1940
2072
  if (shortcut) {
1941
2073
  pi.registerShortcut(shortcut as Parameters<ExtensionAPI["registerShortcut"]>[0], {
1942
- description: "Open captured agent session changes",
2074
+ description: "Open the session's uncommitted Git changes",
1943
2075
  handler: async (ctx) => openChanges(ctx),
1944
2076
  });
1945
2077
  }
@@ -1955,10 +2087,10 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1955
2087
  });
1956
2088
  }
1957
2089
  pi.registerCommand("gentle:customize", {
1958
- description: "Configure appearance, global Vim prompt editing, session-only YOLO permission and prompt history capture.",
2090
+ description: "Configure appearance, audio notifications, Vim editing, session permissions and global Auto/YOLO defaults, and prompt history capture.",
1959
2091
  handler: async (_args, ctx) => {
1960
2092
  if (ctx.mode !== "tui" || !ctx.hasUI) {
1961
- if (ctx.hasUI) ctx.ui.notify("Visual customization requires an interactive terminal.", "warning");
2093
+ if (ctx.hasUI) ctx.ui.notify("Customization requires an interactive terminal.", "warning");
1962
2094
  return;
1963
2095
  }
1964
2096
  closeCustomize?.();
@@ -1966,9 +2098,11 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1966
2098
  let adapter: YoloUiAdapter | undefined;
1967
2099
  let unobserve: (() => void) | undefined;
1968
2100
  let finish: (() => void) | undefined;
2101
+ let customizeView: VisualCustomizeView | undefined;
1969
2102
  const close = () => {
1970
2103
  if (closed) return;
1971
2104
  closed = true;
2105
+ customizeView?.dispose();
1972
2106
  unobserve?.(); adapter?.dispose(); finish?.();
1973
2107
  if (closeCustomize === close) closeCustomize = undefined;
1974
2108
  };
@@ -1979,7 +2113,6 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1979
2113
  let banner = await readBannerConfig(bannerHome).catch(error => { close(); throw error; });
1980
2114
  if (closed) return;
1981
2115
  let activeTheme = ctx.ui.theme.name;
1982
- let customizeView: VisualCustomizeView | undefined;
1983
2116
  let requestCustomizeRender: (() => void) | undefined;
1984
2117
  let category: CustomizeCategory = "Animations";
1985
2118
  const add = (label: CustomizeRow["label"], notice: string, action: () => void | false | Promise<void | false>, preview?: CustomizeRow["preview"]) => rows.push({ category, label, preview, action: async () => {
@@ -2080,6 +2213,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2080
2213
  },
2081
2214
  });
2082
2215
  let yoloDisplay: YoloDisplay = YOLO_DISPLAY.unavailable;
2216
+ let permissionDisplay = "normal", permissionDefault = "normal";
2083
2217
  let requestYoloRender = () => {};
2084
2218
  let refreshing: Promise<void> | undefined;
2085
2219
  let refreshAgain = false;
@@ -2091,9 +2225,16 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2091
2225
  refreshing = (async () => {
2092
2226
  while (refreshAgain && !closed) {
2093
2227
  refreshAgain = false;
2094
- const next = await adapter?.read().catch(() => YOLO_DISPLAY.unavailable) ?? YOLO_DISPLAY.unavailable;
2228
+ const snapshot = await adapter?.readSnapshot?.().catch(() => undefined);
2229
+ const next = snapshot?.yolo ?? await adapter?.read().catch(() => YOLO_DISPLAY.unavailable) ?? YOLO_DISPLAY.unavailable;
2095
2230
  if (closed) return;
2231
+ const nextMode = snapshot?.mode ?? "normal";
2232
+ const nextDefault = snapshot?.defaultMode ?? "normal";
2233
+ if (closed) return;
2234
+ if (refreshAgain) continue;
2096
2235
  yoloDisplay = next;
2236
+ permissionDisplay = nextMode;
2237
+ permissionDefault = nextDefault;
2097
2238
  requestYoloRender();
2098
2239
  }
2099
2240
  })().finally(() => { refreshing = undefined; });
@@ -2113,6 +2254,28 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2113
2254
  await refreshYolo();
2114
2255
  },
2115
2256
  });
2257
+ for (const mode of ["normal", "auto", "yolo"] as const) rows.push({
2258
+ category,
2259
+ label: () => `Session permissions: ${mode.toUpperCase()}${mode === "auto" ? " (beta)" : ""}${permissionDisplay === mode ? " (current)" : ""}`,
2260
+ preview: () => ({ title: "Permissions · current session", sample: mode === "auto"
2261
+ ? "independent session-model action checks · extra inference cost · hard safety and native consent remain"
2262
+ : mode === "yolo" ? "ordinary scoped delivery permission · destructive confirmations remain" : "normal checks · overrides an elevated global default" }),
2263
+ action: async () => {
2264
+ if (closed) return;
2265
+ if (!adapter?.setMode || !await adapter.setMode(mode)) ctx.ui.notify("Session mode unchanged — live primary UI unavailable or changed.", "warning");
2266
+ await refreshYolo();
2267
+ },
2268
+ });
2269
+ for (const mode of ["normal", "auto", "yolo"] as const) rows.push({
2270
+ category,
2271
+ label: () => `Default permissions: ${mode.toUpperCase()}${mode === "auto" ? " (beta)" : ""}${permissionDefault === mode ? " (saved)" : ""}`,
2272
+ preview: () => ({ title: "Permissions · user-global default", sample: `New primary sessions start in ${mode.toUpperCase()}; current session stays unchanged. ${mode === "auto" ? "Additional classifier inference uses the session model; usage is separate." : "Independent safety and consent remain."}` }),
2273
+ action: async () => {
2274
+ if (closed) return;
2275
+ if (!adapter?.saveDefault || !await adapter.saveDefault(mode)) ctx.ui.notify("Default unchanged — trusted UI unavailable or preference could not be saved.", "warning");
2276
+ await refreshYolo();
2277
+ },
2278
+ });
2116
2279
  category = "History";
2117
2280
  // The prompt-history extension re-reads this preference per prompt, so a
2118
2281
  // change applies without restart. An explicit GENTLE_PI_HISTORY_CAPTURE
@@ -2138,6 +2301,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2138
2301
  else ctx.ui.notify(result.enabled ? "Prompt history capture: on. Applies from the next prompt; stored history is kept." : "Prompt history capture: off. New prompts are not recorded; stored history is kept.", "info");
2139
2302
  },
2140
2303
  });
2304
+ rows.push(...buildNotificationRows(ctx));
2141
2305
  category = "Layout";
2142
2306
  for (const value of Object.values(STATUS_PLACEMENT)) add(() => `Status placement: ${value}${visual().statusPlacement === value ? " (current)" : ""}`, pending, () => updateVisual((settings) => ({ ...settings, statusPlacement: value })), () => layoutPreview({ ...visual(), statusPlacement: value }));
2143
2307
  for (const value of Object.values(HEADER_PLACEMENT)) add(() => `Header placement: ${value}${visual().headerPlacement === value ? " (current)" : ""}`, pending, () => updateVisual((settings) => ({ ...settings, headerPlacement: value })), () => layoutPreview({ ...visual(), headerPlacement: value }));
@@ -2160,6 +2324,26 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2160
2324
  },
2161
2325
  () => ({ title: `Cards · ${style}`, sample: `${cardStylePreview[style]}${resolveCardStyle(home).malformed ? " · malformed or unreadable file" : ""}` }),
2162
2326
  );
2327
+ // The content level picks between the quiet tools' result previews
2328
+ // (default) and command-only cards (minimal).
2329
+ const cardContentPreview = {
2330
+ [CARD_CONTENT.DEFAULT]: "≡ read a.ts · result previews + counts · bash rows keep pi's native card",
2331
+ [CARD_CONTENT.MINIMAL]: "≡ read a.ts · command only · ctrl+o expands · bash draws as a Gentle card",
2332
+ };
2333
+ for (const content of Object.values(CARD_CONTENT)) add(
2334
+ () => {
2335
+ const current = resolveCardContent(home);
2336
+ return `Card content: ${content}${current.content === content && !current.malformed ? " (current)" : ""}`;
2337
+ },
2338
+ `Card content: ${content}. Quiet tool cards redraw now; bash rows follow on new calls.`,
2339
+ () => {
2340
+ writeCardContent(content, home);
2341
+ setCardContent(content);
2342
+ renderHost?.requestRender();
2343
+ requestCustomizeRender?.();
2344
+ },
2345
+ () => ({ title: `Cards · content ${content}`, sample: `${cardContentPreview[content]}${resolveCardContent(home).malformed ? " · malformed or unreadable file" : ""}` }),
2346
+ );
2163
2347
  category = "Sections";
2164
2348
  for (const key of VISUAL_SECTION_KEYS) add(
2165
2349
  () => `Section ${key}: ${visual().visibility[key] ? "shown" : "hidden"}`,
@@ -2256,7 +2440,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2256
2440
  if (closed) done(null);
2257
2441
  requestYoloRender = () => { if (!closed) tui.requestRender(); };
2258
2442
  requestCustomizeRender = requestYoloRender;
2259
- customizeView = new VisualCustomizeView({ rows, profiles, theme, requestRender: requestYoloRender, rowsAvailable: () => Math.max(0, Math.floor(tui.terminal.rows * 0.85) - 2), onError: (error) => { if (!closed) ctx.ui.notify(`Visual customization: ${error.message}`, "error"); }, onClose: close });
2443
+ customizeView = new VisualCustomizeView({ rows, profiles, theme, requestRender: requestYoloRender, rowsAvailable: () => Math.max(0, Math.floor(tui.terminal.rows * 0.85) - 2), onError: (error) => { if (!closed) ctx.ui.notify(`${customizeView?.title() ?? "Customization"}: ${error.message}`, "error"); }, onClose: close });
2260
2444
  const view = customizeView;
2261
2445
  // Own interaction lifetime here, leaving the shared view unchanged.
2262
2446
  return {
@@ -2416,7 +2600,14 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2416
2600
  }
2417
2601
  });
2418
2602
  pi.on("agent_end", async (_event, ctx) => {
2419
- await refreshChanges(ctx);
2603
+ // A slow or failing Git never holds the turn end.
2604
+ trackChangeRefresh(refreshChanges(ctx));
2420
2605
  void refreshUsage(ctx, false);
2421
2606
  });
2607
+ pi.on("model_select", (event, ctx) => {
2608
+ announceAdaptiveReasoning(ctx, event.model);
2609
+ });
2610
+ pi.on("thinking_level_select", (_event, ctx) => {
2611
+ announceAdaptiveReasoning(ctx, ctx.model);
2612
+ });
2422
2613
  }