@pellux/goodvibes-tui 2.0.20 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +59 -4
- package/package.json +16 -6
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/verification-ledger.ts +18 -16
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationFeed, the read-model behind the `panel_only` notification
|
|
3
|
+
* target.
|
|
4
|
+
*
|
|
5
|
+
* The SDK's NotificationRouter (`@/runtime/index.ts` -> platform runtime `ui`
|
|
6
|
+
* barrel) can route a notification's `RoutingDecision.target` to
|
|
7
|
+
* `'panel_only'`, meaning: don't surface this in the conversation or the
|
|
8
|
+
* status bar, but it must still reach a real, visible place. Before this
|
|
9
|
+
* file, nothing in this repo consumed that target outside of tests, a
|
|
10
|
+
* view-routed notification (including every burst- or batch-collapsed one)
|
|
11
|
+
* had nowhere to go. This feed is that place: a caller holding a routed
|
|
12
|
+
* `Notification` + `RoutingDecision` pair calls `record()`, and the
|
|
13
|
+
* Notifications modal (input/notifications-modal.ts) shows whatever has
|
|
14
|
+
* accumulated; warning and critical entries also toast.
|
|
15
|
+
*
|
|
16
|
+
* Collapsed groups (reasonCode `burst_collapsed` / `batch_window_collapsed`)
|
|
17
|
+
* are tracked by their `batchKey` and accumulate an honest running count
|
|
18
|
+
* instead of rendering one row per suppressed duplicate.
|
|
19
|
+
*
|
|
20
|
+
* Production wiring, something in the running app actually calling
|
|
21
|
+
* `record()` with live notifications, rides the SDK round that turns on
|
|
22
|
+
* adaptive notification suppression by default; this feed and its modal are
|
|
23
|
+
* the render target that work lands on, built ahead of it so the target is
|
|
24
|
+
* never missing when that switch flips.
|
|
25
|
+
*/
|
|
26
|
+
import type { Notification, RoutingDecision } from '@/runtime/index.ts';
|
|
27
|
+
|
|
28
|
+
export interface NotificationFeedEntry {
|
|
29
|
+
readonly key: string;
|
|
30
|
+
readonly domain: string;
|
|
31
|
+
readonly level: Notification['level'];
|
|
32
|
+
readonly title: string;
|
|
33
|
+
readonly body: string | undefined;
|
|
34
|
+
/** Unix ms of the most recent notification folded into this entry. */
|
|
35
|
+
readonly timestamp: number;
|
|
36
|
+
/** The router's reason, or 'system_notice' for a conversation system notice (recordNotice). */
|
|
37
|
+
readonly reasonCode: RoutingDecision['reasonCode'] | 'system_notice';
|
|
38
|
+
/** How many notifications this entry represents. 1 for a standalone item; >1 for a collapsed group. Always the true count, never estimated. */
|
|
39
|
+
readonly collapsedCount: number;
|
|
40
|
+
/**
|
|
41
|
+
* The view this notification is about (the notification's own panelId or
|
|
42
|
+
* jump action, else one derived from its domain), resolved to a modal by
|
|
43
|
+
* input/views.ts. Undefined when it is not about anything that can be opened.
|
|
44
|
+
*/
|
|
45
|
+
readonly subject?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether this entry toasts: 'always' for a system notice (every one shows
|
|
48
|
+
* as a toast), 'never' for one restored from a saved session (it already
|
|
49
|
+
* happened), absent for a routed notification (warning and critical toast).
|
|
50
|
+
*/
|
|
51
|
+
readonly toast?: 'always' | 'never';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Severity order, for folding two arrivals of one event (the higher level wins). */
|
|
55
|
+
const LEVEL_RANK: Readonly<Record<Notification['level'], number>> = { debug: -1, info: 0, warning: 1, critical: 2 };
|
|
56
|
+
|
|
57
|
+
/** A conversation system notice to keep in the history (see recordNotice). */
|
|
58
|
+
export interface NoticeInput {
|
|
59
|
+
readonly domain: string;
|
|
60
|
+
readonly level: Notification['level'];
|
|
61
|
+
readonly title: string;
|
|
62
|
+
readonly body?: string | undefined;
|
|
63
|
+
readonly timestamp: number;
|
|
64
|
+
/** A notice restored from a saved session: kept in history, never toasted, already seen. */
|
|
65
|
+
readonly restored?: boolean;
|
|
66
|
+
/** The runtime event this notice restates (the SDK's runtimeEventOfNotice key), if any. */
|
|
67
|
+
readonly eventKey?: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Domains whose notifications are about something the Agents modal shows. */
|
|
71
|
+
const AGENT_DOMAINS: ReadonlySet<string> = new Set(['agents', 'tasks', 'workflows', 'automation', 'wrfc', 'orchestration', 'plan']);
|
|
72
|
+
|
|
73
|
+
/** The view a notification is about, or undefined. */
|
|
74
|
+
function subjectOf(notification: Notification): string | undefined {
|
|
75
|
+
if (notification.panelId) return notification.panelId;
|
|
76
|
+
if (notification.action?.type === 'jump_to_panel' && notification.action.panelId) return notification.action.panelId;
|
|
77
|
+
if (AGENT_DOMAINS.has(notification.domain)) return 'agents';
|
|
78
|
+
if (notification.domain === 'security') return 'security';
|
|
79
|
+
if (notification.domain === 'git') return 'changes';
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Plain names for a collapsed group of mixed notifications, by domain. */
|
|
84
|
+
const GROUP_TITLES: Readonly<Record<string, string>> = {
|
|
85
|
+
agents: 'Agent updates',
|
|
86
|
+
tasks: 'Task updates',
|
|
87
|
+
workflows: 'Review chain updates',
|
|
88
|
+
automation: 'Scheduled job updates',
|
|
89
|
+
deliveries: 'Message delivery updates',
|
|
90
|
+
security: 'Security updates',
|
|
91
|
+
ops: 'System updates',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** The title a collapsed group of mixed notifications shows: a plain name, never a raw domain id. */
|
|
95
|
+
export function groupTitle(domain: string): string {
|
|
96
|
+
const known = GROUP_TITLES[domain];
|
|
97
|
+
if (known) return known;
|
|
98
|
+
const words = domain.replace(/[_-]+/g, ' ').trim();
|
|
99
|
+
return words ? `${words[0]!.toUpperCase()}${words.slice(1)} updates` : 'Updates';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Entries kept before the oldest leave. System notices share this bound, so a
|
|
104
|
+
* long session's full notice history stays reachable in the modal.
|
|
105
|
+
*/
|
|
106
|
+
const MAX_ENTRIES = 2000;
|
|
107
|
+
|
|
108
|
+
/** Reason codes whose notifications fold into one running-count entry per batch key, rather than one entry per occurrence. */
|
|
109
|
+
const COLLAPSING_REASON_CODES: ReadonlySet<RoutingDecision['reasonCode']> = new Set([
|
|
110
|
+
'burst_collapsed',
|
|
111
|
+
'batch_window_collapsed',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
export class NotificationFeed {
|
|
115
|
+
private readonly entries = new Map<string, NotificationFeedEntry>();
|
|
116
|
+
/** Insertion order of `entries` keys (oldest first), for bounded eviction. */
|
|
117
|
+
private order: string[] = [];
|
|
118
|
+
private readonly listeners = new Set<() => void>();
|
|
119
|
+
/** Unix ms of the newest entry the user has seen in the Notifications modal. */
|
|
120
|
+
private seenThrough = 0;
|
|
121
|
+
private nextNoticeId = 1;
|
|
122
|
+
/** Per collapsed group: how many of each notification title it folded. */
|
|
123
|
+
private readonly groupTitles = new Map<string, Map<string, number>>();
|
|
124
|
+
/**
|
|
125
|
+
* One entry per runtime event: the entry each event key was recorded in. An
|
|
126
|
+
* agent or chain event reaches the feed twice, from the runtime-bus bridge
|
|
127
|
+
* (record) and as the conversation line restating it (recordNotice); the
|
|
128
|
+
* second arrival folds into the first entry instead of adding another.
|
|
129
|
+
*/
|
|
130
|
+
private readonly eventEntries = new Map<string, string>();
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Record a routed notification. Only notifications actually targeted at
|
|
134
|
+
* `panel_only` are kept, a caller passing anything else is a mistake (the
|
|
135
|
+
* router sent it elsewhere), so it's dropped rather than shown in the
|
|
136
|
+
* wrong place.
|
|
137
|
+
*/
|
|
138
|
+
record(notification: Notification, decision: RoutingDecision, eventKey?: string): void {
|
|
139
|
+
if (decision.target !== 'panel_only') return;
|
|
140
|
+
if (eventKey && this.foldEvent(eventKey, { level: notification.level, body: notification.body, fromNotice: false, restored: false })) return;
|
|
141
|
+
|
|
142
|
+
const collapsing = COLLAPSING_REASON_CODES.has(decision.reasonCode) && Boolean(decision.batchKey);
|
|
143
|
+
const key = collapsing ? `group:${decision.batchKey}` : `single:${notification.id}`;
|
|
144
|
+
const previousCount = collapsing ? (this.entries.get(key)?.collapsedCount ?? 0) : 0;
|
|
145
|
+
|
|
146
|
+
// A collapsed group folds every notification of one domain and level, of
|
|
147
|
+
// any kind. Its title names the group, never just its latest member, and
|
|
148
|
+
// the body counts each kind, so the number shown is what it says it is.
|
|
149
|
+
let title = notification.title;
|
|
150
|
+
let body = notification.body;
|
|
151
|
+
if (collapsing) {
|
|
152
|
+
const counts = this.groupTitles.get(key) ?? new Map<string, number>();
|
|
153
|
+
counts.set(notification.title, (counts.get(notification.title) ?? 0) + 1);
|
|
154
|
+
this.groupTitles.set(key, counts);
|
|
155
|
+
if (counts.size > 1) {
|
|
156
|
+
title = groupTitle(notification.domain);
|
|
157
|
+
body = [...counts].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} \u00d7${n}`).join(', ');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const entry: NotificationFeedEntry = {
|
|
161
|
+
key,
|
|
162
|
+
domain: notification.domain,
|
|
163
|
+
level: notification.level,
|
|
164
|
+
title,
|
|
165
|
+
body,
|
|
166
|
+
timestamp: notification.timestamp,
|
|
167
|
+
reasonCode: decision.reasonCode,
|
|
168
|
+
collapsedCount: previousCount + 1,
|
|
169
|
+
subject: subjectOf(notification),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
this.store(key, entry);
|
|
173
|
+
if (eventKey) this.eventEntries.set(eventKey, key);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Record a conversation system notice ([WRFC] …, [Agents] …, a compaction
|
|
178
|
+
* receipt): its full text, one entry per notice. It toasts unless it was
|
|
179
|
+
* restored from a saved session, which also counts as already seen.
|
|
180
|
+
*/
|
|
181
|
+
recordNotice(input: NoticeInput): NotificationFeedEntry {
|
|
182
|
+
if (input.eventKey) {
|
|
183
|
+
const folded = this.foldEvent(input.eventKey, { level: input.level, body: input.body, fromNotice: true, restored: input.restored === true });
|
|
184
|
+
if (folded) return folded;
|
|
185
|
+
}
|
|
186
|
+
const key = `notice:${this.nextNoticeId++}`;
|
|
187
|
+
const entry: NotificationFeedEntry = {
|
|
188
|
+
key,
|
|
189
|
+
domain: input.domain,
|
|
190
|
+
level: input.level,
|
|
191
|
+
title: input.title,
|
|
192
|
+
body: input.body,
|
|
193
|
+
timestamp: input.timestamp,
|
|
194
|
+
reasonCode: 'system_notice',
|
|
195
|
+
collapsedCount: 1,
|
|
196
|
+
subject: AGENT_DOMAINS.has(input.domain) ? 'agents' : undefined,
|
|
197
|
+
toast: input.restored ? 'never' : 'always',
|
|
198
|
+
};
|
|
199
|
+
if (input.restored) this.seenThrough = Math.max(this.seenThrough, input.timestamp);
|
|
200
|
+
this.store(key, entry);
|
|
201
|
+
if (input.eventKey) this.eventEntries.set(input.eventKey, key);
|
|
202
|
+
return entry;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The second arrival of one runtime event: fold it into the entry the first
|
|
207
|
+
* made, keeping that entry's plain title and place. The conversation line's
|
|
208
|
+
* detail is the fuller one, so it becomes the body; the higher level wins;
|
|
209
|
+
* a live notice makes the entry toast (once, see the toast bridge). A
|
|
210
|
+
* collapsed group that already counted the event is left as it is. Returns
|
|
211
|
+
* the entry, or undefined when the event has no entry yet.
|
|
212
|
+
*/
|
|
213
|
+
private foldEvent(
|
|
214
|
+
eventKey: string,
|
|
215
|
+
arrival: { readonly level: Notification['level']; readonly body: string | undefined; readonly fromNotice: boolean; readonly restored: boolean },
|
|
216
|
+
): NotificationFeedEntry | undefined {
|
|
217
|
+
const key = this.eventEntries.get(eventKey);
|
|
218
|
+
const existing = key === undefined ? undefined : this.entries.get(key);
|
|
219
|
+
if (!key || !existing) return undefined;
|
|
220
|
+
if (existing.collapsedCount > 1) return existing;
|
|
221
|
+
const merged: NotificationFeedEntry = {
|
|
222
|
+
...existing,
|
|
223
|
+
level: LEVEL_RANK[arrival.level] > LEVEL_RANK[existing.level] ? arrival.level : existing.level,
|
|
224
|
+
body: arrival.fromNotice ? (arrival.body ?? existing.body) : (existing.body ?? arrival.body),
|
|
225
|
+
...(arrival.fromNotice && !arrival.restored ? { toast: 'always' as const } : {}),
|
|
226
|
+
};
|
|
227
|
+
this.store(key, merged);
|
|
228
|
+
return merged;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private store(key: string, entry: NotificationFeedEntry): void {
|
|
232
|
+
if (!this.entries.has(key)) {
|
|
233
|
+
this.order.push(key);
|
|
234
|
+
if (this.order.length > MAX_ENTRIES) {
|
|
235
|
+
const evicted = this.order.shift();
|
|
236
|
+
if (evicted !== undefined) { this.entries.delete(evicted); this.groupTitles.delete(evicted); this.forgetEvents(evicted); }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
this.entries.set(key, entry);
|
|
240
|
+
this.emitChange();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Every entry, most recently updated first. */
|
|
244
|
+
list(): readonly NotificationFeedEntry[] {
|
|
245
|
+
const items: NotificationFeedEntry[] = [];
|
|
246
|
+
for (const key of this.order) {
|
|
247
|
+
const entry = this.entries.get(key);
|
|
248
|
+
if (entry) items.push(entry);
|
|
249
|
+
}
|
|
250
|
+
return items.reverse();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
clear(): void {
|
|
254
|
+
this.entries.clear();
|
|
255
|
+
this.groupTitles.clear();
|
|
256
|
+
this.eventEntries.clear();
|
|
257
|
+
this.order = [];
|
|
258
|
+
this.emitChange();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Remove one entry (a collapsed group goes as a whole). Returns whether it existed. */
|
|
262
|
+
dismiss(key: string): boolean {
|
|
263
|
+
if (!this.entries.delete(key)) return false;
|
|
264
|
+
this.groupTitles.delete(key);
|
|
265
|
+
this.forgetEvents(key);
|
|
266
|
+
this.order = this.order.filter((k) => k !== key);
|
|
267
|
+
this.emitChange();
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private forgetEvents(entryKey: string): void {
|
|
272
|
+
for (const [eventKey, owner] of this.eventEntries) if (owner === entryKey) this.eventEntries.delete(eventKey);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Entries updated since the user last looked. */
|
|
276
|
+
unreadCount(): number {
|
|
277
|
+
let n = 0;
|
|
278
|
+
for (const entry of this.entries.values()) if (entry.timestamp > this.seenThrough) n++;
|
|
279
|
+
return n;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Whether an entry was updated since the user last looked. */
|
|
283
|
+
isUnread(entry: NotificationFeedEntry): boolean {
|
|
284
|
+
return entry.timestamp > this.seenThrough;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Everything recorded so far counts as seen. */
|
|
288
|
+
markAllSeen(): void {
|
|
289
|
+
for (const entry of this.entries.values()) this.seenThrough = Math.max(this.seenThrough, entry.timestamp);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Subscribe to feed changes (e.g. to repaint an open modal). Returns an unsubscribe function. */
|
|
293
|
+
subscribe(listener: () => void): () => void {
|
|
294
|
+
this.listeners.add(listener);
|
|
295
|
+
return () => this.listeners.delete(listener);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private emitChange(): void {
|
|
299
|
+
for (const listener of this.listeners) listener();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let sharedFeed: NotificationFeed | null = null;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The process-wide `panel_only` feed. Lazily created so the running app has
|
|
307
|
+
* exactly one feed that every caller and the Notifications modal agree
|
|
308
|
+
* on, while tests construct their own isolated `NotificationFeed`
|
|
309
|
+
* instance instead of reaching for this one.
|
|
310
|
+
*/
|
|
311
|
+
export function getSharedNotificationFeed(): NotificationFeed {
|
|
312
|
+
if (!sharedFeed) sharedFeed = new NotificationFeed();
|
|
313
|
+
return sharedFeed;
|
|
314
|
+
}
|
|
@@ -4,15 +4,15 @@ import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
|
4
4
|
import { activeUiTones, registerThemeRefresh } from '../renderer/theme.ts';
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
|
-
//
|
|
7
|
+
// View palette + core line primitives.
|
|
8
8
|
//
|
|
9
|
-
// The leaf foundation of the
|
|
9
|
+
// The leaf foundation of the view formatting toolkit: both `polish.ts` and
|
|
10
10
|
// `polish-tables.ts` build on these. Kept dependency-free (only grid /
|
|
11
11
|
// terminal-width / ui-primitives) so it never participates in an import cycle.
|
|
12
|
-
// All symbols are re-exported from `polish.ts`,
|
|
12
|
+
// All symbols are re-exported from `polish.ts`, views import from there.
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
export interface
|
|
15
|
+
export interface ViewPalette {
|
|
16
16
|
readonly label: string;
|
|
17
17
|
readonly value: string;
|
|
18
18
|
readonly dim: string;
|
|
@@ -34,10 +34,10 @@ export interface PanelPalette {
|
|
|
34
34
|
// Built from the mode-resolved chrome tones (activeUiTones). Because 100+ call
|
|
35
35
|
// sites read this object by reference, mode changes rebuild it IN PLACE via the
|
|
36
36
|
// registered refresher below rather than re-resolving per call, see theme.ts's
|
|
37
|
-
// active-mode runtime note. This refresher is registered before any
|
|
38
|
-
// extendPalette() runs (
|
|
37
|
+
// active-mode runtime note. This refresher is registered before any view's
|
|
38
|
+
// extendPalette() runs (views import polish → polish-core first), so on a mode
|
|
39
39
|
// flip the base is rebuilt before the extended palettes re-merge from it.
|
|
40
|
-
function
|
|
40
|
+
function buildViewPalette(): Required<ViewPalette> {
|
|
41
41
|
const t = activeUiTones();
|
|
42
42
|
return {
|
|
43
43
|
header: t.fg.primary,
|
|
@@ -59,41 +59,43 @@ function buildPanelPalette(): Required<PanelPalette> {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export const
|
|
63
|
-
registerThemeRefresh(() => Object.assign(
|
|
62
|
+
export const DEFAULT_VIEW_PALETTE: Readonly<Required<ViewPalette>> = buildViewPalette();
|
|
63
|
+
registerThemeRefresh(() => Object.assign(DEFAULT_VIEW_PALETTE as Required<ViewPalette>, buildViewPalette()));
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Extend the base
|
|
66
|
+
* Extend the base view palette with domain-specific colors.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* `extras` is a builder that reads theme tokens (activeTokens() /
|
|
69
|
+
* activeUiTones()); it runs now and again on every theme or mode change, so
|
|
70
|
+
* the domain colors follow the active theme. Raw hex literals do not belong
|
|
71
|
+
* in extras: map each domain color onto a theme token.
|
|
70
72
|
*
|
|
71
73
|
* @example
|
|
72
74
|
* ```ts
|
|
73
|
-
* const C = extendPalette(
|
|
74
|
-
*
|
|
75
|
-
* incident:
|
|
75
|
+
* const C = extendPalette(DEFAULT_VIEW_PALETTE, () => {
|
|
76
|
+
* const p = activeTokens();
|
|
77
|
+
* return { decision: p.info, incident: p.error };
|
|
76
78
|
* });
|
|
77
79
|
* ```
|
|
78
80
|
*/
|
|
79
81
|
export function extendPalette<T extends Record<string, string>>(
|
|
80
|
-
base: typeof
|
|
81
|
-
extras: T,
|
|
82
|
-
): typeof
|
|
83
|
-
const merged = { ...base, ...extras } as typeof
|
|
84
|
-
// Self-register an in-place rebuild so every extendPalette-derived
|
|
85
|
-
// palette
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
registerThemeRefresh(() => Object.assign(merged as Record<string, string>, base, extras));
|
|
82
|
+
base: typeof DEFAULT_VIEW_PALETTE,
|
|
83
|
+
extras: () => T,
|
|
84
|
+
): typeof DEFAULT_VIEW_PALETTE & T {
|
|
85
|
+
const merged = { ...base, ...extras() } as typeof DEFAULT_VIEW_PALETTE & T;
|
|
86
|
+
// Self-register an in-place rebuild so every extendPalette-derived view
|
|
87
|
+
// palette tracks the active theme with zero per-view churn. Runs AFTER the
|
|
88
|
+
// base refresher (registered at module eval, before any view calls this),
|
|
89
|
+
// so `base` already carries the new values when the extras are rebuilt.
|
|
90
|
+
registerThemeRefresh(() => Object.assign(merged as Record<string, string>, base, extras()));
|
|
89
91
|
return merged;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
export function
|
|
94
|
+
export function buildViewLine(
|
|
93
95
|
width: number,
|
|
94
|
-
segments: Array<
|
|
96
|
+
segments: Array<StyledViewSegment | [string, string, string?]>,
|
|
95
97
|
): Line {
|
|
96
|
-
return
|
|
98
|
+
return buildStyledViewLine(
|
|
97
99
|
width,
|
|
98
100
|
segments.map((seg) =>
|
|
99
101
|
Array.isArray(seg) ? { text: seg[0], fg: seg[1], bg: seg[2] } : seg,
|
|
@@ -101,7 +103,7 @@ export function buildPanelLine(
|
|
|
101
103
|
);
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
export interface
|
|
106
|
+
export interface StyledViewSegment {
|
|
105
107
|
readonly text: string;
|
|
106
108
|
readonly fg: string;
|
|
107
109
|
readonly bg?: string;
|
|
@@ -109,13 +111,13 @@ export interface StyledPanelSegment {
|
|
|
109
111
|
readonly dim?: boolean;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
export function
|
|
114
|
+
export function buildSelectableViewLine(
|
|
113
115
|
width: number,
|
|
114
|
-
segments: ReadonlyArray<
|
|
116
|
+
segments: ReadonlyArray<StyledViewSegment>,
|
|
115
117
|
options: { selected?: boolean; selectedBg?: string; fillFg?: string; fillBg?: string; leadingMarker?: string } = {},
|
|
116
118
|
): Line {
|
|
117
119
|
const selected = options.selected ?? false;
|
|
118
|
-
const selectedBg = selected ? (options.selectedBg ??
|
|
120
|
+
const selectedBg = selected ? (options.selectedBg ?? DEFAULT_VIEW_PALETTE.selectBg) : '';
|
|
119
121
|
const fillBg = selectedBg || options.fillBg || '';
|
|
120
122
|
const fillFg = options.fillFg ?? '';
|
|
121
123
|
const cells = createEmptyLine(width);
|
|
@@ -130,8 +132,8 @@ export function buildSelectablePanelLine(
|
|
|
130
132
|
for (const ch of options.leadingMarker) {
|
|
131
133
|
const charWidth = getDisplayWidth(ch);
|
|
132
134
|
if (charWidth <= 0 || col + charWidth > width) break;
|
|
133
|
-
cells[col] = createStyledCell(ch, { fg:
|
|
134
|
-
if (charWidth > 1 && col + 1 < width) cells[col + 1] = createStyledCell(' ', { fg:
|
|
135
|
+
cells[col] = createStyledCell(ch, { fg: DEFAULT_VIEW_PALETTE.info, bg: selectedBg, bold: true });
|
|
136
|
+
if (charWidth > 1 && col + 1 < width) cells[col + 1] = createStyledCell(' ', { fg: DEFAULT_VIEW_PALETTE.info, bg: selectedBg, bold: true });
|
|
135
137
|
col += charWidth;
|
|
136
138
|
}
|
|
137
139
|
}
|
|
@@ -166,10 +168,10 @@ export function buildSelectablePanelLine(
|
|
|
166
168
|
return cells;
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
export function
|
|
171
|
+
export function buildStyledViewLine(
|
|
170
172
|
width: number,
|
|
171
|
-
segments: ReadonlyArray<
|
|
173
|
+
segments: ReadonlyArray<StyledViewSegment>,
|
|
172
174
|
options: { fillBg?: string; fillFg?: string } = {},
|
|
173
175
|
): Line {
|
|
174
|
-
return
|
|
176
|
+
return buildSelectableViewLine(width, segments, options);
|
|
175
177
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
2
|
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
-
import { GLYPHS
|
|
3
|
+
import { GLYPHS } from '../renderer/ui-primitives.ts';
|
|
4
|
+
import { activeUiTones, registerThemeRefresh } from '../renderer/theme.ts';
|
|
4
5
|
// Build on the leaf primitives in ./polish-core.ts (palette + base line
|
|
5
6
|
// builders). polish-core has no back-edge to this file, so there is no cycle.
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
7
|
+
import { buildSelectableViewLine, DEFAULT_VIEW_PALETTE } from './polish-core.ts';
|
|
8
|
+
import type { ViewPalette, StyledViewSegment } from './polish-core.ts';
|
|
8
9
|
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
|
-
// Shared formatting primitives (the toolkit
|
|
11
|
+
// Shared formatting primitives (the toolkit views standardize on).
|
|
11
12
|
//
|
|
12
13
|
// All are display-width aware (via getDisplayWidth / truncateDisplay) so they
|
|
13
14
|
// stay aligned across emoji, CJK, and other wide characters, never hand-roll
|
|
14
|
-
// .slice()/.padEnd() truncation or alignment in
|
|
15
|
+
// .slice()/.padEnd() truncation or alignment in views.
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
|
|
17
18
|
export type ColumnAlign = 'left' | 'right' | 'center';
|
|
@@ -64,7 +65,7 @@ export function buildAlignedRow(
|
|
|
64
65
|
} = {},
|
|
65
66
|
): Line {
|
|
66
67
|
const gap = options.gap ?? 1;
|
|
67
|
-
const segments:
|
|
68
|
+
const segments: StyledViewSegment[] = [];
|
|
68
69
|
const count = Math.min(cells.length, columns.length);
|
|
69
70
|
for (let i = 0; i < count; i++) {
|
|
70
71
|
const col = columns[i]!;
|
|
@@ -78,7 +79,7 @@ export function buildAlignedRow(
|
|
|
78
79
|
dim: cell.dim,
|
|
79
80
|
});
|
|
80
81
|
}
|
|
81
|
-
return
|
|
82
|
+
return buildSelectableViewLine(width, segments, {
|
|
82
83
|
selected: options.selected,
|
|
83
84
|
selectedBg: options.selectedBg,
|
|
84
85
|
fillBg: options.fillBg,
|
|
@@ -101,27 +102,35 @@ interface BadgeSpec {
|
|
|
101
102
|
readonly label: string;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
105
|
+
function buildStatusBadges(): Record<StatusBadgeKind, BadgeSpec> {
|
|
106
|
+
const tones = activeUiTones();
|
|
107
|
+
return {
|
|
108
|
+
pending: { glyph: GLYPHS.status.pending, fg: DEFAULT_VIEW_PALETTE.info, label: 'Pending' },
|
|
109
|
+
running: { glyph: GLYPHS.status.active, fg: tones.state.active, label: 'Running' },
|
|
110
|
+
completed: { glyph: GLYPHS.status.success, fg: DEFAULT_VIEW_PALETTE.good, label: 'Completed' },
|
|
111
|
+
failed: { glyph: GLYPHS.status.failure, fg: DEFAULT_VIEW_PALETTE.bad, label: 'Failed' },
|
|
112
|
+
cancelled: { glyph: GLYPHS.status.skipped, fg: DEFAULT_VIEW_PALETTE.dim, label: 'Cancelled' },
|
|
113
|
+
blocked: { glyph: GLYPHS.status.blocked, fg: tones.state.blocked, label: 'Blocked' },
|
|
114
|
+
review: { glyph: GLYPHS.status.review, fg: DEFAULT_VIEW_PALETTE.info, label: 'Review' },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Rebuilt in place on every theme change (registered after the base palette's
|
|
119
|
+
// own refresher, so DEFAULT_VIEW_PALETTE already carries the new values).
|
|
120
|
+
const STATUS_BADGES: Record<StatusBadgeKind, BadgeSpec> = buildStatusBadges();
|
|
121
|
+
registerThemeRefresh(() => Object.assign(STATUS_BADGES, buildStatusBadges()));
|
|
113
122
|
|
|
114
123
|
/**
|
|
115
124
|
* Canonical status badge (glyph + label) with a consistent color per lifecycle
|
|
116
|
-
* state. Returns
|
|
117
|
-
*
|
|
118
|
-
* Supersedes ad-hoc per-
|
|
125
|
+
* state. Returns StyledViewSegment[], spread into a buildViewLine call:
|
|
126
|
+
* buildViewLine(width, [[' ', C.label], ...buildStatusBadge('running')])
|
|
127
|
+
* Supersedes ad-hoc per-view status→color maps.
|
|
119
128
|
*/
|
|
120
129
|
export function buildStatusBadge(
|
|
121
130
|
kind: StatusBadgeKind,
|
|
122
131
|
label?: string,
|
|
123
132
|
opts?: { glyph?: string; bg?: string; count?: number },
|
|
124
|
-
):
|
|
133
|
+
): StyledViewSegment[] {
|
|
125
134
|
const spec = STATUS_BADGES[kind];
|
|
126
135
|
const glyph = opts?.glyph ?? spec.glyph;
|
|
127
136
|
const text = label ?? spec.label;
|
|
@@ -143,12 +152,12 @@ export interface TreeItemSpec {
|
|
|
143
152
|
/**
|
|
144
153
|
* Build a hierarchical tree row: indentation + expand/collapse glyph + optional
|
|
145
154
|
* icon + label, with right-aligned metadata. Replaces hand-rolled indentation
|
|
146
|
-
* in file-explorer / symbol-outline / orchestration
|
|
155
|
+
* in file-explorer / symbol-outline / orchestration views.
|
|
147
156
|
*/
|
|
148
157
|
export function buildTreeRow(
|
|
149
158
|
width: number,
|
|
150
159
|
item: TreeItemSpec,
|
|
151
|
-
palette:
|
|
160
|
+
palette: ViewPalette,
|
|
152
161
|
options: { selected?: boolean; selectedBg?: string; indentWidth?: number } = {},
|
|
153
162
|
): Line {
|
|
154
163
|
const indent = ' '.repeat(Math.max(0, item.depth) * (options.indentWidth ?? 2));
|
|
@@ -168,7 +177,7 @@ export function buildTreeRow(
|
|
|
168
177
|
const label = truncateDisplay(item.label, labelBudget);
|
|
169
178
|
const usedW = prefixW + getDisplayWidth(label);
|
|
170
179
|
|
|
171
|
-
const segments:
|
|
180
|
+
const segments: StyledViewSegment[] = [
|
|
172
181
|
{ text: prefix, fg: palette.dim },
|
|
173
182
|
{ text: label, fg: item.labelColor ?? palette.value },
|
|
174
183
|
];
|
|
@@ -180,7 +189,7 @@ export function buildTreeRow(
|
|
|
180
189
|
segments.push({ text: m.text, fg: m.fg });
|
|
181
190
|
});
|
|
182
191
|
}
|
|
183
|
-
return
|
|
192
|
+
return buildSelectableViewLine(width, segments, {
|
|
184
193
|
selected: options.selected,
|
|
185
194
|
selectedBg: options.selectedBg ?? palette.selectBg,
|
|
186
195
|
});
|
|
@@ -222,13 +231,13 @@ function resolveColumnWidths(width: number, columns: ReadonlyArray<TableColumn>,
|
|
|
222
231
|
|
|
223
232
|
/**
|
|
224
233
|
* Build a table: a header row plus aligned data rows. Replaces ad-hoc
|
|
225
|
-
* row/column offset math in history/ledger/roster
|
|
234
|
+
* row/column offset math in history/ledger/roster views.
|
|
226
235
|
*/
|
|
227
236
|
export function buildTable(
|
|
228
237
|
width: number,
|
|
229
238
|
columns: ReadonlyArray<TableColumn>,
|
|
230
239
|
rows: ReadonlyArray<TableRow>,
|
|
231
|
-
palette:
|
|
240
|
+
palette: ViewPalette,
|
|
232
241
|
options: { gap?: number; selectedBg?: string } = {},
|
|
233
242
|
): Line[] {
|
|
234
243
|
const gap = options.gap ?? 1;
|