@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,12 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* long-task-notifier, fires push notifications when a turn or agent task
|
|
3
|
-
*
|
|
3
|
+
* ends after running longer than the configured threshold.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* NAMING RULE (owner ruling 2026-09-29): a notification names the work it is
|
|
6
|
+
* about. The title is the turn's name (the user-set conversation title, else
|
|
7
|
+
* the first line of the message that started the turn, trimmed at a word
|
|
8
|
+
* boundary), the body is the outcome: done, failed or cancelled, elapsed time,
|
|
9
|
+
* files changed, tool calls, agents started, review score, and for a failure
|
|
10
|
+
* the reason. Text comes from the SDK's runtime/turn-notification.ts so every
|
|
11
|
+
* channel and host words it the same way.
|
|
12
|
+
*
|
|
13
|
+
* PRIVACY RULE: behavior.notificationsMetadataOnly (default off). When on,
|
|
14
|
+
* every channel this module sends on (desktop and webhook) carries metadata
|
|
15
|
+
* only: the outcome, elapsed time, counts and the session id prefix, never the
|
|
16
|
+
* turn's name or a failure reason. When off, both channels, the webhook
|
|
17
|
+
* included, carry the name and the outcome.
|
|
10
18
|
*
|
|
11
19
|
* Delivery targets (in preference order):
|
|
12
20
|
* 1. Desktop notification (linux notify-send / mac osascript) via SDK
|
|
@@ -32,7 +40,15 @@ import { notifyCompletion } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
32
40
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
33
41
|
import type { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
|
|
34
42
|
import type { FocusTracker } from '@pellux/goodvibes-sdk/platform/runtime/operations';
|
|
35
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
FORCE_NOTIFY_DURATION_MS,
|
|
45
|
+
buildTurnNotification,
|
|
46
|
+
formatWebhookText,
|
|
47
|
+
readNotificationsMetadataOnly,
|
|
48
|
+
readNotifyOnlyWhenUnfocused,
|
|
49
|
+
type ConfigGet,
|
|
50
|
+
type TurnOutcome,
|
|
51
|
+
} from '@pellux/goodvibes-sdk/platform/runtime/operations';
|
|
36
52
|
|
|
37
53
|
/** Default threshold in seconds. Turns shorter than this do not notify. */
|
|
38
54
|
export const NOTIFY_AFTER_SECONDS_DEFAULT = 60;
|
|
@@ -66,9 +82,52 @@ export interface MaybeNotifyLongTaskOptions {
|
|
|
66
82
|
/** Session id for correlation. Must not be a PII value. */
|
|
67
83
|
readonly sessionId: string;
|
|
68
84
|
|
|
85
|
+
/**
|
|
86
|
+
* How the task ended. Overrides `status` when given, so a cancelled turn is
|
|
87
|
+
* told as cancelled rather than failed.
|
|
88
|
+
*/
|
|
89
|
+
readonly outcome?: TurnOutcome | undefined;
|
|
90
|
+
|
|
91
|
+
/** The turn's name (SDK resolveTurnName). Left out of the text when metadata-only is on. */
|
|
92
|
+
readonly name?: string | null | undefined;
|
|
93
|
+
|
|
94
|
+
/** Why a failed or cancelled task stopped. Left out of the text when metadata-only is on. */
|
|
95
|
+
readonly reason?: string | null | undefined;
|
|
96
|
+
|
|
97
|
+
/** What the task did (SDK TurnActivityTally.snapshot()). */
|
|
98
|
+
readonly activity?: {
|
|
99
|
+
readonly toolCalls?: number | undefined;
|
|
100
|
+
readonly filesChanged?: number | undefined;
|
|
101
|
+
readonly agentsStarted?: number | undefined;
|
|
102
|
+
readonly reviewScore?: number | null | undefined;
|
|
103
|
+
} | undefined;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* behavior.notificationsMetadataOnly. When omitted it is read through
|
|
107
|
+
* `configGet`; with neither, it is the default (off).
|
|
108
|
+
*/
|
|
109
|
+
readonly metadataOnly?: boolean | undefined;
|
|
110
|
+
|
|
111
|
+
/** Desktop delivery; defaults to the SDK notifyCompletion. Tests pass a spy to read the text. */
|
|
112
|
+
readonly notifyDesktop?: typeof notifyCompletion | undefined;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* False skips the desktop popup (behavior.notifyOnComplete off for a turn);
|
|
116
|
+
* the webhook still goes out. Default true.
|
|
117
|
+
*/
|
|
118
|
+
readonly desktop?: boolean | undefined;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The desktop popup is also due past this many milliseconds, even under
|
|
122
|
+
* `thresholdSeconds` (and when the threshold is 0): a turn's end passes the
|
|
123
|
+
* SDK Orchestrator's own popup rule here (over 30s), since this notice
|
|
124
|
+
* replaced that popup. The webhook still waits for `thresholdSeconds`.
|
|
125
|
+
*/
|
|
126
|
+
readonly desktopAfterMs?: number | undefined;
|
|
127
|
+
|
|
69
128
|
/**
|
|
70
129
|
* Threshold in seconds from config (behavior.notifyAfterSeconds).
|
|
71
|
-
* 0 means off
|
|
130
|
+
* 0 means off: no webhook, and no popup unless `desktopAfterMs` makes one due.
|
|
72
131
|
* Should be the raw config value; this function normalises it.
|
|
73
132
|
*/
|
|
74
133
|
readonly thresholdSeconds: number;
|
|
@@ -95,28 +154,25 @@ export interface MaybeNotifyLongTaskOptions {
|
|
|
95
154
|
}
|
|
96
155
|
|
|
97
156
|
/**
|
|
98
|
-
* Fires push notifications for a
|
|
157
|
+
* Fires push notifications for a finished long task if the elapsed time
|
|
99
158
|
* exceeds the configured threshold.
|
|
100
159
|
*
|
|
101
160
|
* Returns true when at least one delivery was attempted, false when the
|
|
102
161
|
* call was a no-op (threshold not reached, off-state, or focus-gated off,
|
|
103
162
|
* see `focusTracker`/`configGet` above).
|
|
104
163
|
*
|
|
105
|
-
*
|
|
106
|
-
* status, sessionId). Never includes conversation content.
|
|
164
|
+
* Text: see the NAMING and PRIVACY rules at the top of this file.
|
|
107
165
|
*/
|
|
108
166
|
export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
|
|
109
167
|
const { elapsedMs, status, kind, sessionId, thresholdSeconds, webhookNotifier, focusTracker, configGet } = opts;
|
|
168
|
+
const notifyDesktop = opts.notifyDesktop ?? notifyCompletion;
|
|
110
169
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
logger.debug('long-task-notifier: disabled (threshold=0)');
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Gate: only notify when the task exceeded the threshold.
|
|
170
|
+
// The threshold gates both deliveries (0 is off); a desktop floor
|
|
171
|
+
// (desktopAfterMs) can make the popup alone due before it.
|
|
118
172
|
const elapsedSeconds = Math.floor(elapsedMs / 1000);
|
|
119
|
-
|
|
173
|
+
const thresholdMet = thresholdSeconds !== NOTIFY_AFTER_SECONDS_OFF && elapsedSeconds >= thresholdSeconds;
|
|
174
|
+
const desktopDue = opts.desktop !== false && (thresholdMet || (opts.desktopAfterMs !== undefined && elapsedMs > opts.desktopAfterMs));
|
|
175
|
+
if (!thresholdMet && !desktopDue) {
|
|
120
176
|
logger.debug('long-task-notifier: below threshold', { elapsedSeconds, thresholdSeconds });
|
|
121
177
|
return false;
|
|
122
178
|
}
|
|
@@ -129,25 +185,38 @@ export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
|
|
|
129
185
|
return false;
|
|
130
186
|
}
|
|
131
187
|
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
188
|
+
const metadataOnly = opts.metadataOnly ?? (configGet ? readNotificationsMetadataOnly(configGet) : false);
|
|
189
|
+
const notice = buildTurnNotification({
|
|
190
|
+
outcome: opts.outcome ?? (status === 'ok' ? 'completed' : 'failed'),
|
|
191
|
+
elapsedMs,
|
|
192
|
+
name: opts.name,
|
|
193
|
+
reason: opts.reason,
|
|
194
|
+
sessionId,
|
|
195
|
+
subject: kind,
|
|
196
|
+
...opts.activity,
|
|
197
|
+
}, { metadataOnly });
|
|
137
198
|
|
|
138
199
|
// Delivery 1: desktop notification (notify-send on linux, osascript on mac).
|
|
139
200
|
// notifyCompletion is non-throwing; SDK handles platform absence silently.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
201
|
+
// Its own duration heuristic only pops a desktop notification above 30s;
|
|
202
|
+
// the threshold above is the user's (behavior.notifyAfterSeconds), so a
|
|
203
|
+
// threshold under 30s must still reach the desktop.
|
|
204
|
+
let delivered = false;
|
|
205
|
+
if (desktopDue) {
|
|
206
|
+
delivered = true;
|
|
207
|
+
try {
|
|
208
|
+
notifyDesktop(notice.title, notice.body, Math.max(elapsedMs, FORCE_NOTIFY_DURATION_MS));
|
|
209
|
+
} catch (err) {
|
|
210
|
+
logger.debug('long-task-notifier: desktop notify error', { error: String(err) });
|
|
211
|
+
}
|
|
144
212
|
}
|
|
145
213
|
|
|
146
214
|
// Delivery 2: outbound webhook (ntfy / generic endpoint) if configured.
|
|
147
|
-
if (webhookNotifier) {
|
|
215
|
+
if (webhookNotifier && thresholdMet) {
|
|
148
216
|
const urls = webhookNotifier.getUrls();
|
|
149
217
|
if (urls.length > 0) {
|
|
150
|
-
|
|
218
|
+
delivered = true;
|
|
219
|
+
webhookNotifier.send(formatWebhookText(notice)).catch((err: unknown) => {
|
|
151
220
|
logger.debug('long-task-notifier: webhook send error', { error: String(err) });
|
|
152
221
|
});
|
|
153
222
|
} else {
|
|
@@ -155,7 +224,7 @@ export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
|
|
|
155
224
|
}
|
|
156
225
|
}
|
|
157
226
|
|
|
158
|
-
return
|
|
227
|
+
return delivered;
|
|
159
228
|
}
|
|
160
229
|
|
|
161
230
|
/**
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notices.ts, where a conversation system notice goes.
|
|
3
|
+
*
|
|
4
|
+
* A system notice ([WRFC] …, [Agents] …, a compaction receipt, a failover
|
|
5
|
+
* line) is not part of any turn, so the transcript does not draw it (the
|
|
6
|
+
* concept's turns hold only the turn's own text, beads, lanes and answer;
|
|
7
|
+
* agent and chain state are the lanes' own spawn and merge rows). Each notice
|
|
8
|
+
* becomes a toast in the top right and an entry in the notification history
|
|
9
|
+
* (/notifications), with its full text: the first line is the title, any
|
|
10
|
+
* further lines are the body. The conversation still stores the message, so a
|
|
11
|
+
* saved session keeps it, and restoring that session puts it back in the
|
|
12
|
+
* history without toasting it again.
|
|
13
|
+
*
|
|
14
|
+
* One entry per event: a "[WRFC] …" or "[Agents] …" line that restates an
|
|
15
|
+
* agent or chain event (the SDK's runtimeEventOfNotice names it) is kept under
|
|
16
|
+
* that event's plain title, with the line's detail as the body, and carries
|
|
17
|
+
* the event's key; the terminal's own "[WRFC] …" lines get their plain titles
|
|
18
|
+
* from core/wrfc-notice-titles.ts. The runtime-bus bridge (runtime/notification-dispatch.ts)
|
|
19
|
+
* records the same event with the same key, and the feed keeps one entry for
|
|
20
|
+
* both (NotificationFeed, eventKey).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Notification } from '@/runtime/index.ts';
|
|
24
|
+
import { runtimeEventOfNotice } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap';
|
|
25
|
+
import { shellChainNoticeOf } from './wrfc-notice-titles.ts';
|
|
26
|
+
import type { NotificationFeed } from '../views/notifications-feed.ts';
|
|
27
|
+
import { classifySystemMessage } from '../renderer/system-message.ts';
|
|
28
|
+
|
|
29
|
+
/** Receives every system notice added to (or restored into) the conversation. */
|
|
30
|
+
export type NoticeSink = (content: string, options: { readonly restored: boolean }) => void;
|
|
31
|
+
|
|
32
|
+
/** The pieces of a notice as the history and the toast show them. */
|
|
33
|
+
export interface NoticeParts {
|
|
34
|
+
/** Lower-case bracket tag ('wrfc', 'agents', 'compaction', …) or 'system' when untagged. */
|
|
35
|
+
readonly domain: string;
|
|
36
|
+
readonly level: Notification['level'];
|
|
37
|
+
/** The first non-empty line, in full. */
|
|
38
|
+
readonly title: string;
|
|
39
|
+
/** Every further line, in full, or undefined. */
|
|
40
|
+
readonly body: string | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const TAG_RE = /^\s*\[([^\]\n]+)\]/;
|
|
44
|
+
|
|
45
|
+
export function noticeParts(text: string): NoticeParts {
|
|
46
|
+
const trimmed = text.replace(/^\s*\n+/, '').replace(/\s+$/, '');
|
|
47
|
+
const tag = TAG_RE.exec(trimmed)?.[1];
|
|
48
|
+
const domain = tag ? tag.trim().toLowerCase().replace(/\s+/g, '-') : 'system';
|
|
49
|
+
const kind = classifySystemMessage(trimmed);
|
|
50
|
+
const level: Notification['level'] = kind === 'error' ? 'critical' : kind === 'warning' ? 'warning' : 'info';
|
|
51
|
+
const newline = trimmed.indexOf('\n');
|
|
52
|
+
const title = newline < 0 ? trimmed : trimmed.slice(0, newline);
|
|
53
|
+
const rest = newline < 0 ? '' : trimmed.slice(newline + 1).replace(/^\n+/, '');
|
|
54
|
+
return { domain, level, title, body: rest.length > 0 ? rest : undefined };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface NoticeOptions {
|
|
58
|
+
/** Restored from a saved session: kept in history, not toasted. */
|
|
59
|
+
readonly restored?: boolean;
|
|
60
|
+
readonly now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Keep one system notice in the history; the feed's toast bridge toasts it unless restored. */
|
|
64
|
+
export function publishNotice(feed: NotificationFeed, text: string, options: NoticeOptions = {}): void {
|
|
65
|
+
if (text.trim().length === 0) return;
|
|
66
|
+
const restored = options.restored === true;
|
|
67
|
+
const parts = noticeParts(text);
|
|
68
|
+
const sdkEvent = runtimeEventOfNotice(text);
|
|
69
|
+
const event = sdkEvent ?? shellChainNoticeOf(text);
|
|
70
|
+
if (event) {
|
|
71
|
+
feed.recordNotice({ domain: parts.domain, level: event.level, title: event.title, body: event.detail, timestamp: (options.now ?? Date.now)(), restored, eventKey: sdkEvent?.key });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
feed.recordNotice({ ...parts, timestamp: (options.now ?? Date.now)(), restored });
|
|
75
|
+
}
|
|
@@ -16,7 +16,7 @@ import { resolveOperatorRpc } from '../input/commands/operator-rpc.ts';
|
|
|
16
16
|
import type {
|
|
17
17
|
PairingTailscaleServeReceipt,
|
|
18
18
|
PairingTailscaleStatus,
|
|
19
|
-
} from '../
|
|
19
|
+
} from '../views/modals/pairing-modal.ts';
|
|
20
20
|
|
|
21
21
|
export interface PairingTailscaleGatewayDeps {
|
|
22
22
|
readonly configManager: ConfigManager;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process-output-log.ts, a background process's output as timestamped lines.
|
|
3
|
+
*
|
|
4
|
+
* The process manager keeps each process's stdout and stderr as chunk arrays
|
|
5
|
+
* that grow while it runs, but it does not record when a chunk arrived. This
|
|
6
|
+
* log reads the arrays on every poll, splits new chunks into lines and stamps
|
|
7
|
+
* each line with the time it was first seen. The poll runs twice a second
|
|
8
|
+
* from startup (see shell/session-views.ts), so a stamp is at most about half
|
|
9
|
+
* a second late. A partial line (no newline yet) is held until it completes
|
|
10
|
+
* or the process ends.
|
|
11
|
+
*
|
|
12
|
+
* Bounded: each process keeps its newest MAX_LINES lines; a process the
|
|
13
|
+
* manager no longer lists is dropped on the next poll.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface ProcessLine {
|
|
17
|
+
/** Epoch ms the line was first seen. */
|
|
18
|
+
readonly at: number;
|
|
19
|
+
readonly text: string;
|
|
20
|
+
readonly stream: 'stdout' | 'stderr';
|
|
21
|
+
/** The line reads as an error (drawn in the error color). */
|
|
22
|
+
readonly error: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The slice of a process record this log reads (BackgroundProcess from the SDK). */
|
|
26
|
+
export interface ProcessRecordView {
|
|
27
|
+
readonly stdout: readonly string[];
|
|
28
|
+
readonly stderr: readonly string[];
|
|
29
|
+
readonly done: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Newest lines kept per process. */
|
|
33
|
+
export const MAX_LINES = 5000;
|
|
34
|
+
|
|
35
|
+
const ERROR_LINE = /\b(error|errors|failed|failure|fatal|exception|panic|traceback)\b|ERR!|✘|✖/i;
|
|
36
|
+
const PORT_PATTERNS: readonly RegExp[] = [
|
|
37
|
+
/\b(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\]):(\d{2,5})\b/,
|
|
38
|
+
/\blistening on (?:port )?:?(\d{2,5})\b/i,
|
|
39
|
+
/\bport[ =:]+(\d{2,5})\b/i,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** Terminal control sequences never reach the view: colors and cursor moves are stripped. */
|
|
43
|
+
function clean(text: string): string {
|
|
44
|
+
return text
|
|
45
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
46
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
47
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isErrorLine(text: string): boolean {
|
|
51
|
+
return ERROR_LINE.test(text);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The first port a line announces ("http://localhost:5173/", "listening on 3000"), when it names one. */
|
|
55
|
+
export function portOf(text: string): number | undefined {
|
|
56
|
+
for (const pattern of PORT_PATTERNS) {
|
|
57
|
+
const m = pattern.exec(text);
|
|
58
|
+
if (m) {
|
|
59
|
+
const port = Number(m[1]);
|
|
60
|
+
if (port > 0 && port < 65536) return port;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface StreamCursor { chunks: number; partial: string }
|
|
67
|
+
|
|
68
|
+
interface Entry {
|
|
69
|
+
readonly lines: ProcessLine[];
|
|
70
|
+
readonly out: StreamCursor;
|
|
71
|
+
readonly err: StreamCursor;
|
|
72
|
+
port: number | undefined;
|
|
73
|
+
/** Lines dropped off the front by the bound. */
|
|
74
|
+
dropped: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class ProcessOutputLog {
|
|
78
|
+
private readonly entries = new Map<string, Entry>();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read every listed process's new output. `records` are the processes the
|
|
82
|
+
* manager lists right now; anything else is forgotten.
|
|
83
|
+
*/
|
|
84
|
+
poll(records: ReadonlyMap<string, ProcessRecordView>, now = Date.now()): boolean {
|
|
85
|
+
let changed = false;
|
|
86
|
+
for (const id of [...this.entries.keys()]) if (!records.has(id)) this.entries.delete(id);
|
|
87
|
+
for (const [id, record] of records) {
|
|
88
|
+
let entry = this.entries.get(id);
|
|
89
|
+
if (!entry) {
|
|
90
|
+
entry = { lines: [], out: { chunks: 0, partial: '' }, err: { chunks: 0, partial: '' }, port: undefined, dropped: 0 };
|
|
91
|
+
this.entries.set(id, entry);
|
|
92
|
+
}
|
|
93
|
+
if (this.read(entry, entry.out, record.stdout, 'stdout', record.done, now)) changed = true;
|
|
94
|
+
if (this.read(entry, entry.err, record.stderr, 'stderr', record.done, now)) changed = true;
|
|
95
|
+
}
|
|
96
|
+
return changed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines(id: string): readonly ProcessLine[] {
|
|
100
|
+
return this.entries.get(id)?.lines ?? [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Lines dropped off the front of this process's log by the bound. */
|
|
104
|
+
dropped(id: string): number {
|
|
105
|
+
return this.entries.get(id)?.dropped ?? 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The port the process announced, once one of its lines named it. */
|
|
109
|
+
port(id: string): number | undefined {
|
|
110
|
+
return this.entries.get(id)?.port;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private read(entry: Entry, cursor: StreamCursor, chunks: readonly string[], stream: ProcessLine['stream'], done: boolean, now: number): boolean {
|
|
114
|
+
if (chunks.length < cursor.chunks) cursor.chunks = 0; // the manager trimmed its buffer: start over from what it has
|
|
115
|
+
let changed = false;
|
|
116
|
+
if (chunks.length > cursor.chunks) {
|
|
117
|
+
const text = cursor.partial + chunks.slice(cursor.chunks).join('');
|
|
118
|
+
cursor.chunks = chunks.length;
|
|
119
|
+
const parts = text.split(/\r?\n/);
|
|
120
|
+
cursor.partial = parts.pop() ?? '';
|
|
121
|
+
for (const part of parts) this.push(entry, part, stream, now);
|
|
122
|
+
changed = parts.length > 0;
|
|
123
|
+
}
|
|
124
|
+
if (done && cursor.partial) {
|
|
125
|
+
this.push(entry, cursor.partial, stream, now);
|
|
126
|
+
cursor.partial = '';
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
return changed;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private push(entry: Entry, raw: string, stream: ProcessLine['stream'], now: number): void {
|
|
133
|
+
// A carriage return without a newline redraws the same line (progress bars): keep what is left visible.
|
|
134
|
+
const text = clean(raw.includes('\r') ? raw.slice(raw.lastIndexOf('\r') + 1) : raw).trimEnd();
|
|
135
|
+
entry.lines.push({ at: now, text, stream, error: isErrorLine(text) });
|
|
136
|
+
if (entry.port === undefined) entry.port = portOf(text);
|
|
137
|
+
if (entry.lines.length > MAX_LINES) {
|
|
138
|
+
const extra = entry.lines.length - MAX_LINES;
|
|
139
|
+
entry.lines.splice(0, extra);
|
|
140
|
+
entry.dropped += extra;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The `[Rewind]` prefix is one of the FORCE_CONVERSATION_PREFIXES the
|
|
12
12
|
* system-message router always surfaces inline (see core/system-message-router.ts),
|
|
13
|
-
* so the receipt cannot be routed
|
|
13
|
+
* so the receipt cannot be routed away from the transcript and vanish.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/** The subset of the SDK's RewindReceipt this formatter renders. */
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-focus.ts, which session the terminal is showing: main, one agent,
|
|
3
|
+
* or one background process.
|
|
4
|
+
*
|
|
5
|
+
* Enter on an agent lane (or its row in the Agents modal) opens that agent
|
|
6
|
+
* full screen; Enter on a ▶ bead (or a process row) opens the process. The
|
|
7
|
+
* session chips under the header list every session there is to switch to,
|
|
8
|
+
* and Tab / Shift+Tab cycle them while the composer is empty.
|
|
9
|
+
*
|
|
10
|
+
* Esc inside an agent or process view goes up exactly one level: a process
|
|
11
|
+
* or a top-level agent returns to main; an agent spawned by another agent
|
|
12
|
+
* returns to that agent. Going up never stops or cancels anything. Stopping
|
|
13
|
+
* is ctrl+x pressed twice: the first press arms, the second (within
|
|
14
|
+
* STOP_CONFIRM_MS) stops.
|
|
15
|
+
*
|
|
16
|
+
* Pure state plus the live facts it is handed (SessionFocusDeps); nothing
|
|
17
|
+
* here renders or talks to the runtime directly.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type SessionTarget =
|
|
21
|
+
| { readonly kind: 'main' }
|
|
22
|
+
| { readonly kind: 'agent'; readonly id: string }
|
|
23
|
+
| { readonly kind: 'process'; readonly id: string };
|
|
24
|
+
|
|
25
|
+
/** The status a chip's mark shows. */
|
|
26
|
+
export type SessionMark = 'run' | 'idle' | 'ok' | 'err' | 'cancel' | 'wait';
|
|
27
|
+
|
|
28
|
+
/** One session the chips row lists. */
|
|
29
|
+
export interface SessionEntry {
|
|
30
|
+
readonly target: SessionTarget;
|
|
31
|
+
/** "main", the agent's template name, or the process command. */
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly mark: SessionMark;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SessionFocusDeps {
|
|
37
|
+
/** Agents worth a chip: running or pending (finished ones leave the row). */
|
|
38
|
+
readonly liveAgents: () => ReadonlyArray<{ readonly id: string; readonly name: string; readonly mark: SessionMark }>;
|
|
39
|
+
/** Background processes still running. */
|
|
40
|
+
readonly liveProcesses: () => ReadonlyArray<{ readonly id: string; readonly name: string; readonly mark: SessionMark }>;
|
|
41
|
+
/** Name and mark of any agent (the focused one may have finished), or null when unknown. */
|
|
42
|
+
readonly agent: (id: string) => { readonly name: string; readonly mark: SessionMark } | null;
|
|
43
|
+
/** Name and mark of any process, or null when unknown. */
|
|
44
|
+
readonly process: (id: string) => { readonly name: string; readonly mark: SessionMark } | null;
|
|
45
|
+
/** The agent that spawned this agent, or null when main did (or it is unknown). */
|
|
46
|
+
readonly parentAgent: (id: string) => string | null;
|
|
47
|
+
/** Whether main is working on a turn. */
|
|
48
|
+
readonly mainBusy: () => boolean;
|
|
49
|
+
readonly now?: () => number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const MAIN: SessionTarget = { kind: 'main' };
|
|
53
|
+
|
|
54
|
+
/** How long the first ctrl+x stays armed. */
|
|
55
|
+
export const STOP_CONFIRM_MS = 3000;
|
|
56
|
+
|
|
57
|
+
export function sameTarget(a: SessionTarget, b: SessionTarget): boolean {
|
|
58
|
+
if (a.kind !== b.kind) return false;
|
|
59
|
+
return a.kind === 'main' || a.id === (b as { id: string }).id;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class SessionFocus {
|
|
63
|
+
private currentTarget: SessionTarget = MAIN;
|
|
64
|
+
private armed: { readonly target: SessionTarget; readonly at: number } | null = null;
|
|
65
|
+
private listener: (() => void) | null = null;
|
|
66
|
+
|
|
67
|
+
constructor(private readonly deps: SessionFocusDeps) {}
|
|
68
|
+
|
|
69
|
+
get current(): SessionTarget { return this.currentTarget; }
|
|
70
|
+
/** An agent or process view is showing. */
|
|
71
|
+
get active(): boolean { return this.currentTarget.kind !== 'main'; }
|
|
72
|
+
|
|
73
|
+
/** Be told whenever the view changes (the transcript scroll resets, the frame repaints). */
|
|
74
|
+
onChange(listener: (() => void) | null): void { this.listener = listener; }
|
|
75
|
+
|
|
76
|
+
/** Show `target`. Opening an unknown agent or process is refused (false). */
|
|
77
|
+
open(target: SessionTarget): boolean {
|
|
78
|
+
if (target.kind === 'agent' && this.deps.agent(target.id) === null) return false;
|
|
79
|
+
if (target.kind === 'process' && this.deps.process(target.id) === null) return false;
|
|
80
|
+
if (sameTarget(target, this.currentTarget)) return true;
|
|
81
|
+
this.currentTarget = target;
|
|
82
|
+
this.armed = null;
|
|
83
|
+
this.listener?.();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The level Esc goes back to from `target`. */
|
|
88
|
+
parentOf(target: SessionTarget): SessionTarget {
|
|
89
|
+
if (target.kind !== 'agent') return MAIN;
|
|
90
|
+
const parent = this.deps.parentAgent(target.id);
|
|
91
|
+
return parent !== null && parent !== target.id && this.deps.agent(parent) !== null ? { kind: 'agent', id: parent } : MAIN;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Esc in a view: go up one level. Returns false in main (nothing to go back from). Never stops anything. */
|
|
95
|
+
back(): boolean {
|
|
96
|
+
if (!this.active) return false;
|
|
97
|
+
this.currentTarget = this.parentOf(this.currentTarget);
|
|
98
|
+
this.armed = null;
|
|
99
|
+
this.listener?.();
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The display name of a target. */
|
|
104
|
+
nameOf(target: SessionTarget): string {
|
|
105
|
+
if (target.kind === 'main') return 'main';
|
|
106
|
+
const known = target.kind === 'agent' ? this.deps.agent(target.id) : this.deps.process(target.id);
|
|
107
|
+
return known?.name ?? target.id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** "main › engineer › tester": the path from main down to the current view. */
|
|
111
|
+
breadcrumb(): SessionTarget[] {
|
|
112
|
+
const path: SessionTarget[] = [];
|
|
113
|
+
let at: SessionTarget = this.currentTarget;
|
|
114
|
+
for (let guard = 0; at.kind !== 'main' && guard < 16; guard++) {
|
|
115
|
+
path.unshift(at);
|
|
116
|
+
at = this.parentOf(at);
|
|
117
|
+
}
|
|
118
|
+
return [MAIN, ...path];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Every session there is to switch to: main, then running agents, then
|
|
123
|
+
* running processes. The session being shown is always listed, even after
|
|
124
|
+
* it finished.
|
|
125
|
+
*/
|
|
126
|
+
sessions(): SessionEntry[] {
|
|
127
|
+
const entries: SessionEntry[] = [{ target: MAIN, name: 'main', mark: this.deps.mainBusy() ? 'run' : 'idle' }];
|
|
128
|
+
const agents = this.deps.liveAgents();
|
|
129
|
+
const processes = this.deps.liveProcesses();
|
|
130
|
+
const cur = this.currentTarget;
|
|
131
|
+
const names = new Map<string, number>();
|
|
132
|
+
const label = (name: string): string => {
|
|
133
|
+
const n = (names.get(name) ?? 0) + 1;
|
|
134
|
+
names.set(name, n);
|
|
135
|
+
return n > 1 ? `${name} ${n}` : name;
|
|
136
|
+
};
|
|
137
|
+
const pushAgent = (id: string, name: string, mark: SessionMark): void => { entries.push({ target: { kind: 'agent', id }, name: label(name), mark }); };
|
|
138
|
+
const pushProcess = (id: string, name: string, mark: SessionMark): void => { entries.push({ target: { kind: 'process', id }, name: label(name), mark }); };
|
|
139
|
+
for (const a of agents) pushAgent(a.id, a.name, a.mark);
|
|
140
|
+
if (cur.kind === 'agent' && !agents.some((a) => a.id === cur.id)) {
|
|
141
|
+
const known = this.deps.agent(cur.id);
|
|
142
|
+
if (known) pushAgent(cur.id, known.name, known.mark);
|
|
143
|
+
}
|
|
144
|
+
for (const p of processes) pushProcess(p.id, p.name, p.mark);
|
|
145
|
+
if (cur.kind === 'process' && !processes.some((p) => p.id === cur.id)) {
|
|
146
|
+
const known = this.deps.process(cur.id);
|
|
147
|
+
if (known) pushProcess(cur.id, known.name, known.mark);
|
|
148
|
+
}
|
|
149
|
+
return entries;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The chips row shows only when there is more than one session to switch to. */
|
|
153
|
+
chipsVisible(): boolean {
|
|
154
|
+
return this.sessions().length > 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Tab (+1) / Shift+Tab (-1): show the next or previous session. False when there is nothing to cycle to. */
|
|
158
|
+
cycle(delta: 1 | -1): boolean {
|
|
159
|
+
const list = this.sessions();
|
|
160
|
+
if (list.length < 2) return false;
|
|
161
|
+
const at = Math.max(0, list.findIndex((e) => sameTarget(e.target, this.currentTarget)));
|
|
162
|
+
const next = list[(at + delta + list.length) % list.length]!;
|
|
163
|
+
return this.open(next.target);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* ctrl+x in a view. The first press arms and returns 'armed'; a second
|
|
168
|
+
* press within STOP_CONFIRM_MS returns 'confirmed' (the caller stops the
|
|
169
|
+
* work). In main it returns 'none'.
|
|
170
|
+
*/
|
|
171
|
+
pressStop(): 'armed' | 'confirmed' | 'none' {
|
|
172
|
+
if (!this.active) return 'none';
|
|
173
|
+
const now = this.deps.now?.() ?? Date.now();
|
|
174
|
+
if (this.armed && sameTarget(this.armed.target, this.currentTarget) && now - this.armed.at <= STOP_CONFIRM_MS) {
|
|
175
|
+
this.armed = null;
|
|
176
|
+
return 'confirmed';
|
|
177
|
+
}
|
|
178
|
+
this.armed = { target: this.currentTarget, at: now };
|
|
179
|
+
return 'armed';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether a first ctrl+x is waiting for its confirming press. */
|
|
183
|
+
stopArmed(): boolean {
|
|
184
|
+
if (!this.armed || !sameTarget(this.armed.target, this.currentTarget)) return false;
|
|
185
|
+
const now = this.deps.now?.() ?? Date.now();
|
|
186
|
+
return now - this.armed.at <= STOP_CONFIRM_MS;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
disarm(): void { this.armed = null; }
|
|
190
|
+
}
|