gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,187 @@
1
+ // Standalone v1 codec and pure active-branch replay (gentle-shell#1064).
2
+ // No Pi API, disk access, shared binding store, or orchestrator application.
3
+ import { isValidProfileName } from "./agent-profiles.ts";
4
+ import {
5
+ isSafeAgentName,
6
+ isThinkingLevel,
7
+ normalizeModelId,
8
+ normalizeRoutingEntry,
9
+ type AgentModelConfig,
10
+ type AgentRoutingEntry,
11
+ } from "./model-routing-authority.ts";
12
+
13
+ export const SESSION_PROFILE_CUSTOM_TYPE = "gentle-pi.session-profile/v1";
14
+ const SESSION_PROFILE_FAMILY = "gentle-pi.session-profile/";
15
+
16
+ export type SessionProfileOrigin = "user" | "local" | "repo" | "global";
17
+
18
+ export interface SessionProfileBindPayload {
19
+ kind: "bind";
20
+ origin: SessionProfileOrigin;
21
+ name: string;
22
+ modelProfiles: AgentModelConfig;
23
+ }
24
+
25
+ export interface SessionProfileClearPayload {
26
+ kind: "clear";
27
+ }
28
+
29
+ /** Public structural entry shape only; ordinary messages need no custom fields. */
30
+ export interface SessionProfileEntry {
31
+ type: string;
32
+ customType?: string;
33
+ data?: unknown;
34
+ }
35
+
36
+ export type SessionProfileReadResult =
37
+ | { status: "absent" }
38
+ | { status: "bound"; binding: SessionProfileBindPayload }
39
+ | { status: "cleared" }
40
+ | { status: "invalid" }
41
+ | { status: "unsupported" };
42
+
43
+ export type SessionProfileReplayResult =
44
+ | { status: "absent" }
45
+ | (Exclude<SessionProfileReadResult, { status: "absent" }> & {
46
+ entryIndex: number;
47
+ });
48
+
49
+ function isRecord(value: unknown): value is Record<string, unknown> {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value);
51
+ }
52
+
53
+ function hasOwn(value: Record<string, unknown>, key: string): boolean {
54
+ return Object.hasOwn(value, key);
55
+ }
56
+
57
+ function isOrigin(value: unknown): value is SessionProfileOrigin {
58
+ return (
59
+ value === "user" ||
60
+ value === "local" ||
61
+ value === "repo" ||
62
+ value === "global"
63
+ );
64
+ }
65
+
66
+ /** Validate known fields before the permissive routing helper can drop them. */
67
+ function readRoute(
68
+ value: unknown,
69
+ encoderInput = false,
70
+ ): AgentRoutingEntry | undefined {
71
+ if (typeof value === "string") return normalizeRoutingEntry(value);
72
+ if (!isRecord(value)) return undefined;
73
+ // Typed normalized routes can own optional undefined fields. Omit only those
74
+ // encoder fields; persisted fields and legacy effort retain strict validation.
75
+ const route = encoderInput
76
+ ? Object.fromEntries(
77
+ Object.entries(value).filter(
78
+ ([key, field]) =>
79
+ !((key === "model" || key === "thinking") && field === undefined),
80
+ ),
81
+ )
82
+ : value;
83
+ if (hasOwn(route, "model") && normalizeModelId(route.model) === undefined)
84
+ return undefined;
85
+ if (hasOwn(route, "thinking") && !isThinkingLevel(route.thinking))
86
+ return undefined;
87
+ if (hasOwn(route, "effort") && !isThinkingLevel(route.effort))
88
+ return undefined;
89
+ // Project only own known fields. Unknown extras, even an unknown-only route,
90
+ // carry no routing meaning. Thinking wins over legacy effort as elsewhere.
91
+ const known = Object.fromEntries(
92
+ ["model", "thinking", "effort"]
93
+ .filter((key) => hasOwn(route, key))
94
+ .map((key) => [key, route[key]]),
95
+ );
96
+ const normalized = normalizeRoutingEntry(known);
97
+ if (!normalized) return undefined;
98
+ return Object.fromEntries(
99
+ Object.entries(normalized).filter(([, field]) => field !== undefined),
100
+ );
101
+ }
102
+
103
+ function readSnapshot(
104
+ value: unknown,
105
+ encoderInput = false,
106
+ ): AgentModelConfig | undefined {
107
+ if (!isRecord(value)) return undefined;
108
+ const entries: Array<[string, AgentRoutingEntry]> = [];
109
+ for (const [name, rawRoute] of Object.entries(value)) {
110
+ // Shared agent-name policy, applied here without the forgiving whole-config
111
+ // normalization that would silently discard a bad route. Output below uses
112
+ // own data properties, never prototype setters.
113
+ if (!isSafeAgentName(name)) return undefined;
114
+ const route = readRoute(rawRoute, encoderInput);
115
+ if (route === undefined) return undefined;
116
+ entries.push([name, route]);
117
+ }
118
+ return Object.fromEntries(entries);
119
+ }
120
+
121
+ function readBind(
122
+ data: Record<string, unknown>,
123
+ encoderInput = false,
124
+ ): SessionProfileBindPayload | undefined {
125
+ if (!["origin", "name", "modelProfiles"].every((key) => hasOwn(data, key)))
126
+ return undefined;
127
+ if (!isOrigin(data.origin) || !isValidProfileName(data.name)) return undefined;
128
+ const modelProfiles = readSnapshot(data.modelProfiles, encoderInput);
129
+ if (modelProfiles === undefined) return undefined;
130
+ return { kind: "bind", origin: data.origin, name: data.name, modelProfiles };
131
+ }
132
+
133
+ /** Create a detached user selection payload; never appends or claims durability. */
134
+ export function createSessionProfileBind(
135
+ name: string,
136
+ modelProfiles: AgentModelConfig,
137
+ ): SessionProfileBindPayload {
138
+ const binding = readBind({ origin: "user", name, modelProfiles }, true);
139
+ if (!binding) throw new TypeError("Invalid session profile binding");
140
+ return binding;
141
+ }
142
+
143
+ export function createSessionProfileClear(): SessionProfileClearPayload {
144
+ return { kind: "clear" };
145
+ }
146
+
147
+ /** Unknown family identifiers are terminal unsupported states, regardless of data. */
148
+ export function readSessionProfileEntry(
149
+ entry: SessionProfileEntry,
150
+ ): SessionProfileReadResult {
151
+ if (
152
+ entry.type !== "custom" ||
153
+ typeof entry.customType !== "string" ||
154
+ !entry.customType.startsWith(SESSION_PROFILE_FAMILY)
155
+ )
156
+ return { status: "absent" };
157
+ if (entry.customType !== SESSION_PROFILE_CUSTOM_TYPE)
158
+ return { status: "unsupported" };
159
+ const data = entry.data;
160
+ if (!isRecord(data) || !hasOwn(data, "kind")) return { status: "invalid" };
161
+ if (data.kind === "clear") return { status: "cleared" };
162
+ if (data.kind !== "bind") return { status: "invalid" };
163
+ const binding = readBind(data);
164
+ return binding ? { status: "bound", binding } : { status: "invalid" };
165
+ }
166
+
167
+ /**
168
+ * Caller MUST supply ALREADY disk-corroborated active-branch entries, in branch
169
+ * order (oldest to newest). This function cannot establish that precondition:
170
+ * it does not read disk, call Pi, enforce append-failure recovery, establish
171
+ * durability, or update the shared map. In-memory getBranch() alone is NOT
172
+ * corroboration. Sibling and future paths must be excluded by the caller.
173
+ *
174
+ * The newest family entry wins even when invalid or unsupported; never revive
175
+ * an older binding. Cleared permits current fallback at a later consumer,
176
+ * whereas invalid/unsupported must remain distinct from absent. entryIndex is
177
+ * the actual supplied branch offset for a later adapter's location warning.
178
+ */
179
+ export function replaySessionProfileBranch(
180
+ entries: readonly SessionProfileEntry[],
181
+ ): SessionProfileReplayResult {
182
+ for (let entryIndex = entries.length - 1; entryIndex >= 0; entryIndex--) {
183
+ const result = readSessionProfileEntry(entries[entryIndex]);
184
+ if (result.status !== "absent") return { ...result, entryIndex };
185
+ }
186
+ return { status: "absent" };
187
+ }
@@ -1,7 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { realpathSync, statSync } from "node:fs";
2
+ import { lstatSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, resolve } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
5
 
