@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.
- package/CHANGELOG.md +65 -1
- package/README.md +30 -29
- package/docs/README.md +8 -5
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +28 -19
- 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-reference.ts +2 -2
- 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/live-verifier.ts +0 -10
- package/src/version.ts +1 -1
- 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/verification/verification-ledger.ts +0 -707
- /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
|
@@ -1,779 +1,375 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* renderSettingsModal, the settings modal drawn with the surface kit.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* ✦ Settings › Display › Theme esc
|
|
5
|
+
*
|
|
6
|
+
* ▏Search all settings display
|
|
7
|
+
*
|
|
8
|
+
* ✦ interface ┌ element inset ─────────────────────────────┐
|
|
9
|
+
* Display 9 ◇ Theme goodvibes
|
|
10
|
+
* UI 4 Color palette for the whole interface…
|
|
11
|
+
* ✦ ai routing display.theme · default goodvibes · …
|
|
12
|
+
* Provider 7 Theme mode dark
|
|
13
|
+
*
|
|
14
|
+
* ↑↓ move ⏎ change ←→ category ctrl+r reset to default
|
|
15
|
+
*
|
|
16
|
+
* Search reaches every setting in every category at once (the search row is
|
|
17
|
+
* always live). Categories on the left, plain rows on the right: name, value,
|
|
18
|
+
* and a ◇ when the value differs from the default. The selected row explains
|
|
19
|
+
* itself in place, wrapped in full, with its key, default, type and source
|
|
20
|
+
* (PgUp/PgDn scroll long documentation). The breadcrumb in the title says
|
|
21
|
+
* where you are. Changes to the theme preview live (see theme-settings-actions).
|
|
6
22
|
*/
|
|
7
23
|
|
|
8
|
-
import type {
|
|
9
|
-
import type { SettingsModal, SettingEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
|
|
24
|
+
import type { SettingsModal, SettingEntry, SettingsCategory } from '../input/settings-modal.ts';
|
|
10
25
|
import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { CVV_PROMPT_TRADEOFF_WARNING } from '@pellux/goodvibes-sdk/platform/payments';
|
|
18
|
-
import { GLYPHS } from './ui-primitives.ts';
|
|
19
|
-
import { formatHints, joinHints } from './hint-grammar.ts';
|
|
26
|
+
import { isFeatureValueEnabled } from '@pellux/goodvibes-terminal-shell';
|
|
27
|
+
import { CATEGORY_LABELS, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
|
|
28
|
+
import { connectionRows } from './settings-modal-connections.ts';
|
|
29
|
+
import { categoryItemCount, currentSettingValue, settingContextLines } from './settings-modal-context.ts';
|
|
30
|
+
import { activeTokens } from './theme.ts';
|
|
31
|
+
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
20
32
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
quiet: 'Minimize operational interruptions and surface fewer Human-in-the-Loop prompts.',
|
|
39
|
-
balanced: 'Show important Human-in-the-Loop prompts without turning routine work into noise.',
|
|
40
|
-
operator: 'Surface more operational detail for users actively supervising agents, tools, services, and automation.',
|
|
41
|
-
},
|
|
42
|
-
'behavior.guidanceMode': {
|
|
43
|
-
off: 'Do not add extra guidance beyond direct command output.',
|
|
44
|
-
minimal: 'Show concise guidance only when it helps avoid mistakes.',
|
|
45
|
-
guided: 'Provide more explanation and next-step context during configuration and operations.',
|
|
46
|
-
},
|
|
47
|
-
'permissions.mode': {
|
|
48
|
-
prompt: 'Normal: ask before powerful or risky actions according to tool policy.',
|
|
49
|
-
plan: 'Plan mode: read-only planning posture; writes, commands, and network calls are blocked so the model can plan without changing anything. Toggle with /plan or Shift+Tab.',
|
|
50
|
-
'accept-edits': 'Accept edits: file writes and edits are auto-approved, but exec, network, and escalations are still gated.',
|
|
51
|
-
'allow-all': 'Auto: allow all actions without prompting. Fast, but removes an important safety gate.',
|
|
52
|
-
custom: 'Use per-tool-class permission settings from the rows below.',
|
|
53
|
-
},
|
|
54
|
-
'permissions.backgroundAgents': {
|
|
55
|
-
inherit: 'Background and subagent tool calls run through the SAME session permission mode as foreground work.',
|
|
56
|
-
'allow-all': 'Background and subagent tool calls are exempt from prompting (auto-approved) even when foreground work is gated.',
|
|
57
|
-
},
|
|
58
|
-
'diagnostics.postEdit': {
|
|
59
|
-
on: 'After the model edits or writes a file, run language diagnostics on it and surface any new problems.',
|
|
60
|
-
off: 'Do not run diagnostics automatically after edits.',
|
|
61
|
-
},
|
|
62
|
-
'storage.secretPolicy': {
|
|
63
|
-
preferred_secure: 'Use secure secret storage when available, with supported fallback behavior.',
|
|
64
|
-
require_secure: 'Require secure secret storage and reject plaintext fallback.',
|
|
65
|
-
plaintext_allowed: 'Allow plaintext fallback when secure storage is unavailable.',
|
|
66
|
-
},
|
|
67
|
-
'batch.mode': {
|
|
68
|
-
off: 'Keep daemon work on the immediate local path.',
|
|
69
|
-
explicit: 'Use batch only when callers explicitly request batch execution.',
|
|
70
|
-
'eligible-by-default': 'Allow eligible daemon work to use the batch path unless callers opt out.',
|
|
71
|
-
},
|
|
72
|
-
'controlPlane.hostMode': {
|
|
73
|
-
localhost: 'Bind only to this computer.',
|
|
74
|
-
network: 'Bind for LAN access using the default network host.',
|
|
75
|
-
custom: 'Use the explicit host value in the related host setting.',
|
|
76
|
-
},
|
|
77
|
-
'httpListener.hostMode': {
|
|
78
|
-
localhost: 'Bind only to this computer.',
|
|
79
|
-
network: 'Bind for LAN/webhook access using the default network host.',
|
|
80
|
-
custom: 'Use the explicit host value in the related host setting.',
|
|
81
|
-
},
|
|
82
|
-
'web.hostMode': {
|
|
83
|
-
localhost: 'Serve the browser UI only on this computer.',
|
|
84
|
-
network: 'Serve the browser UI on the LAN.',
|
|
85
|
-
custom: 'Use the explicit host value in the related host setting.',
|
|
86
|
-
},
|
|
87
|
-
'ui.systemMessages': {
|
|
88
|
-
panel: 'Show system messages in panels only.',
|
|
89
|
-
conversation: 'Show system messages inline in the transcript.',
|
|
90
|
-
both: 'Show system messages in both panels and the transcript.',
|
|
91
|
-
},
|
|
92
|
-
'ui.operationalMessages': {
|
|
93
|
-
panel: 'Show operational messages in panels only.',
|
|
94
|
-
conversation: 'Show operational messages inline in the transcript.',
|
|
95
|
-
both: 'Show operational messages in both panels and the transcript.',
|
|
96
|
-
},
|
|
97
|
-
'ui.wrfcMessages': {
|
|
98
|
-
panel: 'Show WRFC messages in panels only.',
|
|
99
|
-
conversation: 'Show WRFC messages inline in the transcript.',
|
|
100
|
-
both: 'Show WRFC messages in both panels and the transcript.',
|
|
101
|
-
},
|
|
102
|
-
'surfaces.telegram.mode': {
|
|
103
|
-
webhook: 'Receive Telegram updates through webhook delivery.',
|
|
104
|
-
polling: 'Poll Telegram for updates from the service.',
|
|
105
|
-
},
|
|
106
|
-
'surfaces.whatsapp.provider': {
|
|
107
|
-
'meta-cloud': 'Use Meta Cloud API credentials and identifiers.',
|
|
108
|
-
bridge: 'Use a bridge service URL/token flow instead of direct Meta Cloud API delivery.',
|
|
109
|
-
},
|
|
110
|
-
'payments.cvvHandling': {
|
|
111
|
-
stored: 'Store the CVV through the secret manager so the daemon can complete a purchase unattended.',
|
|
112
|
-
prompt: CVV_PROMPT_TRADEOFF_WARNING,
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
function paddedWrapped(text: string, width: number, prefix = ''): string[] {
|
|
117
|
-
const safeWidth = Math.max(1, width - getDisplayWidth(prefix));
|
|
118
|
-
const wrapped = wrapText(text, safeWidth);
|
|
119
|
-
if (prefix.length === 0) return wrapped;
|
|
120
|
-
return wrapped.map((line, index) => `${index === 0 ? prefix : ' '.repeat(getDisplayWidth(prefix))}${line}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function formatDefaultValue(value: unknown): string {
|
|
124
|
-
if (value === '') return '(empty)';
|
|
125
|
-
if (value === null || value === undefined) return '(unset)';
|
|
126
|
-
return String(value);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function currentSettingValue(modal: SettingsModal, entry: SettingEntry, selected: boolean): string {
|
|
130
|
-
if (selected && modal.editingMode) {
|
|
131
|
-
// Secret-backed keys (surfaces.*.botToken, .signingSecret, etc., see
|
|
132
|
-
// secret-config.ts) must never echo the in-progress plaintext buffer:
|
|
133
|
-
// not in the row, not in the "Current: ..." context line, not in search
|
|
134
|
-
// results. Reuse the composer's concealed-input mask (concealed-input.ts)
|
|
135
|
-
// rather than a second masking implementation, same bullet-per-character
|
|
136
|
-
// shape, so keystrokes still visibly register without revealing content.
|
|
137
|
-
const buffer = isSecretConfigKey(entry.setting.key) ? maskConcealedText(modal.editBuffer) : modal.editBuffer;
|
|
138
|
-
return `${buffer}${GLYPHS.surface.cursor}`;
|
|
139
|
-
}
|
|
140
|
-
return formatValue(entry);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function buildSettingContext(modal: SettingsModal, entry: SettingEntry): string[] {
|
|
144
|
-
const lines: string[] = [
|
|
145
|
-
getSettingLabel(entry),
|
|
146
|
-
`Key: ${entry.setting.key}`,
|
|
147
|
-
`Current: ${currentSettingValue(modal, entry, true)}`,
|
|
148
|
-
`Default: ${formatDefaultValue(entry.setting.default)}`,
|
|
149
|
-
`Type: ${entry.setting.type}${entry.setting.enumValues ? ` with ${entry.setting.enumValues.length} possible value(s)` : ''}`,
|
|
150
|
-
`Source: ${entry.effectiveSource ?? 'default'}${entry.sourceLabel ? ` from ${entry.sourceLabel}` : ''}`,
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
if (entry.locked) lines.push(`Locked: ${entry.lockReason ?? 'This setting is locked by a higher-priority layer.'}`);
|
|
154
|
-
if (entry.conflict) lines.push(`Conflict: resolve with /settings-sync resolve ${entry.setting.key} local|synced.`);
|
|
155
|
-
|
|
156
|
-
// A settings sub-row owned by a feature unit names its feature so "what does this do" is answerable without scrolling back to the header row.
|
|
157
|
-
if (entry.ownerFlagId) {
|
|
158
|
-
const owner = FEATURE_SETTINGS_BY_ID.get(entry.ownerFlagId);
|
|
159
|
-
if (owner) lines.push(`Part of feature: ${owner.name} (the header row above).`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
lines.push('', entry.setting.description);
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
entry.setting.key === 'ui.systemMessages'
|
|
166
|
-
|| entry.setting.key === 'ui.operationalMessages'
|
|
167
|
-
|| entry.setting.key === 'ui.wrfcMessages'
|
|
168
|
-
) {
|
|
169
|
-
lines.push(`Routing meaning: ${describeUiRouting(String(entry.currentValue))}.`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (entry.setting.type === 'boolean') {
|
|
173
|
-
lines.push('');
|
|
174
|
-
lines.push('Possible values:');
|
|
175
|
-
lines.push('true: enabled or allowed for this setting.');
|
|
176
|
-
lines.push('false: disabled or not allowed for this setting.');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (entry.setting.type === 'enum' && entry.setting.enumValues) {
|
|
180
|
-
lines.push('');
|
|
181
|
-
lines.push('Possible values:');
|
|
182
|
-
const descriptions = ENUM_VALUE_DESCRIPTIONS[entry.setting.key] ?? {};
|
|
183
|
-
for (const value of entry.setting.enumValues) {
|
|
184
|
-
lines.push(`${value}: ${descriptions[value] ?? `Use ${value} for this setting.`}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (isSecretConfigKey(entry.setting.key)) {
|
|
189
|
-
lines.push('');
|
|
190
|
-
lines.push('Secret handling: raw values entered here are stored through the secret manager and the config receives a goodvibes:// secret reference. Empty input clears the config value.');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (entry.setting.type === 'number') {
|
|
194
|
-
lines.push('');
|
|
195
|
-
lines.push('Editing: Enter opens inline edit, then type the value and press Enter to save. Arrow keys only navigate.');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (entry.setting.type === 'string' && !isSecretConfigKey(entry.setting.key)) {
|
|
199
|
-
lines.push('');
|
|
200
|
-
lines.push('Editing: Enter opens inline edit. Delete the current text to save an empty value when that is valid for the setting.');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return lines;
|
|
204
|
-
}
|
|
33
|
+
beginModal,
|
|
34
|
+
clipText,
|
|
35
|
+
finishModal,
|
|
36
|
+
scrollCountText,
|
|
37
|
+
searchRow,
|
|
38
|
+
tailText,
|
|
39
|
+
wrapLines,
|
|
40
|
+
type KitHint,
|
|
41
|
+
type ModalFrame,
|
|
42
|
+
type SurfaceLayer,
|
|
43
|
+
} from './surface-kit.ts';
|
|
44
|
+
import { drawRow, drawScrollingList, measureRow, rememberStart, rememberedStart, type KitRow } from './surface-kit-list.ts';
|
|
45
|
+
import { inset, type KitInset } from './surface-kit-parts.ts';
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Rows
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
205
50
|
|
|
206
51
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* two positions.
|
|
52
|
+
* Keep a long value from crowding the name out (the documentation shows it in
|
|
53
|
+
* full). While editing, the value gets all the room the label leaves, and a
|
|
54
|
+
* draft longer than that keeps its end (where the cursor is) in view.
|
|
211
55
|
*/
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const lines: string[] = [];
|
|
216
|
-
if (setting.type === 'enum' && setting.enumValues) {
|
|
217
|
-
lines.push('', `Mode choices for ${setting.key}:`);
|
|
218
|
-
const activeValues = feature.enablement.enabledValues ?? [];
|
|
219
|
-
const descriptions = ENUM_VALUE_DESCRIPTIONS[setting.key] ?? {};
|
|
220
|
-
for (const value of setting.enumValues) {
|
|
221
|
-
const marks: string[] = [];
|
|
222
|
-
if (value === String(entry.currentValue)) marks.push('current');
|
|
223
|
-
if (feature.enablement.kind === 'enum') {
|
|
224
|
-
marks.push(activeValues.includes(value) ? 'feature on' : 'feature off');
|
|
225
|
-
}
|
|
226
|
-
const suffix = marks.length > 0 ? ` (${marks.join(', ')})` : '';
|
|
227
|
-
lines.push(`${value}${suffix}: ${descriptions[value] ?? `Use ${value} for this setting.`}`);
|
|
228
|
-
}
|
|
229
|
-
} else if (setting.type === 'boolean') {
|
|
230
|
-
lines.push('');
|
|
231
|
-
lines.push('Possible values:');
|
|
232
|
-
lines.push('true: the feature is enabled.');
|
|
233
|
-
lines.push('false: the feature is disabled.');
|
|
234
|
-
}
|
|
235
|
-
return lines;
|
|
56
|
+
function fitValue(value: string, width: number, editing: boolean, label = ''): string {
|
|
57
|
+
if (editing) return tailText(value, Math.max(8, width - getDisplayWidth(label) - 4));
|
|
58
|
+
return clipText(value, Math.max(8, Math.floor(width * 0.45)));
|
|
236
59
|
}
|
|
237
60
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
...(feature.operable === false && feature.inoperableDetail
|
|
261
|
-
? [`Not available in this build: ${feature.inoperableDetail}`]
|
|
262
|
-
: []),
|
|
263
|
-
`Default: ${feature.defaultEnabled ? 'enabled' : 'disabled'}`,
|
|
264
|
-
`Applies: ${feature.restartRequired ? 'on next launch (startup-gated)' : 'immediately'}`,
|
|
265
|
-
...(pendingRestart
|
|
266
|
-
? [`Pending restart: saved as ${persistedState}; effective state stays ${state} until the next launch.`]
|
|
267
|
-
: []),
|
|
268
|
-
'',
|
|
269
|
-
feature.description,
|
|
270
|
-
];
|
|
271
|
-
|
|
272
|
-
lines.push(...buildFeatureOptionLines(entry));
|
|
273
|
-
|
|
274
|
-
lines.push('');
|
|
275
|
-
if (feature.enablement.kind === 'enum') {
|
|
276
|
-
lines.push(`How it turns on: active while ${feature.enablement.key} is ${(feature.enablement.enabledValues ?? []).join(' or ')}.`);
|
|
277
|
-
} else if (feature.enablement.kind === 'constant' && entry.setting.type !== 'boolean') {
|
|
278
|
-
lines.push('How it turns on: always active; the settings listed below tune its behavior directly.');
|
|
279
|
-
} else {
|
|
280
|
-
lines.push(`How it turns on: ${feature.enablement.key} set to true.`);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (feature.settings.length > 1) {
|
|
284
|
-
lines.push('', 'Settings in this feature:');
|
|
285
|
-
for (const key of feature.settings) {
|
|
286
|
-
lines.push(key === feature.enablement.key ? `${key} (this row)` : key);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (state === 'killed' && flag.killReason) {
|
|
291
|
-
lines.push('', `Kill reason: ${flag.killReason}`);
|
|
61
|
+
function settingRow(modal: SettingsModal, entry: SettingEntry, selected: boolean, focused: boolean, width: number, category?: string): KitRow {
|
|
62
|
+
const t = activeTokens();
|
|
63
|
+
const editing = selected && modal.editingMode;
|
|
64
|
+
const danger = modal.currentCategory === 'danger' && !modal.searchFocused;
|
|
65
|
+
if (entry.flag) {
|
|
66
|
+
const { feature, state, pendingRestart } = entry.flag;
|
|
67
|
+
const configOn = isFeatureValueEnabled(feature, entry.currentValue);
|
|
68
|
+
const raw = currentSettingValue(modal, entry, selected);
|
|
69
|
+
// A startup-gated feature changed this session shows its saved value with
|
|
70
|
+
// a restart-pending marker (⟳); the documentation spells it out.
|
|
71
|
+
const value = pendingRestart ? `${raw} ⟳` : raw;
|
|
72
|
+
return {
|
|
73
|
+
label: feature.name,
|
|
74
|
+
desc: category,
|
|
75
|
+
right: fitValue(value, width, editing, feature.name),
|
|
76
|
+
rightFg: valueColor(entry),
|
|
77
|
+
mark: state === 'killed' ? '✕' : configOn ? '●' : '○',
|
|
78
|
+
markFg: state === 'killed' ? t.error : configOn ? t.success : t.textFaint,
|
|
79
|
+
selected: selected && focused,
|
|
80
|
+
current: selected && !focused,
|
|
81
|
+
labelFg: danger ? t.error : undefined,
|
|
82
|
+
};
|
|
292
83
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
: entry
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
:
|
|
306
|
-
|
|
307
|
-
entry.name,
|
|
308
|
-
`Connection: ${entry.connected ? 'connected' : 'disconnected'}`,
|
|
309
|
-
`Role: ${entry.role}`,
|
|
310
|
-
`Trust mode: ${entry.trustMode}`,
|
|
311
|
-
confirmation,
|
|
312
|
-
'',
|
|
313
|
-
scope,
|
|
314
|
-
'',
|
|
315
|
-
'Trust meanings:',
|
|
316
|
-
'constrained: keep MCP activity inside declared paths/hosts and prompt on risk.',
|
|
317
|
-
'ask-on-risk: allow routine MCP operations but ask before risky behavior.',
|
|
318
|
-
'allow-all: allow unrestricted MCP operations for this server after explicit confirmation.',
|
|
319
|
-
'blocked: prevent this MCP server from being used.',
|
|
320
|
-
];
|
|
84
|
+
const flags = [entry.locked ? 'locked' : '', entry.conflict ? 'conflict' : ''].filter(Boolean).join(' · ');
|
|
85
|
+
const desc = [category, flags].filter(Boolean).join(' · ');
|
|
86
|
+
return {
|
|
87
|
+
// Sub-options of a feature unit read as part of the unit above them.
|
|
88
|
+
label: entry.ownerFlagId ? `· ${getSettingLabel(entry)}` : getSettingLabel(entry),
|
|
89
|
+
desc: desc || undefined,
|
|
90
|
+
right: fitValue(currentSettingValue(modal, entry, selected), width, editing, getSettingLabel(entry)),
|
|
91
|
+
rightFg: valueColor(entry),
|
|
92
|
+
mark: entry.isDefault ? undefined : '◇',
|
|
93
|
+
markFg: t.warning,
|
|
94
|
+
selected: selected && focused,
|
|
95
|
+
current: selected && !focused,
|
|
96
|
+
labelFg: danger ? t.error : undefined,
|
|
97
|
+
};
|
|
321
98
|
}
|
|
322
99
|
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const routeReason = inferSubscriptionRouteReason(entry);
|
|
327
|
-
const logout = entry.state === 'active' || entry.state === 'pending'
|
|
328
|
-
? modal.subscriptionLogoutConfirmationTarget === entry.provider
|
|
329
|
-
? `Sign out ${entry.provider}? Enter/y to confirm, n/Esc to cancel.`
|
|
330
|
-
: 'Press Enter to begin sign-out for this provider session.'
|
|
331
|
-
: `Use /subscription login ${entry.provider} start to begin OAuth sign-in for this provider.`;
|
|
332
|
-
return [
|
|
333
|
-
entry.provider,
|
|
334
|
-
`State: ${entry.state}`,
|
|
335
|
-
...(routeReason ? [routeReason] : []),
|
|
336
|
-
logout,
|
|
337
|
-
`Active route: ${entry.activeRoute ?? 'n/a'}`,
|
|
338
|
-
`Preferred route: ${entry.preferredRoute ?? 'n/a'}`,
|
|
339
|
-
`OAuth configured: ${entry.oauthConfigured ? 'yes' : 'no'}`,
|
|
340
|
-
`Freshness: ${entry.authFreshness ?? 'n/a'}`,
|
|
341
|
-
`Expires: ${expires}`,
|
|
342
|
-
...((entry.issues ?? []).length > 0 ? ['', 'Issues:', ...(entry.issues ?? [])] : []),
|
|
343
|
-
...((entry.nextActions ?? []).length > 0 ? ['', 'Next actions:', ...(entry.nextActions ?? [])] : []),
|
|
344
|
-
];
|
|
100
|
+
function categoryOf(entry: SettingEntry): string {
|
|
101
|
+
const prefix = entry.setting.key.split('.')[0] ?? '';
|
|
102
|
+
return CATEGORY_LABELS[prefix as SettingsCategory] ?? prefix;
|
|
345
103
|
}
|
|
346
104
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
105
|
+
/** The rows of the right-hand list for the current view, and which one is selected. */
|
|
106
|
+
function listRows(modal: SettingsModal, width: number): { rows: KitRow[]; selected: number } {
|
|
107
|
+
const focused = modal.searchFocused || (modal.focusRegion ?? 'settings') === 'settings';
|
|
108
|
+
const clampIndex = (n: number): number => Math.max(0, Math.min(modal.selectedIndex, n - 1));
|
|
351
109
|
if (modal.searchFocused) {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
);
|
|
110
|
+
const results = modal.searchResults;
|
|
111
|
+
if (results.length === 0) return { rows: [], selected: -1 };
|
|
112
|
+
const sel = clampIndex(results.length);
|
|
113
|
+
return { rows: results.map((entry, i) => settingRow(modal, entry, i === sel, true, width, categoryOf(entry))), selected: sel };
|
|
114
|
+
}
|
|
115
|
+
const t = activeTokens();
|
|
116
|
+
switch (modal.currentCategory) {
|
|
117
|
+
case 'mcp': {
|
|
118
|
+
const items = modal.mcpEntries;
|
|
119
|
+
if (items.length === 0) return { rows: [{ label: 'No MCP servers registered.', muted: true }], selected: -1 };
|
|
120
|
+
const sel = clampIndex(items.length);
|
|
121
|
+
return {
|
|
122
|
+
rows: items.map((entry, i) => {
|
|
123
|
+
const scope = entry.allowedPaths.length > 0 ? entry.allowedPaths.join(', ') : entry.allowedHosts.length > 0 ? entry.allowedHosts.join(', ') : 'no scope';
|
|
124
|
+
const trust = i === sel && modal.editingMode ? `${modal.editBuffer}▏` : entry.trustMode;
|
|
125
|
+
return {
|
|
126
|
+
label: entry.name,
|
|
127
|
+
desc: `${entry.role} · ${scope}`,
|
|
128
|
+
right: trust,
|
|
129
|
+
mark: entry.connected ? '●' : '○',
|
|
130
|
+
markFg: entry.connected ? t.success : t.textFaint,
|
|
131
|
+
selected: i === sel && focused,
|
|
132
|
+
current: i === sel && !focused,
|
|
133
|
+
};
|
|
134
|
+
}),
|
|
135
|
+
selected: sel,
|
|
136
|
+
};
|
|
362
137
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (
|
|
366
|
-
|
|
138
|
+
case 'subscriptions': {
|
|
139
|
+
const items = modal.subscriptionEntries;
|
|
140
|
+
if (items.length === 0) return { rows: [{ label: 'No provider subscriptions available or configured.', muted: true }], selected: -1 };
|
|
141
|
+
const sel = clampIndex(items.length);
|
|
142
|
+
return {
|
|
143
|
+
rows: items.map((entry, i) => ({
|
|
144
|
+
label: entry.provider,
|
|
145
|
+
desc: [entry.activeRoute ?? '', entry.authFreshness ?? '', inferSubscriptionRouteReason(entry) ?? ''].filter(Boolean).join(' · ') || undefined,
|
|
146
|
+
right: entry.state,
|
|
147
|
+
rightFg: entry.state === 'active' ? t.success : undefined,
|
|
148
|
+
selected: i === sel && focused,
|
|
149
|
+
current: i === sel && !focused,
|
|
150
|
+
})),
|
|
151
|
+
selected: sel,
|
|
152
|
+
};
|
|
367
153
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (category === 'mcp') {
|
|
376
|
-
lines.push(...buildMcpContext(modal, modal.getSelectedMcp()));
|
|
377
|
-
} else if (category === 'subscriptions') {
|
|
378
|
-
lines.push(...buildSubscriptionContext(modal, modal.getSelectedSubscription()));
|
|
379
|
-
} else if (category === 'connections') {
|
|
380
|
-
lines.push(...buildConnectionContext(modal));
|
|
381
|
-
} else {
|
|
382
|
-
const selected = modal.getSelected();
|
|
383
|
-
// A feature-unit header shows the feature's documentation (full
|
|
384
|
-
// description, option shape, settings list, live/restart state); its
|
|
385
|
-
// settings sub-rows and plain settings show the setting context.
|
|
386
|
-
if (selected?.flag) lines.push(...buildFlagContext(selected));
|
|
387
|
-
else if (selected) lines.push(...buildSettingContext(modal, selected));
|
|
388
|
-
else lines.push('No setting is selected in this category.');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
lines.push('', `Category purpose: ${CATEGORY_INFO[category]}`);
|
|
392
|
-
|
|
393
|
-
const wrapped: string[] = [];
|
|
394
|
-
for (const line of lines) {
|
|
395
|
-
if (line === '') {
|
|
396
|
-
wrapped.push('');
|
|
397
|
-
continue;
|
|
154
|
+
case 'connections':
|
|
155
|
+
return { rows: connectionRows(modal, focused), selected: modal.connectionEntries.length > 0 ? clampIndex(modal.connectionEntries.length) : -1 };
|
|
156
|
+
default: {
|
|
157
|
+
const items = modal.currentItems;
|
|
158
|
+
if (items.length === 0) return { rows: [{ label: 'No settings in this category.', muted: true }], selected: -1 };
|
|
159
|
+
const sel = clampIndex(items.length);
|
|
160
|
+
return { rows: items.map((entry, i) => settingRow(modal, entry, i === sel, focused, width)), selected: sel };
|
|
398
161
|
}
|
|
399
|
-
wrapped.push(...paddedWrapped(line, width));
|
|
400
162
|
}
|
|
401
|
-
return wrapped;
|
|
402
163
|
}
|
|
403
164
|
|
|
404
|
-
function
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (category === 'connections') return modal.connectionEntries.length;
|
|
408
|
-
return modal.groups.get(category)?.length ?? 0;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
type CategoryRailEntry =
|
|
412
|
-
| { readonly type: 'group'; readonly label: string }
|
|
413
|
-
| { readonly type: 'category'; readonly category: SettingsCategory; readonly index: number };
|
|
414
|
-
|
|
415
|
-
type CategoryRailRow = {
|
|
416
|
-
readonly text: string;
|
|
417
|
-
readonly type: CategoryRailEntry['type'] | 'more' | 'empty';
|
|
418
|
-
readonly selected: boolean;
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
function buildCategoryRailEntries(): CategoryRailEntry[] {
|
|
422
|
-
const entries: CategoryRailEntry[] = [];
|
|
165
|
+
function categoryRows(modal: SettingsModal): KitRow[] {
|
|
166
|
+
const focused = !modal.searchFocused && modal.focusRegion === 'categories';
|
|
167
|
+
const rows: KitRow[] = [];
|
|
423
168
|
for (const group of SETTINGS_CATEGORY_GROUPS) {
|
|
424
|
-
const categories = group.categories.filter(category => SETTINGS_CATEGORIES.includes(category));
|
|
169
|
+
const categories = group.categories.filter((category) => SETTINGS_CATEGORIES.includes(category));
|
|
425
170
|
if (categories.length === 0) continue;
|
|
426
|
-
|
|
171
|
+
rows.push({ header: group.label });
|
|
427
172
|
for (const category of categories) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
173
|
+
const active = SETTINGS_CATEGORIES.indexOf(category) === modal.categoryIndex && !modal.searchFocused;
|
|
174
|
+
const count = categoryItemCount(modal, category);
|
|
175
|
+
rows.push({
|
|
176
|
+
label: CATEGORY_LABELS[category],
|
|
177
|
+
right: count > 0 ? String(count) : undefined,
|
|
178
|
+
selected: active && focused,
|
|
179
|
+
current: active && !focused,
|
|
432
180
|
});
|
|
433
181
|
}
|
|
434
182
|
}
|
|
435
|
-
return
|
|
183
|
+
return rows;
|
|
436
184
|
}
|
|
437
185
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Documentation block
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
interface DocLine { readonly text: string; readonly fg: string; readonly bold?: boolean }
|
|
191
|
+
|
|
192
|
+
const FACT_PREFIXES = ['Key:', 'Current:', 'Default:', 'Type:', 'Source:', 'Feature:', 'Setting:', 'Applies:', 'Connection:', 'Role:', 'Trust mode:', 'State:', 'Active route:', 'Preferred route:', 'OAuth configured:', 'Freshness:', 'Expires:'];
|
|
193
|
+
const WARN_PREFIXES = ['Locked:', 'Conflict:', 'Pending restart:', 'Not available in this build:', 'Confirmation required:', 'Kill reason:', 'Secret handling:'];
|
|
194
|
+
|
|
195
|
+
/** Fact lines folded into one compact line under the selected row (the row itself shows the current value). */
|
|
196
|
+
const FOLDED_FACTS: ReadonlyArray<readonly [string, (value: string) => string]> = [
|
|
197
|
+
['Key: ', (v) => v],
|
|
198
|
+
['Default: ', (v) => `default ${v}`],
|
|
199
|
+
['Type: ', (v) => v],
|
|
200
|
+
['Source: ', (v) => `source ${v}`],
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
function docLines(modal: SettingsModal, hasSelection: boolean, width: number): DocLine[] {
|
|
204
|
+
const t = activeTokens();
|
|
205
|
+
const source = settingContextLines(modal);
|
|
206
|
+
// With a selected row, the first line (its name) is already on the row.
|
|
207
|
+
const lines = hasSelection ? source.slice(1) : source;
|
|
208
|
+
const facts: string[] = [];
|
|
209
|
+
const rest: string[] = [];
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
if (line.startsWith('Current: ')) continue;
|
|
212
|
+
const fact = FOLDED_FACTS.find(([prefix]) => line.startsWith(prefix));
|
|
213
|
+
if (fact) facts.push(fact[1](line.slice(fact[0].length)));
|
|
214
|
+
else rest.push(line);
|
|
215
|
+
}
|
|
216
|
+
const out: DocLine[] = [];
|
|
217
|
+
if (facts.length > 0) for (const wrapped of wrapLines(facts.join(' · '), width)) out.push({ text: wrapped, fg: t.textFaint });
|
|
218
|
+
for (const line of rest) {
|
|
219
|
+
if (line === '') {
|
|
220
|
+
if (out.length > 0 && out[out.length - 1]!.text !== '') out.push({ text: '', fg: t.textMuted });
|
|
448
221
|
continue;
|
|
449
222
|
}
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
223
|
+
const fg = WARN_PREFIXES.some((p) => line.startsWith(p)) ? t.warning
|
|
224
|
+
: FACT_PREFIXES.some((p) => line.startsWith(p)) ? t.textFaint
|
|
225
|
+
: t.textMuted;
|
|
226
|
+
const bold = line.endsWith(':');
|
|
227
|
+
for (const wrapped of wrapLines(line, width)) out.push({ text: wrapped, fg: bold ? t.text : fg, bold });
|
|
455
228
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return rows.slice(0, height);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function renderSettingRows(modal: SettingsModal, width: number, height: number): string[] {
|
|
462
|
-
const rows: string[] = [];
|
|
463
|
-
const items = modal.currentItems;
|
|
464
|
-
if (items.length === 0) return ['No settings in this category.'];
|
|
465
|
-
const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
|
|
466
|
-
const typeWidth = 9;
|
|
467
|
-
const sourceWidth = 12;
|
|
468
|
-
const defaultWidth = 12;
|
|
469
|
-
const available = Math.max(24, width - typeWidth - sourceWidth - defaultWidth - 13);
|
|
470
|
-
const keyWidth = clamp(Math.floor(available * 0.56), 18, 52);
|
|
471
|
-
const valueWidth = Math.max(10, available - keyWidth);
|
|
472
|
-
rows.push(` ${padDisplay('Setting', keyWidth)} ${padDisplay('Value', valueWidth)} ${padDisplay('Type', typeWidth)} ${padDisplay('Source', sourceWidth)} ${padDisplay('Default', defaultWidth)}`);
|
|
473
|
-
const visibleCount = Math.max(1, height - 2);
|
|
474
|
-
const window = stableWindow(items.length, selectedIndex, visibleCount);
|
|
475
|
-
if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more setting(s) above`);
|
|
476
|
-
|
|
477
|
-
for (let index = window.start; index < window.end; index += 1) {
|
|
478
|
-
const entry = items[index]!;
|
|
479
|
-
const selected = index === selectedIndex;
|
|
480
|
-
const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '•') : entry.isDefault ? ' ' : '◇';
|
|
481
|
-
rows.push(renderSettingTableRow(modal, entry, selected, marker, keyWidth, valueWidth, typeWidth, sourceWidth, defaultWidth));
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more setting(s) below`);
|
|
485
|
-
return rows.slice(0, height);
|
|
229
|
+
while (out.length > 0 && out[out.length - 1]!.text === '') out.pop();
|
|
230
|
+
return out;
|
|
486
231
|
}
|
|
487
232
|
|
|
488
233
|
/**
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
* sub-option owned by a feature unit (entry.ownerFlagId) is indented under its
|
|
492
|
-
* header. Everything else renders as a plain setting row.
|
|
234
|
+
* The complete documentation the selected row explains itself with (its name
|
|
235
|
+
* first), as the wrapped lines the renderer windows over with PgUp/PgDn.
|
|
493
236
|
*/
|
|
494
|
-
function
|
|
495
|
-
modal
|
|
496
|
-
entry: SettingEntry,
|
|
497
|
-
selected: boolean,
|
|
498
|
-
marker: string,
|
|
499
|
-
keyWidth: number,
|
|
500
|
-
valueWidth: number,
|
|
501
|
-
typeWidth: number,
|
|
502
|
-
sourceWidth: number,
|
|
503
|
-
defaultWidth: number,
|
|
504
|
-
): string {
|
|
505
|
-
if (entry.flag) {
|
|
506
|
-
const { feature, state, pendingRestart } = entry.flag;
|
|
507
|
-
const configOn = isFeatureValueEnabled(feature, entry.currentValue);
|
|
508
|
-
const stateMark = state === 'killed' ? '✕' : configOn ? '●' : '○';
|
|
509
|
-
const label = `${stateMark} ${feature.name}`;
|
|
510
|
-
const source = feature.restartRequired ? 'restart' : 'live';
|
|
511
|
-
// A startup-gated feature changed this session shows its saved value with
|
|
512
|
-
// a compact restart-pending marker (⟳), so the row never implies the
|
|
513
|
-
// change already took effect; the documentation pane spells out the full
|
|
514
|
-
// "Pending restart: ..." sentence.
|
|
515
|
-
const rawValue = currentSettingValue(modal, entry, selected);
|
|
516
|
-
const value = pendingRestart ? `${rawValue} ⟳` : rawValue;
|
|
517
|
-
return `${marker} ${padDisplay(label, keyWidth)} ${padDisplay(value, valueWidth)} ${padDisplay('feature', typeWidth)} ${padDisplay(source, sourceWidth)} ${padDisplay(feature.defaultEnabled ? 'enabled' : 'disabled', defaultWidth)}`;
|
|
518
|
-
}
|
|
519
|
-
const value = currentSettingValue(modal, entry, selected);
|
|
520
|
-
const source = `${entry.effectiveSource ?? 'default'}${entry.locked ? ' locked' : ''}${entry.conflict ? ' conflict' : ''}`;
|
|
521
|
-
// Sub-options of a feature unit are indented under their toggle header so the
|
|
522
|
-
// "one unit = toggle + its knobs" grouping reads at a glance.
|
|
523
|
-
const label = entry.ownerFlagId ? ` · ${getSettingLabel(entry)}` : getSettingLabel(entry);
|
|
524
|
-
return `${marker} ${padDisplay(label, keyWidth)} ${padDisplay(value, valueWidth)} ${padDisplay(entry.setting.type, typeWidth)} ${padDisplay(source, sourceWidth)} ${padDisplay(formatDefaultValue(entry.setting.default), defaultWidth)}`;
|
|
237
|
+
export function settingsDocumentation(modal: SettingsModal, width: number): string[] {
|
|
238
|
+
return docLines(modal, false, width).map((line) => line.text);
|
|
525
239
|
}
|
|
526
240
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const window = stableWindow(items.length, selectedIndex, Math.max(1, height - 2));
|
|
539
|
-
if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more MCP server(s) above`);
|
|
540
|
-
for (let index = window.start; index < window.end; index += 1) {
|
|
541
|
-
const entry = items[index]!;
|
|
542
|
-
const selected = index === selectedIndex;
|
|
543
|
-
const trust = selected && modal.editingMode ? `${modal.editBuffer}${GLYPHS.surface.cursor}` : entry.trustMode;
|
|
544
|
-
const scope = entry.allowedPaths.length > 0 ? entry.allowedPaths.join(', ') : entry.allowedHosts.length > 0 ? entry.allowedHosts.join(', ') : 'none';
|
|
545
|
-
const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '•') : ' ';
|
|
546
|
-
rows.push(`${marker} ${padDisplay(entry.name, nameWidth)} ${padDisplay(trust, trustWidth)} ${padDisplay(entry.role, roleWidth)} ${padDisplay(entry.connected ? 'connected' : 'offline', statusWidth)} ${padDisplay(scope, scopeWidth)}`);
|
|
547
|
-
}
|
|
548
|
-
if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more MCP server(s) below`);
|
|
549
|
-
return rows.slice(0, height);
|
|
241
|
+
/** Window the documentation by modal.contextScroll, marking hidden lines honestly. */
|
|
242
|
+
function windowDoc(modal: SettingsModal, lines: DocLine[], budget: number): DocLine[] {
|
|
243
|
+
const t = activeTokens();
|
|
244
|
+
if (lines.length <= budget) return lines;
|
|
245
|
+
const maxOffset = Math.max(0, lines.length - budget);
|
|
246
|
+
const offset = Math.max(0, Math.min(modal.contextScroll ?? 0, maxOffset));
|
|
247
|
+
const shown = lines.slice(offset, offset + budget);
|
|
248
|
+
if (offset > 0) shown[0] = { text: `${offset} more ↑ · pgup`, fg: t.textFaint };
|
|
249
|
+
const below = lines.length - offset - budget;
|
|
250
|
+
if (below > 0) shown[shown.length - 1] = { text: `${below} more ↓ · pgdn`, fg: t.textFaint };
|
|
251
|
+
return shown;
|
|
550
252
|
}
|
|
551
253
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (items.length === 0) return ['No provider subscriptions available or configured.'];
|
|
556
|
-
const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
|
|
557
|
-
const providerWidth = clamp(Math.floor(width * 0.28), 14, 36);
|
|
558
|
-
const stateWidth = 10;
|
|
559
|
-
const routeWidth = 16;
|
|
560
|
-
const freshnessWidth = 14;
|
|
561
|
-
const oauthWidth = 8;
|
|
562
|
-
const noteWidth = Math.max(12, width - providerWidth - stateWidth - routeWidth - freshnessWidth - oauthWidth - 12);
|
|
563
|
-
rows.push(` ${padDisplay('Provider', providerWidth)} ${padDisplay('State', stateWidth)} ${padDisplay('Route', routeWidth)} ${padDisplay('Freshness', freshnessWidth)} ${padDisplay('OAuth', oauthWidth)} ${padDisplay('Note', noteWidth)}`);
|
|
564
|
-
const window = stableWindow(items.length, selectedIndex, Math.max(1, height - 2));
|
|
565
|
-
if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more subscription provider(s) above`);
|
|
566
|
-
for (let index = window.start; index < window.end; index += 1) {
|
|
567
|
-
const entry = items[index]!;
|
|
568
|
-
const selected = index === selectedIndex;
|
|
569
|
-
const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '•') : ' ';
|
|
570
|
-
rows.push(`${marker} ${padDisplay(entry.provider, providerWidth)} ${padDisplay(entry.state, stateWidth)} ${padDisplay(entry.activeRoute ?? 'n/a', routeWidth)} ${padDisplay(entry.authFreshness ?? 'n/a', freshnessWidth)} ${padDisplay(entry.oauthConfigured ? 'yes' : 'no', oauthWidth)} ${padDisplay(inferSubscriptionRouteReason(entry) ?? '', noteWidth)}`);
|
|
571
|
-
}
|
|
572
|
-
if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more subscription provider(s) below`);
|
|
573
|
-
return rows.slice(0, height);
|
|
574
|
-
}
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Right side: the settings column
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
575
257
|
|
|
576
|
-
function
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
rows.push(promptRow);
|
|
258
|
+
function drawSettingsColumn(f: ModalFrame, modal: SettingsModal, p: KitInset): { above: number; below: number } {
|
|
259
|
+
const t = activeTokens();
|
|
260
|
+
const sx = p.x + 4;
|
|
261
|
+
const sr = p.x + p.w - 5;
|
|
262
|
+
const width = sr - sx + 1;
|
|
582
263
|
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
264
|
+
const notices = [
|
|
265
|
+
...(modal.lastSaveTriggeredRestart ? [`Restarting ${modal.lastSaveTriggeredRestart}`] : []),
|
|
266
|
+
...(modal.lastSettingEffectMessage ? [modal.lastSettingEffectMessage] : []),
|
|
267
|
+
].flatMap((n) => wrapLines(n, width));
|
|
268
|
+
const bottom = notices.length > 0 ? p.bottom - notices.length - 1 : p.bottom;
|
|
269
|
+
notices.forEach((line, k) => f.canvas.put(sx, bottom + 2 + k, line, { fg: t.accent }));
|
|
270
|
+
|
|
271
|
+
const { rows, selected } = listRows(modal, width);
|
|
272
|
+
const capacity = Math.max(1, bottom - p.top + 1);
|
|
273
|
+
const doc = docLines(modal, selected >= 0, width);
|
|
274
|
+
|
|
275
|
+
if (selected < 0) {
|
|
276
|
+
let y = p.top;
|
|
277
|
+
for (const row of rows) y += drawRow(f.canvas, y, row, sx, sr, bottom);
|
|
278
|
+
if (rows.length > 0) y++;
|
|
279
|
+
for (const line of windowDoc(modal, doc, Math.max(1, bottom - y + 1))) {
|
|
280
|
+
if (y > bottom) break;
|
|
281
|
+
f.canvas.put(sx, y++, line.text, { fg: line.fg, bold: line.bold });
|
|
282
|
+
}
|
|
283
|
+
return { above: 0, below: 0 };
|
|
587
284
|
}
|
|
588
285
|
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
286
|
+
const heights = rows.map((row) => measureRow(row, sx, sr));
|
|
287
|
+
// The documentation takes what the rows leave, but never less than a few lines.
|
|
288
|
+
const rowsTotal = heights.reduce((a, b) => a + b, 0);
|
|
289
|
+
const docBudget = Math.max(3, Math.min(doc.length, capacity - Math.min(rowsTotal, Math.max(3, Math.floor(capacity * 0.35))) - 2));
|
|
290
|
+
const shownDoc = windowDoc(modal, doc, docBudget);
|
|
291
|
+
const blockH = shownDoc.length > 0 ? shownDoc.length + 2 : 0;
|
|
292
|
+
const rowCapacity = Math.max(1, capacity - blockH);
|
|
293
|
+
|
|
294
|
+
// Scroll so the selected row (and its block) is in view; the window only
|
|
295
|
+
// moves when the selection leaves it.
|
|
296
|
+
const listName = modal.searchFocused ? 'search' : `category:${modal.currentCategory}`;
|
|
297
|
+
const scrollKey = { owner: modal, name: listName };
|
|
298
|
+
let start = Math.max(0, Math.min(rememberedStart(scrollKey) ?? 0, rows.length - 1));
|
|
299
|
+
const span = (from: number, to: number): number => heights.slice(from, to + 1).reduce((a, b) => a + b, 0);
|
|
300
|
+
if (selected < start) start = selected;
|
|
301
|
+
while (start < selected && span(start, selected) > rowCapacity) start++;
|
|
302
|
+
while (start > 0 && span(start - 1, rows.length - 1) <= rowCapacity) start--;
|
|
303
|
+
rememberStart(scrollKey, start);
|
|
304
|
+
|
|
305
|
+
let y = p.top;
|
|
306
|
+
let last = start - 1;
|
|
307
|
+
for (let i = start; i < rows.length; i++) {
|
|
308
|
+
if (y + heights[i]! - 1 > bottom) break;
|
|
309
|
+
if (i !== selected && i > selected && y + heights[i]! - 1 > bottom) break;
|
|
310
|
+
y += drawRow(f.canvas, y, rows[i]!, sx, sr, bottom);
|
|
311
|
+
last = i;
|
|
312
|
+
if (i === selected && shownDoc.length > 0) {
|
|
313
|
+
y++;
|
|
314
|
+
for (const line of shownDoc) {
|
|
315
|
+
if (y > bottom) break;
|
|
316
|
+
f.canvas.put(sx, y++, line.text, { fg: line.fg, bold: line.bold });
|
|
317
|
+
}
|
|
318
|
+
y++;
|
|
319
|
+
}
|
|
613
320
|
}
|
|
614
|
-
|
|
615
|
-
if (window.end < results.length) rows.push(`${GLYPHS.navigation.moreBelow} ${results.length - window.end} more result(s) below`);
|
|
616
|
-
return rows.slice(0, height);
|
|
321
|
+
return { above: start, below: Math.max(0, rows.length - last - 1) };
|
|
617
322
|
}
|
|
618
323
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Hints
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
function settingsHints(modal: SettingsModal): KitHint[] {
|
|
329
|
+
if (modal.resetCategoryConfirm !== null || modal.resetAllConfirm !== null) return [['⏎', 'confirm reset'], ['esc', 'cancel']];
|
|
330
|
+
if (modal.subscriptionLogoutConfirmationTarget) return [['⏎', 'sign out'], ['esc', 'cancel']];
|
|
331
|
+
if (modal.editingMode) return [['⏎', 'save'], ['esc', 'cancel edit']];
|
|
332
|
+
if (modal.searchFocused) return [['↑↓', 'move'], ['⏎', 'change'], ['⌫', 'edit search'], ['ctrl+r', 'reset']];
|
|
333
|
+
if (modal.focusRegion === 'categories') return [['↑↓', 'category'], ['→', 'settings'], ['tab', 'pane']];
|
|
334
|
+
const enter: KitHint = modal.currentCategory === 'mcp' ? ['⏎', 'edit trust']
|
|
335
|
+
: modal.currentCategory === 'subscriptions' ? ['⏎', 'review or sign out']
|
|
336
|
+
: ['⏎', 'change'];
|
|
337
|
+
return [['↑↓', 'move'], enter, ['←→', 'category'], ['pgup pgdn', 'docs'], ['ctrl+r', 'reset'], ['shift+r', 'reset category'], ['ctrl+shift+r', 'reset all']];
|
|
625
338
|
}
|
|
626
339
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return joinHints('Focus categories', formatHints([
|
|
658
|
-
{ key: 'Up/Down', verb: 'Choose' },
|
|
659
|
-
{ key: 'Right/Enter', verb: 'Settings' },
|
|
660
|
-
{ key: 'Tab', verb: 'Pane' },
|
|
661
|
-
{ key: '/', verb: 'Search' },
|
|
662
|
-
{ key: 'Esc', verb: 'Close' },
|
|
663
|
-
]));
|
|
664
|
-
if (modal.currentCategory === 'subscriptions')
|
|
665
|
-
return joinHints('Focus settings', formatHints([
|
|
666
|
-
{ key: 'Up/Down', verb: 'Provider' },
|
|
667
|
-
{ key: 'Left', verb: 'Categories' },
|
|
668
|
-
{ key: 'Tab', verb: 'Pane' },
|
|
669
|
-
{ key: '/', verb: 'Search' },
|
|
670
|
-
{ key: 'Enter', verb: 'Review/sign out' },
|
|
671
|
-
{ key: 'Esc', verb: 'Close' },
|
|
672
|
-
]));
|
|
673
|
-
if (modal.currentCategory === 'mcp')
|
|
674
|
-
return joinHints('Focus settings', formatHints([
|
|
675
|
-
{ key: 'Up/Down', verb: 'Server' },
|
|
676
|
-
{ key: 'Left', verb: 'Categories' },
|
|
677
|
-
{ key: 'Tab', verb: 'Pane' },
|
|
678
|
-
{ key: '/', verb: 'Search' },
|
|
679
|
-
{ key: 'Enter', verb: 'Edit trust' },
|
|
680
|
-
{ key: 'Esc', verb: 'Close' },
|
|
681
|
-
]));
|
|
682
|
-
// Every topical category that hosts feature units flows through the default
|
|
683
|
-
// settings footer below, Enter/Space toggles a feature-unit header exactly
|
|
684
|
-
// as it edits/toggles any setting row.
|
|
685
|
-
// Default settings pane: tier the reset affordances by available width.
|
|
686
|
-
// W<80: minimal, only the most critical action survives.
|
|
687
|
-
// W<160: compact but still shows both reset affordances.
|
|
688
|
-
// W≥160: standard with all navigation tokens.
|
|
689
|
-
if (width < 80)
|
|
690
|
-
return formatHints([{ key: 'R', verb: 'reset' }, { key: 'Esc', verb: 'Close' }]);
|
|
691
|
-
if (width < 160)
|
|
692
|
-
return formatHints([
|
|
693
|
-
{ key: 'Up/Down', verb: 'Move' },
|
|
694
|
-
{ key: 'Enter/Space', verb: 'Edit' },
|
|
695
|
-
{ key: 'PgUp/PgDn', verb: 'docs' },
|
|
696
|
-
{ key: '⇧R', verb: 'reset cat' },
|
|
697
|
-
{ key: '^⇧R', verb: 'reset all' },
|
|
698
|
-
{ key: 'Esc', verb: 'Close' },
|
|
699
|
-
]);
|
|
700
|
-
return joinHints('Focus settings', formatHints([
|
|
701
|
-
{ key: 'Up/Down', verb: 'Setting' },
|
|
702
|
-
{ key: 'Left', verb: 'Categories' },
|
|
703
|
-
{ key: 'Enter/Space', verb: 'Edit/toggle' },
|
|
704
|
-
{ key: 'PgUp/PgDn', verb: 'Scroll docs' },
|
|
705
|
-
{ key: '⇧R', verb: 'reset cat' },
|
|
706
|
-
{ key: '^⇧R', verb: 'reset all' },
|
|
707
|
-
{ key: 'Esc', verb: 'Close' },
|
|
708
|
-
]));
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
export function renderSettingsModal(
|
|
712
|
-
modal: SettingsModal,
|
|
713
|
-
width: number,
|
|
714
|
-
viewportHeight = 24,
|
|
715
|
-
): Line[] {
|
|
716
|
-
const notices = [
|
|
717
|
-
...(modal.lastSaveTriggeredRestart ? [`Restarting ${modal.lastSaveTriggeredRestart}`] : []),
|
|
718
|
-
...(modal.lastSettingEffectMessage ? [modal.lastSettingEffectMessage] : []),
|
|
719
|
-
];
|
|
720
|
-
const metrics = getFullscreenWorkspaceMetrics({ width, height: viewportHeight });
|
|
721
|
-
const categoryRows = renderCategories(modal, metrics.leftWidth - 2, metrics.bodyRows);
|
|
722
|
-
// Documentation pane: long content SCROLLS (PgUp/PgDn) with honest
|
|
723
|
-
// more-above/below markers, it is never silently clipped.
|
|
724
|
-
const allContextLines = buildContextLines(modal, metrics.contextWidth);
|
|
725
|
-
const visibleContext = metrics.contextRows;
|
|
726
|
-
const maxContextScroll = Math.max(0, allContextLines.length - visibleContext);
|
|
727
|
-
const contextOffset = Math.min(Math.max(0, modal.contextScroll ?? 0), maxContextScroll);
|
|
728
|
-
const windowedContext = allContextLines.slice(contextOffset, contextOffset + visibleContext);
|
|
729
|
-
if (contextOffset > 0) {
|
|
730
|
-
windowedContext[0] = `${GLYPHS.navigation.moreAbove} ${contextOffset} more line(s) above; PgUp`;
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Renderer
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
/** Below this many text columns the category list folds into the breadcrumb (←→ still switch). */
|
|
345
|
+
const MIN_LIST_DETAIL_WIDTH = 56;
|
|
346
|
+
|
|
347
|
+
export function renderSettingsModal(modal: SettingsModal, screenWidth: number, screenHeight = 24): SurfaceLayer {
|
|
348
|
+
const selectedEntry = !modal.searchFocused && modal.currentCategory !== 'mcp' && modal.currentCategory !== 'subscriptions' && modal.currentCategory !== 'connections'
|
|
349
|
+
? modal.getSelected()
|
|
350
|
+
: null;
|
|
351
|
+
const crumbs = modal.searchFocused
|
|
352
|
+
? ['Search']
|
|
353
|
+
: [CATEGORY_LABELS[modal.currentCategory], ...(selectedEntry ? [selectedEntry.flag ? selectedEntry.flag.feature.name : getSettingLabel(selectedEntry)] : [])];
|
|
354
|
+
const f = beginModal(screenWidth, screenHeight, { title: 'Settings', crumbs, hints: settingsHints(modal) });
|
|
355
|
+
|
|
356
|
+
const count = modal.searchFocused
|
|
357
|
+
? `${modal.searchResults.length} result${modal.searchResults.length === 1 ? '' : 's'}`
|
|
358
|
+
: CATEGORY_LABELS[modal.currentCategory].toLowerCase();
|
|
359
|
+
searchRow(f, f.top, modal.searchQuery, 'Search all settings', count);
|
|
360
|
+
|
|
361
|
+
const body = f.top + 2;
|
|
362
|
+
const inner = f.r - f.l + 1;
|
|
363
|
+
let detailX = 2;
|
|
364
|
+
// Search spans every category, so the category list steps aside for it.
|
|
365
|
+
if (inner >= MIN_LIST_DETAIL_WIDTH && !modal.searchFocused) {
|
|
366
|
+
const catW = Math.max(18, Math.min(26, Math.round(inner * 0.22)));
|
|
367
|
+
const x1 = f.l + catW - 1;
|
|
368
|
+
drawScrollingList(f.canvas, { rows: categoryRows(modal), top: body, bottom: f.bottom, x0: f.l, x1, scrollKey: { owner: modal, name: 'categories' } });
|
|
369
|
+
detailX = x1 + 3;
|
|
731
370
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const contextRows = windowedContext.map((text, row): WorkspaceRow => {
|
|
737
|
-
const selectedSetting = modal.getSelected();
|
|
738
|
-
const isTitle = (row === 0 && contextOffset === 0) || (selectedSetting !== null && text === getSettingLabel(selectedSetting));
|
|
739
|
-
return {
|
|
740
|
-
text,
|
|
741
|
-
fg: row === 0 && contextOffset === 0 ? PALETTE.title : text.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
|
|
742
|
-
bold: isTitle,
|
|
743
|
-
dim: text.length === 0,
|
|
744
|
-
};
|
|
745
|
-
});
|
|
746
|
-
const controlRows = renderControlRows(modal, metrics.contextWidth, metrics.controlRows).map((text): WorkspaceRow => {
|
|
747
|
-
const selected = text.startsWith(GLYPHS.navigation.selected);
|
|
748
|
-
return {
|
|
749
|
-
text,
|
|
750
|
-
selected,
|
|
751
|
-
fg: selected
|
|
752
|
-
? PALETTE.text
|
|
753
|
-
: text.startsWith('value:') || text.trimStart().startsWith('value:')
|
|
754
|
-
? PALETTE.info
|
|
755
|
-
: rowColorForSetting(modal, text),
|
|
756
|
-
bold: selected,
|
|
757
|
-
dim: text.length === 0,
|
|
758
|
-
};
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
return renderFullscreenWorkspace({
|
|
762
|
-
width,
|
|
763
|
-
height: viewportHeight,
|
|
764
|
-
title: 'Configuration Workspace / Settings',
|
|
765
|
-
leftHeader: 'Categories',
|
|
766
|
-
mainHeader: modal.searchFocused
|
|
767
|
-
? `Search: ${modal.searchQuery || '…'} (${modal.searchResults.length} result${modal.searchResults.length === 1 ? '' : 's'})${notices.length > 0 ? ` · ${notices.join(' · ')}` : ''}`
|
|
768
|
-
: `${CATEGORY_LABELS[modal.currentCategory]} (${categoryItemCount(modal, modal.currentCategory)})${notices.length > 0 ? ` · ${notices.join(' · ')}` : ''}`,
|
|
769
|
-
leftRows: categoryRows.map((row): WorkspaceRow => ({
|
|
770
|
-
text: row.text,
|
|
771
|
-
selected: row.selected,
|
|
772
|
-
kind: row.type === 'group' ? 'group' : row.type === 'more' ? 'more' : row.type === 'empty' ? 'empty' : 'item',
|
|
773
|
-
bold: row.selected || row.type === 'group',
|
|
774
|
-
})),
|
|
775
|
-
contextRows,
|
|
776
|
-
controlRows,
|
|
777
|
-
footer: footerText(modal, width),
|
|
778
|
-
});
|
|
371
|
+
const p = inset(f.canvas, detailX, body, f.r + 2 - detailX + 1, f.bottom - body + 1);
|
|
372
|
+
const hidden = drawSettingsColumn(f, modal, p);
|
|
373
|
+
f.hintRight = scrollCountText(hidden.above, hidden.below);
|
|
374
|
+
return finishModal(f);
|
|
779
375
|
}
|