gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { TuiAltScreen, visibleWidth, type Terminal, type TuiMouseEvent } from "@earendil-works/pi-tui";
4
- import { CHANGE_STATUS, changesModel, type ChangedFile } from "../lib/shell-changes.ts";
4
+ import { CHANGE_STATUS, DIFF_TOO_LARGE, changesModel, type ChangedFile } from "../lib/shell-changes.ts";
5
5
  import { WorktreeChangesView, ChangesView, colorDiff, type ChangesViewDeps } from "../lib/shell-changes-view.ts";
6
6
  import { stripAnsi } from "../lib/terminal-theme.ts";
7
7
 
@@ -106,8 +106,8 @@ test("worktree accordion keeps groups and nested files beside a framed lazy diff
106
106
  const lines = component.render(100);
107
107
  assert.equal(lines.length, 10);
108
108
  assert.match(lines[0], /^╭─ ✎ Changes/);
109
- assert.match(lines[1], /^│ ▾ main · main +│ \+\/main:same.ts +│$/);
110
- assert.match(lines[2], /^│ ▸ M same.ts +\+1 -0 +│/);
109
+ assert.match(lines[1], /^│ ▾ main · main +│ M same.ts +│$/);
110
+ assert.match(lines[2], /^│ ▸ M same.ts +\+1 -0 +│ \+\/main:same.ts +│$/);
111
111
  assert.match(lines[3], /^│ ▸ detached · linked +│/);
112
112
  assert.match(lines[9], /^╰─+╯$/);
113
113
  for (const line of lines) assert.equal(visibleWidth(line), 100);
@@ -131,6 +131,34 @@ test("worktree accordion keeps groups and nested files beside a framed lazy diff
131
131
  assert.equal(closed, 1);
132
132
  });
133
133
 