6
6
  export const SESSION_WORKTREE_ENTRY = "gentle-pi.session-worktree/v1";
7
7
  export const SESSION_WORKTREE_CHANGED = "gentle-pi:session-worktree-changed";
@@ -23,12 +23,16 @@ export function worktreeGitEnvironment(env: NodeJS.ProcessEnv = process.env): No
23
23
  }
24
24
 
25
25
  // Match Pi's ordinary path spelling; Git, not the argument, establishes identity.
26
+ function canonicalDirectory(path: string, cwd: string): string {
27
+ let spelling = path.replace(/^@/, "").replace(/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/g, " ");
28
+ if (spelling === "~" || spelling.startsWith("~/")) spelling = homedir() + spelling.slice(1);
29
+ const canonical = realpathSync(resolve(cwd, spelling));
30
+ return statSync(canonical).isDirectory() ? canonical : dirname(canonical);
31
+ }
32
+
26
33
  export function resolveSessionWorktreeWithGit(path: string, cwd: string, run: typeof execFileSync = execFileSync): WorktreeIdentity | undefined {
27
34
  try {
28
- let spelling = path.replace(/^@/, "").replace(/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/g, " ");
29
- if (spelling === "~" || spelling.startsWith("~/")) spelling = homedir() + spelling.slice(1);
30
- const canonical = realpathSync(resolve(cwd, spelling));
31
- const directory = statSync(canonical).isDirectory() ? canonical : dirname(canonical);
35
+ const directory = canonicalDirectory(path, cwd);
32
36
  // Ambient Git routing must not redirect a path into another repository.
33
37
  const env = worktreeGitEnvironment();
34
38
  const git = (arg: string) => String(run("git", ["--no-optional-locks", "-C", directory, "rev-parse", "--path-format=absolute", arg], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], shell: false, windowsHide: true, env })).replace(/\r?\n$/, "");
