@pellux/goodvibes-tui 2.0.21 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +16 -6
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/verification-ledger.ts +18 -16
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import { type Line, type Cell, createStyledCell, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
2
|
import { UIFactory } from './ui-factory.ts';
|
|
3
|
-
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
3
|
+
import { getDisplayWidth, wrapPreservingIndent } from '../utils/terminal-width.ts';
|
|
4
4
|
import { LAYOUT } from './layout.ts';
|
|
5
5
|
import { SyntaxHighlighter, type SyntaxToken as HLToken } from './syntax-highlighter.ts';
|
|
6
|
-
import {
|
|
6
|
+
import { activeTokens } from './theme.ts';
|
|
7
|
+
import { syntaxStyles } from './syntax-theme.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Regex-fallback tokenizer
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* tree-sitter parse doesn't visibly shift color mid-stream.
|
|
15
|
-
* `accent` and `bg` are chrome (the language-label header bar and the body
|
|
16
|
-
* background), not syntax tokens, and keep their original VS Code Dark+
|
|
17
|
-
* values, folded here from two separate literals (one in the header, one
|
|
18
|
-
* duplicated on the footer line) into a single named source.
|
|
10
|
+
* Regex-fallback tokenizer colours. The syntax hues come from the same role
|
|
11
|
+
* map as the tree-sitter path (syntax-theme.ts), so a code block that starts
|
|
12
|
+
* on the regex fallback and is replaced by the async tree-sitter parse does
|
|
13
|
+
* not shift colour mid-stream. Text with no role uses the theme's text colour
|
|
14
|
+
* (never the terminal default, which can vanish on the themed body fill).
|
|
19
15
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
function fallbackTheme() {
|
|
17
|
+
const styles = syntaxStyles();
|
|
18
|
+
const tokens = activeTokens();
|
|
19
|
+
return {
|
|
20
|
+
string: styles.string.fg,
|
|
21
|
+
number: styles.number.fg,
|
|
22
|
+
keyword: styles.keyword.fg,
|
|
23
|
+
type: styles.type.fg,
|
|
24
|
+
function: styles.function.fg,
|
|
25
|
+
operator: styles.operator.fg,
|
|
26
|
+
property: styles.property.fg,
|
|
27
|
+
comment: styles.comment.fg,
|
|
28
|
+
plain: styles.plain.fg,
|
|
29
|
+
punctuation: tokens.textMuted,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
32
|
|
|
33
33
|
// ─── Language Keyword Maps ───────────────────────────────────────────────────
|
|
34
34
|
|
|
@@ -81,13 +81,14 @@ type SyntaxToken = { text: string; fg: string; bold?: boolean; italic?: boolean
|
|
|
81
81
|
// ─── Tokenizers ──────────────────────────────────────────────────────────────
|
|
82
82
|
|
|
83
83
|
function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
84
|
+
const th = fallbackTheme();
|
|
84
85
|
const tokens: SyntaxToken[] = [];
|
|
85
86
|
let i = 0;
|
|
86
87
|
|
|
87
88
|
while (i < line.length) {
|
|
88
89
|
// Line comment
|
|
89
90
|
if (line.slice(i, i + 2) === '//') {
|
|
90
|
-
tokens.push({ text: line.slice(i), fg:
|
|
91
|
+
tokens.push({ text: line.slice(i), fg: th.comment, italic: true });
|
|
91
92
|
break;
|
|
92
93
|
}
|
|
93
94
|
// String (single, double, template)
|
|
@@ -98,7 +99,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
98
99
|
if (line[j] === '\\') j++;
|
|
99
100
|
j++;
|
|
100
101
|
}
|
|
101
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
102
|
+
tokens.push({ text: line.slice(i, j + 1), fg: th.string });
|
|
102
103
|
i = j + 1;
|
|
103
104
|
continue;
|
|
104
105
|
}
|
|
@@ -106,7 +107,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
106
107
|
if (/[0-9]/.test(line[i])) {
|
|
107
108
|
let j = i;
|
|
108
109
|
while (j < line.length && /[0-9._xXbBoO]/.test(line[j])) j++;
|
|
109
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
110
|
+
tokens.push({ text: line.slice(i, j), fg: th.number });
|
|
110
111
|
i = j;
|
|
111
112
|
continue;
|
|
112
113
|
}
|
|
@@ -116,13 +117,13 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
116
117
|
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
117
118
|
const word = line.slice(i, j);
|
|
118
119
|
if (TS_JS_KEYWORDS.has(word)) {
|
|
119
|
-
tokens.push({ text: word, fg:
|
|
120
|
+
tokens.push({ text: word, fg: th.keyword });
|
|
120
121
|
} else if (TS_TYPES.has(word)) {
|
|
121
|
-
tokens.push({ text: word, fg:
|
|
122
|
+
tokens.push({ text: word, fg: th.type });
|
|
122
123
|
} else if (line[j] === '(') {
|
|
123
|
-
tokens.push({ text: word, fg:
|
|
124
|
+
tokens.push({ text: word, fg: th.function });
|
|
124
125
|
} else {
|
|
125
|
-
tokens.push({ text: word, fg:
|
|
126
|
+
tokens.push({ text: word, fg: th.plain });
|
|
126
127
|
}
|
|
127
128
|
i = j;
|
|
128
129
|
continue;
|
|
@@ -130,7 +131,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
130
131
|
// Operators and punctuation
|
|
131
132
|
const ch = line[i];
|
|
132
133
|
const isOp = '=<>!&|+-*/%^~?:'.includes(ch);
|
|
133
|
-
tokens.push({ text: ch, fg: isOp ?
|
|
134
|
+
tokens.push({ text: ch, fg: isOp ? th.operator : '' });
|
|
134
135
|
i++;
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -138,26 +139,27 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
function tokenizePython(line: string): SyntaxToken[] {
|
|
142
|
+
const th = fallbackTheme();
|
|
141
143
|
const tokens: SyntaxToken[] = [];
|
|
142
144
|
let i = 0;
|
|
143
145
|
|
|
144
146
|
while (i < line.length) {
|
|
145
147
|
if (line[i] === '#') {
|
|
146
|
-
tokens.push({ text: line.slice(i), fg:
|
|
148
|
+
tokens.push({ text: line.slice(i), fg: th.comment, italic: true });
|
|
147
149
|
break;
|
|
148
150
|
}
|
|
149
151
|
if (line[i] === '"' || line[i] === "'") {
|
|
150
152
|
const q = line[i];
|
|
151
153
|
let j = i + 1;
|
|
152
154
|
while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
|
|
153
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
155
|
+
tokens.push({ text: line.slice(i, j + 1), fg: th.string });
|
|
154
156
|
i = j + 1;
|
|
155
157
|
continue;
|
|
156
158
|
}
|
|
157
159
|
if (/[0-9]/.test(line[i])) {
|
|
158
160
|
let j = i;
|
|
159
161
|
while (j < line.length && /[0-9._]/.test(line[j])) j++;
|
|
160
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
162
|
+
tokens.push({ text: line.slice(i, j), fg: th.number });
|
|
161
163
|
i = j;
|
|
162
164
|
continue;
|
|
163
165
|
}
|
|
@@ -166,44 +168,45 @@ function tokenizePython(line: string): SyntaxToken[] {
|
|
|
166
168
|
while (j < line.length && /[\w]/.test(line[j])) j++;
|
|
167
169
|
const word = line.slice(i, j);
|
|
168
170
|
if (PYTHON_KEYWORDS.has(word)) {
|
|
169
|
-
tokens.push({ text: word, fg:
|
|
171
|
+
tokens.push({ text: word, fg: th.keyword });
|
|
170
172
|
} else if (/^[A-Z]/.test(word)) {
|
|
171
|
-
tokens.push({ text: word, fg:
|
|
173
|
+
tokens.push({ text: word, fg: th.type });
|
|
172
174
|
} else if (line[j] === '(') {
|
|
173
|
-
tokens.push({ text: word, fg:
|
|
175
|
+
tokens.push({ text: word, fg: th.function });
|
|
174
176
|
} else {
|
|
175
|
-
tokens.push({ text: word, fg:
|
|
177
|
+
tokens.push({ text: word, fg: th.plain });
|
|
176
178
|
}
|
|
177
179
|
i = j;
|
|
178
180
|
continue;
|
|
179
181
|
}
|
|
180
|
-
tokens.push({ text: line[i], fg:
|
|
182
|
+
tokens.push({ text: line[i], fg: th.plain });
|
|
181
183
|
i++;
|
|
182
184
|
}
|
|
183
185
|
return tokens;
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
function tokenizeBash(line: string): SyntaxToken[] {
|
|
189
|
+
const th = fallbackTheme();
|
|
187
190
|
const tokens: SyntaxToken[] = [];
|
|
188
191
|
let i = 0;
|
|
189
192
|
|
|
190
193
|
while (i < line.length) {
|
|
191
194
|
if (line[i] === '#') {
|
|
192
|
-
tokens.push({ text: line.slice(i), fg:
|
|
195
|
+
tokens.push({ text: line.slice(i), fg: th.comment, italic: true });
|
|
193
196
|
break;
|
|
194
197
|
}
|
|
195
198
|
if (line[i] === '"' || line[i] === "'") {
|
|
196
199
|
const q = line[i];
|
|
197
200
|
let j = i + 1;
|
|
198
201
|
while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
|
|
199
|
-
tokens.push({ text: line.slice(i, j + 1), fg:
|
|
202
|
+
tokens.push({ text: line.slice(i, j + 1), fg: th.string });
|
|
200
203
|
i = j + 1;
|
|
201
204
|
continue;
|
|
202
205
|
}
|
|
203
206
|
if (line[i] === '$') {
|
|
204
207
|
let j = i + 1;
|
|
205
208
|
while (j < line.length && /[\w{}_]/.test(line[j])) j++;
|
|
206
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
209
|
+
tokens.push({ text: line.slice(i, j), fg: th.property });
|
|
207
210
|
i = j;
|
|
208
211
|
continue;
|
|
209
212
|
}
|
|
@@ -212,20 +215,21 @@ function tokenizeBash(line: string): SyntaxToken[] {
|
|
|
212
215
|
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
213
216
|
const word = line.slice(i, j);
|
|
214
217
|
if (BASH_KEYWORDS.has(word)) {
|
|
215
|
-
tokens.push({ text: word, fg:
|
|
218
|
+
tokens.push({ text: word, fg: th.keyword });
|
|
216
219
|
} else {
|
|
217
|
-
tokens.push({ text: word, fg:
|
|
220
|
+
tokens.push({ text: word, fg: th.plain });
|
|
218
221
|
}
|
|
219
222
|
i = j;
|
|
220
223
|
continue;
|
|
221
224
|
}
|
|
222
|
-
tokens.push({ text: line[i], fg:
|
|
225
|
+
tokens.push({ text: line[i], fg: th.plain });
|
|
223
226
|
i++;
|
|
224
227
|
}
|
|
225
228
|
return tokens;
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
function tokenizeJson(line: string): SyntaxToken[] {
|
|
232
|
+
const th = fallbackTheme();
|
|
229
233
|
const tokens: SyntaxToken[] = [];
|
|
230
234
|
let i = 0;
|
|
231
235
|
|
|
@@ -237,9 +241,9 @@ function tokenizeJson(line: string): SyntaxToken[] {
|
|
|
237
241
|
// JSON key: followed by :
|
|
238
242
|
const rest = line.slice(j + 1).trimStart();
|
|
239
243
|
if (rest.startsWith(':')) {
|
|
240
|
-
tokens.push({ text: str, fg:
|
|
244
|
+
tokens.push({ text: str, fg: th.property });
|
|
241
245
|
} else {
|
|
242
|
-
tokens.push({ text: str, fg:
|
|
246
|
+
tokens.push({ text: str, fg: th.string });
|
|
243
247
|
}
|
|
244
248
|
i = j + 1;
|
|
245
249
|
continue;
|
|
@@ -247,32 +251,33 @@ function tokenizeJson(line: string): SyntaxToken[] {
|
|
|
247
251
|
if (/[0-9-]/.test(line[i])) {
|
|
248
252
|
let j = i;
|
|
249
253
|
while (j < line.length && /[0-9.eE+-]/.test(line[j])) j++;
|
|
250
|
-
tokens.push({ text: line.slice(i, j), fg:
|
|
254
|
+
tokens.push({ text: line.slice(i, j), fg: th.number });
|
|
251
255
|
i = j;
|
|
252
256
|
continue;
|
|
253
257
|
}
|
|
254
258
|
const boolNull = ['true', 'false', 'null'].find(k => line.startsWith(k, i));
|
|
255
259
|
if (boolNull) {
|
|
256
|
-
tokens.push({ text: boolNull, fg:
|
|
260
|
+
tokens.push({ text: boolNull, fg: th.keyword });
|
|
257
261
|
i += boolNull.length;
|
|
258
262
|
continue;
|
|
259
263
|
}
|
|
260
|
-
tokens.push({ text: line[i], fg:
|
|
264
|
+
tokens.push({ text: line[i], fg: th.punctuation });
|
|
261
265
|
i++;
|
|
262
266
|
}
|
|
263
267
|
return tokens;
|
|
264
268
|
}
|
|
265
269
|
|
|
266
270
|
function tokenizeYaml(line: string): SyntaxToken[] {
|
|
271
|
+
const th = fallbackTheme();
|
|
267
272
|
const tokens: SyntaxToken[] = [];
|
|
268
273
|
if (line.trimStart().startsWith('#')) {
|
|
269
|
-
return [{ text: line, fg:
|
|
274
|
+
return [{ text: line, fg: th.comment, italic: true }];
|
|
270
275
|
}
|
|
271
276
|
const keyMatch = line.match(/^(\s*)([^:]+)(:)(\s*.*)/);
|
|
272
277
|
if (keyMatch) {
|
|
273
|
-
if (keyMatch[1]) tokens.push({ text: keyMatch[1], fg:
|
|
274
|
-
tokens.push({ text: keyMatch[2], fg:
|
|
275
|
-
tokens.push({ text: keyMatch[3], fg:
|
|
278
|
+
if (keyMatch[1]) tokens.push({ text: keyMatch[1], fg: th.plain });
|
|
279
|
+
tokens.push({ text: keyMatch[2], fg: th.property });
|
|
280
|
+
tokens.push({ text: keyMatch[3], fg: th.punctuation });
|
|
276
281
|
if (keyMatch[4]) {
|
|
277
282
|
const val = keyMatch[4];
|
|
278
283
|
const trimVal = val.trimStart();
|
|
@@ -280,16 +285,41 @@ function tokenizeYaml(line: string): SyntaxToken[] {
|
|
|
280
285
|
const isStr = /^['"]/.test(trimVal);
|
|
281
286
|
const isBool = trimVal === 'true' || trimVal === 'false' || trimVal === 'null' || trimVal === 'yes' || trimVal === 'no';
|
|
282
287
|
const isNum = /^-?[0-9]/.test(trimVal);
|
|
283
|
-
const valFg = isStr ?
|
|
288
|
+
const valFg = isStr ? th.string : isBool ? th.keyword : isNum ? th.number : th.plain;
|
|
284
289
|
tokens.push({ text: val, fg: valFg });
|
|
285
290
|
}
|
|
286
291
|
return tokens;
|
|
287
292
|
}
|
|
288
|
-
return [{ text: line, fg:
|
|
293
|
+
return [{ text: line, fg: th.plain }];
|
|
289
294
|
}
|
|
290
295
|
|
|
291
296
|
function tokenizePlain(line: string): SyntaxToken[] {
|
|
292
|
-
|
|
297
|
+
const th = fallbackTheme();
|
|
298
|
+
return [{ text: line, fg: th.plain }];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ─── Line highlighting outside a code block ─────────────────────────────────
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Syntax colors for lines of code in `lang` (a fence tag or a file extension),
|
|
305
|
+
* one token list per line: the tree-sitter result once it is cached, the regex
|
|
306
|
+
* tokenizer until then. The Changes modal colors diff lines with this.
|
|
307
|
+
*/
|
|
308
|
+
export function highlightCodeLines(codeLines: readonly string[], lang: string): SyntaxToken[][] {
|
|
309
|
+
const hl = lang ? _sharedHighlighter.highlight(codeLines.join('\n'), lang) : null;
|
|
310
|
+
const language = detectLanguage(lang);
|
|
311
|
+
return codeLines.map((line, i) => {
|
|
312
|
+
const fromTree = hl?.[i];
|
|
313
|
+
if (fromTree && fromTree.length > 0) return fromTree as HLToken[];
|
|
314
|
+
switch (language) {
|
|
315
|
+
case 'ts': return tokenizeTsJs(line);
|
|
316
|
+
case 'python': return tokenizePython(line);
|
|
317
|
+
case 'bash': return tokenizeBash(line);
|
|
318
|
+
case 'json': return tokenizeJson(line);
|
|
319
|
+
case 'yaml': return tokenizeYaml(line);
|
|
320
|
+
default: return tokenizePlain(line);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
293
323
|
}
|
|
294
324
|
|
|
295
325
|
// ─── Main Renderer ───────────────────────────────────────────────────────────
|
|
@@ -302,6 +332,17 @@ function tokenizePlain(line: string): SyntaxToken[] {
|
|
|
302
332
|
*/
|
|
303
333
|
const _sharedHighlighter = new SyntaxHighlighter();
|
|
304
334
|
|
|
335
|
+
/**
|
|
336
|
+
* The shared highlighter's state, for anything that caches drawn code: a line
|
|
337
|
+
* drawn with the regex placeholder (a miss) is stale once the generation moves
|
|
338
|
+
* on, and onSyntaxHighlightReady says when to repaint.
|
|
339
|
+
*/
|
|
340
|
+
export function syntaxHighlightGeneration(): number { return _sharedHighlighter.generation; }
|
|
341
|
+
export function syntaxHighlightMisses(): number { return _sharedHighlighter.missCount; }
|
|
342
|
+
export function onSyntaxHighlightReady(listener: () => void): () => void { return _sharedHighlighter.onReady(listener); }
|
|
343
|
+
/** Resolves once no parse is in flight: a frame drawn after it no longer depends on timing or on what drew before. */
|
|
344
|
+
export function settleSyntaxHighlighting(): Promise<void> { return _sharedHighlighter.settle(); }
|
|
345
|
+
|
|
305
346
|
/**
|
|
306
347
|
* renderCodeBlock - Render lines of code with syntax highlighting and line numbers.
|
|
307
348
|
* Returns Line[] for the cell-based pipeline.
|
|
@@ -314,12 +355,18 @@ export function renderCodeBlock(
|
|
|
314
355
|
): Line[] {
|
|
315
356
|
const lines: Line[] = [];
|
|
316
357
|
const language = detectLanguage(lang);
|
|
317
|
-
|
|
358
|
+
// The fill starts at column 3 like every other fill (user messages, the
|
|
359
|
+
// composer), so code text lands on column 5 after the 2-column padding.
|
|
360
|
+
const leftMargin = LAYOUT.LEFT_MARGIN - 1;
|
|
318
361
|
const showLineNumbers = opts.showLineNumbers ?? true;
|
|
319
362
|
const lineNumW = showLineNumbers ? String(codeLines.length).length + 1 : 0; // e.g. "10 "
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
363
|
+
// Text keeps 2 columns from both edges of the code fill (the Measurements
|
|
364
|
+
// table's padding rule), and the fill has a blank row above and below it.
|
|
365
|
+
const PAD = 2;
|
|
366
|
+
const contentStartX = showLineNumbers ? leftMargin + PAD + lineNumW + 1 : leftMargin + PAD;
|
|
367
|
+
const palette = activeTokens();
|
|
368
|
+
const BG = palette.backgroundCode;
|
|
369
|
+
const LINE_NUM_FG = palette.textFaint;
|
|
323
370
|
const effectiveWidth = width - LAYOUT.RIGHT_MARGIN;
|
|
324
371
|
|
|
325
372
|
// Try tree-sitter highlight cache first (populated asynchronously).
|
|
@@ -340,20 +387,32 @@ export function renderCodeBlock(
|
|
|
340
387
|
};
|
|
341
388
|
|
|
342
389
|
// Header bar: language label
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
for (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
//
|
|
390
|
+
// No header bar: the language (when the fence names one) sits muted on the
|
|
391
|
+
// right of the first code row, and only when the code leaves room for it.
|
|
392
|
+
const langLabel = lang;
|
|
393
|
+
const padLine = createEmptyLine(width);
|
|
394
|
+
for (let px = leftMargin; px < effectiveWidth; px++) padLine[px] = createStyledCell(' ', { bg: BG });
|
|
395
|
+
lines.push(padLine);
|
|
396
|
+
|
|
397
|
+
// Code lines. A line wider than the text column wraps the way an opened
|
|
398
|
+
// read or command output does (wrapPreservingIndent: indentation kept,
|
|
399
|
+
// continuation rows indented like the first): text is never cut at the
|
|
400
|
+
// fill's edge. Each character keeps its token's style across the wrap, and
|
|
401
|
+
// a continuation row leaves the line-number column blank.
|
|
402
|
+
const textEnd = effectiveWidth - PAD;
|
|
403
|
+
const textWidth = Math.max(8, textEnd - contentStartX);
|
|
404
|
+
const bodyRow = (): Cell[] => {
|
|
405
|
+
const line: Cell[] = createEmptyLine(width);
|
|
406
|
+
// Paint only the code-block body band so body rows match the header/footer width.
|
|
407
|
+
for (let x = leftMargin; x < effectiveWidth; x++) line[x] = createStyledCell(' ', { bg: BG });
|
|
408
|
+
return line;
|
|
409
|
+
};
|
|
355
410
|
for (let i = 0; i < codeLines.length; i++) {
|
|
356
|
-
|
|
411
|
+
// Control characters (a tab included) take one blank column, as they always have.
|
|
412
|
+
const rawLine = Array.from(codeLines[i] ?? '').map((ch) => {
|
|
413
|
+
const code = ch.charCodeAt(0);
|
|
414
|
+
return code < 32 || code === 127 ? ' ' : ch;
|
|
415
|
+
}).join('');
|
|
357
416
|
const lineNum = String(i + 1).padStart(lineNumW);
|
|
358
417
|
|
|
359
418
|
// Select token source: tree-sitter (accurate) or regex (fallback)
|
|
@@ -361,43 +420,62 @@ export function renderCodeBlock(
|
|
|
361
420
|
hlLines && i < hlLines.length && hlLines[i].length > 0
|
|
362
421
|
? (hlLines[i] as HLToken[])
|
|
363
422
|
: regexTokenize(rawLine);
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
423
|
+
// Each character's style, when the tokens spell the line (plain otherwise).
|
|
424
|
+
const chars = Array.from(rawLine);
|
|
425
|
+
const styles: Array<Pick<SyntaxToken, 'fg' | 'bold' | 'italic'>> = [];
|
|
426
|
+
const spelled = tokens.map((tk) => tk.text).join('').replace(/[\x00-\x1f\x7f]/g, ' ');
|
|
427
|
+
if (spelled === rawLine) {
|
|
428
|
+
for (const token of tokens) for (const _ of Array.from(token.text)) styles.push({ fg: token.fg, bold: token.bold, italic: token.italic });
|
|
429
|
+
} else {
|
|
430
|
+
for (let k = 0; k < chars.length; k++) styles.push({ fg: palette.text });
|
|
369
431
|
}
|
|
370
432
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
433
|
+
const parts = getDisplayWidth(rawLine.replace(/\s+$/, '')) <= textWidth ? [rawLine] : wrapPreservingIndent(rawLine, textWidth);
|
|
434
|
+
let cursor = 0;
|
|
435
|
+
parts.forEach((part, partIndex) => {
|
|
436
|
+
const line = bodyRow();
|
|
437
|
+
let cx = leftMargin + PAD;
|
|
438
|
+
if (showLineNumbers) {
|
|
439
|
+
const label = partIndex === 0 ? lineNum : ' '.repeat(lineNumW);
|
|
440
|
+
for (const ch of label) {
|
|
441
|
+
if (cx >= contentStartX) break;
|
|
442
|
+
line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg: BG });
|
|
443
|
+
}
|
|
444
|
+
line[cx++] = createStyledCell(' ', { bg: BG });
|
|
376
445
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
446
|
+
// A continuation row: the wrap dropped the spaces it broke at and
|
|
447
|
+
// re-added the indent, so the next source character is the first
|
|
448
|
+
// non-space after the cursor.
|
|
449
|
+
const pc = Array.from(part);
|
|
450
|
+
let lead = 0;
|
|
451
|
+
if (partIndex > 0) {
|
|
452
|
+
while (pc[lead] === ' ') lead++;
|
|
453
|
+
while (chars[cursor] === ' ') cursor++;
|
|
454
|
+
}
|
|
455
|
+
cx += lead;
|
|
456
|
+
for (let k = lead; k < pc.length; k++) {
|
|
457
|
+
const ch = pc[k]!;
|
|
458
|
+
const style = styles[cursor] ?? { fg: palette.text };
|
|
459
|
+
cursor++;
|
|
384
460
|
const cw = getDisplayWidth(ch);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// (effectiveWidth), not the full line width, otherwise a 2-column
|
|
393
|
-
// glyph landing on the last body column spills its placeholder cell
|
|
394
|
-
// into the reserved right-margin band the header/footer stop at.
|
|
395
|
-
if (cw === 2 && cx + 1 < effectiveWidth) line[cx + 1] = { ...line[cx], char: '' };
|
|
461
|
+
// A wide glyph that does not fit before the text edge ends the row
|
|
462
|
+
// rather than drawing half of itself into the padding.
|
|
463
|
+
if (cx + cw > textEnd) break;
|
|
464
|
+
line[cx] = createStyledCell(ch, { fg: style.fg, bg: BG, bold: style.bold, italic: style.italic });
|
|
465
|
+
// Bound the wide-glyph placeholder against the text edge so it never
|
|
466
|
+
// spills into the padding or the reserved right-margin band.
|
|
467
|
+
if (cw === 2 && cx + 1 < textEnd) line[cx + 1] = { ...line[cx], char: '' };
|
|
396
468
|
cx += cw;
|
|
397
469
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
470
|
+
if (i === 0 && partIndex === 0 && langLabel) {
|
|
471
|
+
const labelX = textEnd - getDisplayWidth(langLabel);
|
|
472
|
+
if (labelX > cx + 1) {
|
|
473
|
+
let lx = labelX;
|
|
474
|
+
for (const ch of langLabel) line[lx++] = createStyledCell(ch, { fg: palette.textMuted, bg: BG });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
lines.push(line);
|
|
478
|
+
});
|
|
401
479
|
}
|
|
402
480
|
|
|
403
481
|
// Footer line
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderCommandPalette, the command palette drawn with the surface kit.
|
|
3
|
+
*
|
|
4
|
+
* ✦ Commands esc
|
|
5
|
+
*
|
|
6
|
+
* ▏Type a command, setting, model or session 156 commands
|
|
7
|
+
*
|
|
8
|
+
* ✦ suggested ┌ preview ────────────┐
|
|
9
|
+
* Changes /diff Changes
|
|
10
|
+
* Switch model opus 5.5 /model /diff [path]
|
|
11
|
+
* ✦ session Show the working-tree
|
|
12
|
+
* Resume a session /resume diff. …
|
|
13
|
+
*
|
|
14
|
+
* ↑↓ move ⏎ run tab fill composer 120 more ↓
|
|
15
|
+
*
|
|
16
|
+
* The preview shows what the selected command will do before it runs:
|
|
17
|
+
* its full description, usage, aliases, category and what Enter will do.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { PaletteEntry, PaletteView } from '../input/command-palette-types.ts';
|
|
21
|
+
import { activeTokens } from './theme.ts';
|
|
22
|
+
import { beginModal, finishModal, scrollCountText, searchRow, type SurfaceLayer } from './surface-kit.ts';
|
|
23
|
+
import { drawList, type KitRow } from './surface-kit-list.ts';
|
|
24
|
+
import { inset, insetLines, type KitInset } from './surface-kit-parts.ts';
|
|
25
|
+
|
|
26
|
+
function rowsFor(palette: PaletteView): KitRow[] {
|
|
27
|
+
const selected = palette.getSelected();
|
|
28
|
+
const rows: KitRow[] = [];
|
|
29
|
+
const searching = palette.query.trim().length > 0;
|
|
30
|
+
for (const section of palette.sections()) {
|
|
31
|
+
if (!searching) rows.push({ header: section.group });
|
|
32
|
+
for (const entry of section.entries) {
|
|
33
|
+
rows.push({
|
|
34
|
+
label: entry.title,
|
|
35
|
+
desc: palette.describe(entry.id),
|
|
36
|
+
right: entry.right ?? `/${entry.id}`,
|
|
37
|
+
selected: entry === selected,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (rows.length === 0) rows.push({ label: `Nothing matches "${palette.query}".`, muted: true });
|
|
42
|
+
return rows;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function drawPreview(palette: PaletteView, entry: PaletteEntry | null, canvas: Parameters<typeof insetLines>[0], p: KitInset): void {
|
|
46
|
+
const t = activeTokens();
|
|
47
|
+
if (!entry) {
|
|
48
|
+
insetLines(canvas, p, p.l, p.top, [{ text: 'Type to search every command; old view names such as fleet or git find their new homes.', style: { fg: t.textMuted } }]);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const blank = { text: '', style: {} };
|
|
52
|
+
const live = palette.describe(entry.id);
|
|
53
|
+
insetLines(canvas, p, p.l, p.top, [
|
|
54
|
+
{ text: entry.title, style: { fg: t.text, bold: true } },
|
|
55
|
+
{ text: `/${entry.id}${entry.argsHint ? ` ${entry.argsHint}` : ''}`, style: { fg: t.brand } },
|
|
56
|
+
...(live ? [{ text: live, style: { fg: t.textMuted } }] : []),
|
|
57
|
+
blank,
|
|
58
|
+
...(entry.description ? [{ text: entry.description, style: { fg: t.textMuted } }, blank] : []),
|
|
59
|
+
{
|
|
60
|
+
text: entry.needsArgs
|
|
61
|
+
? `Enter fills the composer: it needs ${entry.argsHint ?? 'arguments'}.`
|
|
62
|
+
: 'Enter runs it now; tab fills the composer to add arguments.',
|
|
63
|
+
style: { fg: t.textFaint },
|
|
64
|
+
},
|
|
65
|
+
blank,
|
|
66
|
+
...(entry.aliases.length > 0 ? [{ text: `Also: ${entry.aliases.map((a) => `/${a}`).join(' ')}`, style: { fg: t.textFaint } }] : []),
|
|
67
|
+
...(entry.keywords.length > 0 ? [{ text: `Finds: ${entry.keywords.join(', ')}`, style: { fg: t.textFaint } }] : []),
|
|
68
|
+
{ text: entry.category, style: { fg: t.textFaint } },
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function renderCommandPalette(palette: PaletteView, screenWidth: number, screenHeight: number): SurfaceLayer {
|
|
73
|
+
const f = beginModal(screenWidth, screenHeight, {
|
|
74
|
+
title: 'Commands',
|
|
75
|
+
hints: [['↑↓', 'move'], ['⏎', 'run'], ['tab', 'fill composer']],
|
|
76
|
+
});
|
|
77
|
+
const total = palette.entries.length;
|
|
78
|
+
const shown = palette.flat().length;
|
|
79
|
+
searchRow(f, f.top, palette.query, 'Type a command, setting, model or session', palette.query ? `${shown} of ${total}` : `${total} commands`);
|
|
80
|
+
|
|
81
|
+
const body = f.top + 2;
|
|
82
|
+
const inner = f.r - f.l + 1;
|
|
83
|
+
// A preview when there is room for it; below that the list takes the width.
|
|
84
|
+
const sideBySide = inner >= 70;
|
|
85
|
+
const x1 = sideBySide ? f.l + Math.floor(inner * 0.55) - 1 : f.r;
|
|
86
|
+
const result = drawList(f.canvas, { rows: rowsFor(palette), top: body, bottom: f.bottom, x0: f.l, x1, scrollKey: { owner: palette, name: 'list' } });
|
|
87
|
+
if (sideBySide) {
|
|
88
|
+
const p = inset(f.canvas, x1 + 3, body, f.r + 2 - (x1 + 3) + 1, f.bottom - body + 1);
|
|
89
|
+
drawPreview(palette, palette.getSelected(), f.canvas, p);
|
|
90
|
+
}
|
|
91
|
+
f.hintRight = scrollCountText(result.above, result.below);
|
|
92
|
+
return finishModal(f);
|
|
93
|
+
}
|
|
@@ -23,9 +23,9 @@ export function renderQueuedMessageList(width: number, items: readonly { readonl
|
|
|
23
23
|
const bodyBg = activeTheme().collapsedBodyBg;
|
|
24
24
|
const lines: Line[] = [];
|
|
25
25
|
const header = `${items.length} queued: /queue edit·delete until delivered`;
|
|
26
|
-
lines.push(...renderConversationFragment(header, width, { prefix: ' ⧗ ', prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg
|
|
26
|
+
lines.push(...renderConversationFragment(header, width, { prefix: ' ⧗ ', prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg }));
|
|
27
27
|
items.forEach((item, index) => {
|
|
28
|
-
lines.push(...renderConversationFragment(item.text, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg
|
|
28
|
+
lines.push(...renderConversationFragment(item.text, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg }));
|
|
29
29
|
});
|
|
30
30
|
return lines;
|
|
31
31
|
}
|
|
@@ -54,7 +54,7 @@ export function renderMemoryProvenanceChip(width: number, count: number, entries
|
|
|
54
54
|
const bodyBg = activeTheme().collapsedBodyBg;
|
|
55
55
|
const noun = count === 1 ? 'memory' : 'memories';
|
|
56
56
|
const header = expanded ? `used ${count} ${noun}: Alt+M to hide` : `used ${count} ${noun}: Alt+M to list`;
|
|
57
|
-
const lines: Line[] = renderConversationFragment(header, width, { prefix: ' ◆ ', prefixFg: t.state.info, text: t.fg.dim, bodyBg
|
|
57
|
+
const lines: Line[] = renderConversationFragment(header, width, { prefix: ' ◆ ', prefixFg: t.state.info, text: t.fg.dim, bodyBg });
|
|
58
58
|
if (expanded) {
|
|
59
59
|
entries.forEach((entry, index) => {
|
|
60
60
|
const body = entry.record === undefined
|
|
@@ -62,7 +62,7 @@ export function renderMemoryProvenanceChip(width: number, count: number, entries
|
|
|
62
62
|
: entry.record === null
|
|
63
63
|
? 'no longer available'
|
|
64
64
|
: `${entry.record.summary} · ${entry.record.cls}`;
|
|
65
|
-
lines.push(...renderConversationFragment(body, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.info, text: t.fg.dim, bodyBg
|
|
65
|
+
lines.push(...renderConversationFragment(body, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.info, text: t.fg.dim, bodyBg }));
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
return lines;
|