@pellux/goodvibes-tui 2.0.21 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -1
- package/README.md +30 -29
- package/docs/README.md +8 -5
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +28 -19
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-reference.ts +2 -2
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/live-verifier.ts +0 -10
- package/src/version.ts +1 -1
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- package/src/verification/verification-ledger.ts +0 -707
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
import { type Line, type Cell, createEmptyLine
|
|
2
|
-
import { LAYOUT } from './layout.ts';
|
|
1
|
+
import { type Line, type Cell, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
|
|
3
2
|
import { VERSION } from '../version.ts';
|
|
4
|
-
import {
|
|
3
|
+
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
5
4
|
import type { GitHeaderInfo } from './git-status.ts';
|
|
6
5
|
import { renderHeaderLine } from './header-line.ts';
|
|
7
|
-
import { renderConversationFragment
|
|
8
|
-
import {
|
|
9
|
-
import { activeUiTones } from './theme.ts';
|
|
10
|
-
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
11
|
-
import { abbreviateCount } from '../utils/format-number.ts';
|
|
12
|
-
import { computeContextUsage } from '../core/context-usage.ts';
|
|
13
|
-
import { permissionModeLabel, permissionModeTone } from '../core/permission-mode.ts';
|
|
14
|
-
import { SLEEP_DISABLED_CHIP } from '../core/power-status.ts';
|
|
6
|
+
import { renderConversationFragment } from './conversation-surface.ts';
|
|
7
|
+
import { activeTheme, activeTokens, activeUiTones } from './theme.ts';
|
|
15
8
|
import { renderQueuedMessageList, renderMemoryProvenanceChip, type MemoryProvenanceEntry } from './composer-fragments.ts';
|
|
16
|
-
import {
|
|
17
|
-
import { buildFooterTip, isAgentActive } from './footer-tips.ts';
|
|
9
|
+
import { renderUserMessage } from './user-message.ts';
|
|
18
10
|
import type { StreamMetrics } from '../core/stream-event-wiring.ts';
|
|
19
11
|
import { waitingPhrase, type WaitingState } from '@pellux/goodvibes-sdk/platform/presentation';
|
|
20
12
|
|
|
21
|
-
/** Number of frames before the animated gradient completes one full cycle. */
|
|
22
|
-
const GRADIENT_CYCLE_FRAMES = 50;
|
|
23
13
|
/**
|
|
24
14
|
* Ms since the last STREAM_DELTA before the whimsical phrase rotation freezes
|
|
25
|
-
* and
|
|
15
|
+
* and the status line shows an honest "stalled Ns" / "reconnecting"
|
|
26
16
|
* label instead. Deliberately much shorter than the 30s stream-stall-watchdog
|
|
27
17
|
* hint threshold (stream-stall-watchdog.ts), that threshold gates a
|
|
28
18
|
* low-priority system message about a likely-dead connection; this one gates
|
|
@@ -43,58 +33,32 @@ export interface ThinkingStallInfo {
|
|
|
43
33
|
readonly reconnect?: { readonly attempt: number; readonly maxAttempts: number };
|
|
44
34
|
}
|
|
45
35
|
|
|
46
|
-
/** Format a number: up to 999, then 1.0k, 1.0M, 1.0B, 1.0T */
|
|
47
|
-
function fmtNum(n: number): string {
|
|
48
|
-
return abbreviateCount(n, { bSuffix: true });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Format a running USD cost estimate with a precision that suits its magnitude. */
|
|
52
|
-
function fmtCost(usd: number): string {
|
|
53
|
-
if (!(usd > 0)) return '0.00';
|
|
54
|
-
if (usd < 0.01) return usd.toFixed(4);
|
|
55
|
-
if (usd < 1) return usd.toFixed(3);
|
|
56
|
-
return usd.toFixed(2);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
36
|
/**
|
|
60
37
|
* UIFactory - Generates standard UI fragments without needing Ink/React overhead.
|
|
61
38
|
*/
|
|
62
39
|
export class UIFactory {
|
|
63
40
|
/**
|
|
64
|
-
* The header row
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* marker, both resolved by core/active-model-identity.ts so the header and
|
|
70
|
-
* the footer answer "who is serving?" from one place. `version` defaults to
|
|
71
|
-
* the live build VERSION; tests pass a pinned fixture so golden snapshots
|
|
72
|
-
* don't break on every release bump.
|
|
41
|
+
* The header row, see header-line.ts. `model` is the SERVING backend
|
|
42
|
+
* resolved by core/active-model-identity.ts. `version` defaults to the live
|
|
43
|
+
* build VERSION; tests pass a pinned fixture so golden snapshots don't break
|
|
44
|
+
* on every release bump. `modelNote` is the failover marker drawn after the
|
|
45
|
+
* model while serving differs from the configured selection.
|
|
73
46
|
*/
|
|
74
|
-
public static createHeader(width: number, model: string,
|
|
75
|
-
return renderHeaderLine(width, model,
|
|
47
|
+
public static createHeader(width: number, model: string, title?: string, gitInfo?: GitHeaderInfo, version: string = VERSION, modelNote?: string): Line[] {
|
|
48
|
+
return renderHeaderLine(width, model, title, gitInfo, version, modelNote);
|
|
76
49
|
}
|
|
77
50
|
|
|
78
51
|
/**
|
|
79
|
-
* createMessageBar
|
|
80
|
-
*
|
|
52
|
+
* createMessageBar, a sent user message: a full-width surface fill with the
|
|
53
|
+
* secondary bar (user-message.ts). A cancelled message passes its own fill,
|
|
54
|
+
* error bar and strikethrough.
|
|
81
55
|
*/
|
|
82
56
|
public static createMessageBar(
|
|
83
57
|
width: number, text: string,
|
|
84
|
-
bgColor: string =
|
|
85
|
-
strikethrough = false
|
|
58
|
+
bgColor: string = activeTokens().backgroundPanel, textColor: string = activeTokens().text, barColor: string = activeTokens().secondary,
|
|
59
|
+
strikethrough = false,
|
|
86
60
|
): Line[] {
|
|
87
|
-
|
|
88
|
-
// reads fg.secondary/state.reasoning (light-on-dark), this is conversation
|
|
89
|
-
// content, not the transparent-terminal chrome, so it is not part of the
|
|
90
|
-
// light-terminal chrome flip.
|
|
91
|
-
return renderConversationFragment(text, width, {
|
|
92
|
-
prefix: prefixStr,
|
|
93
|
-
prefixFg: activeUiTones().state.reasoning,
|
|
94
|
-
text: textColor,
|
|
95
|
-
bodyBg: bgColor,
|
|
96
|
-
strikethrough,
|
|
97
|
-
});
|
|
61
|
+
return renderUserMessage(text, width, { bar: barColor, bg: bgColor, text: textColor, strikethrough });
|
|
98
62
|
}
|
|
99
63
|
|
|
100
64
|
/**
|
|
@@ -106,8 +70,7 @@ export class UIFactory {
|
|
|
106
70
|
prefix: ' (...) ',
|
|
107
71
|
prefixFg: t.state.reasoning,
|
|
108
72
|
text: t.fg.dim,
|
|
109
|
-
bodyBg:
|
|
110
|
-
dim: true,
|
|
73
|
+
bodyBg: activeTheme().collapsedBodyBg,
|
|
111
74
|
});
|
|
112
75
|
}
|
|
113
76
|
|
|
@@ -121,389 +84,11 @@ export class UIFactory {
|
|
|
121
84
|
return renderMemoryProvenanceChip(width, count, entries, expanded);
|
|
122
85
|
}
|
|
123
86
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
lastCopyTime: number,
|
|
130
|
-
model?: string,
|
|
131
|
-
toolCount?: number,
|
|
132
|
-
cursorPos?: number,
|
|
133
|
-
workingDir?: string,
|
|
134
|
-
provider?: string,
|
|
135
|
-
contextWindow?: number,
|
|
136
|
-
compactThreshold?: number,
|
|
137
|
-
dangerMode?: boolean,
|
|
138
|
-
lastInputTokens?: number,
|
|
139
|
-
commandArgsHint?: string,
|
|
140
|
-
hitlMode?: string,
|
|
141
|
-
promptFocused: boolean = true,
|
|
142
|
-
composerMode?: string,
|
|
143
|
-
composerStatus?: string,
|
|
144
|
-
composerFlags?: readonly string[],
|
|
145
|
-
composerPendingRisk?: 'none' | 'approval-wait' | 'shell' | 'command' | 'remote',
|
|
146
|
-
compact: boolean = false,
|
|
147
|
-
// Honest cross-surface spine posture. Defined ONLY in adopted-daemon
|
|
148
|
-
// mode (undefined in local-only mode), so the segment is absent otherwise.
|
|
149
|
-
sessionSpineStatus?: 'online' | 'offline',
|
|
150
|
-
// Session permission mode (config value). Rendered as a leading pill in the
|
|
151
|
-
// posture block; undefined suppresses the pill (e.g. bare test callers).
|
|
152
|
-
permissionMode?: string,
|
|
153
|
-
// The web surface's reachable URL, when that surface is enabled, a
|
|
154
|
-
// persistent low-priority context segment; undefined suppresses it.
|
|
155
|
-
webSurfaceUrl?: string,
|
|
156
|
-
// True while power.keepAwake holds, renders the always-visible
|
|
157
|
-
// "sleep disabled" chip in the posture block (the danger-mode idiom: a
|
|
158
|
-
// persistent, colored surface indicator, the safety mechanism itself).
|
|
159
|
-
powerKeepAwake?: boolean,
|
|
160
|
-
// Divergence marker for the model segment (core/active-model-identity.ts):
|
|
161
|
-
// set only while the serving backend is not the user's configured
|
|
162
|
-
// selection, e.g. "failover from abacusai:route-llm". Empty/undefined
|
|
163
|
-
// renders the context-info line exactly as it did before the marker existed.
|
|
164
|
-
modelNote?: string,
|
|
165
|
-
): Line[] {
|
|
166
|
-
const lines: Line[] = [];
|
|
167
|
-
const promptLines = prompt.split('\n');
|
|
168
|
-
const TEXT_COLOR = promptFocused ? '252' : '246';
|
|
169
|
-
const BG_COLOR = promptFocused ? '#2a2a2a' : '#1f2430';
|
|
170
|
-
const BORDER_COLOR = BG_COLOR;
|
|
171
|
-
const boxMargin = 2; const boxWidth = width - (boxMargin * 2); const boxStartX = boxMargin;
|
|
172
|
-
const createBaseLine = () => {
|
|
173
|
-
const l = createEmptyLine(width);
|
|
174
|
-
for (let x = 0; x < width; x++) l[x].bg = '';
|
|
175
|
-
return l;
|
|
176
|
-
};
|
|
177
|
-
const topLine = createBaseLine();
|
|
178
|
-
for (let x = 0; x < boxWidth; x++) topLine[boxStartX + x] = { char: GLYPHS.surface.top, fg: BORDER_COLOR, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
179
|
-
lines.push(topLine);
|
|
180
|
-
promptLines.forEach((text, i) => {
|
|
181
|
-
const contentW = boxWidth - 4;
|
|
182
|
-
const prefix = i === 0 ? ' › ' : ' ';
|
|
183
|
-
// Render text without cursor insertion, cursor is overlaid after
|
|
184
|
-
const rawText = `${prefix}${text}`;
|
|
185
|
-
const paddedText = fitDisplay(rawText, contentW);
|
|
186
|
-
const contentLine = createBaseLine();
|
|
187
|
-
for (let x = 0; x < boxWidth; x++) {
|
|
188
|
-
const char = (x >= 2 && x < boxWidth - 2) ? paddedText[x - 2] || ' ' : ' ';
|
|
189
|
-
contentLine[boxStartX + x] = {
|
|
190
|
-
char,
|
|
191
|
-
fg: (x < 5 && i === 0) ? (promptFocused ? '135' : '244') : TEXT_COLOR,
|
|
192
|
-
bg: BG_COLOR,
|
|
193
|
-
bold: false,
|
|
194
|
-
dim: !promptFocused,
|
|
195
|
-
underline: false,
|
|
196
|
-
italic: false,
|
|
197
|
-
strikethrough: false,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Overlay cursor only while the prompt owns focus.
|
|
202
|
-
if (promptFocused && cursorPos !== undefined) {
|
|
203
|
-
let lineStart = 0;
|
|
204
|
-
for (let li = 0; li < i; li++) lineStart += promptLines[li].length + 1;
|
|
205
|
-
const posInLine = cursorPos - lineStart;
|
|
206
|
-
if (posInLine >= 0 && posInLine <= text.length) {
|
|
207
|
-
// Cursor column in cell coordinates: prefix width (3) + posInLine + box padding (2)
|
|
208
|
-
const cursorX = boxStartX + 2 + prefix.length + posInLine;
|
|
209
|
-
if (cursorX < boxStartX + boxWidth - 2) {
|
|
210
|
-
const cell = contentLine[cursorX];
|
|
211
|
-
// Invert: bright fg on the text bg, swap to make cursor visible
|
|
212
|
-
contentLine[cursorX] = {
|
|
213
|
-
char: cell.char === ' ' ? GLYPHS.surface.cursor : cell.char,
|
|
214
|
-
fg: cell.char === ' ' ? '252' : '#000000',
|
|
215
|
-
bg: cell.char === ' ' ? (promptFocused ? BG_COLOR : '#334155') : '#ffffff',
|
|
216
|
-
bold: false, dim: false, underline: false, italic: false, strikethrough: false
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
} else if (promptFocused && i === promptLines.length - 1) {
|
|
221
|
-
// No cursorPos provided, show block at end (fallback)
|
|
222
|
-
const endX = boxStartX + 2 + prefix.length + text.length;
|
|
223
|
-
if (endX < boxStartX + boxWidth - 2) {
|
|
224
|
-
contentLine[endX] = { char: GLYPHS.surface.cursor, fg: '252', bg: promptFocused ? BG_COLOR : '#334155', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// focus visibility (item 1c): when the panel workspace owns focus
|
|
229
|
-
// and the composer is empty, name the state and the way back, the
|
|
230
|
-
// dimmed prompt alone (TEXT_COLOR/BG_COLOR above) told you nothing was
|
|
231
|
-
// wrong, but not why keystrokes weren't landing here.
|
|
232
|
-
if (!promptFocused && text === '' && i === 0) {
|
|
233
|
-
const hintText = 'panel focused: Esc returns';
|
|
234
|
-
const hintStartX = boxStartX + 2 + prefix.length;
|
|
235
|
-
let hx = hintStartX;
|
|
236
|
-
for (const ch of hintText) {
|
|
237
|
-
if (hx >= boxStartX + boxWidth - 2) break;
|
|
238
|
-
contentLine[hx] = { char: ch, fg: '244', bg: BG_COLOR, bold: false, dim: true, underline: false, italic: false, strikethrough: false };
|
|
239
|
-
hx++;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Overlay args hint: dim grey text after cursor on the last prompt line.
|
|
244
|
-
// Only shown when a commandArgsHint is provided and cursor is at the end of input.
|
|
245
|
-
if (commandArgsHint && i === promptLines.length - 1) {
|
|
246
|
-
// Determine where the cursor sits on this line
|
|
247
|
-
let cursorColOnLine: number;
|
|
248
|
-
if (cursorPos !== undefined) {
|
|
249
|
-
let lineStart = 0;
|
|
250
|
-
for (let li = 0; li < i; li++) lineStart += promptLines[li].length + 1;
|
|
251
|
-
cursorColOnLine = cursorPos - lineStart;
|
|
252
|
-
} else {
|
|
253
|
-
cursorColOnLine = text.length;
|
|
254
|
-
}
|
|
255
|
-
// Only show hint when cursor is at end of the last line (no args typed yet)
|
|
256
|
-
if (cursorColOnLine >= text.length) {
|
|
257
|
-
// Hint starts one cell after the cursor block
|
|
258
|
-
const hintStartX = boxStartX + 2 + prefix.length + text.length + 1;
|
|
259
|
-
const hintLimit = boxStartX + boxWidth - 2; // exclusive right bound
|
|
260
|
-
const avail = Math.max(0, hintLimit - hintStartX);
|
|
261
|
-
// Clamp to the available width with an ellipsis so a long arg-spec
|
|
262
|
-
// (e.g. /marketplace) ends in "…" rather than a hard mid-word cut. (5b)
|
|
263
|
-
let hintText = ' ' + commandArgsHint;
|
|
264
|
-
if (hintText.length > avail) {
|
|
265
|
-
hintText = avail > 1 ? `${hintText.slice(0, avail - 1)}…` : hintText.slice(0, Math.max(0, avail));
|
|
266
|
-
}
|
|
267
|
-
let hx = hintStartX;
|
|
268
|
-
for (const ch of hintText) {
|
|
269
|
-
if (hx >= hintLimit) break;
|
|
270
|
-
contentLine[hx] = { char: ch, fg: '238', bg: BG_COLOR, bold: false, dim: true, underline: false, italic: false, strikethrough: false };
|
|
271
|
-
hx++;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
lines.push(contentLine);
|
|
277
|
-
});
|
|
278
|
-
const bottomLine = createBaseLine();
|
|
279
|
-
for (let x = 0; x < boxWidth; x++) bottomLine[boxStartX + x] = { char: GLYPHS.surface.bottom, fg: BORDER_COLOR, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
280
|
-
// Multi-line indicator lives inside the bottom border (right-aligned) so
|
|
281
|
-
// the footer height stays invariant while the user adds prompt lines.
|
|
282
|
-
if (promptLines.length > 1) {
|
|
283
|
-
const lineCountTag = ` ${promptLines.length}L `;
|
|
284
|
-
let tx = boxStartX + boxWidth - lineCountTag.length - 2;
|
|
285
|
-
for (const ch of lineCountTag) {
|
|
286
|
-
if (tx >= boxStartX + boxWidth - 1) break;
|
|
287
|
-
bottomLine[tx] = { char: ch, fg: '244', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
|
|
288
|
-
tx += 1;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
lines.push(bottomLine);
|
|
292
|
-
// --- Composer posture block (mode / risk / status / flags) ------------
|
|
293
|
-
// Suppressed in compact mode; the ctx-info line no longer duplicates these
|
|
294
|
-
// tokens, so this block is the single home for mode/status/flags.
|
|
295
|
-
// Footer chrome paints on the transparent terminal bg → chrome.* accents
|
|
296
|
-
// (light-terminal-aware); state.info reads on both light and dark terminals.
|
|
297
|
-
const t = activeUiTones();
|
|
298
|
-
const composerTokens: Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> = [];
|
|
299
|
-
// Permission-mode pill, always shown so the active session posture (normal
|
|
300
|
-
// / plan / accept-edits / auto) is visible at a glance. Tone: normal reads
|
|
301
|
-
// dim/neutral, plan reads info (read-only, safe), accept-edits and auto read
|
|
302
|
-
// as caution (raised autonomy). Cycled by Shift+Tab, toggled by /plan.
|
|
303
|
-
if (permissionMode) {
|
|
304
|
-
const modeTone = permissionModeTone(permissionMode);
|
|
305
|
-
const modeFg = modeTone === 'caution' ? t.chrome.warn : modeTone === 'info' ? t.state.info : '244';
|
|
306
|
-
composerTokens.push({ text: ` mode:${permissionModeLabel(permissionMode)} `, fg: modeFg, bold: modeTone !== 'neutral', dim: modeTone === 'neutral' });
|
|
307
|
-
}
|
|
308
|
-
// Context-usage chip, always-visible compaction-pressure indicator so the
|
|
309
|
-
// user sees compaction approaching before it happens. Colored by proximity
|
|
310
|
-
// to the auto-compact threshold (fraction; falls back to 0.85 when unset).
|
|
311
|
-
if (contextWindow && contextWindow > 0) {
|
|
312
|
-
const ctxUsage = computeContextUsage(lastInputTokens ?? 0, contextWindow);
|
|
313
|
-
const ctxThr = compactThreshold && compactThreshold > 0 ? compactThreshold : 0.85;
|
|
314
|
-
const ctxFg = ctxUsage.clampedRatio >= ctxThr ? t.chrome.bad : ctxUsage.clampedRatio >= ctxThr * 0.85 ? t.chrome.warn : '244';
|
|
315
|
-
composerTokens.push({ text: ` ctx:${ctxUsage.pct}% `, fg: ctxFg, bold: ctxUsage.clampedRatio >= ctxThr, dim: ctxUsage.clampedRatio < ctxThr * 0.85 });
|
|
316
|
-
}
|
|
317
|
-
if (composerMode) composerTokens.push({ text: ` ${GLYPHS.status.active} ${composerMode} `, fg: t.state.info, bold: true });
|
|
318
|
-
if (composerPendingRisk && composerPendingRisk !== 'none') {
|
|
319
|
-
const riskColor = composerPendingRisk === 'approval-wait'
|
|
320
|
-
? t.chrome.warn
|
|
321
|
-
: composerPendingRisk === 'shell'
|
|
322
|
-
? t.chrome.bad
|
|
323
|
-
: composerPendingRisk === 'remote'
|
|
324
|
-
? t.chrome.remote
|
|
325
|
-
: t.chrome.warn;
|
|
326
|
-
composerTokens.push({ text: ` risk:${composerPendingRisk} `, fg: riskColor, bold: true });
|
|
327
|
-
}
|
|
328
|
-
// "sleep disabled" chip, always visible while keep-awake holds, in the
|
|
329
|
-
// danger-mode idiom (a persistent warn-tone indicator; the chip is the
|
|
330
|
-
// safety mechanism, so there is no timer and no AC-only variant).
|
|
331
|
-
if (powerKeepAwake) composerTokens.push({ text: ` ${SLEEP_DISABLED_CHIP} `, fg: t.chrome.warn, bold: true });
|
|
332
|
-
if (composerStatus && composerStatus !== 'idle') composerTokens.push({ text: ` state:${composerStatus} `, fg: '244', dim: true });
|
|
333
|
-
if (composerFlags && composerFlags.length > 0) composerTokens.push({ text: ` flags:${composerFlags.join(',')} `, fg: '244', dim: true });
|
|
334
|
-
if (!compact && composerTokens.length > 0) {
|
|
335
|
-
const postureLine = createBaseLine();
|
|
336
|
-
let px = 2;
|
|
337
|
-
for (const token of composerTokens) {
|
|
338
|
-
for (const ch of token.text) {
|
|
339
|
-
if (px >= width) break;
|
|
340
|
-
postureLine[px] = {
|
|
341
|
-
char: ch,
|
|
342
|
-
fg: token.fg,
|
|
343
|
-
bg: '',
|
|
344
|
-
bold: token.bold ?? false,
|
|
345
|
-
dim: token.dim ?? false,
|
|
346
|
-
underline: false,
|
|
347
|
-
italic: false,
|
|
348
|
-
strikethrough: false,
|
|
349
|
-
};
|
|
350
|
-
px += getDisplayWidth(ch);
|
|
351
|
-
}
|
|
352
|
-
if (px >= width) break;
|
|
353
|
-
}
|
|
354
|
-
lines.push(postureLine);
|
|
355
|
-
} else if (compact && powerKeepAwake) {
|
|
356
|
-
// The keep-awake chip is a safety indicator, it survives compact height
|
|
357
|
-
// even though the rest of the posture row (mode/ctx/risk/state/flags) is
|
|
358
|
-
// dropped at height < 30. Its own compact-surviving slot.
|
|
359
|
-
const chipLine = createBaseLine();
|
|
360
|
-
let cx = 2;
|
|
361
|
-
for (const ch of ` ${SLEEP_DISABLED_CHIP} `) {
|
|
362
|
-
if (cx >= width) break;
|
|
363
|
-
chipLine[cx] = { char: ch, fg: t.chrome.warn, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false };
|
|
364
|
-
cx += getDisplayWidth(ch);
|
|
365
|
-
}
|
|
366
|
-
lines.push(chipLine);
|
|
367
|
-
}
|
|
368
|
-
const isRecentlyCopied = Date.now() - lastCopyTime < 2000;
|
|
369
|
-
// Token usage line + running ~$ cost estimate (derived from the usage
|
|
370
|
-
// object and the active model via cost-utils).
|
|
371
|
-
const u = usage as { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; up?: number; down?: number; fleetCostUsd?: number | null };
|
|
372
|
-
const inp = u.input ?? u.up ?? 0;
|
|
373
|
-
const out = u.output ?? u.down ?? 0;
|
|
374
|
-
const cr = u.cacheRead ?? 0;
|
|
375
|
-
const cw = u.cacheWrite ?? 0;
|
|
376
|
-
const total = inp + out + cr + cw;
|
|
377
|
-
const tokenSep = ` ${GLYPHS.navigation.pipeSeparator} `;
|
|
378
|
-
// 'n/a' (not 'unpriced') to stay compact in the single-line footer and
|
|
379
|
-
// match the existing "no priceable data" convention used elsewhere
|
|
380
|
-
// (cockpit-panel formatCost, agent-inspector-shared), the footer has no
|
|
381
|
-
// room for a longer marker before truncation kicks in.
|
|
382
|
-
const mainCostStr = model
|
|
383
|
-
? isModelPriced(model)
|
|
384
|
-
? `~$${fmtCost(calcSessionCost(inp, out, cr, cw, model))}`
|
|
385
|
-
: '~n/a'
|
|
386
|
-
: null;
|
|
387
|
-
// Honest total = your main session + the delegated fleet. We show a SPLIT
|
|
388
|
-
// ("you ~$X · fleet ~$Y") rather than one summed figure: the two costs are
|
|
389
|
-
// tracked and attributed to different actors, and the split directly answers
|
|
390
|
-
// "where did the money go", the exact confusion in the eval where the footer
|
|
391
|
-
// showed only the main session (~$0.046) while the fleet cost ~10x more
|
|
392
|
-
// ($0.446). The fleet segment appears only when there is a real fleet cost, so
|
|
393
|
-
// the idle single-session footer is unchanged.
|
|
394
|
-
const fleetCost = u.fleetCostUsd;
|
|
395
|
-
const hasFleetCost = typeof fleetCost === 'number' && fleetCost > 0;
|
|
396
|
-
const costSegment = hasFleetCost
|
|
397
|
-
? `${tokenSep}you ${mainCostStr ?? '~n/a'} · fleet ~$${fmtCost(fleetCost)}`
|
|
398
|
-
: mainCostStr
|
|
399
|
-
? `${tokenSep}${mainCostStr}`
|
|
400
|
-
: '';
|
|
401
|
-
// Input tokens are unknown until the first assistant usage lands (often the
|
|
402
|
-
// first tool turn). Show "—" rather than a false 0 during that window. (5c)
|
|
403
|
-
const inpDisplay = inp > 0 ? fmtNum(inp) : '—';
|
|
404
|
-
const tokenLine = ` Token Usage [ Input: ${inpDisplay}${tokenSep}Output: ${fmtNum(out)}${tokenSep}Cache Read: ${fmtNum(cr)}${tokenSep}Cache Write: ${fmtNum(cw)}${tokenSep}Total: ${fmtNum(total)}${costSegment} ]`;
|
|
405
|
-
const copiedNotice = isRecentlyCopied ? ` [COPIED] ` : '';
|
|
406
|
-
const statsLine = ' ' + tokenLine + ' '.repeat(Math.max(0, width - 4 - getDisplayWidth(tokenLine) - getDisplayWidth(copiedNotice))) + copiedNotice;
|
|
407
|
-
lines.push(this.stringToLine(statsLine, width, { fg: isRecentlyCopied ? '81' : '244', bold: isRecentlyCopied }));
|
|
408
|
-
// Context usage progress bar, suppressed in compact mode.
|
|
409
|
-
if (!compact && contextWindow && contextWindow > 0) {
|
|
410
|
-
const ctxTokens = lastInputTokens ?? 0;
|
|
411
|
-
const label = ' Context Usage: ';
|
|
412
|
-
// "—" until the first input-token count is known, not a false 0. (5c)
|
|
413
|
-
const ctxDisplay = ctxTokens > 0 ? fmtNum(ctxTokens) : '—';
|
|
414
|
-
const suffix = ` [ ${ctxDisplay} / ${fmtNum(contextWindow)} ]`;
|
|
415
|
-
const barWidth = Math.max(10, Math.min(30, width - getDisplayWidth(label) - getDisplayWidth(suffix) - 8));
|
|
416
|
-
const ctxPct = computeContextUsage(ctxTokens, contextWindow).clampedRatio;
|
|
417
|
-
// Clamp threshold to [0..1]; undefined/0 means no threshold marker.
|
|
418
|
-
const thresholdFraction = (compactThreshold !== undefined && compactThreshold > 0)
|
|
419
|
-
? Math.min(1, compactThreshold)
|
|
420
|
-
: undefined;
|
|
421
|
-
lines.push(this.createProgressBarLine(label, ctxPct, barWidth, width, suffix, thresholdFraction));
|
|
422
|
-
}
|
|
423
|
-
// Context info line (working dir, model+provider, tools, hitl).
|
|
424
|
-
// Suppressed in compact mode. mode/status/flags are intentionally omitted,
|
|
425
|
-
// the posture block above owns them, so they are not duplicated here.
|
|
426
|
-
if (!compact && (workingDir || model)) {
|
|
427
|
-
const home = typeof process !== 'undefined' ? process.env.HOME ?? '' : '';
|
|
428
|
-
const displayDir = workingDir && home && workingDir.startsWith(home)
|
|
429
|
-
? '~' + workingDir.slice(home.length)
|
|
430
|
-
: workingDir ?? '';
|
|
431
|
-
// Segments carry a survival priority (lower = survives longer) so that
|
|
432
|
-
// under width pressure whole low-value segments are dropped before any
|
|
433
|
-
// segment is character-truncated mid-word. cwd/model are the essential
|
|
434
|
-
// orientation segments (priority 0); the spine liveness marker is the
|
|
435
|
-
// daemon-honesty signal and must outlive the two decorative segments
|
|
436
|
-
// (tool count, notify mode), it is ordered ahead of them too, so a
|
|
437
|
-
// narrow line drops "N tools"/"notify:x" whole before spine is at risk.
|
|
438
|
-
const ctxParts: PrioritizedSegment[] = [];
|
|
439
|
-
if (displayDir) ctxParts.push({ text: displayDir, priority: 0 });
|
|
440
|
-
if (model) {
|
|
441
|
-
ctxParts.push({ text: model + (provider ? ` (${provider})` : ''), priority: 0 });
|
|
442
|
-
}
|
|
443
|
-
// Divergence marker, its own segment so a narrow line drops it WHOLE
|
|
444
|
-
// (joinPrioritizedSegments breaks priority ties toward the later
|
|
445
|
-
// segment) rather than truncating it into a half-word. Priority 0: it
|
|
446
|
-
// outranks every decorative segment, and what survives without it is
|
|
447
|
-
// still the SERVING backend, never a claim about the configured one.
|
|
448
|
-
if (modelNote) ctxParts.push({ text: modelNote, priority: 0 });
|
|
449
|
-
// Cross-surface spine posture, plain words, no blame. Adopted mode only.
|
|
450
|
-
if (sessionSpineStatus) ctxParts.push({ text: `spine:${sessionSpineStatus}`, priority: 1 });
|
|
451
|
-
// Web surface reachability, plain, persistent; dropped first under width pressure.
|
|
452
|
-
if (webSurfaceUrl) ctxParts.push({ text: `web:${webSurfaceUrl}`, priority: 2 });
|
|
453
|
-
if (toolCount) ctxParts.push({ text: `${toolCount} tools`, priority: 2 });
|
|
454
|
-
// Labeled "notify" (not "hitl"), /mode (aliased /hitl) governs UX
|
|
455
|
-
// notification verbosity (quiet/balanced/operator), not tool
|
|
456
|
-
// auto-approval, so it must not share vocabulary with the DANGER MODE
|
|
457
|
-
// risk banner rendered a few lines below. Lowest priority: dropped first.
|
|
458
|
-
if (hitlMode) ctxParts.push({ text: `notify:${hitlMode}`, priority: 3 });
|
|
459
|
-
const sep = ` ${GLYPHS.navigation.pipeSeparator} `;
|
|
460
|
-
const ctxBody = joinPrioritizedSegments(ctxParts, sep, width - 3);
|
|
461
|
-
const ctxLine = ' ' + ctxBody;
|
|
462
|
-
lines.push(this.stringToLine(ctxLine, width, { fg: '240', dim: true }));
|
|
463
|
-
}
|
|
464
|
-
if (showExitNotice) {
|
|
465
|
-
const notice = ` !!! Press Ctrl+C again to exit !!! `;
|
|
466
|
-
lines.push(this.stringToLine(fitDisplay(notice, width), width, { fg: '196', bold: true }));
|
|
467
|
-
} else {
|
|
468
|
-
// Persistent discoverability tip. Rotates by context (agent-aware): the
|
|
469
|
-
// process-monitor tip leads while a turn is in flight. See footer-tips.ts.
|
|
470
|
-
const help = ` ${buildFooterTip({ agentActive: isAgentActive(composerStatus) })} `;
|
|
471
|
-
const dangerWarn = dangerMode ? `! DANGER MODE - ALL CHANGES AUTO-APPROVED ` : '';
|
|
472
|
-
const helpW = getDisplayWidth(help);
|
|
473
|
-
const dangerW = getDisplayWidth(dangerWarn);
|
|
474
|
-
const spacerW = Math.max(0, width - helpW - dangerW);
|
|
475
|
-
const combinedLine = help + ' '.repeat(spacerW) + dangerWarn;
|
|
476
|
-
const line = this.stringToLine(truncateDisplay(combinedLine, width), width, { fg: '240', dim: true });
|
|
477
|
-
// Overlay the danger warning in red bold
|
|
478
|
-
if (dangerMode && dangerW > 0) {
|
|
479
|
-
let col = helpW + spacerW;
|
|
480
|
-
for (const ch of dangerWarn) {
|
|
481
|
-
if (col >= width) break;
|
|
482
|
-
const cw = getDisplayWidth(ch);
|
|
483
|
-
line[col] = { char: ch, fg: t.chrome.bad, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false };
|
|
484
|
-
if (cw === 2 && col + 1 < width) line[col + 1] = { ...line[col], char: '' };
|
|
485
|
-
col += cw;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
lines.push(line);
|
|
489
|
-
}
|
|
490
|
-
return lines;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// The rotating "thinking" phrase pool and the honest waiting-state
|
|
494
|
-
// wording (approval/reconnecting/pre-first-token/stalled/thinking) are no
|
|
495
|
-
// longer minted here. They come from the SDK presentation contract's
|
|
496
|
-
// waitingPhrase() (@pellux/goodvibes-sdk/platform/presentation, already
|
|
497
|
-
// adopted by the agent), see createThinkingFragment
|
|
498
|
-
// below. This renderer still decides WHICH state applies from its own
|
|
499
|
-
// stall/reconnect/approval signals (computeStallInfo/computeRenderStallInfo
|
|
500
|
-
// stay renderer-local per the extraction decision record); only the exact
|
|
501
|
-
// wording is shared.
|
|
502
|
-
|
|
503
|
-
// Gradient colors for thinking text, cyan→purple in dark, teal→purple in
|
|
504
|
-
// light (matches splash/brand). Read live per render inside
|
|
505
|
-
// createThinkingFragment via activeUiTones() rather than baked into a static
|
|
506
|
-
// field, so a mode flip re-resolves without a module reload.
|
|
87
|
+
// The waiting-state wording (approval/reconnecting/pre-first-token/stalled/
|
|
88
|
+
// thinking) comes from the SDK presentation contract's waitingPhrase(),
|
|
89
|
+
// shared with the agent. This renderer still decides WHICH state applies
|
|
90
|
+
// from its own stall/reconnect/approval signals (computeStallInfo /
|
|
91
|
+
// computeRenderStallInfo below, see busyPhrase).
|
|
507
92
|
|
|
508
93
|
/**
|
|
509
94
|
* Per-frame stall info from stream metrics, computed from lastDeltaAtMs every render (not
|
|
@@ -524,7 +109,7 @@ export class UIFactory {
|
|
|
524
109
|
* executing. lastDeltaAtMs only tracks STREAM_START/STREAM_DELTA and is
|
|
525
110
|
* never advanced during tool execution (the model isn't producing tokens
|
|
526
111
|
* then), so without this gate any tool call longer than
|
|
527
|
-
* THINKING_STALL_FREEZE_MS would make the
|
|
112
|
+
* THINKING_STALL_FREEZE_MS would make the status line print
|
|
528
113
|
* "Stalled Ns..." directly above the ticking "executing (Ns)" tool row, a
|
|
529
114
|
* false positive during ordinary tool execution (see stream-event-wiring.ts
|
|
530
115
|
* TOOL_EXECUTING/TOOL_SUCCEEDED/TOOL_FAILED/TOOL_CANCELLED handlers).
|
|
@@ -542,17 +127,15 @@ export class UIFactory {
|
|
|
542
127
|
: undefined;
|
|
543
128
|
}
|
|
544
129
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// mirrors the agent's adoption). Precedence matches the contract:
|
|
555
|
-
// approval > reconnecting > pre-first-token > stalled > thinking.
|
|
130
|
+
/**
|
|
131
|
+
* The status line's waiting phrase for a running turn. Decides WHICH honest
|
|
132
|
+
* waiting state applies (renderer-local signals), then takes the exact
|
|
133
|
+
* wording from the SDK presentation contract's waitingPhrase(). Precedence:
|
|
134
|
+
* approval > reconnecting > pre-first-token > stalled > thinking. The
|
|
135
|
+
* whimsical rotation freezes once real silence has lasted long enough to be
|
|
136
|
+
* misleading (THINKING_STALL_FREEZE_MS).
|
|
137
|
+
*/
|
|
138
|
+
public static busyPhrase(frame: number, outputTokens?: number, stallInfo?: ThinkingStallInfo, approvalPending?: boolean): string {
|
|
556
139
|
const isStalled = stallInfo !== undefined && stallInfo.msSinceLastDelta >= THINKING_STALL_FREEZE_MS;
|
|
557
140
|
let state: WaitingState;
|
|
558
141
|
if (approvalPending) state = 'approval';
|
|
@@ -560,163 +143,17 @@ export class UIFactory {
|
|
|
560
143
|
else if (isStalled && (outputTokens ?? 0) === 0) state = 'pre-first-token';
|
|
561
144
|
else if (isStalled) state = 'stalled';
|
|
562
145
|
else state = 'thinking';
|
|
563
|
-
|
|
146
|
+
return waitingPhrase(state, {
|
|
564
147
|
reconnectAttempt: stallInfo?.reconnect?.attempt,
|
|
565
148
|
reconnectMaxAttempts: stallInfo?.reconnect?.maxAttempts,
|
|
566
149
|
msSinceLastDelta: stallInfo?.msSinceLastDelta,
|
|
567
150
|
frame,
|
|
568
151
|
});
|
|
569
|
-
// Token-rate and time-to-first-token readouts are meaningless while waiting on the user, and a
|
|
570
|
-
// "tok/s" figure next to an approval prompt reads as if the model were still working, suppress
|
|
571
|
-
// them; keep the elapsed timer since "how long the approval has waited" is honest.
|
|
572
|
-
const speedSuffix = (!approvalPending && tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
|
|
573
|
-
const elapsedSuffix = elapsedMs !== undefined ? ` (${formatElapsed(elapsedMs)})` : '';
|
|
574
|
-
const ttftSuffix = (!approvalPending && ttftMs !== undefined && ttftMs > 0) ? ` (first token ${formatElapsed(ttftMs)})` : '';
|
|
575
|
-
const text = ` ${spinner} ${phrase}${speedSuffix}${elapsedSuffix}${ttftSuffix} `;
|
|
576
|
-
|
|
577
|
-
const textWidth = Math.max(1, getDisplayWidth(text) - 1);
|
|
578
|
-
const segments: ConversationStatusSegment[] = Array.from(text).map((char, index) => {
|
|
579
|
-
const rawUnwrapped = (index / textWidth) - (frame % GRADIENT_CYCLE_FRAMES) * 0.02;
|
|
580
|
-
const raw = ((rawUnwrapped % 1.0) + 1.0) % 1.0;
|
|
581
|
-
const gradientPos = raw <= 0.5 ? raw * 2 : (1 - raw) * 2;
|
|
582
|
-
return {
|
|
583
|
-
text: char,
|
|
584
|
-
fg: interpolateColor(tones.accent.gradientStart, tones.accent.gradientEnd, gradientPos),
|
|
585
|
-
bold: true,
|
|
586
|
-
};
|
|
587
|
-
});
|
|
588
|
-
if (inputTokens !== undefined || outputTokens !== undefined) {
|
|
589
|
-
const inTok = inputTokens ?? 0;
|
|
590
|
-
const outTok = outputTokens ?? 0;
|
|
591
|
-
segments.push({ text: ` in ${fmtNum(inTok)} `, fg: '243', dim: true });
|
|
592
|
-
segments.push({ text: `out ${fmtNum(outTok)}`, fg: tones.accent.brand });
|
|
593
|
-
}
|
|
594
|
-
const line = createEmptyLine(width);
|
|
595
|
-
let col = 1;
|
|
596
|
-
for (const segment of segments) {
|
|
597
|
-
for (const char of segment.text) {
|
|
598
|
-
if (col >= width) break;
|
|
599
|
-
const charWidth = getDisplayWidth(char);
|
|
600
|
-
if (charWidth <= 0 || col + charWidth > width) break;
|
|
601
|
-
line[col] = {
|
|
602
|
-
char,
|
|
603
|
-
fg: segment.fg,
|
|
604
|
-
bg: '',
|
|
605
|
-
bold: segment.bold ?? false,
|
|
606
|
-
dim: segment.dim ?? false,
|
|
607
|
-
underline: false,
|
|
608
|
-
italic: segment.italic ?? false,
|
|
609
|
-
strikethrough: false,
|
|
610
|
-
};
|
|
611
|
-
if (charWidth === 2 && col + 1 < width) {
|
|
612
|
-
line[col + 1] = { ...line[col], char: '' };
|
|
613
|
-
}
|
|
614
|
-
col += charWidth;
|
|
615
|
-
}
|
|
616
|
-
if (col >= width) break;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const lines: Line[] = [
|
|
620
|
-
this.stringToLine(' '.repeat(width), width),
|
|
621
|
-
line,
|
|
622
|
-
];
|
|
623
|
-
|
|
624
|
-
if (toolPreview) {
|
|
625
|
-
const previewLine = createEmptyLine(width);
|
|
626
|
-
const label = ' tool: ';
|
|
627
|
-
let px = 0;
|
|
628
|
-
for (const ch of label) {
|
|
629
|
-
if (px >= width) break;
|
|
630
|
-
previewLine[px] = {
|
|
631
|
-
char: ch,
|
|
632
|
-
fg: tones.state.info,
|
|
633
|
-
bg: '',
|
|
634
|
-
bold: true,
|
|
635
|
-
dim: false,
|
|
636
|
-
underline: false,
|
|
637
|
-
italic: false,
|
|
638
|
-
strikethrough: false,
|
|
639
|
-
};
|
|
640
|
-
px += getDisplayWidth(ch);
|
|
641
|
-
}
|
|
642
|
-
for (const ch of toolPreview) {
|
|
643
|
-
if (px >= width) break;
|
|
644
|
-
const charWidth = getDisplayWidth(ch);
|
|
645
|
-
if (charWidth <= 0 || px + charWidth > width) break;
|
|
646
|
-
previewLine[px] = {
|
|
647
|
-
char: ch,
|
|
648
|
-
fg: '243',
|
|
649
|
-
bg: '',
|
|
650
|
-
bold: false,
|
|
651
|
-
dim: true,
|
|
652
|
-
underline: false,
|
|
653
|
-
italic: false,
|
|
654
|
-
strikethrough: false,
|
|
655
|
-
};
|
|
656
|
-
if (charWidth === 2 && px + 1 < width) {
|
|
657
|
-
previewLine[px + 1] = { ...previewLine[px], char: '' };
|
|
658
|
-
}
|
|
659
|
-
px += charWidth;
|
|
660
|
-
}
|
|
661
|
-
lines.push(previewLine);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
lines.push(this.stringToLine(' '.repeat(width), width));
|
|
665
|
-
return lines;
|
|
666
152
|
}
|
|
667
153
|
|
|
668
|
-
/**
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
* @param pct - Fill fraction 0..1
|
|
672
|
-
* @param barWidth - Number of bar characters
|
|
673
|
-
* @param lineWidth - Total terminal width to slice to
|
|
674
|
-
* @param suffix - Optional suffix appended after the percentage
|
|
675
|
-
* @param compactThreshold - Optional fraction [0..1] at which a threshold marker is drawn
|
|
676
|
-
* and the color switches from safe to at-threshold. When omitted, falls back to the
|
|
677
|
-
* legacy hardcoded 0.6/0.85 thresholds.
|
|
678
|
-
*/
|
|
679
|
-
private static createProgressBarLine(
|
|
680
|
-
label: string,
|
|
681
|
-
pct: number,
|
|
682
|
-
barWidth: number,
|
|
683
|
-
lineWidth: number,
|
|
684
|
-
suffix?: string,
|
|
685
|
-
compactThreshold?: number,
|
|
686
|
-
): Line {
|
|
687
|
-
const pctDisplay = Math.round(pct * 100);
|
|
688
|
-
const filled = Math.round(pct * barWidth);
|
|
689
|
-
|
|
690
|
-
// Color: when compactThreshold is provided, switch at the threshold;
|
|
691
|
-
// otherwise fall back to legacy hardcoded 0.6 (green) / 0.85 (yellow) / red.
|
|
692
|
-
let color: string;
|
|
693
|
-
if (compactThreshold !== undefined) {
|
|
694
|
-
color = pct < compactThreshold ? '82' : pct < 1.0 ? '220' : '196';
|
|
695
|
-
} else {
|
|
696
|
-
color = pct < 0.6 ? '82' : pct < 0.85 ? '220' : '196';
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// Build bar with optional threshold marker.
|
|
700
|
-
// The marker ('▸') is placed at the threshold column, replacing an empty cell.
|
|
701
|
-
const emptyChar = GLYPHS.meter.empty;
|
|
702
|
-
const filledChar = GLYPHS.meter.filled;
|
|
703
|
-
const thresholdCol = compactThreshold !== undefined
|
|
704
|
-
? Math.round(compactThreshold * barWidth)
|
|
705
|
-
: -1;
|
|
706
|
-
|
|
707
|
-
let bar = '';
|
|
708
|
-
for (let i = 0; i < barWidth; i++) {
|
|
709
|
-
if (i === thresholdCol && i >= filled) {
|
|
710
|
-
// Threshold marker sits in the empty region
|
|
711
|
-
bar += '▸'; // ▸
|
|
712
|
-
} else {
|
|
713
|
-
bar += i < filled ? filledChar : emptyChar;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const pctStr = ` ${pctDisplay}%`;
|
|
718
|
-
const full = label + bar + pctStr + (suffix ?? '');
|
|
719
|
-
return this.stringToLine(truncateDisplay(full, lineWidth), lineWidth, { fg: color, dim: true });
|
|
154
|
+
/** The opt-in partial tool preview row shown under the transcript while a turn runs. */
|
|
155
|
+
public static createToolPreviewRow(width: number, preview: string): Line {
|
|
156
|
+
return this.stringToLine(truncateDisplay(` tool: ${preview}`, width), width, { fg: activeTokens().textFaint });
|
|
720
157
|
}
|
|
721
158
|
|
|
722
159
|
public static stringToLine(text: string, width: number, style: Partial<Cell> = {}): Line {
|