@pellux/goodvibes-tui 2.0.20 → 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.
- package/CHANGELOG.md +70 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +59 -4
- package/package.json +16 -6
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/verification-ledger.ts +18 -16
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /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
|
|
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
|
+
}
|