@pellux/goodvibes-tui 2.0.21 → 2.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 (388) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +16 -6
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -1,352 +1,65 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types';
2
- import { UIFactory } from '../renderer/ui-factory.ts';
3
- import type { PermissionCategory, PermissionRequestAnalysis } from '@pellux/goodvibes-sdk/platform/permissions';
1
+ import type { PermissionCategory } from '@pellux/goodvibes-sdk/platform/permissions';
4
2
  import { buildPermissionApprovalBrief, getDisplayArg } from '@pellux/goodvibes-sdk/platform/permissions';
5
- import { DIFF_TONES, UI_TONES } from '../renderer/ui-primitives.ts';
6
- import { renderDiffView } from '../renderer/diff-view.ts';
7
- import { wrapText } from '../utils/terminal-width.ts';
3
+ import { activeTokens } from '../renderer/theme.ts';
4
+ import type { SurfaceLayer } from '../renderer/surface-kit.ts';
8
5
  import type { HunkSelectionState } from './hunk-selection.ts';
9
- import { readSandboxAskAnnotation } from './sandbox-exec-gate.ts';
6
+ import { rememberScopeKey, renderPermissionCard, type PermissionCardState } from './prompt-card.ts';
10
7
  export { buildPendingPermissionExtras } from './hunk-selection.ts';
8
+ export { permissionButtons, type PermissionButton, type PermissionButtonId, type PermissionCardState } from './prompt-card.ts';
11
9
 
12
10
  import type { PermissionPromptRequest, PermissionPromptDecision, PermissionRequestHandler, PermissionRequest } from '@pellux/goodvibes-sdk/platform/permissions';
13
11
  export type { PermissionPromptRequest, PermissionPromptDecision, PermissionRequestHandler, PermissionRequest };
14
12
 
15
- /** Visible hunk rows before a "+N more" trailer kicks in (Risk 3). */
16
- const MAX_VISIBLE_HUNKS = 8;
17
-
18
- /** Path rows shown before collapsing to a "N files: a, b, +K more" line (2a). */
19
- const MAX_PATH_ROWS = 3;
20
-
21
- /** Diff rows shown for a write/edit ask before the "+N more" trailer. */
22
- const MAX_DIFF_LINES = 10;
23
-
24
- /** Field-label column width shared by every " Label : value" row. */
25
- const FIELD_PREFIX_LEN = 16;
26
-
27
13
  /**
28
14
  * View-only state the card renders beyond the request itself: the typed-reply
29
- * mode/draft (deny-with-reason, exec-prompt answer), the honest count of
30
- * OTHER broker asks still waiting, and the terminal width getPromptHeight
31
- * needs for wrap-dependent rows (createPromptLines takes width positionally).
15
+ * mode/draft (deny-with-reason, exec-prompt answer) and the honest count of
16
+ * OTHER broker asks still waiting.
32
17
  */
33
18
  export interface PromptViewState {
34
19
  readonly replyMode?: 'deny-reason' | 'exec-answer' | undefined;
35
20
  readonly replyBuffer?: string | undefined;
36
21
  readonly queueCount?: number | undefined;
37
- readonly width?: number | undefined;
38
- }
39
-
40
- /** Trailing filename for compact display of a long/absolute path. */
41
- function baseName(p: string): string {
42
- const cleaned = p.replace(/[/\\]+$/, '');
43
- const idx = Math.max(cleaned.lastIndexOf('/'), cleaned.lastIndexOf('\\'));
44
- return idx >= 0 ? cleaned.slice(idx + 1) : cleaned;
45
- }
46
-
47
- /** Make an absolute path relative to the working directory when it sits under it. */
48
- function relativize(p: string, cwd?: string): string {
49
- if (!cwd || !p.startsWith('/')) return p;
50
- if (p === cwd) return '.';
51
- const prefix = cwd.endsWith('/') ? cwd : `${cwd}/`;
52
- return p.startsWith(prefix) ? p.slice(prefix.length) : p;
53
- }
54
-
55
- /** Collect string elements or `el[field]` strings from an array arg. */
56
- function collectField(arr: unknown, field: string): string[] {
57
- if (!Array.isArray(arr)) return [];
58
- const out: string[] = [];
59
- for (const el of arr) {
60
- if (typeof el === 'string') out.push(el);
61
- else if (el && typeof el === 'object' && typeof (el as Record<string, unknown>)[field] === 'string') {
62
- out.push((el as Record<string, unknown>)[field] as string);
63
- }
64
- }
65
- return out;
66
- }
67
-
68
- /**
69
- * The real display target(s) of a tool invocation, the paths/commands/urls a
70
- * user needs to see, extracted from the actual arg shapes so a nested
71
- * `{files:[{path}]}` no longer falls through to a raw JSON blob. (2a.)
72
- * Returns [] when nothing recognisable is present (caller falls back).
73
- */
74
- function cardTargets(args: Record<string, unknown>): string[] {
75
- const files = collectField(args.files, 'path');
76
- if (files.length) return files;
77
- if (typeof args.path === 'string') return [args.path];
78
- if (typeof args.file === 'string') return [args.file];
79
- const commands = collectField(args.commands, 'cmd');
80
- if (commands.length) return commands;
81
- if (typeof args.command === 'string') return [args.command];
82
- if (typeof args.cmd === 'string') return [args.cmd];
83
- if (typeof args.pattern === 'string') return [args.pattern];
84
- const urls = collectField(args.urls, 'url');
85
- if (urls.length) return urls;
86
- const queries = collectField(args.queries, 'query');
87
- if (queries.length) return queries;
88
- if (typeof args.query === 'string') return [args.query];
89
- if (typeof args.task === 'string') return [args.task];
90
- return [];
91
- }
92
-
93
- /** Short human verb for a permission category, for the condensed summary line. */
94
- function categoryVerb(category: PermissionCategory): string {
95
- switch (category) {
96
- case 'write': return 'write';
97
- case 'execute': return 'run';
98
- case 'delegate': return 'delegate';
99
- default: return 'read';
100
- }
101
22
  }