@@ -38,7 +42,69 @@ export function resolveSessionWorktreeWithGit(path: string, cwd: string, run: ty
38
42
  }
39
43
  }
40
44
 
41
- export const resolveSessionWorktree: WorktreeResolver = resolveSessionWorktreeWithGit;
45
+ // Filesystem identity of everything Git used to answer: no `.git` between the
46
+ // directory and its root, and the same root, `.git` entry and common dir.
47
+ // Undefined when any of it cannot be proven, so the answer is not memoized.
48
+ function worktreeFingerprint(directory: string, identity: WorktreeIdentity): string | undefined {
49
+ const same = (a: string, b: string) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
50
+ try {
51
+ for (let current = directory; !same(current, identity.root); current = dirname(current)) {
52
+ if (dirname(current) === current) return undefined;
53
+ try { lstatSync(join(current, ".git")); return undefined; }
54
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") return undefined; }
55
+ }
56
+ const marker = lstatSync(join(identity.root, ".git"));
57
+ const parts = [statSync(identity.root), marker, statSync(identity.commonDir)].map(entry => `${entry.dev}:${entry.ino}`);
58
+ // A linked worktree's `.git` file can be rewritten in place to another clone.
59
+ if (marker.isFile()) parts.push(`${marker.size}:${marker.mtimeMs}`);
60
+ return parts.join("|");
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ export interface WorktreeMemoOptions { ttlMs?: number; now?: () => number }
67
+ interface MemoEntry { identity: WorktreeIdentity; directory: string; fingerprint: string; expiresAt: number }
68
+ const memoInvalidators = new Set<() => void>();
69
+
70
+ // Startup asks the same (cwd, cwd) question from several extensions; each
71
+ // answer costs two synchronous Git children. Only proven repositories are
72
+ // memoized, and every hit re-checks their filesystem identity, so removal,
73
+ // replacement, a nested `.git` and bootstrap (never cached) are seen at once.
74
+ // The TTL bounds what stat cannot see, such as Git config edits.
75
+ export function memoizeWorktreeResolver(resolver: WorktreeResolver, { ttlMs = 5000, now = Date.now }: WorktreeMemoOptions = {}): WorktreeResolver {
76
+ const memo = new Map<string, MemoEntry>();
77
+ memoInvalidators.add(() => memo.clear());
78
+ return (path, cwd) => {
79
+ const key = `${path}\u0000${cwd}`;
80
+ const cached = memo.get(key);
81
+ if (cached && cached.expiresAt > now()) {
82
+ try {
83
+ const directory = canonicalDirectory(path, cwd);
84
+ if (directory === cached.directory && worktreeFingerprint(directory, cached.identity) === cached.fingerprint) return { ...cached.identity };
85
+ } catch { /* Fall through to a fresh resolution. */ }
86
+ }
87
+ memo.delete(key);
88
+ const identity = resolver(path, cwd);
89
+ if (!identity) return undefined;
90
+ try {
91
+ const directory = canonicalDirectory(path, cwd);
92
+ const fingerprint = worktreeFingerprint(directory, identity);
93
+ if (fingerprint !== undefined) {
94
+ if (memo.size >= 256) memo.clear();
95
+ memo.set(key, { identity: { ...identity }, directory, fingerprint, expiresAt: now() + ttlMs });
96
+ }
97
+ } catch { /* Unprovable identity is returned but never memoized. */ }
98
+ return identity;
99
+ };
100
+ }
101
+
102
+ /** Drops every memoized worktree identity; the next lookup asks Git again. */
103
+ export function clearSessionWorktreeMemo(): void {
104
+ for (const invalidate of memoInvalidators) invalidate();
105
+ }
106
+
107
+ export const resolveSessionWorktree: WorktreeResolver = memoizeWorktreeResolver(resolveSessionWorktreeWithGit);
42
108
 
43
109
  export function toolWorktreePath(name: string, input: Record<string, unknown>): string | undefined {
44
110
  if (!["read", "write", "edit", "grep", "find", "ls"].includes(name)) return undefined;
@@ -82,7 +148,7 @@ export class SessionWorktreeRegistry {
82
148
  if (!this.isCurrent()) throw new Error("Cannot register a worktree for an inactive session.");
83
149
  const identity = this.refreshIdentity();
84
150
  const target = this.resolver(path, this.cwd);
85
- if (!target || !identity || target.commonDir !== identity.commonDir) throw new Error("Select an existing worktree in the same Git clone as this session.");
151
+ if (!target || !identity || target.commonDir !== identity.commonDir) throw new Error("Select an existing worktree in the same Git clone as this session. To work in an independent Git repository, delegate with subagent_run with repository_root instead; it asks for your consent first.");
86
152
  return target.root;
87
153
  }
88
154
 
package/lib/shell-bar.ts CHANGED
@@ -6,6 +6,8 @@ import { CARD_TONE, floatRows, panelInnerWidth, renderCard } from "./shell-card.
6
6
  import { REVIEW_SCOPE_UNAVAILABLE, REVIEW_SIDEBAR_LABELS, type ReviewSidebarSnapshot } from "./review-sidebar-state.ts";
7
7
  import type { VisualSettings } from "./visual-customization-policy.ts";
8
8
  import { renderChangesWidget, type ChangesModel } from "./shell-changes.ts";
9
+ import type { RunningJobDisplay } from "./jobs-sidebar-state.ts";
10
+ import { JOB_GLYPH } from "./background-jobs-tools.ts";
9
11
 
10
12
  type Presentation = Pick<VisualSettings, "density" | "visibility">;
11
13
  type HeaderPresentation = Presentation & Partial<Pick<VisualSettings, "headerPlacement" | "statusPlacement">>;
@@ -18,6 +20,7 @@ export { gaugeTone, renderGauge, type GaugeTone };
18
20
 
19
21
  export interface ShellBarModel {
20
22
  review?: ReviewSidebarSnapshot;
23
+ jobs?: RunningJobDisplay[];
21
24
  profile?: string;
22
25
  changes?: { files: number; added: number; deleted: number; notice?: string };
23
26
  cwd: string;
@@ -35,11 +38,10 @@ export interface ShellBarModel {
35
38
  }
36
39
 
37
40
  // The live header row above the fullscreen rail: session identity plus the
38
- // two counters that tick every frame (context, cost). Deliberately narrower
39
- // than ShellBarModel — extension statuses and the working/thinking state
40
- // never reach the header, so there is nothing on this type for them to leak
41
- // through.
41
+ // live counters. Only the structured jobs count may join context/cost when
42
+ // Status is absent; opaque extension statuses and working state stay excluded.
42
43
  export interface ShellHeaderModel {
44
+ jobs?: number;
43
45
  cwd: string;
44
46
  branch: string | null;
45
47
  dirty: number | undefined;
@@ -55,9 +57,14 @@ export interface ShellHeaderModel {
55
57
  usage: ProviderUsage | undefined;
56
58
  }
57
59
 
58
- export function buildShellHeaderModel(model: ShellBarModel): ShellHeaderModel {
60
+ export function buildShellHeaderModel(model: ShellBarModel, showJobs = false): ShellHeaderModel {
59
61
  const { cwd, branch, dirty, modelId, effort, profile, contextPercent, costTotal, subscription, usage } = model;
60
- return { cwd, branch, dirty, modelId, effort, profile, contextPercent, costTotal, subscription, usage };
62
+ return { cwd, branch, dirty, modelId, effort, profile, contextPercent, costTotal, subscription, usage,
63
+ ...(showJobs && model.jobs?.length ? { jobs: model.jobs.length } : {}) };
64
+ }
65
+
66
+ export function shellJobsCount(count: number): string {
67
+ return `${JOB_GLYPH} ${count} job${count === 1 ? "" : "s"}`;
61
68
  }
62
69
 
63
70
  /** A column span (`[start, end)`, in the rendered line's visible columns) a click must land in to hit the usage segment. */
@@ -110,6 +117,13 @@ export function shellEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
110
117
  return !(value === "0" || value === "false" || value === "off");
111
118
  }
112
119
 
120
+ // Herdr's native Pi detection matches Pi's standard loader row ("⠋ Working...").
121
+ // Inside a Herdr pane keep that row, which Pi shows only during real activity,
122
+ // so detection works without a local manifest override.
123
+ export function keepNativeWorkingRow(env: NodeJS.ProcessEnv = process.env): boolean {
124
+ return env.HERDR_ENV === "1" && env.GENTLE_PI_AGENTS_CHILD !== "1";
125
+ }
126
+
113
127
  export function formatTokens(count: number): string {
114
128
  if (count < 1000) return count.toString();
115
129
  if (count < 10_000) return `${(count / 1000).toFixed(1)}k`;
@@ -166,6 +180,7 @@ function buildSegments(model: ShellBarModel, theme: ShellBarTheme, presentation?
166
180
  location,
167
181
  ...(presentation?.visibility.modelDetails === false ? [] : [modelSegment]),
168
182
  ...(presentation?.visibility.usageCost === false ? [] : [context, cost, ...(usage ? [usage] : [])]),
183
+ ...(model.jobs?.length ? [theme.fg(ROLE.STATUS, shellJobsCount(model.jobs.length))] : []),
169
184
  ...statuses,
170
185
  ];
171
186
  }
@@ -237,6 +252,10 @@ export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme
237
252
  ...(model.review.scope !== REVIEW_SCOPE_UNAVAILABLE ? [label(sanitizeStatus(model.review.scope))] : []),
238
253
  ],
239
254
  }] : []),
255
+ ...(model.jobs?.length ? [{
256
+ title: "Jobs",
257
+ lines: [...model.jobs].sort((a, b) => b.startedAt - a.startedAt).map((job) => value(sanitizeStatus(job.label))),
258
+ }] : []),
240
259
  { title: "Integrations", lines: model.statuses.length
241
260
  ? model.statuses.map((status) => theme.fg(ROLE.STATUS, sanitizeStatus(status)))
242
261
  : [label("No status reported")] },
@@ -360,7 +379,11 @@ export function shellHeaderUsageHit(chrome: ShellHeaderChrome, x: number, y: num
360
379
 
361
380
  function headerContent(model: ShellHeaderModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: Presentation): ShellHeaderResult {
362
381
  const targetWidth = Math.max(0, Math.floor(width));
363
- const ctxCost = joinSegments([contextSegment(model.contextPercent, theme), costSegment(model.costTotal, model.subscription, theme)], theme);
382
+ const jobs = model.jobs ? theme.fg(ROLE.STATUS, shellJobsCount(model.jobs)) : undefined;
383
+ const ctxCost = joinSegments([
384
+ ...(jobs ? [jobs] : []),
385
+ ...(presentation?.visibility.usageCost === false ? [] : [contextSegment(model.contextPercent, theme), costSegment(model.costTotal, model.subscription, theme)]),
386
+ ], theme);
364
387
  const windows = model.usage ? (selectUsageLimit(model.usage, model.modelId)?.windows ?? []) : [];
365
388
  const leftStages = headerLeftStages(model, theme, presentation?.visibility.modelDetails !== false)
366
389
  .map((stage) => presentation?.density === "minimal" ? stage.slice(1) : stage);
@@ -381,7 +404,7 @@ function headerContent(model: ShellHeaderModel, theme: ShellBarTheme, width: num
381
404
  ];
382
405
  for (const { leftIndex, usageStage } of attempts) {
383
406
  const usageText = usageStage ? usageSegmentText(windows, theme, usageStage, usageHint) : undefined;
384
- const right = presentation?.visibility.usageCost === false ? "" : usageText ? joinSegments([ctxCost, usageText], theme) : ctxCost;
407
+ const right = usageText ? joinSegments([ctxCost, usageText], theme) : ctxCost;
385
408
  const left = joinSegments(leftStages[leftIndex]!, theme);
386
409
  if (visibleWidth(left) + (right ? RIGHT_PADDING : 0) + visibleWidth(right) > targetWidth) continue;
387
410
  const text = left + " ".repeat(targetWidth - visibleWidth(left) - visibleWidth(right)) + right;
@@ -390,6 +413,10 @@ function headerContent(model: ShellHeaderModel, theme: ShellBarTheme, width: num
390
413
  return { text, usageSpan: { start: usageStart, end: usageStart + visibleWidth(usageText) } };
391
414
  }
392
415
  const brand = presentation?.density === "minimal" ? "" : theme.fg(ROLE.BRAND, theme.bold(HEADER_BRAND));
416
+ if (jobs && visibleWidth(jobs) <= targetWidth) {
417
+ const left = visibleWidth(brand) + RIGHT_PADDING + visibleWidth(jobs) <= targetWidth ? brand : "";
418
+ return { text: left + " ".repeat(targetWidth - visibleWidth(left) - visibleWidth(jobs)) + jobs };
419
+ }
393
420
  return { text: visibleWidth(brand) <= targetWidth ? brand : "" };
394
421
  }
395
422
 
@@ -399,11 +426,14 @@ export function renderShellBelowInputFloat(model: ShellBarModel, theme: ShellBar
399
426
  if (presentation?.headerPlacement !== "below-input") return undefined;
400
427
  const targetWidth = Math.max(0, Math.floor(width));
401
428
  if (floatHeaderPaint(theme, targetWidth, () => "") === undefined) return undefined;
402
- const chrome = renderShellHeaderChrome(buildShellHeaderModel(model), theme, targetWidth, usageHint, presentation);
429
+ const chrome = renderShellHeaderChrome(buildShellHeaderModel(model, presentation.statusPlacement === "hidden"), theme, targetWidth, usageHint, presentation);
403
430
  const changesRow = changes?.files.length && presentation.visibility.changes !== false
404
431
  ? floatHeaderRow(theme, targetWidth, (inner) => renderChangesWidget(changes, theme, inner)[0] ?? "")
405
432
  : undefined;
406
- const statuses = presentation.statusPlacement === "hidden" ? [] : model.statuses.map(sanitizeStatus).filter(Boolean);
433
+ const statuses = presentation.statusPlacement === "hidden" ? [] : [
434
+ ...(model.jobs?.length ? [shellJobsCount(model.jobs.length)] : []),
435
+ ...model.statuses.map(sanitizeStatus).filter(Boolean),
436
+ ];
407
437
  const statusRow = statuses.length
408
438
  ? floatHeaderRow(theme, targetWidth, () => joinSegments(statuses.map((status) => theme.fg(ROLE.STATUS, status)), theme))
409
439
  : undefined;
@@ -429,7 +459,7 @@ export function renderShellBelowInputFloat(model: ShellBarModel, theme: ShellBar
429
459
  export function renderShellBottomOnlyBar(model: ShellBarModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: HeaderPresentation, changes?: ChangesModel): string[] {
430
460
  const grouped = renderShellBelowInputFloat(model, theme, width, usageHint, presentation, changes);
431
461
  if (grouped) return grouped.rows;
432
- const headerModel = buildShellHeaderModel(model);
462
+ const headerModel = buildShellHeaderModel(model, true);
433
463
  // Neon, missing backgrounds and sub-minimum widths keep the old row count.
434
464
  const rows = [headerContent(headerModel, theme, width, usageHint, presentation).text];
435
465
  const statuses = model.statuses.map(sanitizeStatus).filter((status) => status.length > 0).map((status) => theme.fg(ROLE.STATUS, status));
@@ -1,7 +1,7 @@
1
1
  import { Key, matchesKey, truncateToWidth, visibleWidth, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
2
2
  import { sanitizeTerminalText } from "./terminal-theme.ts";
3
- import { basename } from "node:path";
4
- import { CHANGE_STATUS, changesSummary, type ChangedFile, type ChangesModel, type WorktreeChanges } from "./shell-changes.ts";
3
+ import { basename, posix } from "node:path";
4
+ import { CHANGE_STATUS, DIFF_TOO_LARGE, changesSummary, type ChangedFile, type ChangesModel, type WorktreeChanges } from "./shell-changes.ts";
5
5
  import { paintHoverable } from "./shell-hover.ts";
6
6
 
7
7
  // Gentle Shell changes overlay: a framed two-pane view with the working
@@ -37,7 +37,7 @@ const ROLE = {
37
37
  } as const;
38
38
 
39
39
  const HEADER_PREFIXES = ["diff --git", "index ", "--- ", "+++ ", "new file mode", "deleted file mode", "similarity index", "rename from", "rename to", "Binary files"];
40
- const LIST_MAX_WIDTH = 36;
40
+ const LIST_MAX_WIDTH = 48;
41
41
  const LIST_RATIO = 0.35;
42
42
  const CHROME_ROWS = 3;
43
43
  const MIN_BODY_ROWS = 1;
@@ -80,8 +80,32 @@ function fileCounts(file: ChangedFile, theme: ChangesViewTheme): string {
80
80
  return `${theme.fg(ROLE.ADDED, `+${file.added}`)} ${theme.fg(ROLE.REMOVED, `-${file.deleted}`)}`;
81
81
  }
82
82
 
83
+ // The name leads and the directory trails, so a narrow list clips the
84
+ // directory instead of the part that identifies the file.
83
85
  function fileLabel(file: ChangedFile, theme: ChangesViewTheme, hovered: boolean, idleRole: string): string {
84
- return `${paintHoverable(theme, `${FILE_STATUS[file.status]} ${displayText(file.path)}`, hovered, idleRole)} ${fileCounts(file, theme)}`;
86
+ const directory = posix.dirname(file.path.replace(/\\/g, "/"));
87
+ const label = `${paintHoverable(theme, `${FILE_STATUS[file.status]} ${displayText(posix.basename(file.path.replace(/\\/g, "/")))}`, hovered, idleRole)} ${fileCounts(file, theme)}`;
88
+ return directory === "." ? label : `${label} ${theme.fg(ROLE.EMPTY, displayText(directory))}`;
89
+ }
90
+
91
+ // The diff pane's first row: the selected file's full path, clipped from the
92
+ // left so the file name always stays visible.
93
+ const PATH_HEADER_ROWS = 1;
94
+
95
+ function pathHeader(file: ChangedFile, theme: ChangesViewTheme, width: number): string {
96
+ const status = `${FILE_STATUS[file.status]} `;
97
+ let path = displayText(file.path);
98
+ const room = Math.max(1, width - visibleWidth(status));
99
+ if (visibleWidth(path) > room) {
100
+ const characters = [...path];
101
+ while (characters.length > 0 && visibleWidth(`…${characters.join("")}`) > room) characters.shift();
102
+ path = `…${characters.join("")}`;
103
+ }
104
+ return theme.fg(ROLE.SELECTED, status + path);
105
+ }
106
+
107
+ function diffRows(bodyRows: number): number {
108
+ return Math.max(MIN_BODY_ROWS, bodyRows - PATH_HEADER_ROWS);
85
109
  }
86
110
 
87
111
  function fit(text: string, width: number): string {
@@ -90,10 +114,11 @@ function fit(text: string, width: number): string {
90
114
  }
91
115
 
92
116
  // Both standalone files and the worktree accordion use the original frame.
93
- function renderPanes(width: number, rows: number, theme: ChangesViewTheme, title: string, leftLine: (row: number) => string, rightLines: string[], keys: string): string[] {
117
+ function renderPanes(width: number, rows: number, theme: ChangesViewTheme, title: string, leftLine: (row: number) => string, diffLines: string[], keys: string, headerFile?: ChangedFile): string[] {
94
118
  const inner = Math.max(8, width - 2);
95
119
  const listWidth = Math.min(LIST_MAX_WIDTH, Math.floor(inner * LIST_RATIO));
96
120
  const diffWidth = inner - listWidth - 4;
121
+ const rightLines = headerFile ? [pathHeader(headerFile, theme, diffWidth), ...diffLines] : diffLines;
97
122
  const titleText = truncateToWidth(title, inner - 4, "…");
98
123
  const top = theme.fg(ROLE.FRAME, "╭─ ") + theme.fg(ROLE.TITLE, titleText) + theme.fg(ROLE.FRAME, ` ${rule(inner - visibleWidth(titleText) - 3)}╮`);
99
124
  const body: string[] = [];
@@ -292,7 +317,7 @@ export class WorktreeChangesView {
292
317
  };
293
318
  const keys = theme.fg(ROLE.KEY_TEXT, "j/k select enter toggle/open ← parent/fold ctrl+j/k scroll r refresh esc close");
294
319
  this.pointerLayout = pointerLayout(width, height);
295
- return renderPanes(width, height, theme, `✎ Changes · ${this.trees.length} worktrees`, leftLine, preview, keys);
320
+ return renderPanes(width, height, theme, `✎ Changes · ${this.trees.length} worktrees`, leftLine, preview, keys, selected?.file);
296
321
  }
297
322
 
298
323
  invalidate(): void {
@@ -406,8 +431,8 @@ export class ChangesView {
406
431
  // the scroll keys are checked before the open key; a real Enter is CR.
407
432
  if (data === "j" || matchesKey(data, Key.down)) this.select(this.selected + 1);
408
433
  else if (data === "k" || matchesKey(data, Key.up)) this.select(this.selected - 1);
409
- else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("j"))) this.scrollBy(this.bodyRows());
410
- else if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.ctrl("k"))) this.scrollBy(-this.bodyRows());
434
+ else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("j"))) this.scrollBy(diffRows(this.bodyRows()));
435
+ else if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.ctrl("k"))) this.scrollBy(-diffRows(this.bodyRows()));
411
436
  else if (data === "o" || matchesKey(data, Key.enter)) {
412
437
  const file = this.model.files[this.selected];
413
438
  if (file) this.deps.onOpen(file);
@@ -464,7 +489,7 @@ export class ChangesView {
464
489
  this.fileScroll = this.clampFileScroll(this.fileScroll, rows);
465
490
  const keys = KEYS.map(([key, label]) => `${theme.fg(ROLE.KEY, key)} ${theme.fg(ROLE.KEY_TEXT, key === "esc" ? (this.deps.backLabel ?? label) : label)}`).join(" ");
466
491
  this.pointerLayout = pointerLayout(width, rows);
467
- return renderPanes(width, rows, theme, `✎ Changes · ${changesSummary(this.model)}`, (row) => this.fileLine(row), this.visibleDiff(rows), keys);
492
+ return renderPanes(width, rows, theme, `✎ Changes · ${changesSummary(this.model)}`, (row) => this.fileLine(row), this.visibleDiff(rows), keys, this.model.files[this.selected]);
468
493
  }
469
494
 
470
495
  invalidate(): void {
@@ -500,8 +525,9 @@ export class ChangesView {
500
525
  const lines = this.diffs.get(file.path);
501
526
  if (!lines) return [];
502
527
  if (lines.length === 0) return [this.deps.theme.fg(ROLE.EMPTY, EMPTY_DIFF)];
503
- this.diffScroll = Math.min(this.diffScroll, Math.max(0, lines.length - rows));
504
- return lines.slice(this.diffScroll, this.diffScroll + rows);
528
+ const visible = diffRows(rows);
529
+ this.diffScroll = Math.min(this.diffScroll, Math.max(0, lines.length - visible));
530
+ return lines.slice(this.diffScroll, this.diffScroll + visible);
505
531
  }
506
532
 
507
533
  private select(index: number): void {
@@ -534,7 +560,7 @@ export class ChangesView {
534
560
  const file = this.model.files[this.selected];
535
561
  const lines = file ? this.diffs.get(file.path) : undefined;
536
562
  if (!lines) return false;
537
- const next = Math.max(0, Math.min(Math.max(0, lines.length - rows), this.diffScroll + Math.trunc(delta)));
563
+ const next = Math.max(0, Math.min(Math.max(0, lines.length - diffRows(rows)), this.diffScroll + Math.trunc(delta)));
538
564
  if (next === this.diffScroll) return false;
539
565
  this.diffScroll = next;
540
566
  return true;
@@ -557,7 +583,8 @@ export class ChangesView {
557
583
  void this.deps.loadDiff(file).then(
558
584
  (text) => {
559
585
  if (this.disposed) return;
560
- this.diffs.set(file.path, colorDiff(text, this.deps.theme));
586
+ // An oversized diff arrives as one plain line pointing at the editor.
587
+ this.diffs.set(file.path, text === DIFF_TOO_LARGE ? [this.deps.theme.fg(ROLE.EMPTY, text)] : colorDiff(text, this.deps.theme));
561
588
  this.deps.requestRender();
562
589
  },
563
590
  () => {