134
+ // Long paths were clipped to `tools/linkedin-automation/scr…`, hiding the file
135
+ // name. Rows lead with the name and trail the directory; the diff pane shows
136
+ // the full path, clipped from the left so the name always survives.
137
+ test("changes rows lead with the file name and the diff pane shows the full path", async () => {
138
+ const path = "tools/linkedin-automation/scripts/connect.mjs";
139
+ const trees = [{ root: "/main", branch: "feat/linkedin", model: changesModel([file(path, 214, 0, CHANGE_STATUS.ADDED)]) }];
140
+ const component = new WorktreeChangesView(trees, {
141
+ theme: plainTheme, rows: 10,
142
+ loadDiff: async () => "@@ -0,0 +1,1 @@\n+# Scheduled",
143
+ onOpen: () => {}, onClose: () => {}, requestRender() {}, onRefresh() {},
144
+ });
145
+ component.handleInput("\r");
146
+ component.handleInput("j");
147
+ await settle();
148
+ const lines = component.render(120);
149
+ assert.match(lines[2]!, /^│ ▸ A connect\.mjs \+214 -0 tools/, "the file name leads the row");
150
+ assert.match(lines[1]!, /│ A tools\/linkedin-automation\/scripts\/connect\.mjs +│$/, "the diff pane starts with the full path");
151
+ assert.match(lines[2]!, /│ @@ -0,0 \+1,1 @@ +│$/, "the diff follows the path line");
152
+ assert.match(component.render(70)[1]!, /│ A …[^│]*\/connect\.mjs *│$/, "a narrow pane clips the path from the left");
153
+
154
+ const standalone = view({ loadDiff: async () => "+x" }, [file(path, 1, 0, CHANGE_STATUS.ADDED)]).view;
155
+ await settle();
156
+ const single = standalone.render(120);
157
+ assert.match(single[1]!, /│ ▸ A connect\.mjs \+1 -0 tools/);
158
+ assert.match(single[1]!, /│ A tools\/linkedin-automation\/scripts\/connect\.mjs +│$/);
159
+ assert.match(single[2]!, /│ \+x +│$/);
160
+ });
161
+
134
162
  test("worktree accordion selects a clicked file without opening its editor", () => {
135
163
  const opened: string[] = [];
136
164
  const component = new WorktreeChangesView([{ root: "/main", branch: "main", model: changesModel([file("a.ts", 1, 0), file("b.ts", 1, 0)]) }], {
@@ -147,9 +175,10 @@ test("worktree accordion selects a clicked file without opening its editor", ()
147
175
 
148
176
  test("worktree accordion receives native fullscreen press and release as a file click", async () => {
149
177
  let onInput: ((data: string) => void) | undefined;
150
- const terminal: Terminal = {
151
- start(input) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 8; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {},
178
+ const terminalFixture = {
179
+ start(input: Parameters<Terminal["start"]>[0]) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 8; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {}, setProgramStatus() {},
152
180
  };
181
+ const terminal: Terminal = terminalFixture;
153
182
  const opened: string[] = [];
154
183
  const component = new WorktreeChangesView([{ root: "/main", branch: "main", model: changesModel([file("a.ts", 1, 0), file("b.ts", 1, 0)]) }], {
155
184
  theme: plainTheme, rows: 8, loadDiff: async () => "+preview",
@@ -297,9 +326,10 @@ test("non-left gestures pass through with current, stale, and missing layouts in
297
326
  test("right press reaches the native Windows paste fallback when eligible", () => {
298
327
  let onInput: ((data: string) => void) | undefined;
299
328
  let pasted = 0;
300
- const terminal: Terminal = {
301
- start(input) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 12; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {},
329
+ const terminalFixture = {
330
+ start(input: Parameters<Terminal["start"]>[0]) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 12; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {}, setProgramStatus() {},
302
331
  };
332
+ const terminal: Terminal = terminalFixture;
303
333
  const { view: component } = view();
304
334
  const tui = new TuiAltScreen(terminal, false, undefined, { mouse: true, onRightClickPaste: () => { pasted++; } });
305
335
  const platform = Object.getOwnPropertyDescriptor(process, "platform")!;
@@ -413,9 +443,10 @@ test("accordion selection scrolls flattened rows and diff scrolling does not ope
413
443
  await settle();
414
444
  assert.match(component.render(100).join("\n"), /▸ M file-19.ts/);
415
445
  component.handleInput("\x0a");
416
- assert.match(component.render(100)[1], /\+line 5/);
446
+ assert.match(component.render(100)[1], /M file-19\.ts/, "the path line stays fixed while the diff scrolls");
447
+ assert.match(component.render(100)[2], /\+line 4/, "a page is the diff rows below the path line");
417
448
  component.handleInput("\x0b");
418
- assert.match(component.render(100)[1], /\+line 0/);
449
+ assert.match(component.render(100)[2], /\+line 0/);
419
450
  assert.equal(opened, 0);
420
451
  for (const width of [1, 8, 20, 40, 100]) {
421
452
  for (const line of component.render(width)) assert.ok(visibleWidth(line) <= width);
@@ -502,8 +533,9 @@ test("ChangesView renders a framed two-pane layout at the requested size", async
502
533
  for (const line of lines) assert.equal(visibleWidth(line), 80, `"${stripAnsi(line)}" is not 80 wide`);
503
534
  const plain = lines.map(stripAnsi);
504
535
  assert.match(plain[0], /^╭─ ✎ Changes · 2 files · \+12 −1 ─+╮$/);
505
- assert.match(plain[1], /^│ ▸ M lib\/a\.ts +\+2 -1 +│ @@ -1,2 \+1,3 @@ +│$/);
506
- assert.match(plain[2], /^│ A lib\/b\.ts +\+10 -0 +│ const a = 1; +│$/);
536
+ assert.match(plain[1], /^│ ▸ M a\.ts \+2 -1 lib +│ M lib\/a\.ts +│$/);
537
+ assert.match(plain[2], /^│ A b\.ts \+10 -0 lib +│ @@ -1,2 \+1,3 @@ +│$/);
538
+ assert.match(plain[3], /^│ +│ const a = 1; +│$/);
507
539
  assert.match(plain[11], /^╰─+╯$/);
508
540
  assert.match(plain[10], /j\/k file .* o open in editor .* esc close/);
509
541
  });
@@ -532,8 +564,9 @@ test("ChangesView renders tabbed CRLF modified diffs with aligned frame columns"
532
564
  assert.ok(!line.includes("\t"), "raw tab must not leak into rendered line");
533
565
  }
534
566
  const plain = lines.map(stripAnsi);
535
- assert.match(plain[1], /^│ ▸ M lib\/a\.ts +\+2 -1 +│ @@ -1,2 \+1,3 @@ +│$/);
536
- assert.match(plain[2], /^│ A lib\/b\.ts +\+10 -0 +│ +const a = 1; +│$/);
567
+ assert.match(plain[1], /^│ ▸ M a\.ts \+2 -1 lib +│ M lib\/a\.ts +│$/);
568
+ assert.match(plain[2], /^│ A b\.ts \+10 -0 lib +│ @@ -1,2 \+1,3 @@ +│$/);
569
+ assert.match(plain[3], /^│ +│ +const a = 1; +│$/);
537
570
  });
538
571
 
539
572
  test("ChangesView loads the selected diff lazily and moves with j/k and arrows", async () => {
@@ -543,11 +576,11 @@ test("ChangesView loads the selected diff lazily and moves with j/k and arrows",
543
576
  component.handleInput("j");
544
577
  await settle();
545
578
  assert.deepEqual(calls, ["lib/a.ts", "lib/b.ts"]);
546
- assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A lib\/b\.ts/);
579
+ assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
547
580
  component.handleInput("\x1b[A");
548
- assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M lib\/a\.ts/);
581
+ assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M a\.ts/);
549
582
  component.handleInput("k");
550
- assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M lib\/a\.ts/);
583
+ assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M a\.ts/);
551
584
  assert.ok(events.filter((event) => event === "render").length >= 2);
552
585
  });
553
586
 
@@ -557,16 +590,33 @@ test("ChangesView scrolls the diff pane and shows an empty state for files witho
557
590
  await settle();
558
591
  component.handleInput("\x1b[6~");
559
592
  const plain = component.render(80).map(stripAnsi);
560
- assert.doesNotMatch(plain[1], /@@/);
561
- assert.match(plain[1], /\+line \d+/);
593
+ assert.doesNotMatch(plain[2], /@@/);
594
+ assert.match(plain[2], /\+line \d+/);
562
595
  component.handleInput("\x0b");
563
- assert.match(stripAnsi(component.render(80)[1]), /@@/, "ctrl+k scrolls back to the top");
596
+ assert.match(stripAnsi(component.render(80)[2]), /@@/, "ctrl+k scrolls back to the top");
564
597
  component.handleInput("\x0a");
565
- assert.doesNotMatch(stripAnsi(component.render(80)[1]), /@@/, "ctrl+j scrolls a page down");
598
+ assert.doesNotMatch(stripAnsi(component.render(80)[2]), /@@/, "ctrl+j scrolls a page down");
566
599
 
567
600
  const empty = view({ async loadDiff() { return ""; } });
568
601
  await settle();
569
- assert.match(stripAnsi(empty.view.render(80)[1]), /no diff for this file/);
602
+ assert.match(stripAnsi(empty.view.render(80)[2]), /no diff for this file/);
603
+ });
604
+
605
+ test("ChangesView shows an oversized diff as one plain open-in-editor line and stays responsive", async () => {
606
+ const tagged = view({ theme: taggedTheme, async loadDiff() { return DIFF_TOO_LARGE; } }).view;
607
+ await settle();
608
+ const rendered = tagged.render(100).join("\n");
609
+ assert.match(rendered, /<dim>Diff too large/, "one plain line instead of a colored diff");
610
+ assert.doesNotMatch(rendered, /<toolDiff\w+>Diff too large/);
611
+ const { view: component, events } = view({ async loadDiff(target) { return target.path === "lib/a.ts" ? DIFF_TOO_LARGE : DIFF_A; } });
612
+ await settle();
613
+ assert.match(stripAnsi(component.render(140).join("\n")), /Diff too large — press o to open in editor\./);
614
+ component.handleInput("j");
615
+ await settle();
616
+ assert.match(stripAnsi(component.render(140).join("\n")), /\+const c = 4;/);
617
+ component.handleInput("k");
618
+ component.handleInput("o");
619
+ assert.ok(events.includes("open:lib/a.ts"));
570
620
  });
571
621
 
572
622
  test("ChangesView opens the selected file and closes on escape or q", async () => {
@@ -589,7 +639,7 @@ test("ChangesView paints the shared hover role over an unselected file row, neve
589
639
  // Row y=1 is the selected file (lib/a.ts, index 0); row y=2 is lib/b.ts.
590
640
  const hoverOther = component.handleMouse(mouseButton("move", "none", 3, 2, 12));
591
641
  assert.deepEqual(hoverOther, { handled: true, render: true });
592
- assert.match(component.render(80)[2]!, /<warning>[^<]*lib\/b\.ts/, "the hovered, unselected row paints the shared hover role");
642
+ assert.match(component.render(80)[2]!, /<warning>A b\.ts/, "the hovered, unselected row paints the shared hover role");
593
643
  assert.doesNotMatch(component.render(80)[1]!, /<warning>/, "the selected row keeps its own role instead");
594
644
 
595
645
  const hoverSelected = component.handleMouse(mouseButton("move", "none", 3, 1, 12));
@@ -610,15 +660,16 @@ test("ChangesView click selects a file without opening it", async () => {
610
660
  component.handleMouse(mouse("release", 3, 2));
611
661
  component.handleMouse(mouse("click", 3, 2));
612
662
  await settle();
613
- assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A lib\/b\.ts/);
663
+ assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
614
664
  assert.deepEqual(events.filter((event) => event.startsWith("open:")), []);
615
665
  });
616
666
 
617
667
  test("ChangesView receives native fullscreen press and release as a click", async () => {
618
668
  let onInput: ((data: string) => void) | undefined;
619
- const terminal: Terminal = {
620
- start(input) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 12; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {},
669
+ const terminalFixture = {
670
+ start(input: Parameters<Terminal["start"]>[0]) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 12; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {}, setProgramStatus() {},
621
671
  };
672
+ const terminal: Terminal = terminalFixture;
622
673
  const { view: component, events } = view();
623
674
  const tui = new TuiAltScreen(terminal, false, undefined, { mouse: true });
624
675
  tui.setLayoutRoot(component);
@@ -627,7 +678,7 @@ test("ChangesView receives native fullscreen press and release as a click", asyn
627
678
  try {
628
679
  onInput?.("\x1b[<0;4;3M");
629
680
  onInput?.("\x1b[<0;4;3m");
630
- assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A lib\/b\.ts/);
681
+ assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
631
682
  assert.deepEqual(events.filter((event) => event.startsWith("open:")), []);
632
683
  } finally {
633
684
  tui.stop();
@@ -643,7 +694,7 @@ test("ChangesView rebuilds pointer geometry when its overlay height changes", as
643
694
  assert.deepEqual(component.handleMouse(mouse("click", 3, 2, undefined, 8)), { handled: true, render: false });
644
695
  component.render(80);
645
696
  assert.deepEqual(component.handleMouse(mouse("click", 3, 2, undefined, 8)), { handled: true, render: true });
646
- assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A lib\/b\.ts/);
697
+ assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
647
698
  });
648
699
 
649
700
  test("ChangesView scrolls the file list and diff independently, ignores hover, and clears stale pointer layouts", async () => {
@@ -653,15 +704,15 @@ test("ChangesView scrolls the file list and diff independently, ignores hover, a
653
704
  await settle();
654
705
  component.render(80);
655
706
  component.handleMouse(mouse("move", 3, 2, undefined, 8));
656
- assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M lib\/0\.ts/);
707
+ assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M 0\.ts/);
657
708
  assert.deepEqual(events.filter((event) => event.startsWith("open:")), []);
658
709
 
659
710
  assert.deepEqual(component.handleMouse(mouse("wheel", 3, 1, 2, 8)), { handled: true, render: true });
660
- assert.match(stripAnsi(component.render(80)[1]), /^│ M lib\/2\.ts/);
711
+ assert.match(stripAnsi(component.render(80)[1]), /^│ M 2\.ts/);
661
712
  component.update(changesModel(files));
662
- assert.match(stripAnsi(component.render(80)[1]), /^│ M lib\/2\.ts/, "an unchanged live refresh preserves the independently scrolled file viewport");
713
+ assert.match(stripAnsi(component.render(80)[1]), /^│ M 2\.ts/, "an unchanged live refresh preserves the independently scrolled file viewport");
663
714
  assert.deepEqual(component.handleMouse(mouse("wheel", 50, 1, 2, 8)), { handled: true, render: true });
664
- assert.doesNotMatch(stripAnsi(component.render(80)[1]), /@@/);
715
+ assert.doesNotMatch(stripAnsi(component.render(80)[2]), /@@/);
665
716
  assert.deepEqual(component.handleMouse(mouse("wheel", 50, 1, -100, 8)), { handled: true, render: true });
666
717
  assert.deepEqual(component.handleMouse(mouse("wheel", 50, 1, -1, 8)), { handled: true, render: false });
667
718
 
@@ -679,9 +730,9 @@ test("ChangesView keeps keyboard selection visible after file-list scrolling", a
679
730
  component.render(80);
680
731
  component.handleMouse(mouse("wheel", 3, 1, 2, 8));
681
732
  component.handleInput("j");
682
- assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M lib\/1\.ts/);
733
+ assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M 1\.ts/);
683
734
  component.update(changesModel(files.slice(0, 3)));
684
- assert.match(stripAnsi(component.render(80)[1]), /^│ M lib\/0\.ts/);
735
+ assert.match(stripAnsi(component.render(80)[1]), /^│ M 0\.ts/);
685
736
  });
686
737
 
687
738
  test("ChangesView.update keeps the selected file, reloads moved diffs, and survives an empty tree", async () => {
@@ -693,7 +744,7 @@ test("ChangesView.update keeps the selected file, reloads moved diffs, and survi
693
744
 
694
745
  component.update(changesModel([file("lib/a.ts", 5, 1), file("lib/b.ts", 10, 0, CHANGE_STATUS.ADDED), file("lib/c.ts", 1, 0)]));
695
746
  await settle();
696
- assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A lib\/b\.ts/);
747
+ assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
697
748
  assert.deepEqual(calls, ["lib/a.ts", "lib/b.ts"], "unchanged selected file must not reload");
698
749
  component.handleInput("k");
699
750
  await settle();
@@ -9,8 +9,8 @@ import { visibleWidth } from "@earendil-works/pi-tui";
9
9
  import {
10
10
  CHANGE_STATUS,
11
11
  ChangesTracker,
12
+ FILE_WORK_CONCURRENCY,
12
13
  WorktreeChangesTracker,
13
- RootBranchLabels,
14
14
  UNKNOWN_BRANCH,
15
15
  foreignRootBranch,
16
16
  parseWorktrees,
@@ -112,14 +112,6 @@ test("changesSummary and the widget describe the session at a glance", () => {
112
112
  assert.equal(visibleWidth(line), 120, "the command sits on the right edge");
113
113
  });
114
114
 
115
- test("changesSummary and the widget surface the capture limit", () => {
116
- const model = changesModel([file("a.ts", 1, 0)]);
117
- model.notice = "Session change capture limit reached; additional changes are not displayed.";
118
- assert.equal(changesSummary(model), "1 file · +1 −0 · capture limit reached");
119
- assert.match(renderChangesWidget(model, plainTheme, 200)[0], /capture limit reached/);
120
- assert.doesNotMatch(changesSummary(changesModel([file("a.ts", 1, 0)])), /capture limit reached/);
121
- });
122
-
123
115
  test("renderChangesWidget colors counts by direction and yields nothing when clean", () => {
124
116
  const model = changesModel([file("a.ts", 1, 2)]);
125
117
  const [line] = renderChangesWidget(model, taggedTheme, 400);
@@ -167,6 +159,24 @@ test("ChangesTracker reads the working tree on start and again on refresh", asyn
167
159
  assert.deepEqual(calls[1], ["status", "--porcelain=v1", "--untracked-files=all", "-z"]);
168
160
  });
169
161
 
162
+ test("ChangesTracker skips untracked dependency backups but preserves tracked edits and legitimate new files", async () => {
163
+ const backup = "node_modules.backup-20261007-192802/pkg/index.js";
164
+ const nestedBackup = "packages/app/node_modules.backup-local/pkg/index.js";
165
+ const tracked = "node_modules.backup-local/tracked.ts";
166
+ const legitimate = ["src/new.ts", "node_modules.backup-notes.md", "node_modules.backup-/new.ts"];
167
+ const { git } = fakeGit(
168
+ [`4\t2\t${tracked}\n`],
169
+ [[`?? ${backup}`, `?? ${nestedBackup}`, ` M ${tracked}`, ...legitimate.map((path) => `?? ${path}`)].join("\0") + "\0"],
170
+ );
171
+ const counted: string[] = [];
172
+ const tracker = new ChangesTracker(git, async (path) => { counted.push(path); return 3; });
173
+ await tracker.start();
174
+ assert.deepEqual(counted.sort(), [...legitimate].sort());
175
+ assert.deepEqual(tracker.model, changesModel([
176
+ file(tracked, 4, 2), ...legitimate.map((path) => file(path, 3, 0, CHANGE_STATUS.UNTRACKED)),
177
+ ]));
178
+ });
179
+
170
180
  test("ChangesTracker stays quiet outside a git repository", async () => {
171
181
  const { git, calls } = fakeGit([""], [""], 128);
172
182
  const tracker = new ChangesTracker(git);
@@ -210,6 +220,7 @@ test("registered roots hide clean and missing worktrees while metadata never adm
210
220
  assert.equal(tracker.model.files.length, 2, "identical filenames in different roots remain distinct");
211
221
  assert.equal(new Set(tracker.model.files.map((file) => file.path)).size, 2);
212
222
  assert.ok(!calls.includes("/pruned") && !calls.includes("/bare"));
223
+ assert.deepEqual(tracker.failedRoots, ["/gone"], "a registered root Git cannot read is a failure, not a clean root");
213
224
  roots += "worktree /new\0branch refs/heads/new\0\0";
214
225
  registered.push("/new");
215
226
  await tracker.refresh();
@@ -221,6 +232,29 @@ test("registered roots hide clean and missing worktrees while metadata never adm
221
232
  assert.deepEqual(tracker.model, emptyChanges());
222
233
  });
223
234
 
235
+ test("a root whose Git status fails or rejects is kept apart from clean roots", async () => {
236
+ let broken = true;
237
+ const tracker = new WorktreeChangesTracker(async () => ({ stdout: "", code: 0 }), (root) => async (args) => {
238
+ if (broken && root === "/rejects") throw new Error("git timed out");
239
+ if (broken && root === "/fails" && args[0] === "status") return { stdout: "", code: 128 };
240
+ return { stdout: args[0] === "status" && root === "/dirty" ? "?? a.ts\0" : "", code: 0 };
241
+ }, undefined, () => ["/dirty", "/clean", "/fails", "/rejects"]);
242
+ await tracker.start();
243
+ assert.deepEqual(tracker.worktrees.map((tree) => tree.root), ["/dirty"]);
244
+ assert.deepEqual([...tracker.failedRoots].sort(), ["/fails", "/rejects"], "a failed scan is not a clean root");
245
+ assert.equal(tracker.scannedRootCount, 4);
246
+ broken = false;
247
+ await tracker.refresh();
248
+ assert.deepEqual(tracker.failedRoots, [], "a later successful scan clears the failure");
249
+ });
250
+
251
+ test("no registered root is neither a failure nor a scan", async () => {
252
+ const tracker = new WorktreeChangesTracker(async () => { throw new Error("no Git expected"); }, () => async () => { throw new Error("no Git expected"); });
253
+ await tracker.start();
254
+ assert.deepEqual(tracker.failedRoots, []);
255
+ assert.equal(tracker.scannedRootCount, 0);
256
+ });
257
+
224
258
  test("only registered worktrees are scanned, with whole preexisting and untracked contents", async () => {
225
259
  const queried: string[] = [];
226
260
  const roots = ["/used"];
@@ -244,7 +278,7 @@ test("worktree refresh shares one scan so polling faster than discovery cannot p
244
278
  scans++;
245
279
  if (scans === 2) await new Promise<void>((resolve) => { release = resolve; });
246
280
  return { stdout: "", code: 0 };
247
- }, () => async () => ({ stdout: "", code: 0 }));
281
+ }, () => async () => ({ stdout: "", code: 0 }), undefined, () => ["/root"]); // A root to scan; none means no Git at all.
248
282
  await tracker.start();
249
283
  const first = tracker.refresh();
250
284
  const overlapping = tracker.refresh();
@@ -479,57 +513,40 @@ test("ChangesTracker counts the lines of untracked files, which git numstat leav
479
513
  assert.deepEqual(counted, ["notes.md", "empty.md"]);
480
514
  });
481
515
 
482
- // The session-evidence tracker behind /gentle:changes knows roots, never
483
- // branches. The overlay decorates its trees with each root's own HEAD state,
484
- // resolved once per root and reported back so the view can repaint; until git
485
- // answers the tree shows no branch (the view's "detached" fallback), and a
486
- // genuinely detached HEAD stays that way.
487
- test("RootBranchLabels decorates session trees with each root's branch once git answers", async () => {
488
- const calls = new Map<string, string[][]>();
489
- const answers: Record<string, { symbolic: { stdout: string; code: number }; verify: { stdout: string; code: number } }> = {
490
- "/repo": { symbolic: { stdout: "feature\n", code: 0 }, verify: { stdout: "deadbeef\n", code: 0 } },
491
- "/fresh": { symbolic: { stdout: "main\n", code: 0 }, verify: { stdout: "", code: 1 } },
492
- "/pinned": { symbolic: { stdout: "", code: 1 }, verify: { stdout: "deadbeef\n", code: 0 } },
516
+ test("ChangesTracker counts lines and reads revisions for every file with bounded concurrency", async () => {
517
+ const paths = Array.from({ length: 30 }, (_, index) => `file-${String(index).padStart(2, "0")}.ts`);
518
+ const porcelain = [" M tracked.ts\0", ...paths.map((path) => `?? ${path}\0`)].join("");
519
+ const git = async (args: string[]) => ({ stdout: args[0] === "status" ? porcelain : "4\t1\ttracked.ts\n", code: 0 });
520
+ const pending = new Map<string, number>();
521
+ const counted: string[] = [];
522
+ let maxFiles = 0;
523
+ let overlapped = false;
524
+ // Every per-file call stays pending for a moment, so concurrent work overlaps.
525
+ const work = async <T>(path: string, result: () => T): Promise<T> => {
526
+ pending.set(path, (pending.get(path) ?? 0) + 1);
527
+ if (pending.get(path)! > 1) overlapped = true;
528
+ maxFiles = Math.max(maxFiles, pending.size);
529
+ await new Promise((resolve) => setTimeout(resolve, 2));
530
+ const left = pending.get(path)! - 1;
531
+ if (left === 0) pending.delete(path); else pending.set(path, left);
532
+ return result();
493
533
  };
494
- let changed = 0;
495
- const labels = new RootBranchLabels((root) => async (args) => {
496
- calls.set(root, [...(calls.get(root) ?? []), args]);
497
- await new Promise((resolve) => setImmediate(resolve));
498
- return args[0] === "symbolic-ref" ? answers[root]!.symbolic : answers[root]!.verify;
499
- }, () => changed++);
500
- const trees = Object.keys(answers).map((root) => ({ root, model: changesModel([]) }));
501
- const first = labels.decorate(trees);
502
- assert.deepEqual(first.map((tree) => tree.branch), [undefined, undefined, undefined], "nothing is labelled before git answers");
503
- await labels.settled();
504
- assert.equal(changed, 3, "each resolved root asks the view to repaint once");
505
- const second = labels.decorate(trees);
506
- assert.deepEqual(second.map((tree) => tree.branch), ["feature", "no commits yet", undefined]);
507
- labels.decorate(trees);
508
- await labels.settled();
509
- assert.deepEqual([...calls.values()].map((list) => list.length), [2, 2, 1], "one resolution per root, never per decorate call");
510
- assert.equal(changed, 3);
511
- });
512
-
513
- // A root that could not even be asked (removed, git unavailable, the runner
514
- // itself rejecting) must read UNKNOWN_BRANCH -- never the same undefined a
515
- // genuine detached HEAD produces, and never silently stay unlabelled forever.
516
- test("RootBranchLabels labels a root UNKNOWN_BRANCH when it could not be asked, distinct from a real detached HEAD", async () => {
517
- let changed = 0;
518
- const labels = new RootBranchLabels((root) => async () => {
519
- await new Promise((resolve) => setImmediate(resolve));
520
- if (root === "/gone") throw new Error("root removed");
521
- return { stdout: "", code: 1 };
522
- }, () => changed++);
523
- const trees = [
524
- { root: "/gone", model: changesModel([]) },
525
- { root: "/detached", model: changesModel([]) },
526
- ];
527
- labels.decorate(trees);
528
- await labels.settled();
529
- assert.deepEqual(
530
- labels.decorate(trees).map((tree) => tree.branch),
531
- [UNKNOWN_BRANCH, undefined],
532
- "a failed ask reads UNKNOWN_BRANCH; a real detached HEAD still reads undefined (the view's detached fallback)",
533
- );
534
- assert.equal(changed, 2, "a failed ask still resolves and asks the view to repaint, instead of hanging forever");
534
+ const tracker = new ChangesTracker(git, (path) => work(path, () => {
535
+ counted.push(path);
536
+ if (path === "file-07.ts") throw new Error("unreadable");
537
+ return 2;
538
+ }), (path) => work(path, () => {
539
+ if (path === "file-07.ts") throw new Error("unreadable");
540
+ return `rev:${path}`;
541
+ }));
542
+ await tracker.start();
543
+ const files = new Map(tracker.model.files.map((entry) => [entry.path, entry]));
544
+ assert.equal(maxFiles, FILE_WORK_CONCURRENCY, "files run concurrently up to the limit, never past it");
545
+ assert.equal(FILE_WORK_CONCURRENCY, 8);
546
+ assert.ok(overlapped, "a file's line count and revision lookup run together");
547
+ assert.deepEqual([...counted].sort(), paths, "every untracked file is counted once; tracked files keep numstat counts");
548
+ assert.equal(files.size, 31);
549
+ assert.deepEqual(files.get("tracked.ts"), { ...file("tracked.ts", 4, 1), diffRevision: "rev:tracked.ts" });
550
+ assert.deepEqual(files.get("file-07.ts"), file("file-07.ts", 0, 0, CHANGE_STATUS.UNTRACKED), "a failing lookup keeps its fallback");
551
+ for (const path of paths.filter((path) => path !== "file-07.ts")) assert.deepEqual(files.get(path), { ...file(path, 2, 0, CHANGE_STATUS.UNTRACKED), diffRevision: `rev:${path}` });
535
552
  });
@@ -0,0 +1,69 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { initTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { Container, TuiAltScreen } from "@earendil-works/pi-tui";
5
+ import { createChatViewport } from "../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/chat-viewport.js";
6
+ import { buildShellBarModel, type ShellFooterData } from "../extensions/gentle-shell.ts";
7
+ import { createQuietToolRenderer } from "../extensions/quiet-tools.ts";
8
+
9
+ initTheme("dark");
10
+
11
+ test("native fullscreen scroll frames reuse complete tool cards and session statistics", () => {
12
+ let decoration = 0;
13
+ let scans = 0;
14
+ let projections = 0;
15
+ const theme = {
16
+ fg: (_color: string, text: string) => { decoration++; return text; },
17
+ bg: (_color: string, text: string) => { decoration++; return `\x1b[48;5;235m${text}\x1b[49m`; },
18
+ bold: (text: string) => text,
19
+ };
20
+ const ctx = {
21
+ model: { id: "model", contextWindow: 1000 },
22
+ getContextUsage() { projections++; return { percent: 10, contextWindow: 1000 }; },
23
+ modelRegistry: { isUsingOAuth: () => false },
24
+ sessionManager: {
25
+ getEntries() { scans++; return []; }, getSessionId: () => "session", getLeafId: () => "leaf",
26
+ getCwd: () => "/repo", getSessionName: () => "session",
27
+ },
28
+ } as unknown as ExtensionContext;
29
+ const footerData = { getGitBranch: () => "main", getExtensionStatuses: () => new Map() } as unknown as ShellFooterData;
30
+ const pi = { getThinkingLevel: () => "high" } as unknown as ExtensionAPI;
31
+ const document = new Container();
32
+ const renderer = createQuietToolRenderer("read");
33
+ for (let tool = 0; tool < 50; tool++) {
34
+ const context = { args: { path: `src/file-${tool}.ts` }, state: {}, isPartial: false, executionStarted: true };
35
+ document.addChild(renderer.renderCall!(context.args, theme as never, context as never));
36
+ document.addChild(renderer.renderResult!({ content: [{ type: "text", text: `result-${tool}` }], details: {} },
37
+ { expanded: false, isPartial: false }, theme as never, context as never));
38
+ }
39
+ const footer = new Container();
40
+ footer.addChild({ render: () => [`cost ${buildShellBarModel(pi, ctx, footerData).costTotal}`], invalidate() {} });
41
+ const viewport = createChatViewport({
42
+ document, pendingMessages: new Container(), status: new Container(), widgetsAbove: new Container(),
43
+ editor: { render: () => ["prompt"], invalidate() {} }, widgetsBelow: new Container(), footer, scrollbar: "hidden",
44
+ });
45
+ const terminal = {
46
+ start() {}, stop() {}, async drainInput() {}, write() {}, columns: 100, rows: 24,
47
+ moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {},
48
+ setTitle() {}, setProgress() {}, kittyProtocolActive: false, getBufferedInput() { return ""; }, hasPendingInput() { return false; },
49
+ } as never;
50
+ const tui = new TuiAltScreen(terminal, false);
51
+ tui.setLayoutRoot(viewport.root);
52
+ const host = tui as unknown as { doRender(): void; previousScreen: string[] };
53
+ tui.start();
54
+ try {
55
+ host.doRender();
56
+ const warmDecoration = decoration;
57
+ assert.deepEqual([scans, projections], [1, 1]);
58
+ const screens: string[] = [];
59
+ for (let frame = 0; frame < 20; frame++) {
60
+ if (frame % 2) viewport.transcript.scrollToEnd();
61
+ else viewport.transcript.scrollTo(0, { disableFollow: true });
62
+ host.doRender();
63
+ screens.push(host.previousScreen.join("\n"));
64
+ }
65
+ assert.notEqual(screens[0], screens[1], "scroll really changes the visible transcript");
66
+ assert.equal(decoration, warmDecoration, "even the native frame path reuses settled chrome");
67
+ assert.deepEqual([scans, projections], [1, 1], "scroll/pulse-like frames do not rescan unchanged session state");
68
+ } finally { tui.stop(); }
69
+ });