102
23
 
103
- /**
104
- * Single source of truth for how many Line rows the hunk-list section
105
- * occupies: 1 header row + up to MAX_VISIBLE_HUNKS checkbox rows + 1 trailer
106
- * row (rendered as "+N more" when truncated, blank otherwise, always
107
- * present so the row count never depends on which branch fires). Both
108
- * getPromptHeight and createPromptLines call this SAME function, so they
109
- * cannot drift apart (Risk 2, main.ts's render loop reserves viewport
110
- * space from getPromptHeight *before* the real render happens).
111
- */
112
- /**
113
- * The optional sandbox model-judgment tier (see sandbox-judgment.ts in the
114
- * SDK) annotates an escalation ask by pushing one string, verbatim
115
- * "model judgment: looks safe because…" / "model judgment: flags risk
116
- * because…", onto `analysis.reasons`, alongside the sandbox-boundary and
117
- * policy reasons. Pulled out here so it gets its own clearly-labeled row
118
- * instead of blending into the generic Review bullets, where the truncation
119
- * to 2 reasons could silently cut it (it is typically the LAST reason
120
- * pushed, after the boundary line and any policy reasons).
121
- */
122
- const MODEL_JUDGMENT_PREFIX = 'model judgment:';
123
-
124
- function extractModelJudgmentAnnotation(reasons: readonly string[]): { annotation: string | undefined; rest: string[] } {
125
- const idx = reasons.findIndex((r) => r.toLowerCase().startsWith(MODEL_JUDGMENT_PREFIX));
126
- if (idx === -1) return { annotation: undefined, rest: [...reasons] };
127
- return { annotation: reasons[idx], rest: [...reasons.slice(0, idx), ...reasons.slice(idx + 1)] };
128
- }
129
-
130
- function hunkListRowCount(hunkState: HunkSelectionState): number {
131
- return Math.min(hunkState.hunks.length, MAX_VISIBLE_HUNKS) + 2;
24
+ /** The pending-permission fields the card reads (see shell/blocking-input.ts PendingPermissionState). */
25
+ export interface PendingPromptView {
26
+ readonly callId: string;
27
+ readonly hunkState?: HunkSelectionState | undefined;
28
+ readonly detailsExpanded?: boolean | undefined;
29
+ readonly requestedBy?: string | undefined;
30
+ readonly replyMode?: 'deny-reason' | 'exec-answer' | undefined;
31
+ readonly replyBuffer?: string | undefined;
32
+ readonly choice?: number | undefined;
33
+ readonly submenuOpen?: boolean | undefined;
34
+ readonly submenuIndex?: number | undefined;
35
+ readonly scroll?: number | undefined;
132
36
  }
133
37
 
134
38
  /**
135
- * PermissionPromptUI - Renders a permission prompt as Line[] fragments.
136
- *
137
- * Displayed as an overlay injected into the viewport during render.
138
- * The prompt blocks orchestrator execution until the user responds.
39
+ * PermissionPromptUI, the permission dialog (a kit modal with an amber cap,
40
+ * see prompt-card.ts for the layout).
139
41
  *
140
- * Keys:
141
- * y / Y -> Allow once
142
- * a / A -> Allow always (this session)
143
- * n / N -> Deny
144
- * Escape -> Deny
42
+ * Keys (shell/blocking-input.ts): ←→ choose a button, Enter confirms it;
43
+ * y allow once, a allow for this session (1-9 remember at a tier when the
44
+ * request offers tiers), n deny, d details; typing denies with a reason.
145
45
  */
146
46
  export class PermissionPromptUI {
147
- private static fallbackAnalysis(request: PermissionPromptRequest): PermissionRequestAnalysis {
148
- return request.analysis ?? {
149
- classification: request.category,
150
- riskLevel: request.category === 'read' ? 'low' : request.category === 'write' ? 'medium' : 'high',
151
- summary: `Review ${request.tool} request`,
152
- reasons: ['Inspect the target and intent before approving this action.'],
153
- target: getDisplayArg(request.tool, request.args),
154
- targetKind: 'generic',
155
- };
156
- }
157
-
158
- /** The relativised display target(s) for the Path field, with a raw fallback. */
159
- private static resolvedTargets(request: PermissionPromptRequest): string[] {
160
- const cwd = request.workingDirectory;
161
- const targets = cardTargets(request.args).map((t) => relativize(t, cwd));
162
- return targets.length > 0 ? targets : [getDisplayArg(request.tool, request.args)];
163
- }
164
-
165
- /** Path-field row texts, one per target when few, a "N files:" summary when many. */
166
- private static pathRowTexts(targets: string[], maxLen: number): string[] {
167
- const clamp = (s: string): string => (s.length > maxLen ? `...${s.slice(-(maxLen - 3))}` : s);
168
- if (targets.length <= 1) return [clamp(targets[0] ?? '(unknown)')];
169
- if (targets.length <= MAX_PATH_ROWS) return targets.map(clamp);
170
- const shown = targets.slice(0, 2).map(baseName).join(', ');
171
- return [clamp(`${targets.length} files: ${shown}, +${targets.length - 2} more`)];
172
- }
173
-
174
47
  /**
175
- * A low-risk, project-local request is shown condensed (one summary line +
176
- * choices) unless the user expanded it with `d`. High/critical risk, external
177
- * scope, and hunk-selectable edits always show the full block. (2b.)
178
- */
179
- private static isCondensed(
180
- request: PermissionPromptRequest,
181
- hunkState: HunkSelectionState | undefined,
182
- detailsExpanded: boolean,
183
- ): boolean {
184
- if (hunkState || detailsExpanded) return false;
185
- // Shell execution, delegation and network requests always show the full
186
- // card, their action semantics (command, side effects, host, checklist)
187
- // warrant scrutiny even when the risk model rates them low. Only mundane
188
- // low-risk filesystem reads/writes condense. (2b.)
189
- if (request.category === 'execute' || request.category === 'delegate') return false;
190
- // Remember tiers are one-key choices that must be visible to be usable,
191
- // a request carrying rememberOptions always shows the full card.
192
- if (request.rememberOptions && request.rememberOptions.length > 0) return false;
193
- const analysis = this.fallbackAnalysis(request);
194
- if (analysis.riskLevel !== 'low') return false;
195
- const scope = analysis.blastRadius;
196
- return scope === undefined || scope === 'local' || scope === 'project';
197
- }
198
-
199
- /**
200
- * The exact session-scoped permission rule that pressing [A] ("Allow always
201
- * this session") will remember. Mirrors the SDK PermissionManager's
202
- * getApprovalKey (`<tool>:<path>` when a path arg exists, `<tool>:<command>`
203
- * for a command arg, else the bare `<tool>`), so the preview shown to the
204
- * user before they choose is the exact key that gets written. Kept in sync
205
- * with that SDK function by construction.
48
+ * The exact session-scoped permission rule "Allow for session" remembers.
49
+ * Mirrors the SDK PermissionManager's approval key (`<tool>:<path>`,
50
+ * `<tool>:<command>`, else the bare `<tool>`).
206
51
  */
207
52
  static rememberScopeKey(request: PermissionPromptRequest): string {
208
- const args = request.args ?? {};
209
- if (typeof args.path === 'string' && args.path.length > 0) return `${request.tool}:${args.path}`;
210
- if (typeof args.command === 'string' && args.command.length > 0) return `${request.tool}:${args.command}`;
211
- return request.tool;
212
- }
213
-
214
- /** The one-line preview text for what [A] remembers, truncated to the width. */
215
- private static rememberPreviewText(request: PermissionPromptRequest, width: number): string {
216
- const key = this.rememberScopeKey(request);
217
- const prefix = ' Remembers : [A] allows "';
218
- const suffix = '" for this session';
219
- const budget = Math.max(6, width - prefix.length - suffix.length);
220
- const shown = key.length > budget ? `...${key.slice(-(budget - 3))}` : key;
221
- return `${prefix}${shown}${suffix}`;
222
- }
223
-
224
- /**
225
- * The remember block's row texts: one row per SDK remember tier (numbered,
226
- * most specific first) when the request carries rememberOptions, else the
227
- * legacy single [A] session-preview row. Hunk mode has no whole-request
228
- * remember choice at all. Shared by getPromptHeight and createPromptLines.
229
- */
230
- private static rememberRowTexts(
231
- request: PermissionPromptRequest,
232
- hunkState: HunkSelectionState | undefined,
233
- width: number,
234
- ): string[] {
235
- if (hunkState) return [];
236
- const options = request.rememberOptions ?? [];
237
- if (options.length === 0) return [this.rememberPreviewText(request, width)];
238
- const clamp = (s: string): string => (s.length > width ? `${s.slice(0, Math.max(0, width - 3))}...` : s);
239
- return options.map((option, index) => {
240
- const labelCol = index === 0 ? 'Remember ' : ' '.repeat(9);
241
- return clamp(` ${labelCol}: [${index + 1}] ${option.label}; ${option.detail}`);
242
- });
243
- }
244
-
245
- /**
246
- * The FULL command of an execute ask, wrapped across as many rows as it
247
- * needs, never truncated. Empty for non-execute asks and for asks without
248
- * a string command. Shared by getPromptHeight and createPromptLines.
249
- */
250
- private static commandRowTexts(request: PermissionPromptRequest, width: number): string[] {
251
- if (request.category !== 'execute') return [];
252
- // Exec-prompt asks render their command via execPromptRowTexts instead.
253
- if (request.attribution?.kind === 'exec-prompt') return [];
254
- const command = typeof request.args?.command === 'string'
255
- ? request.args.command
256
- : typeof request.args?.cmd === 'string'
257
- ? request.args.cmd
258
- : '';
259
- if (command.length === 0) return [];
260
- const budget = Math.max(10, width - FIELD_PREFIX_LEN);
261
- const wrapped = command.split('\n').flatMap((line) => (line.length > 0 ? wrapText(line, budget) : ['']));
262
- return wrapped.map((row, index) => (index === 0 ? ` Command : ${row}` : `${' '.repeat(FIELD_PREFIX_LEN - 1)}${row}`));
263
- }
264
-
265
- /**
266
- * Unified-diff text lines for a write/edit ask (real colored rendering via
267
- * renderDiffView), capped at MAX_DIFF_LINES with a context trailer naming
268
- * how much is hidden. Null when the ask carries nothing diffable. Hunk-mode
269
- * asks render the interactive hunk list instead. Shared by getPromptHeight
270
- * and createPromptLines.
271
- */
272
- private static writeDiffLines(request: PermissionPromptRequest, hunkState: HunkSelectionState | undefined): string[] | null {
273
- if (hunkState || request.category !== 'write') return null;
274
- const args = request.args ?? {};
275
- const target = typeof args.path === 'string' ? args.path : typeof args.file === 'string' ? args.file : '';
276
- const out: string[] = [];
277
- if (typeof args.content === 'string') {
278
- const contentLines = args.content.split('\n');
279
- out.push(`+++ ${target || '(new content)'}`);
280
- for (const line of contentLines.slice(0, MAX_DIFF_LINES)) out.push(`+${line}`);
281
- if (contentLines.length > MAX_DIFF_LINES) out.push(` … +${contentLines.length - MAX_DIFF_LINES} more lines`);
282
- return out;
283
- }
284
- if (Array.isArray(args.edits)) {
285
- let budget = MAX_DIFF_LINES;
286
- let hidden = 0;
287
- for (const edit of args.edits) {
288
- if (!edit || typeof edit !== 'object') continue;
289
- const e = edit as { path?: unknown; find?: unknown; replace?: unknown };
290
- if (typeof e.find !== 'string' || typeof e.replace !== 'string') continue;
291
- const findLines = e.find.split('\n');
292
- const replaceLines = e.replace.split('\n');
293
- const need = 1 + findLines.length + replaceLines.length;
294
- if (budget < need) {
295
- hidden += 1;
296
- continue;
297
- }
298
- out.push(`@@ ${typeof e.path === 'string' ? e.path : target} @@`);
299
- for (const line of findLines) out.push(`-${line}`);
300
- for (const line of replaceLines) out.push(`+${line}`);
301
- budget -= need;
302
- }
303
- if (out.length === 0) return null;
304
- if (hidden > 0) out.push(` … +${hidden} more edits`);
305
- return out;
306
- }
307
- return null;
308
- }
309
-
310
- /**
311
- * Rows for an exec-prompt ask (a running command waiting on stdin): the
312
- * command and the terminal prompt text, both wrapped in full. Shared by
313
- * getPromptHeight and createPromptLines.
314
- */
315
- private static execPromptRowTexts(request: PermissionPromptRequest, width: number): string[] {
316
- const attribution = request.attribution;
317
- if (attribution?.kind !== 'exec-prompt') return [];
318
- const budget = Math.max(10, width - FIELD_PREFIX_LEN);
319
- const rows: string[] = [];
320
- const pushWrapped = (label: string, text: string): void => {
321
- const wrapped = text.split('\n').flatMap((line) => (line.length > 0 ? wrapText(line, budget) : ['']));
322
- wrapped.forEach((row, index) => {
323
- rows.push(index === 0 ? ` ${label.padEnd(9)}: ${row}` : `${' '.repeat(FIELD_PREFIX_LEN - 1)}${row}`);
324
- });
325
- };
326
- if (typeof attribution.command === 'string' && attribution.command.length > 0) pushWrapped('Running', attribution.command);
327
- if (typeof attribution.prompt === 'string' && attribution.prompt.length > 0) pushWrapped('Asks', attribution.prompt);
328
- return rows;
329
- }
330
-
331
- /** The typed-reply input row text (deny reason / exec answer), or null when no reply mode is active. */
332
- private static replyRowText(view: PromptViewState | undefined, width: number): string | null {
333
- if (!view?.replyMode) return null;
334
- const label = view.replyMode === 'exec-answer' ? 'Answer ' : 'Reason ';
335
- const draft = view.replyBuffer ?? '';
336
- const budget = Math.max(6, width - FIELD_PREFIX_LEN - 1);
337
- const shown = draft.length > budget ? `...${draft.slice(-(budget - 3))}` : draft;
338
- return ` ${label}: ${shown}█`;
53
+ return rememberScopeKey(request);
339
54
  }
340
55
 
341
56
  /**
342
57
  * Assemble the card's view state from the pending-permission slot and the
343
- * broker's live queue (the honest count of OTHER pending asks, coalesced
344
- * asks share one record, so they are never double-counted). One helper so
345
- * main.ts passes identical state to getPromptHeight and createPromptLines.
58
+ * broker's live queue (the honest count of OTHER pending asks; coalesced
59
+ * asks share one record, so they are never double-counted).
346
60
  */
347
61
  static promptViewState(
348
62
  pending: { readonly callId: string; readonly replyMode?: 'deny-reason' | 'exec-answer' | undefined; readonly replyBuffer?: string | undefined },
349
- width: number,
350
63
  broker?: { listApprovals(limit?: number): ReadonlyArray<{ readonly callId: string; readonly status: string }> } | null,
351
64
  ): PromptViewState {
352
65
  let queueCount = 0;
@@ -355,45 +68,31 @@ export class PermissionPromptUI {
355
68
  } catch {
356
69
  queueCount = 0; // an unreadable broker never blocks the card
357
70
  }
358
- return { replyMode: pending.replyMode, replyBuffer: pending.replyBuffer, queueCount, width };
71
+ return { replyMode: pending.replyMode, replyBuffer: pending.replyBuffer, queueCount };
359
72
  }
360
73
 
361
- static getPromptHeight(
362
- request: PermissionPromptRequest,
363
- hunkState?: HunkSelectionState,
364
- detailsExpanded = false,
365
- requestedBy?: string,
366
- view?: PromptViewState,
367
- ): number {
368
- const width = view?.width ?? 80;
369
- // Attribution line (only when known) + the remember block (tier rows when
370
- // the request carries rememberOptions, else the legacy one-line preview)
371
- // are added to BOTH card shapes; keep in sync with createPromptLines.
372
- const attributionLines = requestedBy ? 1 : 0;
373
- const rememberLines = this.rememberRowTexts(request, hunkState, width).length;
374
- // Condensed low-risk card: top separator, title, [attribution], [preview],
375
- // summary, choices, bottom separator, see createPromptLines' condensed branch.
376
- if (this.isCondensed(request, hunkState, detailsExpanded)) return 5 + attributionLines + rememberLines;
377
- const analysis = this.fallbackAnalysis(request);
378
- const { annotation: judgmentAnnotation, rest: reasonsMinusJudgment } = extractModelJudgmentAnnotation(analysis.reasons);
379
- const reasonLines = Math.min(2, Math.max(1, reasonsMinusJudgment.length));
380
- const extraLines = (analysis.host ? 1 : 0) + (analysis.surface ? 1 : 0) + (analysis.sideEffects && analysis.sideEffects.length > 0 ? 1 : 0) + (readSandboxAskAnnotation(request) ? 1 : 0) + (judgmentAnnotation ? 1 : 0);
381
- const hunkLines = hunkState ? hunkListRowCount(hunkState) : 0;
382
- // Item-adopted blocks, each computed by the SAME helper createPromptLines
383
- // renders from, so the two functions cannot drift: the full wrapped
384
- // command (execute), the colored diff (write), the exec-prompt rows, and
385
- // the typed-reply input row.
386
- const commandLines = this.commandRowTexts(request, width).length;
387
- const diffLines = this.writeDiffLines(request, hunkState)?.length ?? 0;
388
- const execPromptLines = this.execPromptRowTexts(request, width).length;
389
- const replyLines = this.replyRowText(view, width) ? 1 : 0;
390
- // Base 12 counted a single arg line; the Path field now spans `pathRows`
391
- // lines and the full card adds one raw-args row (2a reachability), so the
392
- // arg allotment becomes pathRows + 1 → base 12 + pathRows (12 already
393
- // included one of those). See createPromptLines' full branch.
394
- const pathRows = this.pathRowTexts(this.resolvedTargets(request), 999).length;
395
- return 12 + pathRows + reasonLines + extraLines + hunkLines + attributionLines + rememberLines
396
- + commandLines + diffLines + execPromptLines + replyLines;
74
+ /** The permission dialog as a modal layer. */
75
+ static renderPromptModal(
76
+ screenWidth: number,
77
+ screenHeight: number,
78
+ request: PermissionRequest,
79
+ pending: PendingPromptView,
80
+ broker?: Parameters<typeof PermissionPromptUI.promptViewState>[1],
81
+ ): SurfaceLayer {
82
+ const view = this.promptViewState(pending, broker);
83
+ const state: PermissionCardState = {
84
+ hunkState: pending.hunkState,
85
+ detailsExpanded: pending.detailsExpanded,
86
+ requestedBy: pending.requestedBy,
87
+ replyMode: pending.replyMode,
88
+ replyBuffer: pending.replyBuffer,
89
+ queueCount: view.queueCount,
90
+ choice: pending.choice,
91
+ submenuOpen: pending.submenuOpen,
92
+ submenuIndex: pending.submenuIndex,
93
+ scroll: pending.scroll,
94
+ };
95
+ return renderPermissionCard(request, state, screenWidth, screenHeight);
397
96
  }
398
97
 
399
98
  /** Returns the key argument to display for a given tool invocation. */
@@ -401,13 +100,14 @@ export class PermissionPromptUI {
401
100
  return getDisplayArg(tool, args);
402
101
  }
403
102
 
404
- /** Returns the category label and ANSI 256-color code for display. */
103
+ /** Returns the category label and the active-theme color for display. */
405
104
  static getCategoryLabel(category: PermissionCategory): { label: string; color: string } {
105
+ const p = activeTokens();
406
106
  switch (category) {
407
- case 'write': return { label: 'WRITE', color: '220' }; // yellow
408
- case 'execute': return { label: 'EXECUTE', color: '196' }; // red
409
- case 'delegate': return { label: 'DELEGATE', color: '208' }; // orange
410
- default: return { label: 'PERMISSION', color: '244' };
107
+ case 'write': return { label: 'WRITE', color: p.warning };
108
+ case 'execute': return { label: 'EXECUTE', color: p.error };
109
+ case 'delegate': return { label: 'DELEGATE', color: p.blocked };
110
+ default: return { label: 'PERMISSION', color: p.textMuted };
411
111
  }
412
112
  }
413
113
 
@@ -418,267 +118,4 @@ export class PermissionPromptUI {
418
118
  static getSubjectLabel(request: PermissionPromptRequest): string {
419
119
  return buildPermissionApprovalBrief(request).subjectLabel;
420
120
  }
421
-
422
- /**
423
- * createPromptLines - Renders the permission prompt as an array of Lines.
424
- * Injected into the viewport by the render function when a request is pending.
425
- */
426
- static createPromptLines(
427
- width: number,
428
- request: PermissionRequest,
429
- hunkState?: HunkSelectionState,
430
- detailsExpanded = false,
431
- requestedBy?: string,
432
- view?: PromptViewState,
433
- ): Line[] {
434
- const lines: Line[] = [];
435
- const { tool, args, category } = request;
436
- const analysis = this.fallbackAnalysis(request);
437
- const brief = buildPermissionApprovalBrief(request);
438
- const { label, color } = this.getCategoryLabel(category);
439
-
440
- const ACCENT = '135'; // purple
441
- const WARN = color;
442
- const TEXT = '252';
443
- const DIM = '244';
444
-
445
- // Honest queue: how many OTHER broker asks are waiting behind this one.
446
- // They surface in turn as each is answered; the count keeps that visible.
447
- const queueSuffix = view?.queueCount && view.queueCount > 0
448
- ? `: ${view.queueCount} more waiting`
449
- : '';
450
-
451
- // Attribution line (which agent/process is asking) and the remember block
452
- // (numbered tier rows from the SDK's rememberOptions, or the legacy [A]
453
- // session-preview row). Both are shared by the condensed and full cards;
454
- // the remember block is suppressed for a hunk-selection prompt, which has
455
- // no single whole-request remember key.
456
- const pushAttribution = (): void => {
457
- if (!requestedBy) return;
458
- lines.push(UIFactory.stringToLine(` Requested by: ${requestedBy}`.padEnd(width), width, { fg: DIM }));
459
- };
460
- const pushRememberBlock = (): void => {
461
- for (const rowText of this.rememberRowTexts(request, hunkState, width)) {
462
- lines.push(UIFactory.stringToLine(rowText.padEnd(width), width, { fg: DIM }));
463
- }
464
- };
465
-
466
- const maxArgLen = Math.max(10, width - 16);
467
- const pathRows = this.pathRowTexts(this.resolvedTargets(request), maxArgLen);
468
-
469
- // Condensed low-risk / project-local card: one summary line (verb → target
470
- // (scope)) plus the choices. Full block is one `d` away. (2b.)
471
- if (this.isCondensed(request, hunkState, detailsExpanded)) {
472
- lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
473
- lines.push(UIFactory.stringToLine(` [${label}] ${brief.title}${queueSuffix} `.padEnd(width), width, { fg: WARN, bold: true }));
474
- pushAttribution();
475
- const scopeText = analysis.blastRadius ? ` (${analysis.blastRadius})` : '';
476
- const summaryLine = ` ${categoryVerb(category)} → ${pathRows[0]}${scopeText}`;
477
- lines.push(UIFactory.stringToLine(summaryLine.padEnd(width), width, { fg: TEXT }));
478
- pushRememberBlock();
479
- lines.push(UIFactory.stringToLine(
480
- ` [Y] Allow once [A] Allow always (session) [N] Deny [d] details`.padEnd(width),
481
- width, { fg: ACCENT, bold: true }));
482
- lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
483
- return lines;
484
- }
485
-
486
- // Top separator
487
- lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
488
-
489
- // Title bar: category badge + title (+ honest waiting count)
490
- const titleText = brief.title;
491
- const titleLine = ` [${label}] ${titleText}${queueSuffix} `;
492
- lines.push(UIFactory.stringToLine(titleLine.padEnd(width), width, { fg: WARN, bold: true }));
493
-
494
- // Requester attribution (which agent/process raised this request).
495
- pushAttribution();
496
-
497
- // Tool name row
498
- const toolLine = ` Tool : ${tool}`;
499
- lines.push(UIFactory.stringToLine(toolLine.padEnd(width), width, { fg: TEXT }));
500
-
501
- // Path/subject row(s): actual target path(s), one per line when few, a
502
- // "N files: a, b, +K more" summary when many. Raw args move to the Args row
503
- // below so this field never shows a truncated JSON blob. (2a.)
504
- pathRows.forEach((rowText, i) => {
505
- const labelCol = i === 0 ? brief.subjectLabel.padEnd(9) : ' '.repeat(9);
506
- lines.push(UIFactory.stringToLine(` ${labelCol}: ${rowText}`.padEnd(width), width, { fg: TEXT }));
507
- });
508
-
509
- // The FULL command of an execute ask, wrapped, never truncated. The Path
510
- // row above stays as the one-line subject; this block is the whole truth.
511
- for (const rowText of this.commandRowTexts(request, width)) {
512
- lines.push(UIFactory.stringToLine(rowText.padEnd(width), width, { fg: TEXT }));
513
- }
514
-
515
- // Exec-prompt attribution: the running command and the terminal prompt it
516
- // is stuck on, wrapped in full, the typed answer feeds this run's stdin.
517
- for (const rowText of this.execPromptRowTexts(request, width)) {
518
- lines.push(UIFactory.stringToLine(rowText.padEnd(width), width, { fg: WARN }));
519
- }
520
-
521
- // Working directory row
522
- const cwd = request.workingDirectory ?? '(unknown)';
523
- const maxCwdLen = Math.max(10, width - 16);
524
- const truncatedCwd = cwd.length > maxCwdLen ? '...' + cwd.slice(-(maxCwdLen - 3)) : cwd;
525
- const cwdLine = ` Directory : ${truncatedCwd}`;
526
- lines.push(UIFactory.stringToLine(cwdLine.padEnd(width), width, { fg: DIM }));
527
-
528
- const riskLine = ` Risk : ${analysis.riskLevel.toUpperCase()} (${analysis.classification})`;
529
- lines.push(UIFactory.stringToLine(riskLine.padEnd(width), width, { fg: WARN }));
530
-
531
- if (analysis.surface || analysis.blastRadius) {
532
- const surfaceLine = ` Surface : ${analysis.surface ?? 'generic'}${analysis.blastRadius ? ` radius=${analysis.blastRadius}` : ''}`;
533
- lines.push(UIFactory.stringToLine(surfaceLine.padEnd(width), width, { fg: DIM }));
534
- }
535
-
536
- if (analysis.host) {
537
- const hostLine = ` Host : ${analysis.host}`;
538
- lines.push(UIFactory.stringToLine(hostLine.padEnd(width), width, { fg: DIM }));
539
- }
540
-
541
- // Sandbox escalation row: when the sandbox-aware exec gate turned an
542
- // auto-allow into an ask because the boundary-safe command still needs host
543
- // access, name what it wants ("wants network …"). The escalation text is the
544
- // SDK policy's, verbatim, this row only surfaces it.
545
- const sandboxAnnotation = readSandboxAskAnnotation(request);
546
- if (sandboxAnnotation && sandboxAnnotation.sandboxEscalations.length > 0) {
547
- const escalations = sandboxAnnotation.sandboxEscalations.join('; ');
548
- const maxSandboxLen = Math.max(10, width - 16);
549
- const truncatedSandbox =
550
- escalations.length > maxSandboxLen ? `${escalations.slice(0, maxSandboxLen - 3)}...` : escalations;
551
- const sandboxLine = ` Sandbox : ${truncatedSandbox}`;
552
- lines.push(UIFactory.stringToLine(sandboxLine.padEnd(width), width, { fg: WARN }));
553
- }
554
-
555
- const summary = analysis.summary.length > width - 16
556
- ? `${analysis.summary.slice(0, Math.max(0, width - 19))}...`
557
- : analysis.summary;
558
- const summaryLine = ` Summary : ${summary}`;
559
- lines.push(UIFactory.stringToLine(summaryLine.padEnd(width), width, { fg: TEXT }));
560
-
561
- const modeLine = ` Decision : ${brief.decisionModeLabel}`;
562
- lines.push(UIFactory.stringToLine(modeLine.padEnd(width), width, { fg: DIM }));
563
-
564
- if (analysis.sideEffects && analysis.sideEffects.length > 0) {
565
- const effects = analysis.sideEffects.join(', ');
566
- const maxEffectsLen = Math.max(10, width - 16);
567
- const truncatedEffects =
568
- effects.length > maxEffectsLen ? `${effects.slice(0, maxEffectsLen - 3)}...` : effects;
569
- const effectsLine = ` Effects : ${truncatedEffects}`;
570
- lines.push(UIFactory.stringToLine(effectsLine.padEnd(width), width, { fg: DIM }));
571
- }
572
-
573
- // Model-judgment annotation: gets its own clearly-labeled row (never
574
- // subject to the Review truncation below) so a `sandbox-model-judgment`
575
- // verdict, "model judgment: looks safe because…" / "flags risk because…"
576
- //, is never silently cut. See extractModelJudgmentAnnotation.
577
- const { annotation: judgmentAnnotation, rest: reasonsMinusJudgment } = extractModelJudgmentAnnotation(analysis.reasons);
578
- if (judgmentAnnotation) {
579
- const maxJudgmentLen = Math.max(10, width - 16);
580
- const truncatedJudgment =
581
- judgmentAnnotation.length > maxJudgmentLen ? `${judgmentAnnotation.slice(0, maxJudgmentLen - 3)}...` : judgmentAnnotation;
582
- const judgmentLine = ` Judgment : ${truncatedJudgment}`;
583
- lines.push(UIFactory.stringToLine(judgmentLine.padEnd(width), width, { fg: ACCENT, bold: true }));
584
- }
585
-
586
- for (const reason of reasonsMinusJudgment.slice(0, 2)) {
587
- const maxReasonLen = Math.max(10, width - 16);
588
- const truncatedReason =
589
- reason.length > maxReasonLen ? `${reason.slice(0, maxReasonLen - 3)}...` : reason;
590
- const reasonLine = ` Review : ${truncatedReason}`;
591
- lines.push(UIFactory.stringToLine(reasonLine.padEnd(width), width, { fg: DIM }));
592
- }
593
-
594
- const checklist = brief.checklist;
595
- const maxChecklistLen = Math.max(10, width - 16);
596
- const truncatedChecklist =
597
- checklist.length > maxChecklistLen ? `${checklist.slice(0, maxChecklistLen - 3)}...` : checklist;
598
- const checklistLine = ` Checklist : ${truncatedChecklist}`;
599
- lines.push(UIFactory.stringToLine(checklistLine.padEnd(width), width, { fg: DIM }));
600
-
601
- // Raw args row, the full tool arguments live here in the details view so
602
- // the Path field above can render clean path(s), never a JSON blob. (2a.)
603
- const rawArgs = JSON.stringify(args);
604
- const maxRawLen = Math.max(10, width - 16);
605
- const truncatedRaw = rawArgs.length > maxRawLen ? `${rawArgs.slice(0, maxRawLen - 3)}...` : rawArgs;
606
- lines.push(UIFactory.stringToLine(` Args : ${truncatedRaw}`.padEnd(width), width, { fg: DIM }));
607
-
608
- // Blank spacer
609
- lines.push(UIFactory.stringToLine(' '.repeat(width), width));
610
-
611
- // Real colored diff for a write/edit ask (adds green, removals red, hunk
612
- // headers blue, the same diff-view machinery the transcript uses).
613
- const diffTextLines = this.writeDiffLines(request, hunkState);
614
- if (diffTextLines) {
615
- lines.push(...renderDiffView(diffTextLines.join('\n'), width));
616
- }
617
-
618
- if (hunkState) {
619
- // Hunk list, see hunkListRowCount() for the row-count contract this
620
- // block must match exactly (Risk 2: getPromptHeight/createPromptLines
621
- // parity is what keeps main.ts's render loop from clipping the
622
- // viewport).
623
- const { hunks, cursor, selected } = hunkState;
624
- const headerLine = ` Hunks (${selected.size}/${hunks.length} selected):`;
625
- lines.push(UIFactory.stringToLine(headerLine.padEnd(width), width, { fg: TEXT, bold: true }));
626
-
627
- const visible = hunks.slice(0, MAX_VISIBLE_HUNKS);
628
- const maxPreviewLen = Math.max(6, Math.floor((width - 20) / 2));
629
- for (let i = 0; i < visible.length; i++) {
630
- const hunk = visible[i]!;
631
- const box = selected.has(i) ? '[x]' : '[ ]';
632
- const findPreview = hunk.find.replace(/\n/g, '⏎').slice(0, maxPreviewLen);
633
- const replacePreview = hunk.replace.replace(/\n/g, '⏎').slice(0, maxPreviewLen);
634
- const rowText = ` ${box} ${i + 1}. ${hunk.path}: "${findPreview}" -> "${replacePreview}"`;
635
- const isCursor = i === cursor;
636
- lines.push(UIFactory.stringToLine(
637
- rowText.padEnd(width),
638
- width,
639
- {
640
- fg: selected.has(i) ? DIFF_TONES.add : DIFF_TONES.del,
641
- bg: isCursor ? UI_TONES.bg.selected : undefined,
642
- bold: isCursor,
643
- },
644
- ));
645
- }
646
-
647
- const hiddenCount = hunks.length - visible.length;
648
- const trailerLine = hiddenCount > 0 ? ` +${hiddenCount} more` : '';
649
- lines.push(UIFactory.stringToLine(trailerLine.padEnd(width), width, { fg: DIM }));
650
- }
651
-
652
- // Remember block: numbered tier rows (rememberOptions) or the legacy [A]
653
- // session preview (omitted for hunk mode).
654
- pushRememberBlock();
655
-
656
- // Typed-reply input row: the deny reason (Enter denies with it as
657
- // feedback) or the exec-prompt answer (Enter feeds the running command).
658
- const replyRow = this.replyRowText(view, width);
659
- if (replyRow) {
660
- lines.push(UIFactory.stringToLine(replyRow.padEnd(width), width, { fg: TEXT, bold: true }));
661
- }
662
-
663
- // Choices row. A condensable card shown in full is the expanded form, so it
664
- // offers `[d]` to collapse back; other full cards omit the details toggle.
665
- const collapseHint = this.isCondensed(request, hunkState, false) ? ' [d] hide details' : '';
666
- const tierCount = hunkState ? 0 : (request.rememberOptions?.length ?? 0);
667
- const rememberHint = tierCount > 0
668
- ? ` [1${tierCount > 1 ? `-${tierCount}` : ''}] Allow + remember`
669
- : ' [A] Allow always (session)';
670
- const choicesLine = view?.replyMode === 'exec-answer'
671
- ? ` [Enter] Send answer [Esc] ${((view.replyBuffer ?? '').length > 0) ? 'Clear' : 'Decline'} [Ctrl+C] Abort turn`
672
- : view?.replyMode === 'deny-reason'
673
- ? ` [Enter] Deny with this reason [Esc] Back [Ctrl+C] Abort turn`
674
- : hunkState
675
- ? ` [j/k] Navigate [Space] Toggle [A] All [Enter] Apply selected [N] Deny`
676
- : ` [Y] Allow once${rememberHint} [N] Deny type a reason to deny${collapseHint}`;
677
- lines.push(UIFactory.stringToLine(choicesLine.padEnd(width), width, { fg: ACCENT, bold: true }));
678
-
679
- // Bottom separator
680
- lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
681
-
682
- return lines;
683
- }
684
121
  }