@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,104 +0,0 @@
|
|
|
1
|
-
import { getSurfaceContentRows } from './surface-layout.ts';
|
|
2
|
-
|
|
3
|
-
export type OverlayWidthClass = 'narrow' | 'medium' | 'wide';
|
|
4
|
-
|
|
5
|
-
export function getOverlayWidthClass(viewportWidth: number): OverlayWidthClass {
|
|
6
|
-
if (viewportWidth < 88) return 'narrow';
|
|
7
|
-
if (viewportWidth < 120) return 'medium';
|
|
8
|
-
return 'wide';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface OverlayViewportBudgetOptions {
|
|
12
|
-
readonly chromeRows: number;
|
|
13
|
-
readonly minContentRows?: number;
|
|
14
|
-
readonly maxContentRows?: number;
|
|
15
|
-
readonly targetRatio?: number;
|
|
16
|
-
readonly minTotalRows?: number;
|
|
17
|
-
readonly maxTotalRows?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function getOverlayContentBudget(
|
|
21
|
-
viewportHeight: number,
|
|
22
|
-
options: OverlayViewportBudgetOptions,
|
|
23
|
-
): number {
|
|
24
|
-
const safeViewportHeight = Math.max(8, viewportHeight);
|
|
25
|
-
const defaultTargetTotalRows = Math.max(12, Math.min(Math.max(12, safeViewportHeight - 3), Math.round(safeViewportHeight * (options.targetRatio ?? 0.45))));
|
|
26
|
-
const minTotalRows = options.minTotalRows ?? 12;
|
|
27
|
-
const maxTotalRows = options.maxTotalRows ?? Math.max(minTotalRows, Math.min(22, safeViewportHeight - 2));
|
|
28
|
-
const targetTotalRows = Math.max(
|
|
29
|
-
minTotalRows,
|
|
30
|
-
Math.min(maxTotalRows, defaultTargetTotalRows),
|
|
31
|
-
);
|
|
32
|
-
return getSurfaceContentRows({
|
|
33
|
-
viewportHeight,
|
|
34
|
-
chromeRows: options.chromeRows,
|
|
35
|
-
minContentRows: options.minContentRows,
|
|
36
|
-
maxContentRows: options.maxContentRows,
|
|
37
|
-
targetRatio: 1,
|
|
38
|
-
minTotalRows: targetTotalRows,
|
|
39
|
-
maxTotalRows: targetTotalRows,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function getOverlayMaxWidth(
|
|
44
|
-
terminalWidth: number,
|
|
45
|
-
margin: number,
|
|
46
|
-
requestedMaxWidth: number,
|
|
47
|
-
): number {
|
|
48
|
-
const safeRequested = Math.max(24, requestedMaxWidth);
|
|
49
|
-
const reservedSideSpace = margin * 2;
|
|
50
|
-
const viewportBounded = Math.max(24, terminalWidth - reservedSideSpace);
|
|
51
|
-
return Math.min(safeRequested, viewportBounded);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface OverlaySurfaceMetricsOptions {
|
|
55
|
-
readonly margin?: number;
|
|
56
|
-
readonly maxWidth?: number;
|
|
57
|
-
readonly chromeRows: number;
|
|
58
|
-
readonly minContentRows?: number;
|
|
59
|
-
readonly maxContentRows?: number;
|
|
60
|
-
readonly targetRatio?: number;
|
|
61
|
-
readonly minTotalRows?: number;
|
|
62
|
-
readonly maxTotalRows?: number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface OverlaySurfaceMetrics {
|
|
66
|
-
readonly margin: number;
|
|
67
|
-
readonly boxWidth: number;
|
|
68
|
-
readonly contentWidth: number;
|
|
69
|
-
readonly contentRows: number;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function getStableOverlayContentRows(
|
|
73
|
-
contentRows: number,
|
|
74
|
-
minRows = 10,
|
|
75
|
-
): number {
|
|
76
|
-
return Math.max(contentRows, minRows);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function getOverlaySurfaceMetrics(
|
|
80
|
-
viewportWidth: number,
|
|
81
|
-
viewportHeight: number,
|
|
82
|
-
options: OverlaySurfaceMetricsOptions,
|
|
83
|
-
): OverlaySurfaceMetrics {
|
|
84
|
-
// Keep overlays comfortably inset from shell panels and avoid wide, low-information modals.
|
|
85
|
-
const widthClass = getOverlayWidthClass(viewportWidth);
|
|
86
|
-
const margin = options.margin ?? (widthClass === 'wide' ? 6 : 4);
|
|
87
|
-
const defaultMaxWidth = widthClass === 'wide' ? 84 : widthClass === 'medium' ? 78 : 72;
|
|
88
|
-
const boxWidth = getOverlayMaxWidth(viewportWidth, margin, options.maxWidth ?? defaultMaxWidth);
|
|
89
|
-
const contentRows = getOverlayContentBudget(viewportHeight, {
|
|
90
|
-
chromeRows: options.chromeRows,
|
|
91
|
-
minContentRows: options.minContentRows ?? 8,
|
|
92
|
-
maxContentRows: options.maxContentRows ?? 14,
|
|
93
|
-
targetRatio: options.targetRatio ?? 0.45,
|
|
94
|
-
minTotalRows: options.minTotalRows,
|
|
95
|
-
maxTotalRows: options.maxTotalRows,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
margin,
|
|
100
|
-
boxWidth,
|
|
101
|
-
contentWidth: Math.max(1, boxWidth - 4),
|
|
102
|
-
contentRows,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import type { Panel } from '../panels/types.ts';
|
|
3
|
-
import type { InputHandler } from '../input/handler.ts';
|
|
4
|
-
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
5
|
-
import type { PanelCompositeData } from './compositor.ts';
|
|
6
|
-
import { createSplitPaneLayout } from './layout-engine.ts';
|
|
7
|
-
import { renderPanelWorkspaceBar } from './panel-workspace-bar.ts';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Per-panel render cache for dirty-flag skipping.
|
|
11
|
-
*
|
|
12
|
-
* Maintainability hazard, mid-render invalidation race:
|
|
13
|
-
*
|
|
14
|
-
* If an event listener (e.g. a runtime-bus subscriber) fires DURING a panel's
|
|
15
|
-
* `render()` call and mutates state such that `needsRender = true`, the
|
|
16
|
-
* trailing `panel.markRendered()` below will clobber that invalidation back
|
|
17
|
-
* to `false`, causing the next frame to serve stale cached output until
|
|
18
|
-
* something else invalidates the panel.
|
|
19
|
-
*
|
|
20
|
-
* In practice this is rare because `render()` is synchronous and short, and
|
|
21
|
-
* runtime-bus listeners that mutate panel state typically run outside the
|
|
22
|
-
* render pass. Panels that DO expect to mutate from within their own render
|
|
23
|
-
* path (e.g. deferred lazy-load) should call `this.invalidate()` AFTER the
|
|
24
|
-
* trailing work so the next frame picks it up.
|
|
25
|
-
*
|
|
26
|
-
* The fix (now applied): snapshot `needsRender` before calling `render()` and
|
|
27
|
-
* only call `markRendered()` when no concurrent invalidation occurred during
|
|
28
|
-
* the render pass, preserving any mid-render invalidation.
|
|
29
|
-
*/
|
|
30
|
-
interface PanelRenderCache {
|
|
31
|
-
lines: Line[];
|
|
32
|
-
width: number;
|
|
33
|
-
height: number;
|
|
34
|
-
}
|
|
35
|
-
const panelRenderCache = new WeakMap<Panel, PanelRenderCache>();
|
|
36
|
-
/**
|
|
37
|
-
* Per-panel render-generation counter. Incremented whenever invalidate() fires
|
|
38
|
-
* on the panel (tracked here externally since Panel does not expose a version).
|
|
39
|
-
* We monkey-patch each panel the first time it enters renderPanel() by wrapping
|
|
40
|
-
* its invalidate() to bump the counter stored in this map.
|
|
41
|
-
*
|
|
42
|
-
* Race-guard: snapshot the generation before render(), compare after. If it
|
|
43
|
-
* changed, a mid-render invalidation occurred, leave needsRender=true.
|
|
44
|
-
*/
|
|
45
|
-
const panelRenderGen = new WeakMap<Panel, { gen: number }>();
|
|
46
|
-
|
|
47
|
-
function getRenderGenState(panel: Panel): { gen: number } {
|
|
48
|
-
let state = panelRenderGen.get(panel);
|
|
49
|
-
if (!state) {
|
|
50
|
-
state = { gen: 0 };
|
|
51
|
-
panelRenderGen.set(panel, state);
|
|
52
|
-
// Wrap invalidate() to bump generation counter.
|
|
53
|
-
const origInvalidate = panel.invalidate.bind(panel);
|
|
54
|
-
panel.invalidate = () => {
|
|
55
|
-
state!.gen++;
|
|
56
|
-
origInvalidate();
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
return state;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Render a panel, skipping if nothing changed. Returns cached lines on a skip. */
|
|
63
|
-
export function renderPanel(panel: Panel, width: number, height: number): Line[] {
|
|
64
|
-
const cached = panelRenderCache.get(panel);
|
|
65
|
-
if (cached && !panel.needsRender && cached.width === width && cached.height === height) {
|
|
66
|
-
return cached.lines;
|
|
67
|
-
}
|
|
68
|
-
// Snapshot render-generation counter BEFORE calling render(). If an event
|
|
69
|
-
// listener fires during render() and calls panel.invalidate(), the generation
|
|
70
|
-
// counter will be bumped. We compare after render to detect mid-render races.
|
|
71
|
-
const genState = getRenderGenState(panel);
|
|
72
|
-
const genBefore = genState.gen;
|
|
73
|
-
const lines = panel.render(width, height);
|
|
74
|
-
// Only call markRendered() when no mid-render invalidation occurred.
|
|
75
|
-
// If the generation changed during render(), a concurrent invalidate() fired,
|
|
76
|
-
// leave needsRender=true so the next frame re-renders with the new state.
|
|
77
|
-
if (genState.gen === genBefore) {
|
|
78
|
-
panel.markRendered();
|
|
79
|
-
}
|
|
80
|
-
// If gen changed, needsRender is already true (invalidate() set it); do not
|
|
81
|
-
// call markRendered(), the next frame will pick it up.
|
|
82
|
-
panelRenderCache.set(panel, { lines, width, height });
|
|
83
|
-
return lines;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface PanelCompositeBuildResult {
|
|
87
|
-
readonly panelData?: PanelCompositeData;
|
|
88
|
-
readonly panelWidth?: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function buildPanelCompositeData(
|
|
92
|
-
panelManager: PanelManager,
|
|
93
|
-
input: InputHandler,
|
|
94
|
-
panelWidth: number,
|
|
95
|
-
panelHeight: number,
|
|
96
|
-
): PanelCompositeBuildResult {
|
|
97
|
-
if (!panelManager.isVisible() || panelManager.getAllOpen().length === 0 || panelWidth <= 0) {
|
|
98
|
-
return {};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const topPane = panelManager.getTopPane();
|
|
102
|
-
const bottomPane = panelManager.getBottomPane();
|
|
103
|
-
const focusedPane = panelManager.getFocusedPane();
|
|
104
|
-
const workspaceTabs = panelManager.getWorkspaceTabs();
|
|
105
|
-
const verticalSplitRatio = panelManager.getVerticalSplitRatio();
|
|
106
|
-
const hasBottom = panelManager.isBottomPaneVisible() && bottomPane.panels.length > 0;
|
|
107
|
-
const workspaceBar = renderPanelWorkspaceBar(workspaceTabs, panelWidth, input.panelFocused);
|
|
108
|
-
|
|
109
|
-
let topContent: Line[];
|
|
110
|
-
let bottomContent: Line[] | undefined;
|
|
111
|
-
|
|
112
|
-
const topActivePanel = topPane.panels[topPane.activeIndex] ?? null;
|
|
113
|
-
|
|
114
|
-
if (hasBottom) {
|
|
115
|
-
// Single consolidated workspace bar (1 row) + h-separator (1 row); the rest
|
|
116
|
-
// splits between the two panes' content.
|
|
117
|
-
const paneLayout = createSplitPaneLayout(Math.max(0, panelHeight - 1), verticalSplitRatio, {
|
|
118
|
-
topTabRows: 0,
|
|
119
|
-
bottomTabRows: 0,
|
|
120
|
-
});
|
|
121
|
-
const topH = paneLayout.topContentRows;
|
|
122
|
-
const bottomH = paneLayout.bottomContentRows;
|
|
123
|
-
topContent = topActivePanel ? renderPanel(topActivePanel, panelWidth, topH) : [];
|
|
124
|
-
|
|
125
|
-
const bottomActivePanel = bottomPane.panels[bottomPane.activeIndex] ?? null;
|
|
126
|
-
bottomContent = bottomActivePanel ? renderPanel(bottomActivePanel, panelWidth, bottomH) : [];
|
|
127
|
-
} else {
|
|
128
|
-
const topH = Math.max(0, panelHeight - 1);
|
|
129
|
-
topContent = topActivePanel ? renderPanel(topActivePanel, panelWidth, topH) : [];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
panelData: {
|
|
134
|
-
workspaceBar,
|
|
135
|
-
topContent,
|
|
136
|
-
topFocused: input.panelFocused && focusedPane === 'top',
|
|
137
|
-
hasBottomPane: hasBottom,
|
|
138
|
-
bottomContent,
|
|
139
|
-
bottomFocused: input.panelFocused && focusedPane === 'bottom',
|
|
140
|
-
separator: true,
|
|
141
|
-
verticalSplitRatio,
|
|
142
|
-
},
|
|
143
|
-
panelWidth,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import type { WorkspaceTab } from '../panels/panel-manager.ts';
|
|
3
|
-
import { renderTabStrip, type TabHitRegion } from './tab-strip.ts';
|
|
4
|
-
import { UI_TONES } from './ui-primitives.ts';
|
|
5
|
-
|
|
6
|
-
// Theme tokens (no raw hex), keeps the bar in sync with the shared palette.
|
|
7
|
-
const ACTIVE_FG = UI_TONES.fg.primary; // selected tab text
|
|
8
|
-
const ACTIVE_BG = UI_TONES.bg.selected; // selected tab fill (stronger contrast)
|
|
9
|
-
const INACTIVE_FG = UI_TONES.fg.muted; // unselected tabs
|
|
10
|
-
const SEPARATOR_FG = UI_TONES.fg.dim; // │ separators
|
|
11
|
-
const LABEL_FG = UI_TONES.fg.secondary; // PANELS prefix label
|
|
12
|
-
const LABEL_BG = UI_TONES.bg.title; // prefix label background when focused
|
|
13
|
-
const FOCUS_ACCENT_FG = UI_TONES.state.active; // the one focused tab
|
|
14
|
-
|
|
15
|
-
export function renderPanelWorkspaceBar(
|
|
16
|
-
tabs: readonly WorkspaceTab[],
|
|
17
|
-
width: number,
|
|
18
|
-
focused: boolean,
|
|
19
|
-
onLayout?: (regions: readonly TabHitRegion[]) => void,
|
|
20
|
-
): Line {
|
|
21
|
-
return renderTabStrip({
|
|
22
|
-
width,
|
|
23
|
-
onLayout,
|
|
24
|
-
tabs: tabs.map((tab, i) => ({
|
|
25
|
-
// tab.active = selected in its own pane (drives highlighted background).
|
|
26
|
-
// tab.focused = has keyboard focus (drives brighter text / focus indicator).
|
|
27
|
-
// A tab can be active-but-not-focused (selected in the unfocused pane) or
|
|
28
|
-
// active-and-focused (selected in the focused pane).
|
|
29
|
-
//
|
|
30
|
-
// The pane marker uses ▲/▼ (top/bottom pane). The old '^'/'v'
|
|
31
|
-
// prefixes read as a Ctrl caret (^) implying a non-existent Ctrl+<tab>
|
|
32
|
-
// hotkey. The REAL per-tab jump key is Alt+N (panel-tab-1..9), rendered
|
|
33
|
-
// here as ⌥N for the first nine tabs so the shown affordance matches the
|
|
34
|
-
// binding (tabs past nine have no Alt+N chord, so no index is shown).
|
|
35
|
-
label: `${tab.pane === 'bottom' ? '▼' : '▲'}${i < 9 ? ` ⌥${i + 1}` : ''} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
|
|
36
|
-
active: tab.active,
|
|
37
|
-
})),
|
|
38
|
-
prefixLabel: ' PANELS ',
|
|
39
|
-
suffixLabel: ` ${tabs.length} `,
|
|
40
|
-
style: {
|
|
41
|
-
activeFg: ACTIVE_FG,
|
|
42
|
-
activeBg: ACTIVE_BG,
|
|
43
|
-
activeBold: focused,
|
|
44
|
-
inactiveFg: INACTIVE_FG,
|
|
45
|
-
separatorFg: SEPARATOR_FG,
|
|
46
|
-
// Accent the PANELS chip when focused so it ties to the focus border.
|
|
47
|
-
labelFg: focused ? FOCUS_ACCENT_FG : LABEL_FG,
|
|
48
|
-
labelBg: focused ? LABEL_BG : '',
|
|
49
|
-
labelBold: focused,
|
|
50
|
-
overflowFg: SEPARATOR_FG,
|
|
51
|
-
trailingFg: INACTIVE_FG,
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import { UIFactory } from './ui-factory.ts';
|
|
3
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
|
-
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
5
|
-
import { activeUiTones } from './theme.ts';
|
|
6
|
-
import { formatHints } from './hint-grammar.ts';
|
|
7
|
-
import { voiceCaptureRowVisible, type VoiceCaptureIndicatorState } from '../core/voice-capture-status.ts';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* renderProcessIndicator, shows a one-line summary of active background
|
|
11
|
-
* processes below the input area.
|
|
12
|
-
*
|
|
13
|
-
* Dimmed when no processes are active, highlighted (cyan) when agents or
|
|
14
|
-
* background exec processes are running. Includes an `[Enter] View` hint
|
|
15
|
-
* (hint-grammar bracket form) when active.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* Agent-count label. The footer counts only ACTIVE agents, while the fleet
|
|
19
|
-
* lists every node (running, terminal, chains, watchers). Label the count
|
|
20
|
-
* "active" so it is never misread as a grand total, the [Enter] View hint
|
|
21
|
-
* opens the fleet for the full picture. (item 5d.)
|
|
22
|
-
*/
|
|
23
|
-
function agentCountLabel(agentCount: number): string {
|
|
24
|
-
return `${agentCount} agent${agentCount !== 1 ? 's' : ''} active`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function renderProcessIndicator(
|
|
28
|
-
width: number,
|
|
29
|
-
agentCount: number,
|
|
30
|
-
toolCount: number,
|
|
31
|
-
focused: boolean = false,
|
|
32
|
-
agentProgress?: string,
|
|
33
|
-
): Line[] {
|
|
34
|
-
const total = agentCount + toolCount;
|
|
35
|
-
const renderPlainStatus = (text: string, style: { fg: string; bold?: boolean; dim?: boolean }): Line[] => (
|
|
36
|
-
[UIFactory.stringToLine(` ${text}`, width, style)]
|
|
37
|
-
);
|
|
38
|
-
const renderFocusedStatus = (text: string): Line[] => {
|
|
39
|
-
const bg = '#31506f';
|
|
40
|
-
const fg = '#eefaff';
|
|
41
|
-
const markerFg = UI_TONES.accent.browser;
|
|
42
|
-
const line = UIFactory.stringToLine(' '.repeat(width), width, { fg: '238' });
|
|
43
|
-
const prefix = `${GLYPHS.navigation.selected} `;
|
|
44
|
-
const body = truncateDisplay(text, Math.max(0, width - 8), '');
|
|
45
|
-
const highlighted = ` ${prefix}${body} `;
|
|
46
|
-
const startX = 2;
|
|
47
|
-
for (let i = 0; i < highlighted.length && startX + i < width - 2; i++) {
|
|
48
|
-
const ch = highlighted[i]!;
|
|
49
|
-
const isMarker = i < prefix.length + 1;
|
|
50
|
-
line[startX + i].char = ch;
|
|
51
|
-
line[startX + i].fg = isMarker ? markerFg : fg;
|
|
52
|
-
line[startX + i].bg = bg;
|
|
53
|
-
line[startX + i].bold = true;
|
|
54
|
-
line[startX + i].dim = false;
|
|
55
|
-
}
|
|
56
|
-
return [line];
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// --- Focused state: always render before idle/active branches ---
|
|
60
|
-
if (focused) {
|
|
61
|
-
const parts: string[] = [];
|
|
62
|
-
if (agentCount > 0) parts.push(agentCountLabel(agentCount));
|
|
63
|
-
if (toolCount > 0) parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
|
|
64
|
-
const label = total === 0
|
|
65
|
-
? `No background processes ${formatHints([{ key: 'Esc', verb: 'Back to input' }])}`
|
|
66
|
-
: `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)} ${formatHints([{ key: 'Enter', verb: 'Open' }, { key: 'Esc', verb: 'Back to input' }])}`;
|
|
67
|
-
return renderFocusedStatus(label);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (total === 0) {
|
|
71
|
-
return renderPlainStatus('No background processes', { fg: '238', dim: true });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Build the label: "bg: 2 agents | Turn 3 | write - src/foo.ts"
|
|
75
|
-
const parts: string[] = [];
|
|
76
|
-
if (agentCount > 0) {
|
|
77
|
-
parts.push(agentCountLabel(agentCount));
|
|
78
|
-
}
|
|
79
|
-
if (toolCount > 0) {
|
|
80
|
-
parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
|
|
81
|
-
}
|
|
82
|
-
// Append the first running agent's progress (truncated to fit)
|
|
83
|
-
/**
|
|
84
|
-
* Number of columns reserved for the agent count label and hint text.
|
|
85
|
-
* Breakdown: "bg: N agents" prefix (~15 chars) + " | " separator (~3)
|
|
86
|
-
* + " [Enter] View " hint (~16) + padding (~9) ≈ 43 chars.
|
|
87
|
-
*/
|
|
88
|
-
const PROGRESS_RESERVED_CHARS = 43;
|
|
89
|
-
const progressMaxLen = Math.max(0, width - PROGRESS_RESERVED_CHARS); // reserve space for count + hint
|
|
90
|
-
// Truncate by display width (not JS string length) so wide/CJK glyphs in the
|
|
91
|
-
// agent progress text don't overflow the reserved budget.
|
|
92
|
-
const progressSuffix = agentProgress && progressMaxLen > 10
|
|
93
|
-
? ` | ${truncateDisplay(agentProgress, progressMaxLen, '...')}`
|
|
94
|
-
: '';
|
|
95
|
-
const label = `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)}${progressSuffix}`;
|
|
96
|
-
const hint = ` ${formatHints([{ key: 'Enter', verb: 'View' }])}`;
|
|
97
|
-
return renderPlainStatus(`${label}${hint}`, { fg: activeUiTones().accent.brand, bold: true });
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Sentence each capture state renders as. Written out per state rather than
|
|
102
|
-
* assembled from fragments because these are the words that tell a user whether
|
|
103
|
-
* their microphone is open, "listening" and "recording" mean different things
|
|
104
|
-
* and a row that blurred them would be worse than no row.
|
|
105
|
-
*/
|
|
106
|
-
const VOICE_CAPTURE_LABELS: Record<VoiceCaptureIndicatorState['kind'], string> = {
|
|
107
|
-
requesting: 'opening the microphone',
|
|
108
|
-
recording: 'recording: press the voice-input key again to stop',
|
|
109
|
-
transcribing: 'transcribing what you said',
|
|
110
|
-
'wake-listening': 'listening for the wake phrase',
|
|
111
|
-
'wake-capturing': 'wake heard: recording what follows',
|
|
112
|
-
'wake-restarting': 'capture stream ended: restarting',
|
|
113
|
-
'wake-latched': 'wake detection stopped',
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* renderVoiceCaptureIndicator, the persistent row shown while a microphone is
|
|
118
|
-
* open, below the input area beside the process indicator.
|
|
119
|
-
*
|
|
120
|
-
* It exists because a held-open capture device is otherwise invisible: wake
|
|
121
|
-
* detection runs for as long as the feature is on, and nothing else on screen
|
|
122
|
-
* would say so. `voice.wake.indicator` chooses between `statusline` (one dim
|
|
123
|
-
* row), `banner` (a highlighted row that is hard to miss) and `off`; a
|
|
124
|
-
* push-to-talk recording always renders, because the user pressed a key one
|
|
125
|
-
* moment ago and is waiting on it.
|
|
126
|
-
*
|
|
127
|
-
* Returns no lines when nothing is captured, or when the wake rows are turned
|
|
128
|
-
* off, the caller splices whatever comes back, so an empty array is "no row".
|
|
129
|
-
*/
|
|
130
|
-
export function renderVoiceCaptureIndicator(
|
|
131
|
-
width: number,
|
|
132
|
-
state: VoiceCaptureIndicatorState | null,
|
|
133
|
-
): Line[] {
|
|
134
|
-
if (!voiceCaptureRowVisible(state) || state === null) return [];
|
|
135
|
-
const tones = activeUiTones();
|
|
136
|
-
const marker = state.kind === 'wake-latched' ? GLYPHS.status.blocked : GLYPHS.status.active;
|
|
137
|
-
const device = state.deviceLabel !== null ? ` ${GLYPHS.navigation.pipeSeparator} ${state.deviceLabel}` : '';
|
|
138
|
-
const extra = state.detail !== undefined && state.detail.length > 0
|
|
139
|
-
? ` ${GLYPHS.navigation.pipeSeparator} ${state.detail}`
|
|
140
|
-
: '';
|
|
141
|
-
const body = `${marker} Voice: ${VOICE_CAPTURE_LABELS[state.kind]}${device}${extra}`;
|
|
142
|
-
const isWakeRow = state.kind.startsWith('wake-');
|
|
143
|
-
const fg = state.kind === 'wake-latched' || state.kind === 'wake-restarting'
|
|
144
|
-
? tones.chrome.warn
|
|
145
|
-
: tones.accent.control;
|
|
146
|
-
|
|
147
|
-
if (isWakeRow && state.indicator === 'banner') {
|
|
148
|
-
// The prominent variant: the row is filled to the terminal width on the
|
|
149
|
-
// footer background so it reads as a standing condition, not a passing note.
|
|
150
|
-
const line = UIFactory.stringToLine(' '.repeat(width), width, { fg: tones.chrome.faint });
|
|
151
|
-
const text = ` ${truncateDisplay(body, Math.max(0, width - 4), '…')} `;
|
|
152
|
-
for (let i = 0; i < text.length && 1 + i < width - 1; i++) {
|
|
153
|
-
const cell = line[1 + i]!;
|
|
154
|
-
cell.char = text[i]!;
|
|
155
|
-
cell.fg = fg;
|
|
156
|
-
cell.bg = tones.bg.footer;
|
|
157
|
-
cell.bold = true;
|
|
158
|
-
cell.dim = false;
|
|
159
|
-
}
|
|
160
|
-
return [line];
|
|
161
|
-
}
|
|
162
|
-
return [UIFactory.stringToLine(` ${truncateDisplay(body, Math.max(0, width - 4), '…')}`, width, { fg, bold: true })];
|
|
163
|
-
}
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
3
|
-
import { buildStyledPanelLine, type StyledPanelSegment } from '../panels/polish.ts';
|
|
4
|
-
|
|
5
|
-
export interface TabStripItem {
|
|
6
|
-
readonly label: string;
|
|
7
|
-
readonly active?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface TabStripStyle {
|
|
11
|
-
readonly activeFg: string;
|
|
12
|
-
readonly activeBg?: string;
|
|
13
|
-
readonly activeBold?: boolean;
|
|
14
|
-
readonly inactiveFg: string;
|
|
15
|
-
readonly inactiveBg?: string;
|
|
16
|
-
readonly separatorFg: string;
|
|
17
|
-
readonly labelFg?: string;
|
|
18
|
-
readonly labelBg?: string;
|
|
19
|
-
readonly labelBold?: boolean;
|
|
20
|
-
readonly overflowFg?: string;
|
|
21
|
-
readonly trailingFg?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Absolute column range a rendered tab occupies, for click hit-testing. */
|
|
25
|
-
export interface TabHitRegion {
|
|
26
|
-
/** Index into the original `tabs` array. */
|
|
27
|
-
readonly index: number;
|
|
28
|
-
/** First column (inclusive) the tab occupies on the line. */
|
|
29
|
-
readonly startCol: number;
|
|
30
|
-
/** Last column (exclusive) the tab occupies on the line. */
|
|
31
|
-
readonly endCol: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface TabStripConfig {
|
|
35
|
-
readonly width: number;
|
|
36
|
-
readonly tabs: readonly TabStripItem[];
|
|
37
|
-
readonly style: TabStripStyle;
|
|
38
|
-
readonly prefixLabel?: string;
|
|
39
|
-
readonly suffixLabel?: string;
|
|
40
|
-
/** Right-side overflow glyph (more tabs after the window). Default '›'. */
|
|
41
|
-
readonly overflowIndicator?: string;
|
|
42
|
-
/** Left-side overflow glyph (more tabs before the window). Default '‹'. */
|
|
43
|
-
readonly overflowIndicatorLeft?: string;
|
|
44
|
-
/**
|
|
45
|
-
* Optional callback invoked with the absolute column ranges of each rendered
|
|
46
|
-
* tab. Used by the compositor to hit-test mouse clicks onto tabs. Additive,
|
|
47
|
-
* the function still returns a `Line` so existing callers are unaffected.
|
|
48
|
-
*/
|
|
49
|
-
readonly onLayout?: (regions: readonly TabHitRegion[]) => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const SEPARATOR = '│';
|
|
53
|
-
|
|
54
|
-
interface WindowResult {
|
|
55
|
-
segments: StyledPanelSegment[];
|
|
56
|
-
/** Per-tab column ranges relative to the start of the tab block. */
|
|
57
|
-
regions: Array<{ index: number; startCol: number; endCol: number }>;
|
|
58
|
-
overflowLeft: boolean;
|
|
59
|
-
overflowRight: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Build the visible tab segments, windowed so the ACTIVE tab is always shown.
|
|
64
|
-
*
|
|
65
|
-
* Tabs are laid out left-to-right, but when they don't all fit we keep a
|
|
66
|
-
* contiguous window that contains the active tab and grow it outward (biased
|
|
67
|
-
* toward following tabs) until the budget is exhausted. Left/right overflow is
|
|
68
|
-
* reported so the caller can render `‹`/`›` indicators.
|
|
69
|
-
*/
|
|
70
|
-
function makeSegments(
|
|
71
|
-
tabs: readonly TabStripItem[],
|
|
72
|
-
style: TabStripStyle,
|
|
73
|
-
maxWidth: number,
|
|
74
|
-
indicatorWidth: number,
|
|
75
|
-
): WindowResult {
|
|
76
|
-
const empty: WindowResult = { segments: [], regions: [], overflowLeft: false, overflowRight: false };
|
|
77
|
-
if (tabs.length === 0 || maxWidth <= 0) return empty;
|
|
78
|
-
|
|
79
|
-
const sepWidth = getDisplayWidth(SEPARATOR);
|
|
80
|
-
// Per-tab entry width: leading separator + (bracketed if active) label + trailing space.
|
|
81
|
-
const labelText = (tab: TabStripItem) => (tab.active ? `[${tab.label}]` : tab.label);
|
|
82
|
-
const entryWidths = tabs.map((tab) => sepWidth + getDisplayWidth(labelText(tab)) + 1);
|
|
83
|
-
|
|
84
|
-
const activeIndex = Math.max(0, tabs.findIndex((t) => t.active));
|
|
85
|
-
const n = tabs.length;
|
|
86
|
-
const closing = sepWidth; // trailing │ after the last visible tab
|
|
87
|
-
const totalAll = entryWidths.reduce((a, b) => a + b, 0) + closing;
|
|
88
|
-
|
|
89
|
-
let start = activeIndex;
|
|
90
|
-
let end = activeIndex; // inclusive
|
|
91
|
-
let used = entryWidths[activeIndex] + closing;
|
|
92
|
-
|
|
93
|
-
if (totalAll <= maxWidth) {
|
|
94
|
-
// Everything fits, show all tabs, no windowing.
|
|
95
|
-
start = 0;
|
|
96
|
-
end = n - 1;
|
|
97
|
-
} else {
|
|
98
|
-
// Need windowing. Reserve room for indicators on whichever side still has
|
|
99
|
-
// hidden tabs; the reserve is recomputed each step so reaching an edge
|
|
100
|
-
// frees its indicator column.
|
|
101
|
-
const budget = maxWidth;
|
|
102
|
-
let leftDone = start === 0;
|
|
103
|
-
let rightDone = end === n - 1;
|
|
104
|
-
let preferRight = true;
|
|
105
|
-
|
|
106
|
-
const fits = (extra: number, nextStart: number, nextEnd: number) => {
|
|
107
|
-
const leftRes = nextStart > 0 ? indicatorWidth : 0;
|
|
108
|
-
const rightRes = nextEnd < n - 1 ? indicatorWidth : 0;
|
|
109
|
-
return used + extra + leftRes + rightRes <= budget;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
while (!leftDone || !rightDone) {
|
|
113
|
-
const goRight = !rightDone && (preferRight || leftDone);
|
|
114
|
-
if (goRight) {
|
|
115
|
-
const ne = end + 1;
|
|
116
|
-
if (fits(entryWidths[ne], start, ne)) {
|
|
117
|
-
end = ne;
|
|
118
|
-
used += entryWidths[ne];
|
|
119
|
-
if (end === n - 1) rightDone = true;
|
|
120
|
-
preferRight = false;
|
|
121
|
-
} else {
|
|
122
|
-
rightDone = true;
|
|
123
|
-
}
|
|
124
|
-
} else if (!leftDone) {
|
|
125
|
-
const ns = start - 1;
|
|
126
|
-
if (fits(entryWidths[ns], ns, end)) {
|
|
127
|
-
start = ns;
|
|
128
|
-
used += entryWidths[ns];
|
|
129
|
-
if (start === 0) leftDone = true;
|
|
130
|
-
preferRight = true;
|
|
131
|
-
} else {
|
|
132
|
-
leftDone = true;
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const segments: StyledPanelSegment[] = [];
|
|
141
|
-
const regions: Array<{ index: number; startCol: number; endCol: number }> = [];
|
|
142
|
-
let col = 0;
|
|
143
|
-
for (let i = start; i <= end; i++) {
|
|
144
|
-
const tab = tabs[i];
|
|
145
|
-
const text = labelText(tab);
|
|
146
|
-
const entryStart = col;
|
|
147
|
-
segments.push({ text: SEPARATOR, fg: style.separatorFg });
|
|
148
|
-
segments.push({
|
|
149
|
-
text: `${text} `,
|
|
150
|
-
fg: tab.active ? style.activeFg : style.inactiveFg,
|
|
151
|
-
bg: tab.active ? style.activeBg : style.inactiveBg,
|
|
152
|
-
bold: tab.active ? (style.activeBold ?? false) : false,
|
|
153
|
-
});
|
|
154
|
-
col += entryWidths[i];
|
|
155
|
-
regions.push({ index: i, startCol: entryStart, endCol: col });
|
|
156
|
-
}
|
|
157
|
-
// Closing separator.
|
|
158
|
-
segments.push({ text: SEPARATOR, fg: style.separatorFg });
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
segments,
|
|
162
|
-
regions,
|
|
163
|
-
overflowLeft: start > 0,
|
|
164
|
-
overflowRight: end < n - 1,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function renderTabStrip(config: TabStripConfig): Line {
|
|
169
|
-
const {
|
|
170
|
-
width,
|
|
171
|
-
tabs,
|
|
172
|
-
style,
|
|
173
|
-
prefixLabel,
|
|
174
|
-
suffixLabel,
|
|
175
|
-
overflowIndicator = '›',
|
|
176
|
-
overflowIndicatorLeft = '‹',
|
|
177
|
-
onLayout,
|
|
178
|
-
} = config;
|
|
179
|
-
|
|
180
|
-
const prefixWidth = prefixLabel ? getDisplayWidth(prefixLabel) + 1 : 0;
|
|
181
|
-
const reservedSuffixWidth = suffixLabel ? getDisplayWidth(suffixLabel) : 0;
|
|
182
|
-
const indicatorWidth = Math.max(
|
|
183
|
-
getDisplayWidth(overflowIndicator),
|
|
184
|
-
getDisplayWidth(overflowIndicatorLeft),
|
|
185
|
-
);
|
|
186
|
-
const maxTabWidth = Math.max(0, width - prefixWidth - reservedSuffixWidth);
|
|
187
|
-
|
|
188
|
-
const baseSegments: StyledPanelSegment[] = [];
|
|
189
|
-
if (prefixLabel) {
|
|
190
|
-
baseSegments.push({
|
|
191
|
-
text: prefixLabel,
|
|
192
|
-
fg: style.labelFg ?? style.inactiveFg,
|
|
193
|
-
bg: style.labelBg,
|
|
194
|
-
bold: style.labelBold ?? false,
|
|
195
|
-
});
|
|
196
|
-
baseSegments.push({ text: ' ', fg: style.separatorFg });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const { segments: tabSegments, regions, overflowLeft, overflowRight } =
|
|
200
|
-
makeSegments(tabs, style, maxTabWidth, indicatorWidth);
|
|
201
|
-
|
|
202
|
-
const leftSegments: StyledPanelSegment[] = [];
|
|
203
|
-
if (overflowLeft) {
|
|
204
|
-
leftSegments.push({ text: overflowIndicatorLeft, fg: style.overflowFg ?? style.separatorFg });
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const suffixSegments: StyledPanelSegment[] = [];
|
|
208
|
-
if (overflowRight) {
|
|
209
|
-
suffixSegments.push({ text: overflowIndicator, fg: style.overflowFg ?? style.separatorFg });
|
|
210
|
-
}
|
|
211
|
-
if (suffixLabel) {
|
|
212
|
-
suffixSegments.push({ text: suffixLabel, fg: style.trailingFg ?? style.inactiveFg });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Report absolute hit regions: prefix + optional left indicator, then tabs.
|
|
216
|
-
if (onLayout) {
|
|
217
|
-
const offset = prefixWidth + (overflowLeft ? getDisplayWidth(overflowIndicatorLeft) : 0);
|
|
218
|
-
onLayout(regions.map((r) => ({ index: r.index, startCol: offset + r.startCol, endCol: offset + r.endCol })));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return buildStyledPanelLine(width, [...baseSegments, ...leftSegments, ...tabSegments, ...suffixSegments]);
|
|
222
|
-
}
|