@pellux/goodvibes-tui 2.0.21 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- 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
|
@@ -31,18 +31,18 @@
|
|
|
31
31
|
* 2. The window is one the provider vouched for, not the fallback constant.
|
|
32
32
|
* A model whose real window happens to equal the fallback is
|
|
33
33
|
* indistinguishable from "not resolved yet", and in that ambiguity
|
|
34
|
-
* staying quiet is the honest choice, the
|
|
34
|
+
* staying quiet is the honest choice, the Usage modal and /context
|
|
35
35
|
* still show the real numbers either way.
|
|
36
36
|
* Neither guard touches 'compacting', which reports work that is provably
|
|
37
37
|
* running rather than a prediction about headroom.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { DEFAULT_CONTEXT_WINDOW } from '@pellux/goodvibes-sdk/platform/providers';
|
|
41
|
-
import type {
|
|
41
|
+
import type { ViewSessionMaintenanceLevel } from '../views/session-maintenance.ts';
|
|
42
42
|
|
|
43
43
|
interface ContextStatusHintOptions {
|
|
44
44
|
/** Maintenance level from evaluateSessionMaintenance. */
|
|
45
|
-
readonly level:
|
|
45
|
+
readonly level: ViewSessionMaintenanceLevel;
|
|
46
46
|
/** Whether auto-compaction is active (threshold > 0 in config). */
|
|
47
47
|
readonly autoCompactEnabled: boolean;
|
|
48
48
|
/** Current usage percent 0–100. */
|
|
@@ -104,7 +104,7 @@ function buildContextStatusHint(options: ContextStatusHintOptions): string | nul
|
|
|
104
104
|
*/
|
|
105
105
|
export function resolveContextStatusHint(input: {
|
|
106
106
|
readonly evaluate: (args: { readonly currentTokens: number; readonly contextWindow: number }) => {
|
|
107
|
-
readonly level:
|
|
107
|
+
readonly level: ViewSessionMaintenanceLevel;
|
|
108
108
|
readonly autoCompactEnabled: boolean;
|
|
109
109
|
readonly usagePct: number;
|
|
110
110
|
};
|
|
@@ -81,3 +81,23 @@ export function replaceViewportWithOverlay(
|
|
|
81
81
|
next.push(...overlay);
|
|
82
82
|
return next;
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Center the non-blank rows of a viewport vertically (the home splash). A
|
|
87
|
+
* body that already fills the height is returned unchanged.
|
|
88
|
+
*/
|
|
89
|
+
export function centerViewportContent(viewport: readonly Line[], height: number, width: number): Line[] {
|
|
90
|
+
const isBlank = (line: Line): boolean => line.every((cell) => cell.char === ' ' || cell.char === '');
|
|
91
|
+
const first = viewport.findIndex((line) => !isBlank(line));
|
|
92
|
+
if (first < 0) return [...viewport];
|
|
93
|
+
let last = viewport.length - 1;
|
|
94
|
+
while (last > first && isBlank(viewport[last]!)) last--;
|
|
95
|
+
const body = viewport.slice(first, last + 1);
|
|
96
|
+
if (body.length >= height) return [...viewport];
|
|
97
|
+
const top = Math.floor((height - body.length) / 2);
|
|
98
|
+
const out: Line[] = [];
|
|
99
|
+
for (let i = 0; i < top; i++) out.push(createEmptyLine(width));
|
|
100
|
+
out.push(...body);
|
|
101
|
+
while (out.length < height) out.push(createEmptyLine(width));
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
@@ -20,6 +20,10 @@ import { renderBlockActionsMenu } from './block-actions-overlay.ts';
|
|
|
20
20
|
import { renderAutocompleteOverlay } from './autocomplete-overlay.ts';
|
|
21
21
|
import { renderOnboardingWizard } from './onboarding/onboarding-wizard.ts';
|
|
22
22
|
import { overlayViewportBottom, replaceViewportWithOverlay } from './conversation-layout.ts';
|
|
23
|
+
import type { SurfaceLayer } from './surface-kit.ts';
|
|
24
|
+
import { layerToLines } from './surface-kit-extra.ts';
|
|
25
|
+
import { renderToasts } from './surface-kit-parts.ts';
|
|
26
|
+
import { getSharedToastCenter } from './toast-center.ts';
|
|
23
27
|
|
|
24
28
|
export interface ConversationOverlayContext {
|
|
25
29
|
readonly input: InputHandler;
|
|
@@ -31,44 +35,39 @@ export interface ConversationOverlayContext {
|
|
|
31
35
|
readonly contextWindow?: number;
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/** True while a modal covers the screen, so the composer-anchored popups stay hidden. */
|
|
39
|
+
function modalOpen(input: InputHandler): boolean {
|
|
40
|
+
return input.modelPicker.active || input.settingsModal.active || input.configModal.active || input.mcpWorkspace.active
|
|
41
|
+
|| input.sessionPickerModal.active || input.profilePickerModal.active || input.bookmarkModal.active
|
|
42
|
+
|| input.blockActionsMenu.active || input.selectionModal.active || input.contextInspectorModal.active
|
|
43
|
+
|| input.helpOverlayActive || input.shortcutsOverlayActive || input.surfaceModals.active;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The parts of the conversation area that are rows rather than modals:
|
|
48
|
+
* onboarding (which owns the whole viewport), the @ and slash popups anchored
|
|
49
|
+
* to the composer (never dimmed: you are still typing), and the search bars.
|
|
50
|
+
* Every modal is a layer instead, see buildConversationLayers.
|
|
51
|
+
*/
|
|
34
52
|
export function applyConversationOverlays(
|
|
35
53
|
viewport: Line[],
|
|
36
54
|
context: ConversationOverlayContext,
|
|
37
55
|
): Line[] {
|
|
38
|
-
const { input,
|
|
56
|
+
const { input, conversationWidth, viewportHeight } = context;
|
|
39
57
|
let next = viewport;
|
|
40
58
|
const bottomDockInset = 1 + (input.searchManager.active || input.historySearch.active ? 1 : 0);
|
|
41
|
-
|
|
42
|
-
// Overlay posture rule: workspaces (onboarding, model, settings, mcp, help,
|
|
43
|
-
// shortcuts) claim the full viewport and are mutually exclusive with docked
|
|
44
|
-
// pickers/modals. Once a fullscreen workspace claims the screen, no docked
|
|
45
|
-
// overlay is drawn on top of it, enforcing a single visible overlay even if
|
|
46
|
-
// stray state survives the handler's clearModalStack. The input layer keeps
|
|
47
|
-
// the modal stack to one entry; this is the renderer-side backstop.
|
|
48
|
-
let fullscreenClaimed = false;
|
|
59
|
+
const covered = modalOpen(input);
|
|
49
60
|
|
|
50
61
|
if (input.onboardingWizard.active) {
|
|
51
|
-
const
|
|
52
|
-
next = replaceViewportWithOverlay(
|
|
53
|
-
fullscreenClaimed = true;
|
|
62
|
+
const layer = renderOnboardingWizard(input.onboardingWizard, conversationWidth, viewportHeight);
|
|
63
|
+
next = replaceViewportWithOverlay(layerToLines(layer, conversationWidth, viewportHeight), conversationWidth, viewportHeight);
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
if (!
|
|
66
|
+
if (!input.onboardingWizard.active && !covered && input.filePicker.active) {
|
|
57
67
|
const lines = renderFilePickerOverlay(input.filePicker, conversationWidth, viewportHeight);
|
|
58
68
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
if (input.modelPicker.active) {
|
|
62
|
-
const lines = renderModelWorkspace(input.modelPicker, conversationWidth, viewportHeight);
|
|
63
|
-
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
64
|
-
fullscreenClaimed = true;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!fullscreenClaimed && input.selectionModal.active) {
|
|
68
|
-
const lines = renderSelectionModalOverlay(input.selectionModal, conversationWidth, viewportHeight);
|
|
69
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
71
|
if (input.searchManager.active) {
|
|
73
72
|
next.push(...renderSearchOverlay(input.searchManager, conversationWidth));
|
|
74
73
|
}
|
|
@@ -77,66 +76,7 @@ export function applyConversationOverlays(
|
|
|
77
76
|
next.push(...renderHistorySearchOverlay(input.historySearch, conversationWidth));
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
// were removed with those modals, F2 now opens the Fleet panel, which
|
|
82
|
-
// subsumes the live process tree.
|
|
83
|
-
|
|
84
|
-
if (!fullscreenClaimed && input.contextInspectorModal.active) {
|
|
85
|
-
const lines = renderContextInspector(conversation, conversationWidth, viewportHeight, contextWindow);
|
|
86
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (input.settingsModal.active) {
|
|
90
|
-
const lines = renderSettingsModal(input.settingsModal, conversationWidth, viewportHeight);
|
|
91
|
-
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
92
|
-
fullscreenClaimed = true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!fullscreenClaimed && input.configModal.active) {
|
|
96
|
-
const lines = renderConfigModal(input.configModal, conversationWidth, viewportHeight);
|
|
97
|
-
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
98
|
-
fullscreenClaimed = true;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (input.mcpWorkspace.active) {
|
|
102
|
-
const lines = renderMcpWorkspace(input.mcpWorkspace, conversationWidth, viewportHeight);
|
|
103
|
-
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
104
|
-
fullscreenClaimed = true;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!fullscreenClaimed && input.sessionPickerModal.active) {
|
|
108
|
-
const lines = renderSessionPickerModal(input.sessionPickerModal, conversationWidth, viewportHeight);
|
|
109
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!fullscreenClaimed && input.profilePickerModal.active) {
|
|
113
|
-
const lines = renderProfilePickerModal(input.profilePickerModal, conversationWidth, viewportHeight);
|
|
114
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!fullscreenClaimed && input.bookmarkModal.active) {
|
|
118
|
-
const lines = renderBookmarkModal(input.bookmarkModal, conversationWidth, viewportHeight);
|
|
119
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!fullscreenClaimed && input.blockActionsMenu.active) {
|
|
123
|
-
const lines = renderBlockActionsMenu(input.blockActionsMenu, conversationWidth, viewportHeight);
|
|
124
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (input.helpOverlayActive) {
|
|
128
|
-
const lines = renderHelpOverlay(conversationWidth, keybindingsManager, commandRegistry.getAll(), input.helpScrollOffset, viewportHeight);
|
|
129
|
-
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
130
|
-
fullscreenClaimed = true;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (input.shortcutsOverlayActive) {
|
|
134
|
-
const lines = renderShortcutsOverlay(conversationWidth, keybindingsManager, input.shortcutsScrollOffset, viewportHeight);
|
|
135
|
-
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
136
|
-
fullscreenClaimed = true;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!fullscreenClaimed && input.commandMode && input.autocomplete?.isActive) {
|
|
79
|
+
if (!input.onboardingWizard.active && !covered && input.commandMode && input.autocomplete?.isActive) {
|
|
140
80
|
const lines = renderAutocompleteOverlay(input.autocomplete, conversationWidth, viewportHeight);
|
|
141
81
|
if (lines.length > 0) {
|
|
142
82
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
@@ -145,3 +85,73 @@ export function applyConversationOverlays(
|
|
|
145
85
|
|
|
146
86
|
return next;
|
|
147
87
|
}
|
|
88
|
+
|
|
89
|
+
export interface ConversationLayerContext {
|
|
90
|
+
readonly input: InputHandler;
|
|
91
|
+
readonly conversation: ConversationManager;
|
|
92
|
+
readonly commandRegistry: CommandRegistry;
|
|
93
|
+
readonly keybindingsManager: KeybindingsManager;
|
|
94
|
+
/** Full terminal size: modals are sized against the whole screen. */
|
|
95
|
+
readonly screenWidth: number;
|
|
96
|
+
readonly screenHeight: number;
|
|
97
|
+
readonly contextWindow?: number;
|
|
98
|
+
/** The permission dialog, when a request is waiting (drawn above the other modals). */
|
|
99
|
+
readonly permission?: SurfaceLayer | null;
|
|
100
|
+
/** Rows at the top of the screen held by the header (and the session chips row); toasts start below them. */
|
|
101
|
+
readonly headerRows?: number;
|
|
102
|
+
/** Rows at the bottom of the screen held by the footer (the throbber and its empty row while main works, the composer, the status line); toasts stay above them. */
|
|
103
|
+
readonly footerRows?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type ModalEntry = readonly [name: string, active: boolean, render: () => SurfaceLayer | null];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Modal surfaces drawn with the surface kit, as layers the compositor stamps
|
|
110
|
+
* over the dimmed screen (surface-compose.ts). Normally one modal is open;
|
|
111
|
+
* when one opens another (settings opening the model picker or a theme
|
|
112
|
+
* picker), both are drawn in the order they were opened (the input layer's
|
|
113
|
+
* modal stack), the newer on top, each dimming what is under it. Kit-host
|
|
114
|
+
* modals (command palette, confirm dialog), the permission dialog and toasts
|
|
115
|
+
* sit above all of them.
|
|
116
|
+
*/
|
|
117
|
+
export function buildConversationLayers(context: ConversationLayerContext): SurfaceLayer[] {
|
|
118
|
+
const { input, screenWidth: w, screenHeight: h } = context;
|
|
119
|
+
// Onboarding owns the screen; only the pickers it launches draw over it.
|
|
120
|
+
const onboarding = input.onboardingWizard.active;
|
|
121
|
+
const entries: ModalEntry[] = [
|
|
122
|
+
['modelPicker', input.modelPicker.active, () => renderModelWorkspace(input.modelPicker, w, h)],
|
|
123
|
+
['settings', input.settingsModal.active, () => renderSettingsModal(input.settingsModal, w, h)],
|
|
124
|
+
['mcpWorkspace', input.mcpWorkspace.active, () => renderMcpWorkspace(input.mcpWorkspace, w, h)],
|
|
125
|
+
['config', !onboarding && input.configModal.active, () => renderConfigModal(input.configModal, w, h)],
|
|
126
|
+
['sessionPicker', !onboarding && input.sessionPickerModal.active, () => renderSessionPickerModal(input.sessionPickerModal, w, h)],
|
|
127
|
+
['profilePicker', !onboarding && input.profilePickerModal.active, () => renderProfilePickerModal(input.profilePickerModal, w, h)],
|
|
128
|
+
['bookmark', !onboarding && input.bookmarkModal.active, () => renderBookmarkModal(input.bookmarkModal, w, h)],
|
|
129
|
+
['blockActions', !onboarding && input.blockActionsMenu.active, () => renderBlockActionsMenu(input.blockActionsMenu, w, h)],
|
|
130
|
+
['contextInspector', !onboarding && input.contextInspectorModal.active,
|
|
131
|
+
() => renderContextInspector(context.conversation, w, h, context.contextWindow, input.contextInspectorModal)],
|
|
132
|
+
['selection', input.selectionModal.active, () => renderSelectionModalOverlay(input.selectionModal, w, h)],
|
|
133
|
+
['help', input.helpOverlayActive,
|
|
134
|
+
() => renderHelpOverlay(w, h, context.keybindingsManager, context.commandRegistry.getAll(), input.helpScrollOffset, input.overlayFilters.help)],
|
|
135
|
+
['shortcuts', input.shortcutsOverlayActive,
|
|
136
|
+
() => renderShortcutsOverlay(w, h, context.keybindingsManager, input.shortcutsScrollOffset, input.overlayFilters.shortcuts)],
|
|
137
|
+
];
|
|
138
|
+
const active = entries.filter(([, isActive]) => isActive);
|
|
139
|
+
// Opened order first (the modal stack), then any active modal the stack does not name.
|
|
140
|
+
const rank = (name: string): number => {
|
|
141
|
+
const index = input.modalStack.indexOf(name);
|
|
142
|
+
return index >= 0 ? index : input.modalStack.length + entries.findIndex(([n]) => n === name);
|
|
143
|
+
};
|
|
144
|
+
active.sort((a, b) => rank(a[0]) - rank(b[0]));
|
|
145
|
+
const layers: SurfaceLayer[] = [];
|
|
146
|
+
for (const [, , render] of active) {
|
|
147
|
+
const layer = render();
|
|
148
|
+
if (layer) layers.push(layer);
|
|
149
|
+
}
|
|
150
|
+
// Kit modals (command palette, confirm dialog, ...) stack on top of everything.
|
|
151
|
+
layers.push(...input.surfaceModals.render(w, h));
|
|
152
|
+
if (context.permission) layers.push(context.permission);
|
|
153
|
+
// Toasts sit above everything and are never dimmed, between the header and the footer.
|
|
154
|
+
const toasts = renderToasts(w, h, getSharedToastCenter().visible(), { top: context.headerRows ?? 1, bottom: h - (context.footerRows ?? 2) });
|
|
155
|
+
if (toasts) layers.push(toasts);
|
|
156
|
+
return layers;
|
|
157
|
+
}
|
|
@@ -2,7 +2,13 @@ import { type Line, createEmptyLine, createStyledCell } from '@pellux/goodvibes-
|
|
|
2
2
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
3
3
|
import { LAYOUT } from './layout.ts';
|
|
4
4
|
import { GLYPHS } from './ui-primitives.ts';
|
|
5
|
-
import { foldPreviewText
|
|
5
|
+
import { foldPreviewText } from '@pellux/goodvibes-terminal-shell';
|
|
6
|
+
|
|
7
|
+
/** Column of a transcript event row's marker (the ● of a flush row). */
|
|
8
|
+
const MARKER_COL = LAYOUT.LEFT_MARGIN - 1;
|
|
9
|
+
/** Column where the row's segment run begins (segments carry their own leading blank). */
|
|
10
|
+
const CONTENT_COL = MARKER_COL + 2;
|
|
11
|
+
import { activeTokens } from './theme.ts';
|
|
6
12
|
|
|
7
13
|
export interface ConversationSurfacePalette {
|
|
8
14
|
readonly accent: string;
|
|
@@ -103,18 +109,11 @@ export function renderConversationFragment(
|
|
|
103
109
|
content: string,
|
|
104
110
|
width: number,
|
|
105
111
|
palette: ConversationFragmentPalette,
|
|
106
|
-
indentCols = 0,
|
|
107
112
|
): Line[] {
|
|
108
|
-
// A
|
|
109
|
-
|
|
110
|
-
// column as the parent row's own ` ▸ N lines ` badge. A flush fragment (a user
|
|
111
|
-
// message ghost box) keeps the box margin it has always used.
|
|
112
|
-
const indent = Math.max(0, indentCols);
|
|
113
|
-
const margin = indent > 0 ? treeContentCol(indent) : LAYOUT.USER_BOX_MARGIN;
|
|
113
|
+
// A fragment (a user message ghost box) keeps the box margin it has always used.
|
|
114
|
+
const margin = LAYOUT.USER_BOX_MARGIN;
|
|
114
115
|
const prefixWidth = getDisplayWidth(palette.prefix);
|
|
115
|
-
//
|
|
116
|
-
// fragment past the right edge, a narrow terminal shrinks the preview text
|
|
117
|
-
// instead of silently truncating its tail.
|
|
116
|
+
// A narrow terminal shrinks the preview text instead of silently truncating its tail.
|
|
118
117
|
const maxContentWidth = Math.max(1, width - margin - LAYOUT.USER_BOX_MARGIN - prefixWidth - 2);
|
|
119
118
|
const wrapped = wrapText(content, maxContentWidth);
|
|
120
119
|
const contentWidth = wrapped.length > 0 ? Math.max(...wrapped.map((line) => getDisplayWidth(line))) : 0;
|
|
@@ -193,19 +192,18 @@ export function renderConversationFoldedRow(
|
|
|
193
192
|
tone: ConversationEventTone,
|
|
194
193
|
details: readonly ConversationStatusSegment[],
|
|
195
194
|
preview: string,
|
|
196
|
-
indentCols = 0,
|
|
197
195
|
): Line {
|
|
198
196
|
// What is left of the row after the marker, the label and the badges.
|
|
199
197
|
const usedCols = (tone.label ? getDisplayWidth(` ${tone.label} `) : 0)
|
|
200
198
|
+ details.reduce((sum, segment) => sum + getDisplayWidth(segment.text), 0);
|
|
201
|
-
const availCols = width - LAYOUT.RIGHT_MARGIN -
|
|
199
|
+
const availCols = width - LAYOUT.RIGHT_MARGIN - CONTENT_COL - usedCols - 1;
|
|
202
200
|
// Whether a preview renders at all, and its flattening, are the policy's call
|
|
203
201
|
// (foldPreviewText returns null for "no preview"). Only the TRUNCATION is
|
|
204
202
|
// ours: display width is a product-local rule the policy deliberately leaves
|
|
205
203
|
// to the caller, and it must truncate, never wrap.
|
|
206
204
|
const trimmed = foldPreviewText(preview, availCols);
|
|
207
205
|
if (trimmed === null) {
|
|
208
|
-
return renderConversationEventLine(width, tone, details
|
|
206
|
+
return renderConversationEventLine(width, tone, details);
|
|
209
207
|
}
|
|
210
208
|
const text = getDisplayWidth(trimmed) > availCols
|
|
211
209
|
? `${trimmed.slice(0, Math.max(1, availCols - 1))}…`
|
|
@@ -213,12 +211,11 @@ export function renderConversationFoldedRow(
|
|
|
213
211
|
return renderConversationEventLine(
|
|
214
212
|
width,
|
|
215
213
|
tone,
|
|
216
|
-
[...details, { text: `${text} `, fg:
|
|
217
|
-
indentCols,
|
|
214
|
+
[...details, { text: `${text} `, fg: activeTokens().textFaint }],
|
|
218
215
|
);
|
|
219
216
|
}
|
|
220
217
|
|
|
221
|
-
|
|
218
|
+
function renderConversationStatusLine(
|
|
222
219
|
width: number,
|
|
223
220
|
segments: readonly ConversationStatusSegment[],
|
|
224
221
|
options: {
|
|
@@ -226,29 +223,17 @@ export function renderConversationStatusLine(
|
|
|
226
223
|
readonly markerFg?: string;
|
|
227
224
|
readonly markerBg?: string;
|
|
228
225
|
readonly bodyBg?: string;
|
|
229
|
-
/**
|
|
230
|
-
* Tree-branch indent in columns (see conversation-tree.ts). Shifts the
|
|
231
|
-
* marker and content columns together, so a branch row keeps the same
|
|
232
|
-
* marker→content relationship a flush row has. Callers pass an indent
|
|
233
|
-
* already clamped by treeIndentCols(), so this never eats the content
|
|
234
|
-
* budget below the guaranteed minimum.
|
|
235
|
-
*/
|
|
236
|
-
readonly indentCols?: number;
|
|
237
226
|
} = {},
|
|
238
227
|
): Line {
|
|
239
228
|
const line = createEmptyLine(width);
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
// sit on one grid: the marker where its depth's glyph belongs, the segment run
|
|
243
|
-
// two columns further right.
|
|
244
|
-
const markerCol = treeBranchCol(indent);
|
|
245
|
-
const startCol = treeContentCol(indent);
|
|
229
|
+
const markerCol = MARKER_COL;
|
|
230
|
+
const startCol = CONTENT_COL;
|
|
246
231
|
const endCol = Math.max(startCol, width - LAYOUT.RIGHT_MARGIN);
|
|
247
232
|
const marker = options.marker ?? '▌';
|
|
248
233
|
const markerWidth = getDisplayWidth(marker);
|
|
249
234
|
if (markerCol >= 0 && markerCol < width && markerWidth > 0) {
|
|
250
235
|
const markerStyle = {
|
|
251
|
-
fg: options.markerFg ??
|
|
236
|
+
fg: options.markerFg ?? activeTokens().textMuted,
|
|
252
237
|
bg: options.markerBg ?? options.bodyBg ?? '',
|
|
253
238
|
bold: true,
|
|
254
239
|
};
|
|
@@ -277,12 +262,9 @@ export function renderConversationEventLine(
|
|
|
277
262
|
width: number,
|
|
278
263
|
tone: ConversationEventTone,
|
|
279
264
|
details: readonly ConversationStatusSegment[] = [],
|
|
280
|
-
indentCols = 0,
|
|
281
265
|
): Line {
|
|
282
|
-
// An empty label is legitimate
|
|
283
|
-
//
|
|
284
|
-
// on every one of them is exactly the boilerplate this layout removes. The
|
|
285
|
-
// row then leads with its own first informative detail segment.
|
|
266
|
+
// An empty label is legitimate: the row then leads with its own first
|
|
267
|
+
// informative detail segment.
|
|
286
268
|
const labelSegments = tone.label
|
|
287
269
|
? [{ text: ` ${tone.label} `, fg: tone.labelFg, bold: true }]
|
|
288
270
|
: [];
|
|
@@ -298,7 +280,6 @@ export function renderConversationEventLine(
|
|
|
298
280
|
{
|
|
299
281
|
marker: tone.marker,
|
|
300
282
|
markerFg: tone.markerFg,
|
|
301
|
-
indentCols,
|
|
302
283
|
},
|
|
303
284
|
);
|
|
304
285
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diff-tint.ts, row tints for a diff that stay visible on the fill they sit on.
|
|
3
|
+
*
|
|
4
|
+
* The theme's diff tokens (diffAddedBg, diffRemovedBg and their line-number
|
|
5
|
+
* variants) are tuned against the terminal background. A diff drawn inside a
|
|
6
|
+
* lighter element inset can land within a few color units of them: gruvbox
|
|
7
|
+
* light's added row is 7 units from its element fill, rosepine light's removed
|
|
8
|
+
* line-number tint 5. Those rows read as untinted.
|
|
9
|
+
*
|
|
10
|
+
* visibleTint keeps the theme's token whenever it is far enough from the fill.
|
|
11
|
+
* When it is not, it pushes the token further from the fill along the same
|
|
12
|
+
* direction (the same hue shift, just stronger), and when the token carries
|
|
13
|
+
* no usable direction it mixes the fill toward the diff's sign color instead.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mixHex } from '@pellux/goodvibes-sdk/platform/presentation';
|
|
17
|
+
import { activeDiffTones, activeTokens } from './theme.ts';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Smallest Euclidean sRGB distance (0..441) between a diff row's tint and the
|
|
21
|
+
* fill under it. A flat fill change becomes noticeable side by side at about 8
|
|
22
|
+
* units; a full-width row has to read as tinted at a glance, from across the
|
|
23
|
+
* modal, so the floor is three times that. For reference, GitHub dark's own
|
|
24
|
+
* added-row tint sits about 20 units from its canvas and reads as faint.
|
|
25
|
+
*/
|
|
26
|
+
const MIN_DIFF_TINT_DISTANCE = 24;
|
|
27
|
+
|
|
28
|
+
type Rgb = readonly [number, number, number];
|
|
29
|
+
|
|
30
|
+
function parse(hex: string): Rgb | null {
|
|
31
|
+
const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
|
32
|
+
if (!m) return null;
|
|
33
|
+
const n = parseInt(m[1]!, 16);
|
|
34
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toHex(c: Rgb): string {
|
|
38
|
+
return `#${c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('')}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Euclidean distance between two #rrggbb colors (0 when either does not parse). */
|
|
42
|
+
function colorDistance(a: string, b: string): number {
|
|
43
|
+
const ca = parse(a);
|
|
44
|
+
const cb = parse(b);
|
|
45
|
+
if (!ca || !cb) return 0;
|
|
46
|
+
return Math.hypot(ca[0] - cb[0], ca[1] - cb[1], ca[2] - cb[2]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `token` when it is at least `floor` from `fill`; otherwise the closest color
|
|
51
|
+
* that is, found first along the token's own direction from the fill, then by
|
|
52
|
+
* mixing the fill toward `tone` (the diff sign color). Colors that do not parse
|
|
53
|
+
* come back unchanged.
|
|
54
|
+
*/
|
|
55
|
+
function visibleTint(token: string, fill: string, tone: string, floor = MIN_DIFF_TINT_DISTANCE): string {
|
|
56
|
+
const t = parse(token);
|
|
57
|
+
const f = parse(fill);
|
|
58
|
+
if (!t || !f) return token;
|
|
59
|
+
const len = Math.hypot(t[0] - f[0], t[1] - f[1], t[2] - f[2]);
|
|
60
|
+
if (len >= floor) return token;
|
|
61
|
+
// Aim one unit past the floor so rounding to whole channels never lands short.
|
|
62
|
+
const target = floor + 1;
|
|
63
|
+
if (len >= 4) {
|
|
64
|
+
const k = target / len;
|
|
65
|
+
const pushed = toHex([f[0] + (t[0] - f[0]) * k, f[1] + (t[1] - f[1]) * k, f[2] + (t[2] - f[2]) * k]);
|
|
66
|
+
if (colorDistance(pushed, fill) >= floor) return pushed;
|
|
67
|
+
}
|
|
68
|
+
const full = colorDistance(tone, fill);
|
|
69
|
+
if (full <= 0) return token;
|
|
70
|
+
return mixHex(fill, tone, Math.min(1, target / full));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DiffRowTints {
|
|
74
|
+
readonly addedRow: string;
|
|
75
|
+
readonly removedRow: string;
|
|
76
|
+
readonly addedGutter: string;
|
|
77
|
+
readonly removedGutter: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const tintCache = new Map<string, DiffRowTints>();
|
|
81
|
+
|
|
82
|
+
/** The active theme's diff tints, each kept visible on `fill` (the inset the diff sits on). */
|
|
83
|
+
export function diffRowTints(fill: string): DiffRowTints {
|
|
84
|
+
const t = activeTokens();
|
|
85
|
+
const tones = activeDiffTones();
|
|
86
|
+
const key = [fill, t.diffAddedBg, t.diffRemovedBg, t.diffAddedLineNumberBg, t.diffRemovedLineNumberBg, tones.add, tones.del].join('|');
|
|
87
|
+
const cached = tintCache.get(key);
|
|
88
|
+
if (cached) return cached;
|
|
89
|
+
const tints: DiffRowTints = {
|
|
90
|
+
addedRow: visibleTint(t.diffAddedBg, fill, tones.add),
|
|
91
|
+
removedRow: visibleTint(t.diffRemovedBg, fill, tones.del),
|
|
92
|
+
addedGutter: visibleTint(t.diffAddedLineNumberBg, fill, tones.add),
|
|
93
|
+
removedGutter: visibleTint(t.diffRemovedLineNumberBg, fill, tones.del),
|
|
94
|
+
};
|
|
95
|
+
if (tintCache.size > 64) tintCache.clear();
|
|
96
|
+
tintCache.set(key, tints);
|
|
97
|
+
return tints;
|
|
98
|
+
}
|
|
@@ -1,101 +1,74 @@
|
|
|
1
|
-
import { type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
-
import type { FilePickerModal } from '../input/file-picker.ts';
|
|
4
|
-
import {
|
|
5
|
-
createOverlayBoxLayout,
|
|
6
|
-
createOverlayContentLine,
|
|
7
|
-
createOverlayFilledBorderLine,
|
|
8
|
-
DEFAULT_OVERLAY_PALETTE,
|
|
9
|
-
OVERLAY_GLYPHS,
|
|
10
|
-
putOverlayText,
|
|
11
|
-
} from './overlay-box.ts';
|
|
12
|
-
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
13
|
-
|
|
14
1
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
2
|
+
* renderFilePickerOverlay, the @ file popup.
|
|
3
|
+
*
|
|
4
|
+
* Anchored to the composer with no dimming, the same shape as the slash
|
|
5
|
+
* popup (┃ bar, surface fill, no frame, no hint rows). Its first row is the
|
|
6
|
+
* typed query ("@ query▏" and the match count), since the query lives in the
|
|
7
|
+
* picker rather than the composer. Fuzzy-matched letters of the file name are
|
|
8
|
+
* drawn in the brand color; the directory part is dimmer than the name, so
|
|
9
|
+
* the path dims toward the root.
|
|
17
10
|
*/
|
|
11
|
+
|
|
12
|
+
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
13
|
+
import type { FilePickerModal } from '../input/file-picker.ts';
|
|
14
|
+
import { activeTokens } from './theme.ts';
|
|
15
|
+
import { renderPopup } from './surface-kit-parts.ts';
|
|
16
|
+
import type { KitRow, KitSpan } from './surface-kit-list.ts';
|
|
17
|
+
|
|
18
|
+
function popupRows(viewportHeight: number): number {
|
|
19
|
+
return Math.max(4, Math.min(12, Math.floor(viewportHeight * 0.5)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Split a path into directory and name spans, marking the query's letters (in order) in the name. */
|
|
23
|
+
function fileSpans(file: string, query: string, selected: boolean): KitSpan[] {
|
|
24
|
+
const t = activeTokens();
|
|
25
|
+
const trimmed = file.endsWith('/') ? file.slice(0, -1) : file;
|
|
26
|
+
const cut = trimmed.lastIndexOf('/') + 1;
|
|
27
|
+
const dir = file.slice(0, cut);
|
|
28
|
+
const name = file.slice(cut);
|
|
29
|
+
const spans: KitSpan[] = [];
|
|
30
|
+
if (dir) spans.push({ text: dir, fg: t.textFaint });
|
|
31
|
+
const wanted = [...query.toLowerCase()].filter((ch) => ch !== ' ');
|
|
32
|
+
let q = 0;
|
|
33
|
+
let run = '';
|
|
34
|
+
let runHit = false;
|
|
35
|
+
const flush = (): void => {
|
|
36
|
+
if (run) spans.push({ text: run, fg: runHit && !selected ? t.brand : t.text, bold: selected || runHit });
|
|
37
|
+
run = '';
|
|
38
|
+
};
|
|
39
|
+
for (const ch of name) {
|
|
40
|
+
const hit = q < wanted.length && ch.toLowerCase() === wanted[q];
|
|
41
|
+
if (hit) q++;
|
|
42
|
+
if (run && hit !== runHit) flush();
|
|
43
|
+
runHit = hit;
|
|
44
|
+
run += ch;
|
|
45
|
+
}
|
|
46
|
+
flush();
|
|
47
|
+
return spans;
|
|
48
|
+
}
|
|
49
|
+
|
|
18
50
|
export function renderFilePickerOverlay(
|
|
19
51
|
picker: FilePickerModal,
|
|
20
52
|
width: number,
|
|
21
53
|
viewportHeight = 24,
|
|
22
54
|
): Line[] {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const titleFg = DEFAULT_OVERLAY_PALETTE.titleFg;
|
|
34
|
-
const bodyFg = DEFAULT_OVERLAY_PALETTE.bodyFg;
|
|
35
|
-
const mutedFg = DEFAULT_OVERLAY_PALETTE.mutedFg;
|
|
36
|
-
const selectedBg = DEFAULT_OVERLAY_PALETTE.selectedBg;
|
|
37
|
-
|
|
38
|
-
// Title bar
|
|
39
|
-
const titleLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, borderFg, DEFAULT_OVERLAY_PALETTE.titleBg);
|
|
40
|
-
putOverlayText(titleLine, layout.margin + 2, layout.width - 4, 'Select File', { fg: titleFg, bold: true });
|
|
41
|
-
lines.push(titleLine);
|
|
42
|
-
|
|
43
|
-
// Search input
|
|
44
|
-
const queryDisplay = picker.query || '';
|
|
45
|
-
const searchLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.inputBg);
|
|
46
|
-
const searchPrefix = '@ ';
|
|
47
|
-
const queryText = fitDisplay(`${queryDisplay}${picker.searchFocused ? OVERLAY_GLYPHS.cursor : ''}`, Math.max(0, contentW - getDisplayWidth(searchPrefix)));
|
|
48
|
-
putOverlayText(searchLine, layout.margin + 2, getDisplayWidth(searchPrefix), searchPrefix, { fg: picker.searchFocused ? bodyFg : mutedFg });
|
|
49
|
-
putOverlayText(searchLine, layout.margin + 2 + getDisplayWidth(searchPrefix), contentW - getDisplayWidth(searchPrefix), queryText, { fg: picker.query.length > 0 || picker.searchFocused ? bodyFg : mutedFg });
|
|
50
|
-
lines.push(searchLine);
|
|
51
|
-
|
|
52
|
-
// Separator
|
|
53
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
54
|
-
|
|
55
|
-
// Results
|
|
55
|
+
const t = activeTokens();
|
|
56
|
+
const count = picker.results.length === 0 ? '' : `${picker.results.length} file${picker.results.length === 1 ? '' : 's'}`;
|
|
57
|
+
const header: KitRow = {
|
|
58
|
+
spans: [
|
|
59
|
+
{ text: `@${picker.query}`, fg: t.text, bold: true },
|
|
60
|
+
{ text: '▏', fg: t.brand },
|
|
61
|
+
],
|
|
62
|
+
right: count || undefined,
|
|
63
|
+
};
|
|
64
|
+
const rows: KitRow[] = [];
|
|
56
65
|
if (picker.results.length === 0) {
|
|
57
|
-
|
|
58
|
-
putOverlayText(noResults, layout.margin + 2, contentW, fitDisplay('No matching files', contentW), { fg: '244', dim: true });
|
|
59
|
-
lines.push(noResults);
|
|
66
|
+
rows.push({ label: picker.query ? 'No matching files' : 'Loading files…', muted: true });
|
|
60
67
|
} else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
picker.selectedIndex - Math.floor(maxVisible / 2),
|
|
66
|
-
picker.results.length - maxVisible,
|
|
67
|
-
));
|
|
68
|
-
}
|
|
69
|
-
const endIdx = Math.min(startIdx + maxVisible, picker.results.length);
|
|
70
|
-
|
|
71
|
-
for (let i = startIdx; i < endIdx; i++) {
|
|
72
|
-
const file = picker.results[i];
|
|
73
|
-
const isSelected = i === picker.selectedIndex;
|
|
74
|
-
const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
|
|
75
|
-
const displayFile = getDisplayWidth(file) > contentW - 2
|
|
76
|
-
? truncateDisplay(file, contentW - 2)
|
|
77
|
-
: file;
|
|
78
|
-
const line = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
79
|
-
putOverlayText(
|
|
80
|
-
line,
|
|
81
|
-
layout.margin + 2,
|
|
82
|
-
contentW,
|
|
83
|
-
fitDisplay(indicator + fitDisplay(displayFile, contentW - 2), contentW),
|
|
84
|
-
{
|
|
85
|
-
fg: isSelected ? titleFg : file.endsWith('/') ? titleFg : bodyFg,
|
|
86
|
-
bg: isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
87
|
-
bold: isSelected,
|
|
88
|
-
},
|
|
89
|
-
);
|
|
90
|
-
lines.push(line);
|
|
91
|
-
}
|
|
68
|
+
picker.results.forEach((file, index) => {
|
|
69
|
+
const selected = index === picker.selectedIndex;
|
|
70
|
+
rows.push({ spans: fileSpans(file, picker.query, selected), selected });
|
|
71
|
+
});
|
|
92
72
|
}
|
|
93
|
-
|
|
94
|
-
// Bottom border with hints
|
|
95
|
-
const hints = '[Up/Down] Navigate [/] Search [Enter] Select [Esc] Cancel';
|
|
96
|
-
const bottomLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
97
|
-
putOverlayText(bottomLine, layout.margin + 2, layout.width - 4, truncateDisplay(hints, layout.width - 4), { fg: mutedFg, dim: true });
|
|
98
|
-
lines.push(bottomLine);
|
|
99
|
-
|
|
100
|
-
return lines;
|
|
73
|
+
return renderPopup({ width, rows, header, maxRows: popupRows(viewportHeight), scrollOwner: picker }).lines;
|
|
101
74
|
}
|