@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,95 +1,199 @@
|
|
|
1
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import { ModalFactory, type ModalSection, type ModalTab } from './modal-factory.ts';
|
|
3
|
-
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
4
|
-
import { activeUiTones } from './theme.ts';
|
|
5
|
-
import type { ConfigModal, ConfigModalRenderModel } from '../input/config-modal.ts';
|
|
6
|
-
|
|
7
|
-
// Matches ModalFactory.createModal's own box-width computation (margin 4,
|
|
8
|
-
// requested max width 76, see renderConfigModalModel below) so the
|
|
9
|
-
// wrap-clamp in ConfigModal.getRenderModel() measures against the SAME width
|
|
10
|
-
// the list section will actually wrap at, at the current terminal size.
|
|
11
|
-
const MODAL_MARGIN = 4;
|
|
12
|
-
const MODAL_MAX_WIDTH = 76;
|
|
13
1
|
/**
|
|
14
|
-
* renderConfigModal, the single render path for every ConfigModalSurface
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
2
|
+
* renderConfigModal, the single render path for every ConfigModalSurface
|
|
3
|
+
* (hooks, plugins, marketplace, skills, security, policy, knowledge, memory,
|
|
4
|
+
* work plan, keybindings, pairing, planning, local auth, services, providers,
|
|
5
|
+
* remote, sandbox, settings sync, devices, subscription, provider health).
|
|
6
|
+
*
|
|
7
|
+
* Drawn with the modal surface kit: soft caps, ✦ title with the esc keycap,
|
|
8
|
+
* tabs (the active one carries the gradient), the always-live search row,
|
|
9
|
+
* the surface's posture lines, kit rows (group headers where the surface has
|
|
10
|
+
* sections, the selected row as the gradient), then status and notes, and the
|
|
11
|
+
* keycap hint row. Reads the host's frozen-structure-plus-live-values render
|
|
12
|
+
* model, so live ticks repaint values without moving rows.
|
|
19
13
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
14
|
+
|
|
15
|
+
import { activeTokens } from './theme.ts';
|
|
16
|
+
import {
|
|
17
|
+
beginModal,
|
|
18
|
+
clipText,
|
|
19
|
+
finishModal,
|
|
20
|
+
modalGeometry,
|
|
21
|
+
searchRow,
|
|
22
|
+
scrollCountText,
|
|
23
|
+
MODAL_PAD_X,
|
|
24
|
+
type KitHint,
|
|
25
|
+
type ModalFrame,
|
|
26
|
+
type SurfaceLayer,
|
|
27
|
+
} from './surface-kit.ts';
|
|
28
|
+
import { drawRow, measureRow, type KitRow } from './surface-kit-list.ts';
|
|
29
|
+
import {
|
|
30
|
+
drawTabRows,
|
|
31
|
+
drawTextBlock,
|
|
32
|
+
kitHintsFromStrings,
|
|
33
|
+
tabRowCount,
|
|
34
|
+
textBlockHeight,
|
|
35
|
+
type TextLine,
|
|
36
|
+
} from './surface-kit-extra.ts';
|
|
37
|
+
import type { ConfigModal, ConfigModalRenderModel, ConfigModalRenderRow } from '../input/config-modal.ts';
|
|
38
|
+
|
|
39
|
+
function kitRowFor(row: ConfigModalRenderRow): KitRow {
|
|
40
|
+
const label = row.label.replace(/\n/g, ' ');
|
|
41
|
+
if (row.header) return { header: label };
|
|
42
|
+
return {
|
|
43
|
+
label,
|
|
44
|
+
selected: row.selected,
|
|
45
|
+
labelFg: row.style?.fg,
|
|
46
|
+
bold: row.style?.bold === true,
|
|
47
|
+
muted: row.stale,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Layout {
|
|
52
|
+
readonly hints: KitHint[];
|
|
53
|
+
readonly topLines: TextLine[];
|
|
54
|
+
readonly bottomLines: TextLine[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function layoutFor(model: ConfigModalRenderModel): Layout {
|
|
58
|
+
const t = activeTokens();
|
|
59
|
+
const base: KitHint[] = [['↑↓', 'move']];
|
|
60
|
+
if (model.tabs.length > 1) base.push(['←→', 'tab']);
|
|
61
|
+
const { hints, notes } = kitHintsFromStrings(model.hints, base);
|
|
62
|
+
const topLines: TextLine[] = [];
|
|
63
|
+
if (model.degraded) topLines.push({ text: `⚠ ${model.degraded}`, style: { fg: t.warning, bold: true } });
|
|
64
|
+
for (const line of model.header) topLines.push({ text: line, style: { fg: t.textMuted } });
|
|
65
|
+
const bottomLines: TextLine[] = notes.map((text) => ({ text, style: { fg: t.textFaint } }));
|
|
66
|
+
if (model.status) bottomLines.push({ text: model.status, style: { fg: model.confirmPending ? t.warning : t.accent, bold: model.confirmPending } });
|
|
67
|
+
return { hints, topLines, bottomLines };
|
|
33
68
|
}
|
|
34
69
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Render the open config modal as a SurfaceLayer in screen coordinates.
|
|
72
|
+
* Also tells the host how many list rows fit (setViewportRows) so its
|
|
73
|
+
* row windowing matches what is drawn.
|
|
74
|
+
*/
|
|
75
|
+
export function renderConfigModal(modal: ConfigModal, screenWidth: number, screenHeight: number): SurfaceLayer {
|
|
76
|
+
const t = activeTokens();
|
|
77
|
+
const geometry = modalGeometry(screenWidth, screenHeight);
|
|
78
|
+
const textWidth = Math.max(1, geometry.w - 2 * MODAL_PAD_X);
|
|
79
|
+
|
|
80
|
+
// First pass: header, status and hints decide how many rows the list gets.
|
|
81
|
+
const first = modal.getRenderModel(textWidth);
|
|
82
|
+
const firstLayout = layoutFor(first);
|
|
83
|
+
const f0 = beginModal(screenWidth, screenHeight, { title: first.title || 'Settings', hints: firstLayout.hints });
|
|
84
|
+
const firstCompact = compactionFor(first, firstLayout, f0);
|
|
85
|
+
modal.setViewportRows(Math.max(3, listCapacity(first, firstLayout, f0, firstCompact)));
|
|
41
86
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
87
|
+
const model = modal.getRenderModel(textWidth);
|
|
88
|
+
const layout = layoutFor(model);
|
|
89
|
+
const f = beginModal(screenWidth, screenHeight, { title: model.title || 'Settings', hints: layout.hints });
|
|
90
|
+
const compact = compactionFor(model, layout, f);
|
|
91
|
+
const { canvas, l, r } = f;
|
|
92
|
+
const gap = compact.tight ? 0 : 1;
|
|
93
|
+
|
|
94
|
+
let y = f.top;
|
|
95
|
+
if (model.tabs.length > 1) {
|
|
96
|
+
const active = Math.max(0, model.tabs.findIndex((tab) => tab.active));
|
|
97
|
+
y += drawTabRows(canvas, l, y, model.tabs.map((tab) => tab.label), active, r) + gap;
|
|
45
98
|
}
|
|
99
|
+
const activeTab = model.tabs.find((tab) => tab.active);
|
|
100
|
+
const placeholder = `Filter ${(activeTab?.label ?? 'rows').toLowerCase()}`;
|
|
101
|
+
const count = model.search.total === 0
|
|
102
|
+
? ''
|
|
103
|
+
: model.search.query.length > 0
|
|
104
|
+
? `${model.search.matched} of ${model.search.total}`
|
|
105
|
+
: `${model.search.total} ${model.search.total === 1 ? 'item' : 'items'}`;
|
|
106
|
+
searchRow(f, y, model.search.query, placeholder, count || undefined);
|
|
107
|
+
y += 1 + gap;
|
|
46
108
|
|
|
47
|
-
|
|
48
|
-
|
|
109
|
+
if (layout.topLines.length > 0 && !compact.dropTop) {
|
|
110
|
+
y = drawTextBlock(canvas, l, y, r - l + 1, layout.topLines, f.bottom) + 1;
|
|
49
111
|
}
|
|
50
|
-
if (model.header.length > 0) sections.push({ type: 'separator' });
|
|
51
112
|
|
|
113
|
+
const bottomRows = bottomRowsFor(layout, r - l + 1);
|
|
114
|
+
const listBottom = f.bottom - bottomRows;
|
|
115
|
+
if (bottomRows > 0) drawTextBlock(canvas, l, listBottom + 2, r - l + 1, layout.bottomLines, f.bottom);
|
|
116
|
+
|
|
117
|
+
let cutLines = 0;
|
|
52
118
|
if (model.rows.length === 0) {
|
|
53
|
-
|
|
119
|
+
drawTextBlock(canvas, l, y, r - l + 1, [{ text: model.emptyText ?? 'Nothing to show.', style: { fg: t.textMuted } }], listBottom);
|
|
54
120
|
} else {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
121
|
+
cutLines = drawRows(model, canvas, y, listBottom, l, r);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const above = model.scroll.offset;
|
|
125
|
+
const below = Math.max(0, model.scroll.total - model.scroll.offset - model.rows.length);
|
|
126
|
+
// A single row taller than the list is cut at its bottom; say so honestly.
|
|
127
|
+
const cut = cutLines > 0 ? `${cutLines} more ${cutLines === 1 ? 'line' : 'lines'} ↓` : '';
|
|
128
|
+
f.hintRight = [scrollCountText(above, below), cut].filter(Boolean).join(' · ');
|
|
129
|
+
return finishModal(f);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** How the body gives way on short screens: posture lines go first, then the blank rows under tabs and search. */
|
|
133
|
+
interface Compaction {
|
|
134
|
+
readonly dropTop: boolean;
|
|
135
|
+
readonly tight: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const MIN_LIST_ROWS = 3;
|
|
139
|
+
|
|
140
|
+
function compactionFor(model: ConfigModalRenderModel, layout: Layout, f: ModalFrame): Compaction {
|
|
141
|
+
const steps: Compaction[] = [
|
|
142
|
+
{ dropTop: false, tight: false },
|
|
143
|
+
{ dropTop: true, tight: false },
|
|
144
|
+
{ dropTop: true, tight: true },
|
|
145
|
+
];
|
|
146
|
+
for (const step of steps) {
|
|
147
|
+
if (listCapacity(model, layout, f, step) >= MIN_LIST_ROWS) return step;
|
|
65
148
|
}
|
|
149
|
+
return steps[steps.length - 1]!;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function listCapacity(model: ConfigModalRenderModel, layout: Layout, f: ModalFrame, compact: Compaction): number {
|
|
153
|
+
const width = f.r - f.l + 1;
|
|
154
|
+
const gap = compact.tight ? 0 : 1;
|
|
155
|
+
let y = f.top;
|
|
156
|
+
if (model.tabs.length > 1) y += tabRowCount(model.tabs.map((tab) => tab.label), f.l, f.r) + gap;
|
|
157
|
+
y += 1 + gap;
|
|
158
|
+
if (layout.topLines.length > 0 && !compact.dropTop) y += textBlockHeight(layout.topLines, width) + 1;
|
|
159
|
+
return f.bottom - y + 1 - bottomRowsFor(layout, width);
|
|
160
|
+
}
|
|
66
161
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Keep the content region a stable height so live-value ticks never change
|
|
72
|
-
// the box size: frozen header + separator + the fixed visible-row window.
|
|
73
|
-
const targetContentRows =
|
|
74
|
-
(model.degraded ? 2 : 0) +
|
|
75
|
-
model.header.length +
|
|
76
|
-
(model.header.length > 0 ? 1 : 0) +
|
|
77
|
-
model.scroll.visible;
|
|
78
|
-
|
|
79
|
-
const helpers = model.status
|
|
80
|
-
? [{ content: model.status, accent: true }]
|
|
81
|
-
: undefined;
|
|
82
|
-
|
|
83
|
-
return ModalFactory.createModal(
|
|
84
|
-
{
|
|
85
|
-
title: model.title,
|
|
86
|
-
width: MODAL_MAX_WIDTH,
|
|
87
|
-
tabs,
|
|
88
|
-
sections,
|
|
89
|
-
targetContentRows,
|
|
90
|
-
helpers,
|
|
91
|
-
hints: [...model.hints],
|
|
92
|
-
},
|
|
93
|
-
width,
|
|
94
|
-
);
|
|
162
|
+
/** Rows kept under the list for notes and status (plus the blank row above them). */
|
|
163
|
+
function bottomRowsFor(layout: Layout, width: number): number {
|
|
164
|
+
return layout.bottomLines.length > 0 ? textBlockHeight(layout.bottomLines, width) + 1 : 0;
|
|
95
165
|
}
|
|
166
|
+
|
|
167
|
+
function drawRows(
|
|
168
|
+
model: ConfigModalRenderModel,
|
|
169
|
+
canvas: ReturnType<typeof beginModal>['canvas'],
|
|
170
|
+
top: number,
|
|
171
|
+
bottom: number,
|
|
172
|
+
l: number,
|
|
173
|
+
r: number,
|
|
174
|
+
): number {
|
|
175
|
+
const rows = model.rows.map(kitRowFor);
|
|
176
|
+
let cut = 0;
|
|
177
|
+
const capacity = Math.max(1, bottom - top + 1);
|
|
178
|
+
const needed = rows.reduce((n, row, k) => n + measureRow(row, l, r) + (k > 0 && row.header !== undefined ? 1 : 0), 0);
|
|
179
|
+
const gaps = needed <= capacity;
|
|
180
|
+
let y = top;
|
|
181
|
+
model.rows.forEach((source, k) => {
|
|
182
|
+
if (y > bottom) return;
|
|
183
|
+
const row = rows[k]!;
|
|
184
|
+
if (gaps && k > 0 && row.header !== undefined) y++;
|
|
185
|
+
if (y > bottom) return;
|
|
186
|
+
if (source.style?.bg) {
|
|
187
|
+
// Preformatted rows with their own background (the pairing QR code)
|
|
188
|
+
// are drawn cell for cell: wrapping would break the pattern.
|
|
189
|
+
canvas.put(l, y, clipText(source.label, r - l + 1), { fg: source.style.fg, bg: source.style.bg });
|
|
190
|
+
y++;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const used = drawRow(canvas, y, row, l, r, bottom);
|
|
194
|
+
cut += measureRow(row, l, r) - used;
|
|
195
|
+
y += used;
|
|
196
|
+
});
|
|
197
|
+
return cut;
|
|
198
|
+
}
|
|
199
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderConfirmDialog, a small centered dialog on the kit surface:
|
|
3
|
+
*
|
|
4
|
+
* ✦ Delete session? esc
|
|
5
|
+
*
|
|
6
|
+
* This removes "test" and its 3 messages from this project.
|
|
7
|
+
* It cannot be undone.
|
|
8
|
+
*
|
|
9
|
+
* Cancel Delete
|
|
10
|
+
*
|
|
11
|
+
* Content-sized and centered, over the dimmed screen. The confirming button
|
|
12
|
+
* carries the dialog's tone (red for destructive actions); the safe choice is
|
|
13
|
+
* on the left. The chosen button is filled, the other sits on the element fill.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ConfirmView } from '../input/confirm-dialog-types.ts';
|
|
17
|
+
import { activeTokens } from './theme.ts';
|
|
18
|
+
import { beginModal, finishModal, modalInnerWidth, standardModalWidth, wrapLines, type SurfaceLayer } from './surface-kit.ts';
|
|
19
|
+
import { buttonWidth } from './surface-kit-parts.ts';
|
|
20
|
+
|
|
21
|
+
/** Preferred width of a confirm dialog. */
|
|
22
|
+
const CONFIRM_WIDTH = 64;
|
|
23
|
+
|
|
24
|
+
export function renderConfirmDialog(dialog: ConfirmView, screenWidth: number, screenHeight: number): SurfaceLayer {
|
|
25
|
+
const t = activeTokens();
|
|
26
|
+
const { title, body, confirmLabel, tone } = dialog.options;
|
|
27
|
+
const cancelLabel = dialog.options.cancelLabel ?? 'Cancel';
|
|
28
|
+
const width = Math.min(CONFIRM_WIDTH, standardModalWidth(screenWidth));
|
|
29
|
+
const lines = body.split('\n').flatMap((para) => (para.trim() === '' ? [''] : wrapLines(para, modalInnerWidth(width))));
|
|
30
|
+
// Fill rows: padding, title, blank, body, blank, buttons, empty last row.
|
|
31
|
+
const f = beginModal(screenWidth, screenHeight, { title, width, height: lines.length + 6, center: true });
|
|
32
|
+
let y = f.top;
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (y > f.bottom - 2) break;
|
|
35
|
+
f.canvas.put(f.l, y++, line, { fg: t.textMuted });
|
|
36
|
+
}
|
|
37
|
+
const toneBg = tone === 'danger' ? t.error : tone === 'warning' ? t.warning : t.brand;
|
|
38
|
+
const confirmW = buttonWidth(confirmLabel);
|
|
39
|
+
const cancelW = buttonWidth(cancelLabel);
|
|
40
|
+
const by = f.bottom;
|
|
41
|
+
const confirmX = f.r - confirmW + 1;
|
|
42
|
+
const cancelX = confirmX - 2 - cancelW;
|
|
43
|
+
f.canvas.put(cancelX, by, ` ${cancelLabel} `, dialog.chosen === 0
|
|
44
|
+
? { fg: t.selectedListItemText, bg: t.textMuted, bold: true }
|
|
45
|
+
: { fg: t.textMuted, bg: t.border });
|
|
46
|
+
f.canvas.put(confirmX, by, ` ${confirmLabel} `, dialog.chosen === 1
|
|
47
|
+
? { fg: t.selectedListItemText, bg: toneBg, bold: true }
|
|
48
|
+
: { fg: toneBg, bg: t.border, bold: true });
|
|
49
|
+
return finishModal(f);
|
|
50
|
+
}
|
|
@@ -1,24 +1,42 @@
|
|
|
1
|
-
import { type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import { ModalFactory } from './modal-factory.ts';
|
|
3
1
|
import type { ConversationManager } from '../core/conversation';
|
|
4
|
-
import { getOverlayContentBudget, getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
|
|
5
2
|
import { estimateTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
6
|
-
import {
|
|
3
|
+
import { activeTokens } from './theme.ts';
|
|
4
|
+
import {
|
|
5
|
+
beginModal,
|
|
6
|
+
finishModal,
|
|
7
|
+
scrollCountText,
|
|
8
|
+
type KitHint,
|
|
9
|
+
type SurfaceLayer,
|
|
10
|
+
} from './surface-kit.ts';
|
|
11
|
+
import { drawList, measureRow, type KitRow } from './surface-kit-list.ts';
|
|
12
|
+
import { drawTextBlock, modalHeightFor, modalTextWidth, textBlockHeight, type TextLine } from './surface-kit-extra.ts';
|
|
7
13
|
|
|
8
14
|
// ─── ContextInspectorModal ────────────────────────────────────────────────────
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
* ContextInspectorModal, state for the context inspector overlay.
|
|
17
|
+
* ContextInspectorModal, state for the context inspector overlay. The list
|
|
18
|
+
* opens on the newest messages; ↑ scrolls back toward older ones.
|
|
12
19
|
*/
|
|
13
20
|
export class ContextInspectorModal {
|
|
14
21
|
public active = false;
|
|
22
|
+
/** Rows scrolled back from the newest message (0 = the newest are in view). */
|
|
23
|
+
public scrollBack = 0;
|
|
24
|
+
/** The furthest the list can scroll back, recorded by the renderer. */
|
|
25
|
+
public maxScrollBack = 0;
|
|
15
26
|
|
|
16
27
|
open(): void {
|
|
17
28
|
this.active = true;
|
|
29
|
+
this.scrollBack = 0;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
close(): void {
|
|
21
33
|
this.active = false;
|
|
34
|
+
this.scrollBack = 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Positive scrolls back toward older messages, negative toward the newest. */
|
|
38
|
+
scrollBy(delta: number): void {
|
|
39
|
+
this.scrollBack = Math.max(0, Math.min(this.maxScrollBack, this.scrollBack + delta));
|
|
22
40
|
}
|
|
23
41
|
}
|
|
24
42
|
|
|
@@ -34,179 +52,123 @@ function fmtPct(ratio: number): string {
|
|
|
34
52
|
return `${(ratio * 100).toFixed(1)}%`;
|
|
35
53
|
}
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* context. Highlights large consumers (>10%). Shows total vs context window
|
|
42
|
-
* capacity and suggests compaction targets.
|
|
43
|
-
*
|
|
44
|
-
* @param conversation The conversation manager to inspect.
|
|
45
|
-
* @param width Terminal width.
|
|
46
|
-
* @param _height Terminal height (reserved for future scrolling).
|
|
47
|
-
* @param contextWindow Optional context window size for capacity display.
|
|
48
|
-
*/
|
|
49
|
-
export function renderContextInspector(
|
|
50
|
-
conversation: ConversationManager,
|
|
51
|
-
width: number,
|
|
52
|
-
viewportHeight = 24,
|
|
53
|
-
contextWindow = 0,
|
|
54
|
-
): Line[] {
|
|
55
|
-
const messages = conversation.getMessagesForLLM();
|
|
56
|
-
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
57
|
-
margin: 1,
|
|
58
|
-
maxWidth: 78,
|
|
59
|
-
chromeRows: 7,
|
|
60
|
-
minContentRows: 6,
|
|
61
|
-
maxContentRows: 10,
|
|
62
|
-
});
|
|
63
|
-
const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 8);
|
|
64
|
-
|
|
65
|
-
if (messages.length === 0) {
|
|
66
|
-
return ModalFactory.createModal({
|
|
67
|
-
title: 'Context Inspector',
|
|
68
|
-
width: metrics.boxWidth,
|
|
69
|
-
margin: metrics.margin,
|
|
70
|
-
targetContentRows,
|
|
71
|
-
sections: [
|
|
72
|
-
{ type: 'text', content: 'No messages in conversation yet.' },
|
|
73
|
-
],
|
|
74
|
-
hints: ['[Esc] Close'],
|
|
75
|
-
}, width);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Token accounting ──────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
type MsgEntry = {
|
|
81
|
-
role: string;
|
|
82
|
-
tokens: number;
|
|
83
|
-
label: string;
|
|
84
|
-
};
|
|
55
|
+
interface MsgEntry {
|
|
56
|
+
readonly tokens: number;
|
|
57
|
+
readonly label: string;
|
|
58
|
+
}
|
|
85
59
|
|
|
60
|
+
function messageEntries(conversation: ConversationManager): { entries: MsgEntry[]; total: number } {
|
|
86
61
|
const entries: MsgEntry[] = [];
|
|
87
|
-
let
|
|
88
|
-
|
|
89
|
-
for (const msg of messages) {
|
|
62
|
+
let total = 0;
|
|
63
|
+
for (const msg of conversation.getMessagesForLLM()) {
|
|
90
64
|
const role = msg.role;
|
|
91
65
|
let text = '';
|
|
92
66
|
if (typeof msg.content === 'string') {
|
|
93
67
|
text = msg.content;
|
|
94
68
|
} else if (Array.isArray(msg.content)) {
|
|
95
|
-
// ContentPart[]
|
|
96
69
|
text = (msg.content as Array<{ type: string; text?: string }>)
|
|
97
70
|
.filter((p) => p.type === 'text')
|
|
98
71
|
.map((p) => p.text ?? '')
|
|
99
72
|
.join('');
|
|
100
73
|
}
|
|
101
74
|
// Include tool call text for assistant messages
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (const tc of
|
|
105
|
-
text += tc.name + JSON.stringify(tc.arguments);
|
|
106
|
-
}
|
|
75
|
+
const toolCalls = (msg as { toolCalls?: Array<{ name: string; arguments: unknown }> }).toolCalls;
|
|
76
|
+
if (role === 'assistant' && toolCalls) {
|
|
77
|
+
for (const tc of toolCalls) text += tc.name + JSON.stringify(tc.arguments);
|
|
107
78
|
}
|
|
108
79
|
const tokens = estimateTokens(text);
|
|
109
|
-
|
|
110
|
-
|
|
80
|
+
total += tokens;
|
|
81
|
+
// A one-line excerpt identifies the message; the inspector is about its
|
|
82
|
+
// token cost, not its content.
|
|
83
|
+
const excerpt = (limit: number): string => `${text.slice(0, limit).replace(/\s+/g, ' ')}${text.length > limit ? '…' : ''}`;
|
|
111
84
|
let label: string;
|
|
112
|
-
if (role === 'user') {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const toolMsg = msg as { callId?: string };
|
|
118
|
-
label = `tool-result (${(toolMsg.callId ?? '').slice(0, 12)})`;
|
|
119
|
-
} else {
|
|
120
|
-
label = role;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
entries.push({ role, tokens, label });
|
|
85
|
+
if (role === 'user') label = `user: ${excerpt(60)}`;
|
|
86
|
+
else if (role === 'assistant') label = `assistant: ${excerpt(56)}`;
|
|
87
|
+
else if (role === 'tool') label = `tool result ${((msg as { callId?: string }).callId ?? '').slice(0, 12)}`;
|
|
88
|
+
else label = role;
|
|
89
|
+
entries.push({ tokens, label });
|
|
124
90
|
}
|
|
91
|
+
return { entries, total };
|
|
92
|
+
}
|
|
125
93
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Render the context inspector as a SurfaceLayer in screen coordinates.
|
|
96
|
+
*
|
|
97
|
+
* Lists each message with its estimated token count and share of the total
|
|
98
|
+
* (right-aligned), marks large consumers (>10%) with an amber ●, shows the
|
|
99
|
+
* total against the context window, and suggests compaction targets.
|
|
100
|
+
*/
|
|
101
|
+
export function renderContextInspector(
|
|
102
|
+
conversation: ConversationManager,
|
|
103
|
+
screenWidth: number,
|
|
104
|
+
screenHeight = 24,
|
|
105
|
+
contextWindow = 0,
|
|
106
|
+
modal?: ContextInspectorModal,
|
|
107
|
+
): SurfaceLayer {
|
|
108
|
+
const t = activeTokens();
|
|
109
|
+
const { entries, total } = messageEntries(conversation);
|
|
110
|
+
|
|
111
|
+
if (entries.length === 0) {
|
|
112
|
+
const lines: TextLine[] = [{ text: 'No messages in conversation yet.', style: { fg: t.textMuted } }];
|
|
113
|
+
const width = modalTextWidth(screenWidth, screenHeight);
|
|
114
|
+
const height = modalHeightFor(screenWidth, screenHeight, {}, textBlockHeight(lines, width));
|
|
115
|
+
const f = beginModal(screenWidth, screenHeight, { title: 'Context inspector', height, center: true });
|
|
116
|
+
drawTextBlock(f.canvas, f.l, f.top, f.r - f.l + 1, lines, f.bottom);
|
|
117
|
+
return finishModal(f);
|
|
118
|
+
}
|
|
143
119
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
120
|
+
const largeThreshold = total * 0.10;
|
|
121
|
+
const large = entries.filter((e) => e.tokens > largeThreshold);
|
|
122
|
+
const capacity = contextWindow > 0 ? ` · ${fmtN(total)} of ${fmtN(contextWindow)} (${fmtPct(total / contextWindow)})` : '';
|
|
123
|
+
const topLines: TextLine[] = [
|
|
124
|
+
{ text: `Total ~${fmtN(total)} tokens in ${entries.length} message${entries.length === 1 ? '' : 's'}${capacity}`, style: { fg: t.text, bold: true } },
|
|
125
|
+
];
|
|
126
|
+
if (contextWindow > 0 && total / contextWindow >= 0.80) {
|
|
127
|
+
topLines.push({ text: 'Context is 80% full or more. Run /compact to free space.', style: { fg: t.warning, bold: true } });
|
|
128
|
+
}
|
|
129
|
+
const bottomLines: TextLine[] = [];
|
|
130
|
+
if (large.length > 0) {
|
|
131
|
+
const share = fmtPct(large.reduce((s, e) => s + e.tokens, 0) / total);
|
|
132
|
+
bottomLines.push({ text: `● ${large.length} message${large.length > 1 ? 's use' : ' uses'} ${share} of context (each over 10%).`, style: { fg: t.accent } });
|
|
133
|
+
bottomLines.push({ text: 'Run /compact to summarise and reduce context size.', style: { fg: t.textFaint } });
|
|
150
134
|
}
|
|
151
135
|
|
|
152
|
-
|
|
136
|
+
const hints: KitHint[] = [['↑↓', 'scroll']];
|
|
137
|
+
const f = beginModal(screenWidth, screenHeight, { title: 'Context inspector', hints });
|
|
138
|
+
const width = f.r - f.l + 1;
|
|
139
|
+
let top = drawTextBlock(f.canvas, f.l, f.top, width, topLines, f.bottom) + 1;
|
|
140
|
+
const bottomRows = bottomLines.length > 0 ? textBlockHeight(bottomLines, width) + 1 : 0;
|
|
141
|
+
const listBottom = f.bottom - bottomRows;
|
|
142
|
+
if (bottomRows > 0) drawTextBlock(f.canvas, f.l, listBottom + 2, width, bottomLines, f.bottom);
|
|
153
143
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
144
|
+
const rows: KitRow[] = entries.map((e) => {
|
|
145
|
+
const isLarge = e.tokens > largeThreshold;
|
|
146
|
+
return {
|
|
147
|
+
label: e.label,
|
|
148
|
+
right: `${fmtPct(total > 0 ? e.tokens / total : 0)} · ~${fmtN(e.tokens)}`,
|
|
149
|
+
mark: isLarge ? '●' : undefined,
|
|
150
|
+
markFg: t.warning,
|
|
151
|
+
labelFg: isLarge ? t.warning : undefined,
|
|
152
|
+
bold: isLarge,
|
|
153
|
+
};
|
|
159
154
|
});
|
|
160
|
-
const display = entries.slice(-maxVisibleRows);
|
|
161
|
-
const startOffset = entries.length - display.length;
|
|
162
|
-
if (startOffset > 0) {
|
|
163
|
-
sections.push({
|
|
164
|
-
type: 'text',
|
|
165
|
-
content: `(${startOffset} older messages not shown)`,
|
|
166
|
-
style: { dim: true },
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
155
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
sections.push({
|
|
179
|
-
type: 'text',
|
|
180
|
-
content: line,
|
|
181
|
-
style: isLarge ? { fg: UI_TONES.state.warn, bold: true } : {},
|
|
182
|
-
});
|
|
156
|
+
// Open on the newest messages: find the first row of the tail that fits,
|
|
157
|
+
// then step back by the modal's scroll position.
|
|
158
|
+
const listCapacity = Math.max(1, listBottom - top + 1);
|
|
159
|
+
let tailStart = rows.length;
|
|
160
|
+
let used = 0;
|
|
161
|
+
while (tailStart > 0 && used + measureRow(rows[tailStart - 1]!, f.l, f.r) <= listCapacity) {
|
|
162
|
+
tailStart--;
|
|
163
|
+
used += measureRow(rows[tailStart]!, f.l, f.r);
|
|
183
164
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (largeMsgs.length > 0) {
|
|
188
|
-
sections.push({ type: 'separator' });
|
|
189
|
-
const largePct = fmtPct(
|
|
190
|
-
largeMsgs.reduce((s, e) => s + e.tokens, 0) / totalTokens,
|
|
191
|
-
);
|
|
192
|
-
sections.push({
|
|
193
|
-
type: 'text',
|
|
194
|
-
content: `Compaction hint: ${largeMsgs.length} message${largeMsgs.length > 1 ? 's' : ''} use ${largePct} of context.`,
|
|
195
|
-
style: { fg: '#00ffcc' },
|
|
196
|
-
});
|
|
197
|
-
sections.push({
|
|
198
|
-
type: 'text',
|
|
199
|
-
content: 'Run /compact to summarise and reduce context size.',
|
|
200
|
-
style: { dim: true },
|
|
201
|
-
});
|
|
165
|
+
if (modal) {
|
|
166
|
+
modal.maxScrollBack = tailStart;
|
|
167
|
+
modal.scrollBack = Math.min(modal.scrollBack, tailStart);
|
|
202
168
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
targetContentRows,
|
|
209
|
-
sections,
|
|
210
|
-
hints: ['[*] >10% of context', '[Esc] Close'],
|
|
211
|
-
}, width);
|
|
169
|
+
const scrollStart = Math.max(0, tailStart - (modal?.scrollBack ?? 0));
|
|
170
|
+
if (top > listBottom) top = listBottom;
|
|
171
|
+
const res = drawList(f.canvas, { rows, top, bottom: listBottom, x0: f.l, x1: f.r, scrollStart });
|
|
172
|
+
f.hintRight = scrollCountText(res.above, res.below);
|
|
173
|
+
return finishModal(f);
|
|
212
174
|
}
|