gentle-pi 3.7.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
package/lib/shell-card.ts CHANGED
@@ -23,12 +23,45 @@ export interface Card {
23
23
 
24
24
  export interface CardTheme {
25
25
  fg(color: string, text: string): string;
26
+ /** Pi themes have it; the float style needs it to paint its panel. */
27
+ bg?(color: string, text: string): string;
28
+ }
29
+
30
+ // Conversation cards come in two styles, chosen in /gentle:customize. Pi loads
31
+ // every extension with its own jiti loader (moduleCache:false), so each one
32
+ // gets a copy of this module; the global symbol keeps one style per process.
33
+ export const CARD_STYLE = {
34
+ NEON: "neon",
35
+ FLOAT: "float",
36
+ } as const;
37
+
38
+ export type CardStyle = (typeof CARD_STYLE)[keyof typeof CARD_STYLE];
39
+
40
+ const CARD_STYLE_SLOT = Symbol.for("gentle-pi.card-style");
41
+ const styleState = globalThis as typeof globalThis & { [CARD_STYLE_SLOT]?: unknown };
42
+
43
+ export function cardStyle(): CardStyle {
44
+ return styleState[CARD_STYLE_SLOT] === CARD_STYLE.NEON ? CARD_STYLE.NEON : CARD_STYLE.FLOAT;
45
+ }
46
+
47
+ export function setCardStyle(style: CardStyle): void {
48
+ styleState[CARD_STYLE_SLOT] = style;
26
49
  }
27
50
 
28
51
  export interface CardRenderOptions {
29
52
  expanded: boolean;
53
+ /** Optional physical-row budget for useful tool previews; notices default to one. */
54
+ previewRows?: number;
30
55
  /** Right-aligned hint in the top rule, e.g. the expand key. May carry ANSI. */
31
56
  hint?: string;
57
+ /**
58
+ * Fixed chrome panels (Agents, Todos, Status rail) opt in here. The float
59
+ * style then paints them like float cards, centered between two padding
60
+ * rows with a blank row between header and body: `panelExtraRows` taller
61
+ * than neon when a body exists, with the header on `panelHeaderRow`.
62
+ * Neon keeps the outlined frame. Conversation cards never set it.
63
+ */
64
+ panel?: boolean;
32
65
  }
33
66
 
34
67
  export const CARD_GLYPH = "✿";
@@ -54,8 +87,39 @@ const BODY_ROLE = "text";
54
87
  const SEPARATOR = "·";
55
88
  const FRAME_COLUMNS = 4;
56
89
 
90
+ // The characters that draw a card around its content. The float style keeps
91
+ // every row the same width: the left edge becomes an accent bar and the rules
92
+ // and the right rail become spaces. Rows are built from these by position, so
93
+ // content that happens to contain box-drawing characters is never rewritten.
94
+ interface CardChrome {
95
+ topLeft: string;
96
+ topLead: string;
97
+ topRight: string;
98
+ rule: string;
99
+ side: string;
100
+ rail: string;
101
+ bottomLeft: string;
102
+ bottomRight: string;
103
+ }
104
+
105
+ const OUTLINE_CHROME: CardChrome = { topLeft: "╭", topLead: "─ ", topRight: "╮", rule: "─", side: "│", rail: "│", bottomLeft: "╰", bottomRight: "╯" };
106
+ // `▎ ` is one cell narrower than `╭─ `: the spare cell moves to the right end
107
+ // so the heading glyph starts in the same column as the body text.
108
+ const FLOAT_CHROME: CardChrome = { topLeft: "▎", topLead: " ", topRight: " ", rule: " ", side: "▎", rail: " ", bottomLeft: "▎", bottomRight: " " };
109
+ let chrome = OUTLINE_CHROME;
110
+
111
+ function withChrome<T>(next: CardChrome, run: () => T): T {
112
+ const previous = chrome;
113
+ chrome = next;
114
+ try {
115
+ return run();
116
+ } finally {
117
+ chrome = previous;
118
+ }
119
+ }
120
+
57
121
  function rule(length: number): string {
58
- return "─".repeat(Math.max(0, length));
122
+ return chrome.rule.repeat(Math.max(0, length));
59
123
  }
60
124
 
61
125
  function titleText(card: Card, theme: CardTheme): { styled: string; width: number } {
@@ -92,13 +156,13 @@ export function cardTop(card: Card, theme: CardTheme, width: number, hint?: stri
92
156
  const styledTitleWidth = title.width <= titleWidth ? title.width : visibleWidth(styledTitle);
93
157
  const fill = rule(targetWidth - styledTitleWidth - 5 - hintWidth);
94
158
  const tail = shownHint ? ` ${theme.fg(HINT_ROLE, shownHint)} ` : "";
95
- return theme.fg(FRAME_ROLE[card.tone], "╭") + frame(theme, card.tone, "─ ") + styledTitle + frame(theme, card.tone, ` ${fill}`) + tail + frame(theme, card.tone, "╮");
159
+ return theme.fg(FRAME_ROLE[card.tone], chrome.topLeft) + frame(theme, card.tone, chrome.topLead) + styledTitle + frame(theme, card.tone, ` ${fill}`) + tail + frame(theme, card.tone, chrome.topRight);
96
160
  }
97
161
 
98
162
  export function cardLine(text: string, tone: CardTone, theme: CardTheme, width: number): string {
99
163
  const targetWidth = Math.max(0, Math.floor(width));
100
164
  if (targetWidth === 0) return "";
101
- const left = theme.fg(FRAME_ROLE[tone], "│");
165
+ const left = theme.fg(FRAME_ROLE[tone], chrome.side);
102
166
  if (targetWidth === 1) return left;
103
167
  if (targetWidth === 2) return left + frame(theme, tone, "│");
104
168
  if (targetWidth === 3) return `${left} ${frame(theme, tone, "│")}`;
@@ -106,34 +170,239 @@ export function cardLine(text: string, tone: CardTone, theme: CardTheme, width:
106
170
  const innerWidth = targetWidth - FRAME_COLUMNS;
107
171
  const clipped = innerWidth === 0 ? "" : truncateToWidth(text, innerWidth, "…");
108
172
  const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)));
109
- return `${left} ${clipped}${padding} ${frame(theme, tone, "│")}`;
173
+ return `${left} ${clipped}${padding} ${frame(theme, tone, chrome.rail)}`;
110
174
  }
