@pellux/goodvibes-tui 2.0.21 → 2.1.1

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 (391) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -29
  3. package/docs/README.md +8 -5
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +28 -19
  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-reference.ts +2 -2
  52. package/src/input/command-registry.ts +77 -35
  53. package/src/input/commands/acp-runtime.ts +19 -9
  54. package/src/input/commands/channel-runtime.ts +2 -2
  55. package/src/input/commands/ci-runtime.ts +1 -1
  56. package/src/input/commands/codebase-runtime.ts +1 -1
  57. package/src/input/commands/config.ts +7 -1
  58. package/src/input/commands/context-window.ts +25 -19
  59. package/src/input/commands/control-room-runtime.ts +18 -18
  60. package/src/input/commands/conversation-runtime.ts +0 -1
  61. package/src/input/commands/cost-runtime.ts +29 -20
  62. package/src/input/commands/diff-runtime.ts +35 -181
  63. package/src/input/commands/eval.ts +1 -1
  64. package/src/input/commands/experience-runtime.ts +3 -7
  65. package/src/input/commands/fresh-conversation.ts +86 -0
  66. package/src/input/commands/git-runtime.ts +5 -22
  67. package/src/input/commands/graph-runtime.ts +1 -1
  68. package/src/input/commands/guidance-runtime.ts +1 -1
  69. package/src/input/commands/health-runtime.ts +8 -9
  70. package/src/input/commands/hooks-runtime.ts +2 -2
  71. package/src/input/commands/hosted-runtime.ts +3 -3
  72. package/src/input/commands/incident-runtime.ts +3 -3
  73. package/src/input/commands/intelligence-runtime.ts +6 -9
  74. package/src/input/commands/knowledge.ts +2 -2
  75. package/src/input/commands/legacy-panel-command.ts +57 -0
  76. package/src/input/commands/local-auth-runtime.ts +8 -9
  77. package/src/input/commands/local-provider-runtime.ts +7 -4
  78. package/src/input/commands/local-runtime.ts +12 -15
  79. package/src/input/commands/local-setup-review.ts +1 -1
  80. package/src/input/commands/local-setup.ts +13 -19
  81. package/src/input/commands/memory.ts +1 -1
  82. package/src/input/commands/notify-runtime.ts +9 -0
  83. package/src/input/commands/operator-runtime.ts +10 -12
  84. package/src/input/commands/permissions-provenance.ts +3 -3
  85. package/src/input/commands/planning-runtime.ts +7 -7
  86. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  87. package/src/input/commands/platform-services-runtime.ts +1 -1
  88. package/src/input/commands/policy-dispatch.ts +2 -2
  89. package/src/input/commands/policy.ts +3 -3
  90. package/src/input/commands/provider.ts +1 -1
  91. package/src/input/commands/remote-runtime.ts +1 -1
  92. package/src/input/commands/review-runtime.ts +39 -94
  93. package/src/input/commands/rewind-runtime.ts +81 -108
  94. package/src/input/commands/runtime-services.ts +15 -17
  95. package/src/input/commands/services-runtime.ts +2 -2
  96. package/src/input/commands/session-content.ts +21 -5
  97. package/src/input/commands/session-workflow.ts +13 -38
  98. package/src/input/commands/settings-sync-runtime.ts +2 -2
  99. package/src/input/commands/shell-core.ts +19 -4
  100. package/src/input/commands/skills-runtime.ts +2 -2
  101. package/src/input/commands/status-runtime.ts +13 -0
  102. package/src/input/commands/subscription-runtime.ts +1 -1
  103. package/src/input/commands/tasks-runtime.ts +2 -8
  104. package/src/input/commands/work-plan-runtime.ts +4 -4
  105. package/src/input/commands/workstream-attempts.ts +24 -47
  106. package/src/input/commands/workstream-runtime.ts +2 -2
  107. package/src/input/commands/worktree-runtime.ts +3 -3
  108. package/src/input/commands.ts +3 -1
  109. package/src/input/composer-editor.ts +44 -0
  110. package/src/input/concealed-input.ts +1 -1
  111. package/src/input/config-modal-types.ts +20 -8
  112. package/src/input/config-modal.ts +106 -105
  113. package/src/input/confirm-dialog-types.ts +28 -0
  114. package/src/input/confirm-dialog.ts +70 -0
  115. package/src/input/delete-key-policy.ts +3 -3
  116. package/src/input/feed-context-factory.ts +15 -16
  117. package/src/input/handler-command-route.ts +7 -42
  118. package/src/input/handler-content-actions.ts +16 -15
  119. package/src/input/handler-feed-routes.ts +26 -211
  120. package/src/input/handler-feed.ts +68 -95
  121. package/src/input/handler-interactions.ts +5 -3
  122. package/src/input/handler-modal-routes.ts +215 -216
  123. package/src/input/handler-modal-stack.ts +81 -11
  124. package/src/input/handler-modal-token-routes.ts +4 -0
  125. package/src/input/handler-onboarding.ts +0 -1
  126. package/src/input/handler-picker-routes.ts +65 -107
  127. package/src/input/handler-session-view-route.ts +149 -0
  128. package/src/input/handler-shortcuts.ts +16 -154
  129. package/src/input/handler-types.ts +4 -2
  130. package/src/input/handler-ui-state.ts +45 -15
  131. package/src/input/handler-work-tree-route.ts +123 -0
  132. package/src/input/handler.ts +25 -70
  133. package/src/input/keybindings.ts +16 -71
  134. package/src/input/masked-entry-modal.ts +117 -0
  135. package/src/input/mcp-workspace.ts +48 -17
  136. package/src/input/model-picker-filter.ts +8 -0
  137. package/src/input/model-picker-open.ts +99 -0
  138. package/src/input/model-picker-types.ts +1 -1
  139. package/src/input/model-picker.ts +92 -9
  140. package/src/input/notifications-modal.ts +123 -0
  141. package/src/input/overlay-filter.ts +26 -0
  142. package/src/input/profile-picker-modal.ts +37 -8
  143. package/src/input/search.ts +3 -3
  144. package/src/input/selection-modal-queue.ts +2 -0
  145. package/src/input/selection-modal.ts +28 -16
  146. package/src/input/session-picker-modal.ts +33 -8
  147. package/src/input/settings-modal-activation.ts +7 -2
  148. package/src/input/settings-modal-data.ts +11 -6
  149. package/src/input/settings-modal-types.ts +1 -1
  150. package/src/input/settings-modal.ts +20 -20
  151. package/src/input/submission-intent.ts +1 -1
  152. package/src/input/submission-router.ts +4 -9
  153. package/src/input/surface-modal-host.ts +110 -0
  154. package/src/input/theme-settings-actions.ts +82 -0
  155. package/src/input/usage-modal.ts +183 -0
  156. package/src/input/views.ts +97 -0
  157. package/src/main.ts +175 -165
  158. package/src/permissions/hunk-selection.ts +1 -1
  159. package/src/permissions/prompt-card.ts +511 -0
  160. package/src/permissions/prompt.ts +59 -622
  161. package/src/renderer/agents-modal.ts +310 -0
  162. package/src/renderer/autocomplete-overlay.ts +42 -220
  163. package/src/renderer/back-to-bottom.ts +80 -0
  164. package/src/renderer/block-actions-overlay.ts +30 -48
  165. package/src/renderer/bookmark-modal.ts +48 -79
  166. package/src/renderer/changes-modal.ts +325 -0
  167. package/src/renderer/code-block.ts +184 -106
  168. package/src/renderer/command-palette.ts +93 -0
  169. package/src/renderer/composer-fragments.ts +4 -4
  170. package/src/renderer/composer.ts +146 -0
  171. package/src/renderer/compositor.ts +26 -160
  172. package/src/renderer/config-modal.ts +185 -81
  173. package/src/renderer/confirm-dialog.ts +50 -0
  174. package/src/renderer/context-inspector.ts +116 -154
  175. package/src/renderer/context-status-hint.ts +4 -4
  176. package/src/renderer/conversation-layout.ts +20 -0
  177. package/src/renderer/conversation-overlays.ts +94 -84
  178. package/src/renderer/conversation-surface.ts +19 -38
  179. package/src/renderer/diff-tint.ts +98 -0
  180. package/src/renderer/file-picker-overlay.ts +63 -90
  181. package/src/renderer/footer-targets.ts +38 -0
  182. package/src/renderer/git-status.ts +3 -4
  183. package/src/renderer/header-line.ts +140 -117
  184. package/src/renderer/help-overlay.ts +133 -219
  185. package/src/renderer/history-search-overlay.ts +15 -12
  186. package/src/renderer/lane-graph/bead.ts +800 -0
  187. package/src/renderer/lane-graph/glyphs.ts +134 -0
  188. package/src/renderer/lane-graph/layout.ts +248 -0
  189. package/src/renderer/lane-graph/paint.ts +537 -0
  190. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  191. package/src/renderer/layout-engine.ts +2 -57
  192. package/src/renderer/layout.ts +7 -13
  193. package/src/renderer/markdown-inline.ts +2 -1
  194. package/src/renderer/markdown-table.ts +17 -12
  195. package/src/renderer/markdown.ts +92 -34
  196. package/src/renderer/masked-entry-modal.ts +78 -0
  197. package/src/renderer/mcp-workspace.ts +166 -203
  198. package/src/renderer/model-workspace.ts +381 -436
  199. package/src/renderer/notifications-modal.ts +116 -0
  200. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  201. package/src/renderer/process-view.ts +192 -0
  202. package/src/renderer/profile-picker-modal.ts +58 -110
  203. package/src/renderer/search-overlay.ts +6 -3
  204. package/src/renderer/selection-modal-overlay.ts +87 -327
  205. package/src/renderer/semantic-diff.ts +2 -2
  206. package/src/renderer/session-chips.ts +102 -0
  207. package/src/renderer/session-picker-modal.ts +177 -297
  208. package/src/renderer/settings-modal-connections.ts +14 -23
  209. package/src/renderer/settings-modal-context.ts +367 -0
  210. package/src/renderer/settings-modal-helpers.ts +6 -11
  211. package/src/renderer/settings-modal.ts +322 -726
  212. package/src/renderer/shell-surface.ts +277 -175
  213. package/src/renderer/shortcuts-overlay.ts +217 -0
  214. package/src/renderer/status-line.ts +475 -0
  215. package/src/renderer/surface-compose.ts +174 -0
  216. package/src/renderer/surface-kit-extra.ts +303 -0
  217. package/src/renderer/surface-kit-list.ts +366 -0
  218. package/src/renderer/surface-kit-parts.ts +360 -0
  219. package/src/renderer/surface-kit.ts +568 -0
  220. package/src/renderer/syntax-highlighter.ts +280 -197
  221. package/src/renderer/syntax-theme.ts +51 -0
  222. package/src/renderer/system-message.ts +0 -1
  223. package/src/renderer/term-caps.ts +42 -1
  224. package/src/renderer/terminal-bg-probe.ts +116 -19
  225. package/src/renderer/terminal-palette-probe.ts +322 -0
  226. package/src/renderer/terminal-palette-reader.ts +183 -0
  227. package/src/renderer/terminal-palette.ts +58 -0
  228. package/src/renderer/theme-mode-config.ts +16 -1
  229. package/src/renderer/theme.ts +234 -207
  230. package/src/renderer/thinking.ts +0 -1
  231. package/src/renderer/throbber.ts +188 -0
  232. package/src/renderer/toast-center.ts +119 -0
  233. package/src/renderer/ui-factory.ts +39 -602
  234. package/src/renderer/ui-primitives.ts +8 -25
  235. package/src/renderer/usage-modal.ts +353 -0
  236. package/src/renderer/user-message.ts +58 -0
  237. package/src/renderer/voice-capture-chip.ts +53 -0
  238. package/src/runtime/bootstrap-command-context.ts +0 -5
  239. package/src/runtime/bootstrap-command-parts.ts +38 -89
  240. package/src/runtime/bootstrap-core.ts +13 -15
  241. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  242. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  243. package/src/runtime/bootstrap-shell.ts +16 -50
  244. package/src/runtime/bootstrap.ts +14 -14
  245. package/src/runtime/client/fleet-union.ts +5 -5
  246. package/src/runtime/client/hosted-exit.ts +2 -2
  247. package/src/runtime/client/hosted-sessions.ts +1 -1
  248. package/src/runtime/client/spine-adoption.ts +1 -1
  249. package/src/runtime/context-accounting-source.ts +5 -4
  250. package/src/runtime/index.ts +1 -0
  251. package/src/runtime/notification-dispatch.ts +102 -29
  252. package/src/runtime/recovery-offer-wiring.ts +4 -1
  253. package/src/runtime/recovery-prompt.ts +31 -15
  254. package/src/runtime/runtime-services-types.ts +10 -6
  255. package/src/runtime/services.ts +17 -12
  256. package/src/runtime/session-continuity-hints.ts +2 -5
  257. package/src/runtime/session-inbound-inputs.ts +1 -1
  258. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  259. package/src/runtime/startup-typeahead-gate.ts +103 -0
  260. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  261. package/src/runtime/store/domains/index.ts +0 -8
  262. package/src/runtime/store/selectors/index.ts +0 -28
  263. package/src/runtime/store/state.ts +0 -8
  264. package/src/runtime/ui-services.ts +4 -6
  265. package/src/runtime/usage-tracker.ts +348 -0
  266. package/src/runtime/wrfc-persistence.ts +4 -4
  267. package/src/shell/blocking-input.ts +92 -2
  268. package/src/shell/recovery-input-helpers.ts +1 -1
  269. package/src/shell/session-views.ts +539 -0
  270. package/src/shell/status-report.ts +86 -0
  271. package/src/shell/transcript-scroll.ts +68 -0
  272. package/src/shell/ui-openers.ts +84 -172
  273. package/src/shell/view-openers.ts +205 -0
  274. package/src/tools/wrfc-agent-guard.ts +22 -2
  275. package/src/utils/format-duration.ts +4 -8
  276. package/src/utils/splash-lines.ts +9 -1
  277. package/src/utils/terminal-width.ts +56 -0
  278. package/src/verification/live-verifier.ts +0 -10
  279. package/src/version.ts +1 -1
  280. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  281. package/src/{panels → views}/builtin-modals.ts +22 -21
  282. package/src/views/builtin-views.ts +135 -0
  283. package/src/{panels → views}/confirm-state.ts +9 -9
  284. package/src/{panels → views}/diff-review-model.ts +10 -3
  285. package/src/{panels → views}/eval-registry.ts +1 -1
  286. package/src/{panels → views}/fleet-acts.ts +49 -85
  287. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  288. package/src/{panels → views}/fleet-gateway.ts +5 -5
  289. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  290. package/src/{panels → views}/fleet-read-model.ts +9 -9
  291. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  292. package/src/{panels → views}/fleet-spawn.ts +33 -49
  293. package/src/{panels → views}/fleet-steer.ts +12 -12
  294. package/src/{panels → views}/fleet-stop.ts +4 -4
  295. package/src/{panels → views}/fleet-transcript.ts +26 -27
  296. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  297. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  298. package/src/views/modal-surface-registry.ts +41 -0
  299. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  300. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  301. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  302. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  303. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  304. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  305. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  306. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  307. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  308. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  309. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  310. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  311. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  312. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  313. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  314. package/src/{panels → views}/modals/security-modal.ts +22 -16
  315. package/src/{panels → views}/modals/services-modal.ts +3 -3
  316. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  317. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  318. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  319. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  320. package/src/views/notifications-feed.ts +314 -0
  321. package/src/{panels → views}/polish-core.ts +38 -36
  322. package/src/{panels → views}/polish-tables.ts +35 -26
  323. package/src/{panels → views}/polish.ts +101 -95
  324. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  325. package/src/{panels → views}/provider-health-routes.ts +14 -14
  326. package/src/{panels → views}/search-focus.ts +8 -8
  327. package/src/{panels → views}/session-maintenance.ts +11 -11
  328. package/src/views/skills-discovery.ts +159 -0
  329. package/src/views/view-deps.ts +80 -0
  330. package/src/views/view-panel-adapter.ts +53 -0
  331. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  332. package/src/core/conversation-fold.ts +0 -101
  333. package/src/core/conversation-turn-structure.ts +0 -464
  334. package/src/core/conversation-user-receipts.ts +0 -42
  335. package/src/input/commands/operator-panel-runtime.ts +0 -186
  336. package/src/input/panel-integration-actions.ts +0 -33
  337. package/src/input/panel-mouse-geometry.ts +0 -97
  338. package/src/input/panel-paste-flood-guard.ts +0 -86
  339. package/src/panels/base-panel.ts +0 -301
  340. package/src/panels/builtin/agent.ts +0 -27
  341. package/src/panels/builtin/development.ts +0 -92
  342. package/src/panels/builtin/knowledge.ts +0 -12
  343. package/src/panels/builtin/operations.ts +0 -177
  344. package/src/panels/builtin/session.ts +0 -81
  345. package/src/panels/builtin/shared.ts +0 -357
  346. package/src/panels/builtin-panels.ts +0 -28
  347. package/src/panels/cost-tracker-panel.ts +0 -746
  348. package/src/panels/diff-panel.ts +0 -797
  349. package/src/panels/diff-review-panel.ts +0 -395
  350. package/src/panels/expandable-list-panel.ts +0 -189
  351. package/src/panels/fleet-deep-link.ts +0 -31
  352. package/src/panels/fleet-panel.ts +0 -799
  353. package/src/panels/git-panel.ts +0 -800
  354. package/src/panels/hosted-session-panel.ts +0 -221
  355. package/src/panels/index.ts +0 -21
  356. package/src/panels/local-auth-panel.ts +0 -478
  357. package/src/panels/notifications-feed.ts +0 -127
  358. package/src/panels/notifications-panel.ts +0 -160
  359. package/src/panels/panel-confirm-overlay.ts +0 -72
  360. package/src/panels/panel-manager.ts +0 -791
  361. package/src/panels/plugins-panel.ts +0 -371
  362. package/src/panels/scrollable-list-panel.ts +0 -498
  363. package/src/panels/skills-panel.ts +0 -454
  364. package/src/panels/token-budget-panel.ts +0 -688
  365. package/src/panels/types.ts +0 -191
  366. package/src/panels/wrfc-panel-format.ts +0 -133
  367. package/src/renderer/diff-view.ts +0 -134
  368. package/src/renderer/fleet-tab-strip.ts +0 -56
  369. package/src/renderer/footer-tips.ts +0 -53
  370. package/src/renderer/fullscreen-primitives.ts +0 -146
  371. package/src/renderer/fullscreen-workspace.ts +0 -194
  372. package/src/renderer/hint-grammar.ts +0 -52
  373. package/src/renderer/modal-factory.ts +0 -480
  374. package/src/renderer/overlay-box.ts +0 -154
  375. package/src/renderer/overlay-viewport.ts +0 -104
  376. package/src/renderer/panel-composite.ts +0 -145
  377. package/src/renderer/panel-workspace-bar.ts +0 -54
  378. package/src/renderer/process-indicator.ts +0 -163
  379. package/src/renderer/tab-strip.ts +0 -222
  380. package/src/renderer/text-layout.ts +0 -31
  381. package/src/renderer/tool-call.ts +0 -276
  382. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  383. package/src/renderer/tool-result-summary.ts +0 -219
  384. package/src/runtime/diagnostics/panels/index.ts +0 -21
  385. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  386. package/src/runtime/perf/panel-contracts.ts +0 -32
  387. package/src/runtime/store/domains/panels.ts +0 -117
  388. package/src/verification/verification-ledger.ts +0 -707
  389. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  390. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  391. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -113,7 +113,7 @@ export function buildHunkSelectionState(request: PermissionPromptRequest): HunkS
