@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,28 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* renderSessionPickerModal,
|
|
3
|
-
* using ModalFactory.
|
|
2
|
+
* renderSessionPickerModal, the /sessions picker drawn with the modal surface kit.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* "Cross-surface sessions" section is appended, badged kind/status/project
|
|
12
|
-
* (parity with the webui SessionsView) with one of three honest states,
|
|
13
|
-
* true-empty, offline, or stale, never a silently-collapsed list. In
|
|
14
|
-
* 'local' mode (no sessionBroker wired, e.g. every pre-existing caller/test)
|
|
15
|
-
* this section is entirely absent and the box sizing is unchanged.
|
|
4
|
+
* Saved sessions grouped by recency (today, yesterday, this week, earlier)
|
|
5
|
+
* with message count and age right-aligned and muted, an always-live search
|
|
6
|
+
* row, and two read-only groups: sessions hosted on the daemon and the
|
|
7
|
+
* cross-surface session union (each with its honest state: never read,
|
|
8
|
+
* offline, stale, empty). Deleting arms the row in red and asks for the same
|
|
9
|
+
* key again, in place.
|
|
16
10
|
*/
|
|
17
11
|
|
|
18
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
19
12
|
import type { SharedSessionRecord } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
20
|
-
import {
|
|
21
|
-
import { UI_TONES } from './ui-primitives.ts';
|
|
13
|
+
import { activeTokens } from './theme.ts';
|
|
22
14
|
import type { SessionPickerModal } from '../input/session-picker-modal.ts';
|
|
23
15
|
import { formatTimestamp } from './modal-utils.ts';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
16
|
+
import { beginModal, drawWrapped, finishModal, scrollCountText, searchRow, wrapLines, type KitHint, type SurfaceLayer } from './surface-kit.ts';
|
|
17
|
+
import { drawList, drawScrollingList, measureRow, type KitRow } from './surface-kit-list.ts';
|
|
26
18
|
|
|
27
19
|
// ---------------------------------------------------------------------------
|
|
28
20
|
// Cross-surface badge helpers (parity with webui's src/lib/sessions-union.ts)
|
|
@@ -110,31 +102,6 @@ function hostedRosterNote(roster: SessionPickerModal['hostedRoster']): string |
|
|
|
110
102
|
return null;
|
|
111
103
|
}
|
|
112
104
|
|
|
113
|
-
/**
|
|
114
|
-
* One hosted row: the id first, then what it is doing, what leaving it would
|
|
115
|
-
* do, and finally its title.
|
|
116
|
-
*
|
|
117
|
-
* The id leads because it is the ACTIONABLE part, the row exists so `/hosted
|
|
118
|
-
* attach <id>` can be typed off it, and a narrow terminal clips the tail. What
|
|
119
|
-
* gets clipped must therefore be the descriptive end of the line, never the
|
|
120
|
-
* thing the user has to retype.
|
|
121
|
-
*/
|
|
122
|
-
function hostedRowLabel(
|
|
123
|
-
record: SessionPickerModal['hostedRoster']['sessions'][number],
|
|
124
|
-
contentWidth: number,
|
|
125
|
-
): string {
|
|
126
|
-
const policy = record.effectiveDetachPolicy === 'survive' ? 'survives detach' : 'ends on detach';
|
|
127
|
-
const attached = record.attachedClients.length > 0 ? `${record.attachedClients.length} attached` : 'nobody attached';
|
|
128
|
-
// Fitted to the content width MINUS the list indent modal-factory adds: at
|
|
129
|
-
// the full width the row wraps to a second line, which the section's row
|
|
130
|
-
// accounting does not reserve, and the trailing attach hint is then what the
|
|
131
|
-
// tail clip eats.
|
|
132
|
-
return fitDisplay(
|
|
133
|
-
`${record.id} · ${record.status} · ${policy} · ${attached} · ${record.title || 'untitled'}`,
|
|
134
|
-
Math.max(8, contentWidth - 4),
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
105
|
/**
|
|
139
106
|
* Whether the hosted section renders at all.
|
|
140
107
|
*
|
|
@@ -148,25 +115,6 @@ function hostedSectionVisible(modal: SessionPickerModal): boolean {
|
|
|
148
115
|
return roster.sessions.length > 0 || roster.capturedAt !== null || roster.note !== null;
|
|
149
116
|
}
|
|
150
117
|
|
|
151
|
-
function hostedRowCount(modal: SessionPickerModal): number {
|
|
152
|
-
return Math.min(MAX_HOSTED_ROWS, modal.hostedRoster.sessions.length);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Extra content rows the hosted section needs: separator + header (+note)
|
|
157
|
-
* + rows (+overflow line + the attach hint). Counted for the same reason the
|
|
158
|
-
* cross-surface section counts its own, an uncounted trailing row is silently
|
|
159
|
-
* eaten by modal-factory's tail clip.
|
|
160
|
-
*/
|
|
161
|
-
function hostedExtraRows(modal: SessionPickerModal): number {
|
|
162
|
-
if (!hostedSectionVisible(modal)) return 0;
|
|
163
|
-
const rows = hostedRowCount(modal);
|
|
164
|
-
const noteRow = hostedRosterNote(modal.hostedRoster) ? 1 : 0;
|
|
165
|
-
const overflowRow = modal.hostedRoster.sessions.length > MAX_HOSTED_ROWS ? 1 : 0;
|
|
166
|
-
const hintRow = rows > 0 ? 1 : 0;
|
|
167
|
-
return 2 + noteRow + overflowRow + hintRow + rows;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
118
|
/**
|
|
171
119
|
* The exact honest note for the current state, or null when the union view
|
|
172
120
|
* needs no caveat (fresh, with rows). Precedence: offline > stale >
|
|
@@ -185,27 +133,108 @@ function crossSurfaceNote(view: SessionPickerModal['crossSurfaceView'], rowCount
|
|
|
185
133
|
return null;
|
|
186
134
|
}
|
|
187
135
|
|
|
188
|
-
|
|
189
|
-
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Rows
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
const DAY_MS = 86_400_000;
|
|
141
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
142
|
+
|
|
143
|
+
type RecencyGroup = 'Today' | 'Yesterday' | 'This week' | 'Earlier';
|
|
144
|
+
|
|
145
|
+
function startOfDay(ms: number): number {
|
|
146
|
+
const d = new Date(ms);
|
|
147
|
+
d.setHours(0, 0, 0, 0);
|
|
148
|
+
return d.getTime();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function recencyGroup(ts: number, now: number): RecencyGroup {
|
|
152
|
+
const today = startOfDay(now);
|
|
153
|
+
if (ts >= today) return 'Today';
|
|
154
|
+
if (ts >= today - DAY_MS) return 'Yesterday';
|
|
155
|
+
if (ts >= today - 6 * DAY_MS) return 'This week';
|
|
156
|
+
return 'Earlier';
|
|
190
157
|
}
|
|
191
158
|
|
|
192
|
-
/**
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
159
|
+
/** Short age for the right column: HH:MM today, "Mon D" this year, the full date otherwise. */
|
|
160
|
+
function shortWhen(ts: number, now: number): string {
|
|
161
|
+
if (!ts) return 'unknown';
|
|
162
|
+
const d = new Date(ts);
|
|
163
|
+
if (ts >= startOfDay(now)) return formatTimestamp(ts).slice(11);
|
|
164
|
+
if (d.getFullYear() === new Date(now).getFullYear()) return `${MONTHS[d.getMonth()]} ${d.getDate()}`;
|
|
165
|
+
return formatTimestamp(ts).slice(0, 10);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function hostedRows(modal: SessionPickerModal): KitRow[] {
|
|
169
|
+
if (!hostedSectionVisible(modal)) return [];
|
|
170
|
+
const t = activeTokens();
|
|
171
|
+
const roster = modal.hostedRoster;
|
|
172
|
+
const rows: KitRow[] = [{ header: 'Hosted', headerRight: 'on the daemon' }];
|
|
173
|
+
const note = hostedRosterNote(roster);
|
|
174
|
+
if (note) rows.push({ label: note, labelFg: roster.note ? t.warning : t.textFaint });
|
|
175
|
+
for (const record of roster.sessions.slice(0, MAX_HOSTED_ROWS)) {
|
|
176
|
+
const policy = record.effectiveDetachPolicy === 'survive' ? 'survives detach' : 'ends on detach';
|
|
177
|
+
const attached = record.attachedClients.length > 0 ? `${record.attachedClients.length} attached` : 'nobody attached';
|
|
178
|
+
const running = record.status === 'running';
|
|
179
|
+
const terminated = record.status === 'terminated';
|
|
180
|
+
rows.push({
|
|
181
|
+
label: record.title || 'untitled',
|
|
182
|
+
desc: `${record.id} · ${record.status} · ${policy} · ${attached}`,
|
|
183
|
+
mark: running ? '◐' : terminated ? '○' : '●',
|
|
184
|
+
markFg: running ? t.brand : terminated ? t.textFaint : t.textMuted,
|
|
185
|
+
muted: terminated,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (roster.sessions.length > MAX_HOSTED_ROWS) rows.push({ label: `showing ${MAX_HOSTED_ROWS} of ${roster.sessions.length}`, labelFg: t.textFaint });
|
|
189
|
+
if (roster.sessions.length > 0) rows.push({ label: 'Join one with /hosted attach <id>', labelFg: t.textFaint });
|
|
190
|
+
return rows;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function crossSurfaceRows(modal: SessionPickerModal): KitRow[] {
|
|
194
|
+
if (modal.crossSurfaceView.mode === 'local') return [];
|
|
195
|
+
const t = activeTokens();
|
|
196
|
+
const rows: KitRow[] = [{ header: 'Other surfaces', headerRight: modal.crossSurfaceView.mode }];
|
|
197
|
+
const note = crossSurfaceNote(modal.crossSurfaceView, modal.crossSurfaceSessions.length);
|
|
198
|
+
if (note) rows.push({ label: note, labelFg: modal.crossSurfaceView.offlineNote ? t.warning : t.textFaint });
|
|
199
|
+
for (const record of modal.crossSurfaceSessions.slice(0, MAX_CROSS_SURFACE_ROWS)) {
|
|
200
|
+
const reaped = isReapedRecord(record);
|
|
201
|
+
const closed = isClosedStatus(record.status);
|
|
202
|
+
rows.push({
|
|
203
|
+
label: record.title || record.id,
|
|
204
|
+
desc: `${kindLabel(record.kind)} · ${statusLabel(record)} · ${projectLabel(record.project)}`,
|
|
205
|
+
// Reaped rows get their own tone: they reopen on the next heartbeat, unlike a deliberate close.
|
|
206
|
+
labelFg: reaped ? t.info : undefined,
|
|
207
|
+
muted: closed && !reaped,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (modal.crossSurfaceSessions.length > MAX_CROSS_SURFACE_ROWS) {
|
|
211
|
+
rows.push({ label: `showing ${MAX_CROSS_SURFACE_ROWS} of ${modal.crossSurfaceSessions.length}`, labelFg: t.textFaint });
|
|
212
|
+
}
|
|
213
|
+
if (hasVisibleReapedRow(modal)) rows.push({ label: REAPED_BADGE_HINT, labelFg: t.info });
|
|
214
|
+
return rows;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function localRows(modal: SessionPickerModal, now: number): Map<RecencyGroup, KitRow[]> {
|
|
218
|
+
const t = activeTokens();
|
|
219
|
+
const groups = new Map<RecencyGroup, KitRow[]>();
|
|
220
|
+
modal.visibleSessions().forEach((sess, index) => {
|
|
221
|
+
const selected = index === modal.selectedIndex;
|
|
222
|
+
const armed = modal.deleteConfirmationTarget === sess.name && selected;
|
|
223
|
+
const group = recencyGroup(sess.timestamp, now);
|
|
224
|
+
const title = sess.title && sess.title !== sess.name ? sess.title : '';
|
|
225
|
+
const row: KitRow = armed
|
|
226
|
+
? { label: `Press d again to delete "${sess.name}"`, right: `${sess.messageCount} msgs · ${shortWhen(sess.timestamp, now)}`, danger: true, mark: '✕', markFg: t.error, selected }
|
|
227
|
+
: {
|
|
228
|
+
label: title || sess.name,
|
|
229
|
+
desc: title ? sess.name : undefined,
|
|
230
|
+
right: `${sess.messageCount} msgs · ${shortWhen(sess.timestamp, now)}`,
|
|
231
|
+
selected,
|
|
232
|
+
};
|
|
233
|
+
const list = groups.get(group) ?? [];
|
|
234
|
+
list.push(row);
|
|
235
|
+
groups.set(group, list);
|
|
236
|
+
});
|
|
237
|
+
return groups;
|
|
209
238
|
}
|
|
210
239
|
|
|
211
240
|
// ---------------------------------------------------------------------------
|
|
@@ -213,231 +242,82 @@ function crossSurfaceExtraRows(modal: SessionPickerModal): number {
|
|
|
213
242
|
// ---------------------------------------------------------------------------
|
|
214
243
|
|
|
215
244
|
/**
|
|
216
|
-
* Render the session picker
|
|
245
|
+
* Render the session picker as a modal layer.
|
|
217
246
|
*
|
|
218
|
-
* @param modal
|
|
219
|
-
* @param
|
|
247
|
+
* @param modal SessionPickerModal state.
|
|
248
|
+
* @param screenWidth Terminal width.
|
|
249
|
+
* @param screenHeight Terminal height.
|
|
250
|
+
* @param now Clock for the recency groups (tests pass a fixed value).
|
|
220
251
|
*/
|
|
221
252
|
export function renderSessionPickerModal(
|
|
222
253
|
modal: SessionPickerModal,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// the exact pre-T2 formula, so local-only output is byte-for-byte
|
|
245
|
-
// unaffected.
|
|
246
|
-
let visibleRows: number;
|
|
247
|
-
if (extraRows === 0) {
|
|
248
|
-
visibleRows = metrics.contentRows;
|
|
249
|
-
} else {
|
|
250
|
-
const localBudget = Math.max(0, targetContentRows - extraRows);
|
|
251
|
-
const overhead = 2; // header + separator row, present whenever sessions.length > 0
|
|
252
|
-
const remaining = Math.max(0, localBudget - overhead);
|
|
253
|
-
visibleRows = modal.sessions.length > remaining
|
|
254
|
-
? Math.max(3, remaining - 2) // reserve 2 more rows for the local "[x-y of N]" overflow line
|
|
255
|
-
: Math.max(3, remaining);
|
|
254
|
+
screenWidth: number,
|
|
255
|
+
screenHeight = 24,
|
|
256
|
+
now: number = Date.now(),
|
|
257
|
+
): SurfaceLayer {
|
|
258
|
+
const t = activeTokens();
|
|
259
|
+
const armed = modal.deleteConfirmationTarget !== null;
|
|
260
|
+
const hints: KitHint[] = modal.query.length === 0
|
|
261
|
+
? [['↑↓', 'move'], ['⏎', 'open'], ['d', armed ? 'confirm delete' : 'delete']]
|
|
262
|
+
: [['↑↓', 'move'], ['⏎', 'open'], ['⌫', 'edit search']];
|
|
263
|
+
const f = beginModal(screenWidth, screenHeight, { title: 'Sessions', hints });
|
|
264
|
+
|
|
265
|
+
const visible = modal.visibleSessions();
|
|
266
|
+
const count = modal.query.length > 0 ? `${visible.length} of ${modal.sessions.length}` : `${modal.sessions.length} saved`;
|
|
267
|
+
searchRow(f, f.top, modal.query, 'Search sessions', count);
|
|
268
|
+
|
|
269
|
+
// Status and the armed-delete note sit at the bottom of the body, wrapped in full.
|
|
270
|
+
const footLines: Array<{ text: string; fg: string }> = [];
|
|
271
|
+
const width = f.r - f.l + 1;
|
|
272
|
+
// While armed, the row itself carries the "press d again" prompt.
|
|
273
|
+
if (modal.statusMessage && !armed) {
|
|
274
|
+
for (const line of wrapLines(modal.statusMessage, width)) footLines.push({ text: line, fg: armed ? t.warning : t.accent });
|
|
256
275
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const sections: import('./modal-factory.ts').ModalSection[] = [];
|
|
260
|
-
|
|
261
|
-
if (modal.sessions.length === 0) {
|
|
262
|
-
sections.push({
|
|
263
|
-
type: 'text',
|
|
264
|
-
content: 'No saved sessions.',
|
|
265
|
-
style: { fg: '244', dim: true },
|
|
266
|
-
});
|
|
267
|
-
sections.push({
|
|
268
|
-
type: 'text',
|
|
269
|
-
content: 'Use /save [name] to save the current session.',
|
|
270
|
-
style: { fg: '240', dim: true },
|
|
271
|
-
});
|
|
272
|
-
} else {
|
|
273
|
-
// Proportional column widths that adapt to the modal's content width:
|
|
274
|
-
// timestamp ~22% (clamped 10..16), messages ~12% (clamped 4..8), and the
|
|
275
|
-
// name column absorbs the remainder so the row always fills contentW.
|
|
276
|
-
const tsW = Math.min(16, Math.max(10, Math.floor(contentW * 0.22)));
|
|
277
|
-
const msgW = Math.min(8, Math.max(4, Math.floor(contentW * 0.12)));
|
|
278
|
-
const nameW = Math.max(8, contentW - tsW - msgW - 4); // 4 = separators/spaces
|
|
279
|
-
|
|
280
|
-
// Column header
|
|
281
|
-
const nameHdr = fitDisplay('Name', nameW);
|
|
282
|
-
const tsHdr = fitDisplay('Saved', tsW);
|
|
283
|
-
const msgHdr = fitDisplay('Msgs', msgW);
|
|
284
|
-
sections.push({
|
|
285
|
-
type: 'text',
|
|
286
|
-
content: `${nameHdr} ${tsHdr} ${msgHdr}`,
|
|
287
|
-
style: { fg: '240', dim: true },
|
|
288
|
-
});
|
|
289
|
-
sections.push({ type: 'separator' });
|
|
290
|
-
|
|
291
|
-
const visibleSessions = modal.sessions.slice(modal.scrollOffset, modal.scrollOffset + visibleRows);
|
|
292
|
-
const listItems: import('./modal-factory.ts').ModalListItem[] = visibleSessions.map((sess, idx) => {
|
|
293
|
-
const isSelected = modal.scrollOffset + idx === modal.selectedIndex;
|
|
294
|
-
|
|
295
|
-
const nameStr = fitDisplay(sess.name, nameW);
|
|
296
|
-
|
|
297
|
-
const tsStr = fitDisplay(formatTimestamp(sess.timestamp), tsW);
|
|
298
|
-
const msgStr = fitDisplay(String(sess.messageCount), msgW);
|
|
299
|
-
|
|
300
|
-
const label = `${nameStr} ${tsStr} ${msgStr}`;
|
|
301
|
-
return { label, selected: isSelected };
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
sections.push({ type: 'list', items: listItems });
|
|
305
|
-
if (modal.sessions.length > visibleRows) {
|
|
306
|
-
sections.push({ type: 'separator' });
|
|
307
|
-
sections.push({
|
|
308
|
-
type: 'text',
|
|
309
|
-
content: `[${modal.scrollOffset + 1}-${Math.min(modal.sessions.length, modal.scrollOffset + visibleRows)} of ${modal.sessions.length}]`,
|
|
310
|
-
style: { fg: '244', dim: true },
|
|
311
|
-
});
|
|
312
|
-
}
|
|
276
|
+
if (armed) {
|
|
277
|
+
for (const line of wrapLines(`Deletion is armed for ${modal.deleteConfirmationTarget}. Move selection or press Esc to cancel.`, width)) footLines.push({ text: line, fg: t.textFaint });
|
|
313
278
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
sections.push({
|
|
328
|
-
type: 'text',
|
|
329
|
-
content: note,
|
|
330
|
-
style: { fg: modal.crossSurfaceView.offlineNote ? UI_TONES.state.warn : '244', dim: !modal.crossSurfaceView.offlineNote },
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
if (modal.crossSurfaceSessions.length > 0) {
|
|
334
|
-
const rows = modal.crossSurfaceSessions.slice(0, MAX_CROSS_SURFACE_ROWS);
|
|
335
|
-
const listItems: import('./modal-factory.ts').ModalListItem[] = rows.map((record) => {
|
|
336
|
-
const reaped = isReapedRecord(record);
|
|
337
|
-
const closed = isClosedStatus(record.status);
|
|
338
|
-
const label = fitDisplay(
|
|
339
|
-
`${record.title || record.id} · ${kindLabel(record.kind)} · ${statusLabel(record)} · ${projectLabel(record.project)}`,
|
|
340
|
-
contentW,
|
|
341
|
-
);
|
|
342
|
-
// Reaped rows get their own tone (not plain dim-closed): the session
|
|
343
|
-
// will auto-reopen on the next heartbeat, unlike a deliberate close.
|
|
344
|
-
const style = reaped ? { fg: UI_TONES.state.info } : closed ? { fg: '244', dim: true } : undefined;
|
|
345
|
-
return { label, style };
|
|
346
|
-
});
|
|
347
|
-
sections.push({ type: 'list', items: listItems });
|
|
348
|
-
if (modal.crossSurfaceSessions.length > MAX_CROSS_SURFACE_ROWS) {
|
|
349
|
-
sections.push({
|
|
350
|
-
type: 'text',
|
|
351
|
-
content: `[showing ${MAX_CROSS_SURFACE_ROWS} of ${modal.crossSurfaceSessions.length}]`,
|
|
352
|
-
style: { fg: '244', dim: true },
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
// W4 UX-lens: only ever rendered when a visible row actually carries
|
|
356
|
-
// the 'reaped' badge, never speculative noise.
|
|
357
|
-
if (hasVisibleReapedRow(modal)) {
|
|
358
|
-
sections.push({
|
|
359
|
-
type: 'text',
|
|
360
|
-
content: REAPED_BADGE_HINT,
|
|
361
|
-
style: { fg: UI_TONES.state.info, dim: true },
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
279
|
+
const listBottom = footLines.length > 0 ? f.bottom - footLines.length - 1 : f.bottom;
|
|
280
|
+
|
|
281
|
+
const rows: KitRow[] = [];
|
|
282
|
+
const local = localRows(modal, now);
|
|
283
|
+
const pushGroup = (name: RecencyGroup): void => {
|
|
284
|
+
const group = local.get(name);
|
|
285
|
+
if (group && group.length > 0) rows.push({ header: name }, ...group);
|
|
286
|
+
};
|
|
287
|
+
if (modal.sessions.length === 0) {
|
|
288
|
+
rows.push({ label: 'No saved sessions.', labelFg: t.textFaint });
|
|
289
|
+
rows.push({ label: 'Use /save [name] to save the current session.', labelFg: t.textFaint });
|
|
290
|
+
} else if (visible.length === 0) {
|
|
291
|
+
rows.push({ label: `No sessions match "${modal.query}".`, labelFg: t.textFaint });
|
|
365
292
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (modal.hostedRoster.sessions.length > 0) {
|
|
387
|
-
const rows = modal.hostedRoster.sessions.slice(0, MAX_HOSTED_ROWS);
|
|
388
|
-
sections.push({
|
|
389
|
-
type: 'list',
|
|
390
|
-
items: rows.map((record) => ({
|
|
391
|
-
label: hostedRowLabel(record, contentW),
|
|
392
|
-
// A terminated hosted session is kept with its reason until retention
|
|
393
|
-
// retires it; dimming it keeps it readable without looking live.
|
|
394
|
-
style: record.status === 'terminated'
|
|
395
|
-
? { fg: '244', dim: true }
|
|
396
|
-
: record.status === 'running' ? { fg: UI_TONES.state.good } : undefined,
|
|
397
|
-
})),
|
|
398
|
-
});
|
|
399
|
-
if (modal.hostedRoster.sessions.length > MAX_HOSTED_ROWS) {
|
|
400
|
-
sections.push({
|
|
401
|
-
type: 'text',
|
|
402
|
-
content: `[showing ${MAX_HOSTED_ROWS} of ${modal.hostedRoster.sessions.length}]`,
|
|
403
|
-
style: { fg: '244', dim: true },
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
sections.push({
|
|
407
|
-
type: 'text',
|
|
408
|
-
content: 'Join one with /hosted attach <id>',
|
|
409
|
-
style: { fg: '244', dim: true },
|
|
410
|
-
});
|
|
411
|
-
}
|
|
293
|
+
pushGroup('Today');
|
|
294
|
+
pushGroup('Yesterday');
|
|
295
|
+
pushGroup('This week');
|
|
296
|
+
pushGroup('Earlier');
|
|
297
|
+
|
|
298
|
+
// Hosted and cross-surface sessions are read-only (not selectable), so they
|
|
299
|
+
// are pinned under the saved sessions instead of scrolling with them: a
|
|
300
|
+
// long saved list can never push them out of reach. Past half the body
|
|
301
|
+
// they count what they hide.
|
|
302
|
+
const pinned: KitRow[] = [...hostedRows(modal), ...crossSurfaceRows(modal)];
|
|
303
|
+
const top = f.top + 2;
|
|
304
|
+
let savedBottom = listBottom;
|
|
305
|
+
if (pinned.length > 0) {
|
|
306
|
+
const pinnedNeed = pinned.reduce((sum, row, i) => sum + measureRow(row, f.l, f.r) + (i > 0 && row.header !== undefined ? 1 : 0), 0);
|
|
307
|
+
const pinnedRows = Math.min(pinnedNeed, Math.max(3, Math.floor((listBottom - top + 1) / 2)));
|
|
308
|
+
const pinnedTop = listBottom - pinnedRows + 1;
|
|
309
|
+
drawScrollingList(f.canvas, { rows: pinned, top: pinnedTop, bottom: listBottom, x0: f.l, x1: f.r });
|
|
310
|
+
savedBottom = pinnedTop - 2;
|
|
412
311
|
}
|
|
413
312
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
sections.push({
|
|
418
|
-
type: 'text',
|
|
419
|
-
content: modal.statusMessage,
|
|
420
|
-
style: { fg: modal.deleteConfirmationTarget ? '#f59e0b' : '#00ffcc' },
|
|
421
|
-
});
|
|
422
|
-
}
|
|
313
|
+
modal.setVisibleRows(Math.max(3, savedBottom - top + 1));
|
|
314
|
+
const result = drawList(f.canvas, { rows, top, bottom: savedBottom, x0: f.l, x1: f.r, scrollKey: { owner: modal, name: 'sessions' } });
|
|
315
|
+
f.hintRight = scrollCountText(result.above, result.below);
|
|
423
316
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
style: { fg: '244', dim: true },
|
|
429
|
-
});
|
|
317
|
+
let y = listBottom + 2;
|
|
318
|
+
for (const line of footLines) {
|
|
319
|
+
drawWrapped(f.canvas, f.l, y, width, line.text, { fg: line.fg }, f.bottom);
|
|
320
|
+
y++;
|
|
430
321
|
}
|
|
431
|
-
|
|
432
|
-
return ModalFactory.createModal(
|
|
433
|
-
{
|
|
434
|
-
title: 'Sessions',
|
|
435
|
-
width: boxW,
|
|
436
|
-
margin: boxMargin,
|
|
437
|
-
targetContentRows,
|
|
438
|
-
sections,
|
|
439
|
-
hints: ['[\u2191\u2193] Navigate', '[Enter] Load', '[d] Arm / Delete', '[Esc] Close'],
|
|
440
|
-
},
|
|
441
|
-
width,
|
|
442
|
-
);
|
|
322
|
+
return finishModal(f);
|
|
443
323
|
}
|
|
@@ -9,40 +9,31 @@
|
|
|
9
9
|
|
|
10
10
|
import type { SettingsModal } from '../input/settings-modal.ts';
|
|
11
11
|
import { selectedConnectionEntry } from '../input/settings-modal-connections.ts';
|
|
12
|
-
import { connectionSurfaceLabel
|
|
13
|
-
import {
|
|
14
|
-
import { clamp, padDisplay, stableWindow } from './fullscreen-workspace.ts';
|
|
12
|
+
import { connectionSurfaceLabel } from '../input/commands/connection-status.ts';
|
|
13
|
+
import type { KitRow } from './surface-kit-list.ts';
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
|
-
* The Connections rows: one
|
|
16
|
+
* The Connections rows: one row per surface, saying what it actually is.
|
|
18
17
|
*
|
|
19
18
|
* `checking` is rendered as its own state rather than blanked, because a row
|
|
20
19
|
* that showed nothing while the probe was in flight would read as "nothing
|
|
21
20
|
* configured", the one thing this category exists to stop claiming falsely.
|
|
22
21
|
*/
|
|
23
|
-
export function
|
|
24
|
-
const rows: string[] = [];
|
|
22
|
+
export function connectionRows(modal: SettingsModal, selectable: boolean): KitRow[] {
|
|
25
23
|
const items = modal.connectionEntries;
|
|
26
|
-
if (items.length === 0) return ['No connection surfaces are known to this build.'];
|
|
27
|
-
const selectedIndex =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const entry = items[index]!;
|
|
36
|
-
const selected = index === selectedIndex;
|
|
37
|
-
const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '\u2022') : ' ';
|
|
38
|
-
rows.push(`${marker} ${padDisplay(connectionSurfaceLabel(entry.surface), surfaceWidth)} ${padDisplay(entry.state, stateWidth)} ${padDisplay(entry.detail, detailWidth)}`);
|
|
39
|
-
}
|
|
40
|
-
if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more connection(s) below`);
|
|
41
|
-
return rows.slice(0, height);
|
|
24
|
+
if (items.length === 0) return [{ label: 'No connection surfaces are known to this build.', muted: true }];
|
|
25
|
+
const selectedIndex = Math.max(0, Math.min(modal.selectedIndex, items.length - 1));
|
|
26
|
+
return items.map((entry, index) => ({
|
|
27
|
+
label: connectionSurfaceLabel(entry.surface),
|
|
28
|
+
desc: entry.detail,
|
|
29
|
+
right: entry.state,
|
|
30
|
+
selected: selectable && index === selectedIndex,
|
|
31
|
+
current: !selectable && index === selectedIndex,
|
|
32
|
+
}));
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
/**
|
|
45
|
-
* The detail
|
|
36
|
+
* The detail for one connection: the state, what it means, and every next
|
|
46
37
|
* step verbatim. The next steps are the daemon's requirements, so they are
|
|
47
38
|
* listed in full rather than summarized, a truncated instruction is a wrong
|
|
48
39
|
* instruction.
|