111
175
 
112
- export function cardBottom(tone: CardTone, theme: CardTheme, width: number): string {
176
+ export function cardBottom(tone: CardTone, theme: CardTheme, width: number, content?: string): string {
113
177
  const targetWidth = Math.max(0, Math.floor(width));
114
178
  if (targetWidth === 0) return "";
115
- const left = theme.fg(FRAME_ROLE[tone], "╰");
179
+ const left = theme.fg(FRAME_ROLE[tone], chrome.bottomLeft);
116
180
  if (targetWidth === 1) return left;
117
- return left + frame(theme, tone, `${rule(targetWidth - 2)}╯`);
181
+ if (content === undefined || visibleWidth(content) === 0) return left + frame(theme, tone, `${rule(targetWidth - 2)}${chrome.bottomRight}`);
182
+ const contentWidth = visibleWidth(content) + 2;
183
+ // Responsive: the duration rides the closing rule only when it fits with a minimum fill.
184
+ if (targetWidth - 2 - contentWidth < 3) return left + frame(theme, tone, `${rule(targetWidth - 2)}${chrome.bottomRight}`);
185
+ return left + frame(theme, tone, `${rule(targetWidth - 2 - contentWidth)} `) + theme.fg(HINT_ROLE, content) + frame(theme, tone, ` ${chrome.bottomRight}`);
118
186
  }
119
187
 
120
188
  export function cardInnerWidth(width: number): number {
121
189
  return Math.max(1, width - FRAME_COLUMNS);
122
190
  }
123
191
 
192
+ /** Shared body for whole cards and separate call/result components. Budgets apply after wrapping. */
193
+ export function cardBodyRows(
194
+ rows: readonly string[], tone: CardTone, theme: CardTheme, width: number,
195
+ options: CardRenderOptions,
196
+ ): string[] {
197
+ if (width <= 0) return [];
198
+ const wrapped = rows.flatMap((row) => row.split("\n").flatMap((line) => line === "" ? [""] : wrapTextWithAnsi(line, cardInnerWidth(width))));
199
+ const limit = Math.max(0, Math.floor(options.previewRows ?? 3));
200
+ const shown = options.expanded ? wrapped : wrapped.slice(0, limit);
201
+ return shown.map((line) => cardLine(line, tone, theme, width));
202
+ }
203
+
204
+ // Pi draws a tool row as the call component followed, once a result exists,
205
+ // by the result component, which closes the frame. The call cannot see the
206
+ // result, so result renderers mark the row state both share, and a call still
207
+ // waiting for its first result closes its own frame.
208
+ const RESULT_MARK = Symbol.for("gentle-pi.card-result");
209
+ const RUNNING_TEXT = "running…";
210
+ const RUNNING_ROLE = "muted";
211
+
212
+ /** The fields of pi's tool render context that tell whether a result exists. */
213
+ export interface CardRowContext {
214
+ isPartial?: boolean;
215
+ state?: unknown;
216
+ }
217
+
218
+ /** Result renderers mark the shared row state: from now on a result component closes the card. */
219
+ export function markCardResult(state: unknown): void {
220
+ if (state !== null && typeof state === "object") (state as Record<symbol, unknown>)[RESULT_MARK] = true;
221
+ }
222
+
223
+ /** Whether a call card must close its own frame. Read it at render time: pi builds the result component after the call. */
224
+ export function cardAwaitingResult(context: CardRowContext): boolean {
225
+ if (context.isPartial === false) return false;
226
+ const state = context.state;
227
+ return !(state !== null && typeof state === "object" && (state as Record<symbol, unknown>)[RESULT_MARK] === true);
228
+ }
229
+
230
+ /** The muted body row of a call card that is still waiting for its result. */
231
+ export function cardRunningLine(tone: CardTone, theme: CardTheme, width: number): string {
232
+ return cardLine(theme.fg(RUNNING_ROLE, RUNNING_TEXT), tone, theme, width);
233
+ }
234
+
235
+ /** Quiet calls may continue long or multiline headings inside the same frame. */
236
+ export function cardTopRows(card: Card, theme: CardTheme, width: number, hint?: string): string[] {
237
+ const target = Math.max(0, Math.floor(width));
238
+ if (target === 0) return [];
239
+ if (target < 8) return [cardTop(card, theme, target, hint)];
240
+ const [first = "", ...rest] = card.title.split("\n");
241
+ // Reserve the hint before wrapping, plus one rule column and enough heading
242
+ // space to retain identity. This policy belongs only to continuing tool tops.
243
+ const available = target - 6 - visibleWidth(`${card.glyph ?? CARD_GLYPH} `) - (card.subtitle ? visibleWidth(card.subtitle) + 3 : 0);
244
+ const hintWidth = hint ? visibleWidth(hint) + 2 : 0;
245
+ const shownHint = hint && available - hintWidth >= 12 ? hint : undefined;
246
+ const room = Math.max(1, available - (shownHint ? hintWidth : 0));
247
+ const [head = "", ...overflow] = wrapTextWithAnsi(first, room);
248
+ return [
249
+ cardTop({ ...card, title: head }, theme, target, shownHint),
250
+ ...cardBodyRows([...overflow, ...rest], card.tone, theme, target, { expanded: true }),
251
+ ];
252
+ }
253
+
254
+ /** The rows one card component draws, by position. */
255
+ export interface CardParts {
256
+ /** The top rule plus any heading continuation rows. */
257
+ head?: readonly string[];
258
+ body?: readonly string[];
259
+ /** The closing rule, when this component closes the card. */
260
+ bottom?: string;
261
+ /** A component above this one already drew the heading. */
262
+ afterHeading?: boolean;
263
+ }
264
+
265
+ const FLOAT_MARGIN = 1;
266
+ const FLOAT_MIN_WIDTH = 10;
267
+ const FLOAT_BG_ROLE: Record<CardTone, string> = {
268
+ [CARD_TONE.INFO]: "toolSuccessBg",
269
+ [CARD_TONE.SUCCESS]: "toolSuccessBg",
270
+ [CARD_TONE.WARNING]: "toolPendingBg",
271
+ [CARD_TONE.ERROR]: "toolErrorBg",
272
+ };
273
+ const BG_RESET = "\x1b[49m";
274
+ // Full resets (pi-tui truncation inserts one) and background resets.
275
+ const BG_CLEARING = /\x1b\[(?:0|49)?m/g;
276
+
277
+ function panelOpener(theme: CardTheme, tone: CardTone): string {
278
+ if (typeof theme.bg !== "function") return "";
279
+ try {
280
+ const painted = theme.bg(FLOAT_BG_ROLE[tone], "");
281
+ return painted.endsWith(BG_RESET) ? painted.slice(0, -BG_RESET.length) : "";
282
+ } catch {
283
+ // A theme without the tool background keeps the outlined card.
284
+ return "";
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Draws one conversation card component in the active style. The outlined
290
+ * style returns the parts unchanged; the float style renders them one margin
291
+ * narrower with the float chrome and paints a tone background behind every
292
+ * row. A blank row sits above the heading and between it and the body.
293
+ */
294
+ export function floatRows(tone: CardTone, theme: CardTheme, width: number, render: (width: number) => CardParts): string[] {
295
+ const target = Math.max(0, Math.floor(width));
296
+ const open = floatOpener(theme, tone, target);
297
+ if (!open) {
298
+ const { head = [], body = [], bottom } = render(width);
299
+ return [...head, ...body, ...(bottom === undefined ? [] : [bottom])];
300
+ }
301
+ const inner = target - FLOAT_MARGIN * 2;
302
+ const rows = withChrome(FLOAT_CHROME, () => {
303
+ const { head = [], body = [], bottom, afterHeading = false } = render(inner);
304
+ const blank = cardBottom(tone, theme, inner);
305
+ return [
306
+ ...(head.length > 0 ? [blank, ...head] : []),
307
+ ...(body.length > 0 && (head.length > 0 || afterHeading) ? [blank] : []),
308
+ ...body,
309
+ ...(bottom === undefined ? [] : [bottom]),
310
+ ];
311
+ });
312
+ return paintFloat(rows, open);
313
+ }
314
+
315
+ /** The tone background opener when the float style applies at this width, or "" for the outlined card. */
316
+ function floatOpener(theme: CardTheme, tone: CardTone, width: number): string {
317
+ return cardStyle() === CARD_STYLE.FLOAT && Math.floor(width) >= FLOAT_MIN_WIDTH ? panelOpener(theme, tone) : "";
318
+ }
319
+
320
+ // Paints every row behind its tone background, re-armed after any reset the
321
+ // content carries, inside a transparent one-column margin on both sides.
322
+ // Padding rows keep their tone-coloured accent just like content rows.
323
+ function paintFloat(rows: readonly string[], open: string): string[] {
324
+ const margin = " ".repeat(FLOAT_MARGIN);
325
+ return rows.map((row) => `${margin}${open}${row.replace(BG_CLEARING, (reset) => reset + open)}${BG_RESET}${margin}`);
326
+ }
327
+
328
+ /**
329
+ * Content columns of a panel body (Agents, Todos, Status rail) in the active
330
+ * style. Callers that pre-wrap or pre-fit rows use it so a float panel, two
331
+ * columns narrower than the outlined frame, never re-wraps or clips them.
332
+ */
333
+ export function panelInnerWidth(theme: CardTheme, width: number, tone: CardTone = CARD_TONE.INFO): number {
334
+ return floatOpener(theme, tone, width) ? cardInnerWidth(Math.floor(width) - FLOAT_MARGIN * 2) : cardInnerWidth(width);
335
+ }
336
+
337
+ /**
338
+ * The row a panel draws its header on in the active style: 0 for the
339
+ * outlined frame, 1 below the float panel's top padding row. Callers
340
+ * hit-test their header control with it.
341
+ */
342
+ export function panelHeaderRow(theme: CardTheme, width: number, tone: CardTone = CARD_TONE.INFO): number {
343
+ return floatOpener(theme, tone, width) ? 1 : 0;
344
+ }
345
+
346
+ /**
347
+ * Rows a float panel with a body adds over the outlined frame: the top
348
+ * padding row and the separator below the header, or 0 for the outlined
349
+ * frame. Callers with a row budget spend them from the body.
350
+ */
351
+ export function panelExtraRows(theme: CardTheme, width: number, tone: CardTone = CARD_TONE.INFO): number {
352
+ return floatOpener(theme, tone, width) ? 2 : 0;
353
+ }
354
+
124
355
  export function renderCard(card: Card, theme: CardTheme, width: number, options: CardRenderOptions): string[] {
125
- const innerWidth = Math.max(1, width - FRAME_COLUMNS);
356
+ // Only opt-in tool previews collapse to nothing at nonpositive widths; the
357
+ // legacy path keeps its empty-row shape for widgets that call it unguarded.
358
+ if (options.previewRows !== undefined) return width <= 0 ? [] : floatRows(card.tone, theme, width, (inner) => ({
359
+ head: [cardTop(card, theme, inner, options.hint)],
360
+ body: cardBodyRows(card.body.map((line) => theme.fg(BODY_ROLE, line)), card.tone, theme, inner, options),
361
+ bottom: cardBottom(card.tone, theme, inner),
362
+ }));
363
+ // Without the panel opt-in, legacy cards always keep the outlined frame.
364
+ if (chrome !== OUTLINE_CHROME) return withChrome(OUTLINE_CHROME, () => renderCard(card, theme, width, options));
365
+ const open = options.panel ? floatOpener(theme, card.tone, width) : "";
366
+ if (open) return paintFloat(withChrome(FLOAT_CHROME, () => floatPanel(card, theme, Math.floor(width) - FLOAT_MARGIN * 2, options)), open);
126
367
  const top = cardTop(card, theme, width, options.hint);
127
368
  const bottom = cardBottom(card.tone, theme, width);
369
+ return [top, ...cardText(card, theme, cardInnerWidth(width), options.expanded).map((line) => cardLine(line, card.tone, theme, width)), bottom];
370
+ }
371
+
372
+ /** Body text of a legacy card or panel, wrapped to its content columns, before the side rails. */
373
+ function cardText(card: Card, theme: CardTheme, innerWidth: number, expanded: boolean): string[] {
128
374
  const lines = bodyLines(card, innerWidth);
129
- const body = (() => {
130
- if (lines.length === 0) return [];
131
- if (!options.expanded) {
132
- const first = lines.find((line) => line !== "") ?? "";
133
- const clipped = lines.length > 1 ? truncateToWidth(first, Math.max(1, innerWidth - 1), "") + "…" : first;
134
- return [cardLine(theme.fg(BODY_ROLE, clipped), card.tone, theme, width)];
135
- }
136
- return lines.map((line) => cardLine(line === "" ? "" : theme.fg(BODY_ROLE, line), card.tone, theme, width));
137
- })();
138
- return [top, ...body, bottom];
375
+ if (lines.length === 0) return [];
376
+ if (!expanded) {
377
+ const first = lines.find((line) => line !== "") ?? "";
378
+ const clipped = lines.length > 1 ? truncateToWidth(first, Math.max(1, innerWidth - 1), "") + "…" : first;
379
+ return [theme.fg(BODY_ROLE, clipped)];
380
+ }
381
+ return lines.map((line) => (line === "" ? "" : theme.fg(BODY_ROLE, line)));
382
+ }
383
+
384
+ // Float panels: the float card chrome. Padding rows that keep the accent bar
385
+ // sit above the heading, between heading and body (only when a body exists)
386
+ // and in the bottom rule's place, centering the content: panelExtraRows taller
387
+ // than neon, with the heading on row 1 (see panelHeaderRow).
388
+ function floatPanel(card: Card, theme: CardTheme, width: number, options: CardRenderOptions): string[] {
389
+ const blank = cardBottom(card.tone, theme, width);
390
+ const body = cardText(card, theme, cardInnerWidth(width), options.expanded).map((line) => cardLine(line, card.tone, theme, width));
391
+ return [blank, panelHeader(card, theme, width, options.hint), ...(body.length > 0 ? [blank, ...body] : []), blank];
392
+ }
393
+
394
+ function fitRow(text: string, width: number): string {
395
+ const clipped = visibleWidth(text) <= width ? text : truncateToWidth(text, width, "…");
396
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
397
+ }
398
+
399
+ // `▎ <glyph> <title> <subtitle>` left and the hint right, ending in the same
400
+ // two columns the body rows keep for their right edge.
401
+ function panelHeader(card: Card, theme: CardTheme, width: number, hint?: string): string {
402
+ const lead = theme.fg(FRAME_ROLE[card.tone], chrome.topLeft) + chrome.topLead;
403
+ const subtitle = card.subtitle ? ` ${theme.fg(SUBTITLE_ROLE, card.subtitle)}` : "";
404
+ const left = `${lead}${theme.fg(TITLE_ROLE[card.tone], `${card.glyph ?? CARD_GLYPH} ${card.title}`)}${subtitle}`;
405
+ const right = hint ? `${theme.fg(SUBTITLE_ROLE, hint)}${chrome.topRight}` : "";
406
+ const gap = width - visibleWidth(left) - visibleWidth(right);
407
+ return right && gap >= 1 ? `${left}${" ".repeat(gap)}${right}` : fitRow(left, width);
139
408
  }
@@ -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;
@@ -56,7 +56,8 @@ function rule(length: number): string {
56
56
 
57
57
  export function colorDiff(text: string, theme: ChangesViewTheme): string[] {
58
58
  const lines: string[] = [];
59
- for (const line of text.split("\n")) {
59
+ for (const rawLine of text.split("\n")) {
60
+ const line = sanitizeTerminalText(rawLine.replace(/\r/g, "")).replace(/\t/g, " ");
60
61
  if (line === "" || HEADER_PREFIXES.some((prefix) => line.startsWith(prefix))) continue;
61
62
  if (line.startsWith("@@")) lines.push(theme.fg(ROLE.HUNK, line));
62
63
  else if (line.startsWith("+")) lines.push(theme.fg("toolDiffAdded", line));
@@ -79,8 +80,32 @@ function fileCounts(file: ChangedFile, theme: ChangesViewTheme): string {
79
80
  return `${theme.fg(ROLE.ADDED, `+${file.added}`)} ${theme.fg(ROLE.REMOVED, `-${file.deleted}`)}`;
80
81
  }
81
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.
82
85
  function fileLabel(file: ChangedFile, theme: ChangesViewTheme, hovered: boolean, idleRole: string): string {
83
- 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);
84
109
  }
85
110
 
86
111
  function fit(text: string, width: number): string {
@@ -89,10 +114,11 @@ function fit(text: string, width: number): string {
89
114
  }
90
115
 
91
116
  // Both standalone files and the worktree accordion use the original frame.
92
- 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[] {
93
118
  const inner = Math.max(8, width - 2);
94
119
  const listWidth = Math.min(LIST_MAX_WIDTH, Math.floor(inner * LIST_RATIO));
95
120
  const diffWidth = inner - listWidth - 4;
121
+ const rightLines = headerFile ? [pathHeader(headerFile, theme, diffWidth), ...diffLines] : diffLines;
96
122
  const titleText = truncateToWidth(title, inner - 4, "…");
97
123
  const top = theme.fg(ROLE.FRAME, "╭─ ") + theme.fg(ROLE.TITLE, titleText) + theme.fg(ROLE.FRAME, ` ${rule(inner - visibleWidth(titleText) - 3)}╮`);
98
124
  const body: string[] = [];
@@ -291,7 +317,7 @@ export class WorktreeChangesView {
291
317
  };
292
318
  const keys = theme.fg(ROLE.KEY_TEXT, "j/k select enter toggle/open ← parent/fold ctrl+j/k scroll r refresh esc close");
293
319
  this.pointerLayout = pointerLayout(width, height);
294
- 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);
295
321
  }
296
322
 
297
323
  invalidate(): void {
@@ -405,8 +431,8 @@ export class ChangesView {
405
431
  // the scroll keys are checked before the open key; a real Enter is CR.
406
432
  if (data === "j" || matchesKey(data, Key.down)) this.select(this.selected + 1);
407
433
  else if (data === "k" || matchesKey(data, Key.up)) this.select(this.selected - 1);
408
- else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("j"))) this.scrollBy(this.bodyRows());
409
- 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()));
410
436
  else if (data === "o" || matchesKey(data, Key.enter)) {
411
437
  const file = this.model.files[this.selected];
412
438
  if (file) this.deps.onOpen(file);
@@ -463,7 +489,7 @@ export class ChangesView {
463
489
  this.fileScroll = this.clampFileScroll(this.fileScroll, rows);
464
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(" ");
465
491
  this.pointerLayout = pointerLayout(width, rows);
466
- 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]);
467
493
  }
468
494
 
469
495
  invalidate(): void {
@@ -499,8 +525,9 @@ export class ChangesView {
499
525
  const lines = this.diffs.get(file.path);
500
526
  if (!lines) return [];
501
527
  if (lines.length === 0) return [this.deps.theme.fg(ROLE.EMPTY, EMPTY_DIFF)];
502
- this.diffScroll = Math.min(this.diffScroll, Math.max(0, lines.length - rows));
503
- 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);
504
531
  }
505
532
 
506
533
  private select(index: number): void {
@@ -533,7 +560,7 @@ export class ChangesView {
533
560
  const file = this.model.files[this.selected];
534
561
  const lines = file ? this.diffs.get(file.path) : undefined;
535
562
  if (!lines) return false;
536
- 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)));
537
564
  if (next === this.diffScroll) return false;
538
565
  this.diffScroll = next;
539
566
  return true;
@@ -556,7 +583,8 @@ export class ChangesView {
556
583
  void this.deps.loadDiff(file).then(
557
584
  (text) => {
558
585
  if (this.disposed) return;
559
- 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));
560
588
  this.deps.requestRender();
561
589
  },
562
590
  () => {