113
113
  /**
114
114
  * applyHunkKey, pure reducer over raw stdin `data` for an active hunk-selection
115
115
  * prompt. Mirrors the outer y/a/n prompt's raw-data key routing (not the
116
- * normalized panel key vocabulary) since Space must be distinguishable from
116
+ * normalized key vocabulary) since Space must be distinguishable from
117
117
  * the trimmed/lowercased `key` the outer switch uses.
118
118
  */
119
119
  export function applyHunkKey(state: HunkSelectionState, data: string): HunkKeyResult {
@@ -0,0 +1,511 @@
1
+ /**
2
+ * prompt-card.ts, the permission dialog drawn with the modal surface kit.
3
+ *
4
+ * ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ (amber: this needs you) ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
5
+ *
6
+ * △ Run a command? engineer · 1 more waiting
7
+ *
8
+ * ┌ element inset ─────────────────────────────────────────────────┐
9
+ * $ rm -rf dist && bun run build
10
+ * └────────────────────────────────────────────────────────────────┘
11
+ *
12
+ * high risk deletes files recursively sandbox · network off
13
+ *
14
+ * Allow once Allow for session ▸ Deny d details
15
+ *
16
+ * ←→ choose ⏎ confirm y allow once a session n deny d details
17
+ *
18
+ * The thing being approved is the whole card: the command itself (in full,
19
+ * wrapped), or the target path(s). Risk shows as one colored chip. Edits show
20
+ * the actual diff with tinted rows; hunk-selectable edits give every hunk its
21
+ * own checkbox. Every other fact (directory, sandbox, policy judgment, review
22
+ * reasons, the raw arguments) is one key away (d), wrapped in full. Choices are
23
+ * buttons: arrows pick, Enter confirms, and y / a / n (and 1-9 for remember
24
+ * tiers) still work. "Allow for session ▸" opens the remember tiers as a
25
+ * submenu when the request offers them. Typing starts a denial with a reason.
26
+ *
27
+ * Pure rendering; the keys live in shell/blocking-input.ts, which shares
28
+ * permissionButtons() with this file so the two can never disagree.
29
+ */
30
+
31
+ import type { PermissionCategory, PermissionPromptRequest, PermissionRequestAnalysis } from '@pellux/goodvibes-sdk/platform/permissions';
32
+ import { buildPermissionApprovalBrief, getDisplayArg } from '@pellux/goodvibes-sdk/platform/permissions';
33
+ import { activeTokens } from '../renderer/theme.ts';
34
+ import {
35
+ beginModal,
36
+ finishModal,
37
+ layoutHintRows,
38
+ hintLayoutWidth,
39
+ MODAL_PAD_X,
40
+ modalInnerWidth,
41
+ scrollCountText,
42
+ standardModalWidth,
43
+ wrapLines,
44
+ type KitHint,
45
+ type SurfaceLayer,
46
+ } from '../renderer/surface-kit.ts';
47
+ import { chip, button, buttonWidth } from '../renderer/surface-kit-parts.ts';
48
+ import { drawRow, measureRow, type KitRow, type KitSpan } from '../renderer/surface-kit-list.ts';
49
+ import type { HunkSelectionState } from './hunk-selection.ts';
50
+ import { readSandboxAskAnnotation } from './sandbox-exec-gate.ts';
51
+
52
+ /** Hunks listed before a "+N more hunks" trailer. */
53
+ export const MAX_VISIBLE_HUNKS = 8;
54
+ /** Diff rows shown for a whole-file write before a "+N more" trailer. */
55
+ const MAX_DIFF_LINES = 10;
56
+ /** Target rows listed before they collapse to an "N files: …" summary. */
57
+ const MAX_TARGET_ROWS = 3;
58
+ /** Diff rows shown under one hunk before a trailer. */
59
+ const MAX_HUNK_LINES = 6;
60
+
61
+ /** What the card shows beyond the request itself. */
62
+ export interface PermissionCardState {
63
+ readonly hunkState?: HunkSelectionState | undefined;
64
+ readonly detailsExpanded?: boolean | undefined;
65
+ readonly requestedBy?: string | undefined;
66
+ readonly replyMode?: 'deny-reason' | 'exec-answer' | undefined;
67
+ readonly replyBuffer?: string | undefined;
68
+ /** OTHER broker asks waiting behind this one. */
69
+ readonly queueCount?: number | undefined;
70
+ /** The chosen button (index into permissionButtons). */
71
+ readonly choice?: number | undefined;
72
+ /** The remember-tier submenu is open. */
73
+ readonly submenuOpen?: boolean | undefined;
74
+ readonly submenuIndex?: number | undefined;
75
+ /** First body row shown when the card is taller than the screen. */
76
+ readonly scroll?: number | undefined;
77
+ }
78
+
79
+ export type PermissionButtonId = 'allow' | 'session' | 'remember' | 'apply' | 'deny';
80
+
81
+ export interface PermissionButton {
82
+ readonly id: PermissionButtonId;
83
+ readonly label: string;
84
+ }
85
+
86
+ /** The card's buttons, in order (the safe answers are Allow once / Apply first, Deny last). */
87
+ export function permissionButtons(request: PermissionPromptRequest, state: Pick<PermissionCardState, 'hunkState' | 'replyMode'>): PermissionButton[] {
88
+ if (state.replyMode === 'exec-answer') return [];
89
+ if (state.hunkState) return [{ id: 'apply', label: 'Apply selected' }, { id: 'deny', label: 'Deny' }];
90
+ const tiers = request.rememberOptions?.length ?? 0;
91
+ return [
92
+ { id: 'allow', label: 'Allow once' },
93
+ tiers > 0 ? { id: 'remember', label: 'Allow for session ▸' } : { id: 'session', label: 'Allow for session' },
94
+ { id: 'deny', label: 'Deny' },
95
+ ];
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Request facts
100
+ // ---------------------------------------------------------------------------
101
+
102
+ export function fallbackAnalysis(request: PermissionPromptRequest): PermissionRequestAnalysis {
103
+ return request.analysis ?? {
104
+ classification: request.category,
105
+ riskLevel: request.category === 'read' ? 'low' : request.category === 'write' ? 'medium' : 'high',
106
+ summary: `Review ${request.tool} request`,
107
+ reasons: ['Inspect the target and intent before approving this action.'],
108
+ target: getDisplayArg(request.tool, request.args),
109
+ targetKind: 'generic',
110
+ };
111
+ }
112
+
113
+ const MODEL_JUDGMENT_PREFIX = 'model judgment:';
114
+
115
+ /** The sandbox model-judgment annotation gets its own row so it is never cut from the reasons. */
116
+ export function extractModelJudgmentAnnotation(reasons: readonly string[]): { annotation: string | undefined; rest: string[] } {
117
+ const idx = reasons.findIndex((r) => r.toLowerCase().startsWith(MODEL_JUDGMENT_PREFIX));
118
+ if (idx === -1) return { annotation: undefined, rest: [...reasons] };
119
+ return { annotation: reasons[idx], rest: [...reasons.slice(0, idx), ...reasons.slice(idx + 1)] };
120
+ }
121
+
122
+ function relativize(p: string, cwd?: string): string {
123
+ if (!cwd || !p.startsWith('/')) return p;
124
+ if (p === cwd) return '.';
125
+ const prefix = cwd.endsWith('/') ? cwd : `${cwd}/`;
126
+ return p.startsWith(prefix) ? p.slice(prefix.length) : p;
127
+ }
128
+
129
+ function collectField(arr: unknown, field: string): string[] {
130
+ if (!Array.isArray(arr)) return [];
131
+ const out: string[] = [];
132
+ for (const el of arr) {
133
+ if (typeof el === 'string') out.push(el);
134
+ else if (el && typeof el === 'object' && typeof (el as Record<string, unknown>)[field] === 'string') out.push((el as Record<string, unknown>)[field] as string);
135
+ }
136
+ return out;
137
+ }
138
+
139
+ /** The real display target(s) of a tool call (paths, commands, urls), relativised; a fallback when none is recognisable. */
140
+ export function cardTargets(request: PermissionPromptRequest): string[] {
141
+ const args = request.args ?? {};
142
+ const pick = (): string[] => {
143
+ const files = collectField(args.files, 'path');
144
+ if (files.length) return files;
145
+ if (typeof args.path === 'string') return [args.path];
146
+ if (typeof args.file === 'string') return [args.file];
147
+ const commands = collectField(args.commands, 'cmd');
148
+ if (commands.length) return commands;
149
+ if (typeof args.command === 'string') return [args.command];
150
+ if (typeof args.cmd === 'string') return [args.cmd];
151
+ if (typeof args.pattern === 'string') return [args.pattern];
152
+ const urls = collectField(args.urls, 'url');
153
+ if (urls.length) return urls;
154
+ const queries = collectField(args.queries, 'query');
155
+ if (queries.length) return queries;
156
+ if (typeof args.query === 'string') return [args.query];
157
+ if (typeof args.task === 'string') return [args.task];
158
+ return [];
159
+ };
160
+ const targets = pick().map((t) => relativize(t, request.workingDirectory));
161
+ return targets.length > 0 ? targets : [getDisplayArg(request.tool, args)];
162
+ }
163
+
164
+ function commandOf(request: PermissionPromptRequest): string {
165
+ const args = request.args ?? {};
166
+ return typeof args.command === 'string' ? args.command : typeof args.cmd === 'string' ? args.cmd : '';
167
+ }
168
+
169
+ /** The exact session rule "Allow for session" remembers (mirrors the SDK's approval key). */
170
+ export function rememberScopeKey(request: PermissionPromptRequest): string {
171
+ const args = request.args ?? {};
172
+ if (typeof args.path === 'string' && args.path.length > 0) return `${request.tool}:${args.path}`;
173
+ if (typeof args.command === 'string' && args.command.length > 0) return `${request.tool}:${args.command}`;
174
+ return request.tool;
175
+ }
176
+
177
+ function riskColor(level: PermissionRequestAnalysis['riskLevel']): string {
178
+ const t = activeTokens();
179
+ if (level === 'low') return t.success;
180
+ if (level === 'medium') return t.warning;
181
+ return t.error;
182
+ }
183
+
184
+ /** The request category, named on the title row. */
185
+ function categoryLabel(category: PermissionCategory): string {
186
+ switch (category) {
187
+ case 'write': return 'WRITE';
188
+ case 'execute': return 'EXECUTE';
189
+ case 'delegate': return 'DELEGATE';
190
+ default: return 'PERMISSION';
191
+ }
192
+ }
193
+
194
+ function titleGlyph(category: PermissionCategory): string {
195
+ return category === 'read' ? '○' : '△';
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Body rows
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /** One visual row of the card body. */
203
+ interface CardRow {
204
+ readonly spans: readonly KitSpan[];
205
+ /** Fill the row as part of an element inset (bg), with this tint instead when set. */
206
+ readonly inset?: boolean;
207
+ readonly tint?: string;
208
+ readonly right?: { readonly text: string; readonly fg: string };
209
+ /** The hunk cursor row (drawn as the selected row). */
210
+ readonly cursor?: boolean;
211
+ }
212
+
213
+ /** Wrap code keeping its leading indentation on every wrapped line. */
214
+ function wrapCode(line: string, width: number): string[] {
215
+ const indent = /^\s*/.exec(line)?.[0] ?? '';
216
+ const room = Math.max(4, width - indent.length);
217
+ return wrapLines(line.slice(indent.length), room).map((part) => `${indent}${part}`);
218
+ }
219
+
220
+ function textRows(text: string, width: number, span: Omit<KitSpan, 'text'>, indent = ''): CardRow[] {
221
+ return text.split('\n').flatMap((para) => wrapLines(para, Math.max(1, width - indent.length)))
222
+ .map((line, i) => ({ spans: [{ ...span, text: `${i === 0 ? '' : indent}${line}` }] }));
223
+ }
224
+
225
+ function insetRows(rows: CardRow[]): CardRow[] {
226
+ const blank: CardRow = { spans: [], inset: true };
227
+ return [blank, ...rows.map((r) => ({ ...r, inset: true })), blank];
228
+ }
229
+
230
+ function labelled(label: string, value: string, width: number, fg: string): CardRow[] {
231
+ const t = activeTokens();
232
+ const pad = 12;
233
+ const lines = value.split('\n').flatMap((para) => wrapLines(para, Math.max(8, width - pad)));
234
+ return lines.map((line, i) => ({
235
+ spans: i === 0
236
+ ? [{ text: label.padEnd(pad), fg: t.textMuted }, { text: line, fg }]
237
+ : [{ text: `${' '.repeat(pad)}${line}`, fg }],
238
+ }));
239
+ }
240
+
241
+ function diffRows(lines: readonly string[], width: number): CardRow[] {
242
+ const t = activeTokens();
243
+ const rows: CardRow[] = [];
244
+ for (const raw of lines) {
245
+ const sign = raw.startsWith('+') ? '+' : raw.startsWith('-') ? '-' : raw.startsWith('@@') ? '@' : ' ';
246
+ const body = sign === '@' || sign === ' ' ? raw : raw.slice(1);
247
+ const tint = sign === '+' ? t.diffAddedBg : sign === '-' ? t.diffRemovedBg : undefined;
248
+ const fg = sign === '@' ? t.info : sign === ' ' ? t.textFaint : t.text;
249
+ const signFg = sign === '+' ? t.success : sign === '-' ? t.error : t.textFaint;
250
+ wrapCode(body, Math.max(4, width - 2)).forEach((part, i) => {
251
+ rows.push({
252
+ spans: sign === '@' || sign === ' '
253
+ ? [{ text: part, fg }]
254
+ : [{ text: i === 0 ? `${sign} ` : ' ', fg: signFg }, { text: part, fg }],
255
+ inset: true,
256
+ tint,
257
+ });
258
+ });
259
+ }
260
+ return rows;
261
+ }
262
+
263
+ /** Unified-diff text for a whole-file write or an edit list (not hunk mode), capped with an honest trailer. */
264
+ export function writeDiffLines(request: PermissionPromptRequest, hunkState: HunkSelectionState | undefined): string[] | null {
265
+ if (hunkState || request.category !== 'write') return null;
266
+ const args = request.args ?? {};
267
+ const target = typeof args.path === 'string' ? args.path : typeof args.file === 'string' ? args.file : '';
268
+ const out: string[] = [];
269
+ if (typeof args.content === 'string') {
270
+ const contentLines = args.content.split('\n');
271
+ out.push(`@@ ${target || '(new content)'} @@`);
272
+ for (const line of contentLines.slice(0, MAX_DIFF_LINES)) out.push(`+${line}`);
273
+ if (contentLines.length > MAX_DIFF_LINES) out.push(` … +${contentLines.length - MAX_DIFF_LINES} more lines`);
274
+ return out;
275
+ }
276
+ if (Array.isArray(args.edits)) {
277
+ let budget = MAX_DIFF_LINES;
278
+ let hidden = 0;
279
+ for (const edit of args.edits) {
280
+ if (!edit || typeof edit !== 'object') continue;
281
+ const e = edit as { path?: unknown; find?: unknown; replace?: unknown };
282
+ if (typeof e.find !== 'string' || typeof e.replace !== 'string') continue;
283
+ const findLines = e.find.split('\n');
284
+ const replaceLines = e.replace.split('\n');
285
+ const need = 1 + findLines.length + replaceLines.length;
286
+ if (budget < need) {
287
+ hidden += 1;
288
+ continue;
289
+ }
290
+ out.push(`@@ ${typeof e.path === 'string' ? e.path : target} @@`);
291
+ for (const line of findLines) out.push(`-${line}`);
292
+ for (const line of replaceLines) out.push(`+${line}`);
293
+ budget -= need;
294
+ }
295
+ if (out.length === 0) return null;
296
+ if (hidden > 0) out.push(` … +${hidden} more edits`);
297
+ return out;
298
+ }
299
+ return null;
300
+ }
301
+
302
+ function subjectRows(request: PermissionPromptRequest, width: number): CardRow[] {
303
+ const t = activeTokens();
304
+ const attribution = request.attribution;
305
+ if (attribution?.kind === 'exec-prompt') {
306
+ const rows: CardRow[] = [];
307
+ if (typeof attribution.command === 'string' && attribution.command.length > 0) rows.push(...labelled('Running', attribution.command, width, t.text));
308
+ if (typeof attribution.prompt === 'string' && attribution.prompt.length > 0) rows.push(...labelled('Asks', attribution.prompt, width, t.warning));
309
+ return rows;
310
+ }
311
+ const command = request.category === 'execute' ? commandOf(request) : '';
312
+ if (command) {
313
+ // The FULL command, wrapped, never truncated.
314
+ return command.split('\n').flatMap((para, p) => wrapCode(para, Math.max(4, width - 2)).map((line, i) => ({
315
+ spans: [{ text: p === 0 && i === 0 ? '$ ' : ' ', fg: t.textFaint }, { text: line, fg: t.text }],
316
+ })));
317
+ }
318
+ const brief = buildPermissionApprovalBrief(request);
319
+ const targets = cardTargets(request);
320
+ // Up to three targets are listed in full; more collapse to a summary (the
321
+ // complete list stays in the details' Args row).
322
+ if (targets.length > MAX_TARGET_ROWS) {
323
+ const base = (p: string): string => p.replace(/[/\\]+$/, '').split(/[/\\]/).pop() ?? p;
324
+ return labelled(brief.subjectLabel, `${targets.length} files: ${targets.slice(0, 2).map(base).join(', ')}, +${targets.length - 2} more`, width, t.text);
325
+ }
326
+ return targets.flatMap((target, i) => labelled(i === 0 ? brief.subjectLabel : '', target, width, t.text));
327
+ }
328
+
329
+ function hunkRows(hunkState: HunkSelectionState, width: number): CardRow[] {
330
+ const t = activeTokens();
331
+ const rows: CardRow[] = [];
332
+ const { hunks, cursor, selected } = hunkState;
333
+ rows.push(...textRows(`${selected.size} of ${hunks.length} hunks selected`, width + 4, { fg: t.textMuted }));
334
+ hunks.slice(0, MAX_VISIBLE_HUNKS).forEach((hunk, i) => {
335
+ const on = selected.has(i);
336
+ wrapLines(`${i + 1}. ${hunk.path}`, Math.max(4, width)).forEach((part, k) => rows.push({
337
+ spans: [{ text: k === 0 ? (on ? '[x] ' : '[ ] ') : ' ', fg: on ? t.success : t.textFaint, bold: true }, { text: part, fg: on ? t.text : t.textMuted }],
338
+ cursor: i === cursor,
339
+ }));
340
+ const lines = [...hunk.find.split('\n').map((l) => `-${l}`), ...hunk.replace.split('\n').map((l) => `+${l}`)];
341
+ const shown = diffRows(lines.slice(0, MAX_HUNK_LINES), width);
342
+ rows.push(...insetRows(lines.length > MAX_HUNK_LINES
343
+ ? [...shown, { spans: [{ text: `+${lines.length - MAX_HUNK_LINES} more lines`, fg: t.textFaint }] }]
344
+ : shown));
345
+ });
346
+ if (hunks.length > MAX_VISIBLE_HUNKS) rows.push({ spans: [{ text: `+${hunks.length - MAX_VISIBLE_HUNKS} more hunks`, fg: t.textFaint }] });
347
+ return rows;
348
+ }
349
+
350
+ function detailRows(request: PermissionPromptRequest, state: PermissionCardState, width: number): CardRow[] {
351
+ const t = activeTokens();
352
+ const analysis = fallbackAnalysis(request);
353
+ const brief = buildPermissionApprovalBrief(request);
354
+ const { rest: reasons } = extractModelJudgmentAnnotation(analysis.reasons);
355
+ const rows: CardRow[] = [];
356
+ rows.push(...labelled('Tool', request.tool, width, t.text));
357
+ rows.push(...labelled('Directory', request.workingDirectory ?? '(unknown)', width, t.textMuted));
358
+ rows.push(...labelled('Risk', `${analysis.riskLevel} (${analysis.classification})`, width, riskColor(analysis.riskLevel)));
359
+ if (analysis.surface || analysis.blastRadius) rows.push(...labelled('Surface', `${analysis.surface ?? 'generic'}${analysis.blastRadius ? ` · radius ${analysis.blastRadius}` : ''}`, width, t.textMuted));
360
+ if (analysis.host) rows.push(...labelled('Host', analysis.host, width, t.textMuted));
361
+ const sandbox = readSandboxAskAnnotation(request);
362
+ if (sandbox && sandbox.sandboxEscalations.length > 0) rows.push(...labelled('Sandbox', sandbox.sandboxEscalations.join('; '), width, t.warning));
363
+ rows.push(...labelled('Decision', brief.decisionModeLabel, width, t.textMuted));
364
+ if (analysis.sideEffects && analysis.sideEffects.length > 0) rows.push(...labelled('Effects', analysis.sideEffects.join(', '), width, t.textMuted));
365
+ reasons.forEach((reason, i) => rows.push(...labelled(i === 0 ? 'Review' : '', reason, width, t.textMuted)));
366
+ rows.push(...labelled('Checklist', brief.checklist, width, t.textMuted));
367
+ if (!state.hunkState) rows.push(...labelled('Remembers', `"Allow for session" remembers ${rememberScopeKey(request)}`, width, t.textMuted));
368
+ rows.push(...labelled('Args', JSON.stringify(request.args ?? {}), width, t.textFaint));
369
+ return rows;
370
+ }
371
+
372
+ function bodyRows(request: PermissionPromptRequest, state: PermissionCardState, width: number): CardRow[] {
373
+ const t = activeTokens();
374
+ const analysis = fallbackAnalysis(request);
375
+ const rows: CardRow[] = [];
376
+ rows.push(...insetRows(subjectRows(request, width - 4)));
377
+ rows.push({ spans: [] });
378
+ // Risk chip + summary; the model judgment on its own row so it is never cut.
379
+ const summary = wrapLines(analysis.summary, Math.max(8, width - 14));
380
+ summary.forEach((line, i) => rows.push({
381
+ spans: i === 0
382
+ ? [{ text: `\u0000${analysis.riskLevel} risk`, fg: riskColor(analysis.riskLevel) }, { text: ` ${line}`, fg: t.textMuted }]
383
+ : [{ text: `${' '.repeat(analysis.riskLevel.length + 9)}${line}`, fg: t.textMuted }],
384
+ }));
385
+ const { annotation } = extractModelJudgmentAnnotation(analysis.reasons);
386
+ if (annotation) rows.push(...labelled('Judgment', annotation, width, t.accent));
387
+ const diff = writeDiffLines(request, state.hunkState);
388
+ if (diff) {
389
+ rows.push({ spans: [] });
390
+ rows.push(...insetRows(diffRows(diff, width - 4)));
391
+ }
392
+ if (state.hunkState) {
393
+ rows.push({ spans: [] });
394
+ rows.push(...hunkRows(state.hunkState, width - 4));
395
+ }
396
+ if (state.detailsExpanded) {
397
+ rows.push({ spans: [] });
398
+ rows.push(...detailRows(request, state, width));
399
+ }
400
+ if (state.replyMode) {
401
+ const label = state.replyMode === 'exec-answer' ? 'Answer' : 'Reason';
402
+ rows.push({ spans: [] });
403
+ rows.push(...insetRows(labelled(label, `${state.replyBuffer ?? ''}▏`, width - 4, t.text)));
404
+ }
405
+ return rows;
406
+ }
407
+
408
+ function cardHints(request: PermissionPromptRequest, state: PermissionCardState): KitHint[] {
409
+ if (state.replyMode === 'exec-answer') return [['⏎', 'send answer'], ['esc', (state.replyBuffer ?? '').length > 0 ? 'clear' : 'decline'], ['ctrl+c', 'abort turn']];
410
+ if (state.replyMode === 'deny-reason') return [['⏎', 'deny with this reason'], ['esc', 'back'], ['ctrl+c', 'abort turn']];
411
+ if (state.submenuOpen) return [['↑↓', 'tier'], ['⏎', 'allow and remember'], ['esc', 'back']];
412
+ if (state.hunkState) return [['↑↓', 'hunk'], ['space', 'toggle'], ['a', 'all'], ['←→', 'choose'], ['⏎', 'confirm'], ['n', 'deny']];
413
+ const tiers = request.rememberOptions?.length ?? 0;
414
+ return [
415
+ ['←→', 'choose'], ['⏎', 'confirm'], ['y', 'allow once'],
416
+ tiers > 0 ? [tiers > 1 ? `1-${tiers}` : '1', 'remember'] : ['a', 'allow for session'],
417
+ ['n', 'deny'], ['type', 'a reason to deny'],
418
+ ];
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Renderer
423
+ // ---------------------------------------------------------------------------
424
+
425
+ export function renderPermissionCard(request: PermissionPromptRequest, state: PermissionCardState, screenWidth: number, screenHeight: number): SurfaceLayer {
426
+ const t = activeTokens();
427
+ const width = standardModalWidth(screenWidth);
428
+ const inner = modalInnerWidth(width);
429
+ const hints = cardHints(request, state);
430
+ const hintRows = layoutHintRows(hints, hintLayoutWidth(inner)).length;
431
+ const body = bodyRows(request, state, inner);
432
+ const buttons = permissionButtons(request, state);
433
+ const tiers = state.submenuOpen ? (request.rememberOptions ?? []) : [];
434
+ const submenuIndex = Math.max(0, Math.min(state.submenuIndex ?? 0, tiers.length - 1));
435
+ const tierRows = tiers.map((tier, i): KitRow => ({ label: `${i + 1}. ${tier.label}`, desc: tier.detail, selected: i === submenuIndex }));
436
+ // The submenu is indented 2 columns under the buttons it belongs to.
437
+ const tierX0 = MODAL_PAD_X + 2;
438
+ const tierX1 = width - MODAL_PAD_X - 1;
439
+ const tierHeight = tierRows.reduce((sum, row) => sum + measureRow(row, tierX0, tierX1), 0);
440
+ // Pinned under the scrolling body: blank, buttons row, then the submenu.
441
+ const pinned = (buttons.length > 0 ? 2 : 0) + (tierRows.length > 0 ? tierHeight + 1 : 0);
442
+ // Fill rows: padding, title, blank, body..., pinned, blank, hints, empty last row.
443
+ const f = beginModal(screenWidth, screenHeight, {
444
+ title: buildPermissionApprovalBrief(request).title,
445
+ titleGlyph: { char: titleGlyph(request.category), fg: t.warning },
446
+ width,
447
+ height: body.length + pinned + hintRows + 5,
448
+ center: true,
449
+ cap: 'warning',
450
+ escKey: false,
451
+ hints,
452
+ });
453
+
454
+ const right = [categoryLabel(request.category), state.requestedBy, state.queueCount && state.queueCount > 0 ? `${state.queueCount} more waiting` : ''].filter(Boolean).join(' · ');
455
+ if (right) f.canvas.right(f.r, f.t, right, { fg: t.textFaint });
456
+
457
+ const scrollBottom = f.bottom - pinned;
458
+ const capacity = Math.max(1, scrollBottom - f.top + 1);
459
+ const maxScroll = Math.max(0, body.length - capacity);
460
+ let scroll = Math.max(0, Math.min(state.scroll ?? 0, maxScroll));
461
+ // Keep the hunk cursor in view.
462
+ const cursorRow = body.findIndex((r) => r.cursor);
463
+ if (cursorRow >= 0) {
464
+ if (cursorRow < scroll) scroll = cursorRow;
465
+ else if (cursorRow >= scroll + capacity) scroll = cursorRow - capacity + 1;
466
+ }
467
+ for (let i = 0; i < capacity && scroll + i < body.length; i++) {
468
+ const row = body[scroll + i]!;
469
+ const y = f.top + i;
470
+ if (row.inset) f.canvas.fill(f.l - 2, y, f.r - f.l + 5, 1, row.tint ?? t.backgroundElement);
471
+ if (row.cursor) {
472
+ drawRow(f.canvas, y, { spans: row.spans.map((s) => ({ ...s, fg: undefined })), selected: true }, f.l, f.r);
473
+ continue;
474
+ }
475
+ let x = row.inset ? f.l : f.l;
476
+ for (const span of row.spans) {
477
+ if (span.text.startsWith('\u0000')) {
478
+ x = chip(f.canvas, x, y, span.text.slice(1), span.fg ?? t.warning);
479
+ continue;
480
+ }
481
+ x = f.canvas.put(x, y, span.text, { fg: span.fg ?? t.text, bold: span.bold });
482
+ }
483
+ if (row.right) f.canvas.right(f.r, y, row.right.text, { fg: row.right.fg });
484
+ }
485
+ if (body.length > capacity) f.hintRight = scrollCountText(scroll, Math.max(0, body.length - scroll - capacity));
486
+
487
+ let y = scrollBottom + 1;
488
+ if (buttons.length > 0) {
489
+ y++;
490
+ const choice = Math.max(0, Math.min(state.choice ?? 0, buttons.length - 1));
491
+ let bx = f.l;
492
+ buttons.forEach((b, i) => {
493
+ if (bx + buttonWidth(b.label) - 1 > f.r) return;
494
+ bx = button(f.canvas, bx, y, b.label, i === choice, b.id === 'deny' ? 'danger' : 'warning') + 2;
495
+ });
496
+ const detailsHint = state.detailsExpanded ? 'hide details' : 'details';
497
+ if (!state.hunkState && bx + detailsHint.length + 4 <= f.r) {
498
+ const start = f.canvas.right(f.r, y, detailsHint, { fg: t.textFaint });
499
+ f.canvas.put(start - 2, y, 'd', { fg: t.text, bold: true });
500
+ }
501
+ y++;
502
+ }
503
+ if (tierRows.length > 0) {
504
+ y++;
505
+ for (const row of tierRows) {
506
+ if (y > f.bottom) break;
507
+ y += drawRow(f.canvas, y, row, tierX0, tierX1, f.bottom);
508
+ }
509
+ }
510
+ return finishModal(f);
511
+ }