@pellux/goodvibes-agent 0.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/.goodvibes/GOODVIBES.md +35 -0
- package/.goodvibes/agents/reviewer.md +48 -0
- package/.goodvibes/skills/add-provider/SKILL.md +199 -0
- package/CHANGELOG.md +25 -0
- package/README.md +74 -0
- package/bin/goodvibes-agent.ts +2 -0
- package/docs/README.md +23 -0
- package/docs/deployment-and-services.md +57 -0
- package/docs/getting-started.md +53 -0
- package/docs/release-and-publishing.md +46 -0
- package/package.json +134 -0
- package/scripts/check-bun.sh +20 -0
- package/src/audio/player.ts +156 -0
- package/src/audio/spoken-turn-controller.ts +203 -0
- package/src/audio/spoken-turn-model-routing.ts +117 -0
- package/src/audio/spoken-turn-wiring.ts +44 -0
- package/src/audio/text-chunker.ts +110 -0
- package/src/cli/bundle-command.ts +227 -0
- package/src/cli/completion.ts +90 -0
- package/src/cli/config-overrides.ts +159 -0
- package/src/cli/endpoints.ts +63 -0
- package/src/cli/entrypoint.ts +172 -0
- package/src/cli/help.ts +299 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/management-commands.ts +426 -0
- package/src/cli/management.ts +744 -0
- package/src/cli/network-posture.ts +46 -0
- package/src/cli/package-verification.ts +123 -0
- package/src/cli/parser.ts +369 -0
- package/src/cli/provider-auth-routes.ts +22 -0
- package/src/cli/provider-classification.ts +107 -0
- package/src/cli/redaction.ts +105 -0
- package/src/cli/service-command.ts +26 -0
- package/src/cli/service-posture.ts +482 -0
- package/src/cli/status.ts +383 -0
- package/src/cli/surface-command.ts +247 -0
- package/src/cli/tui-startup.ts +32 -0
- package/src/cli/types.ts +69 -0
- package/src/cli-flags.ts +21 -0
- package/src/config/goodvibes-home-audit.ts +465 -0
- package/src/config/index.ts +57 -0
- package/src/config/provider-model.ts +23 -0
- package/src/config/secret-config.ts +119 -0
- package/src/config/secrets.ts +71 -0
- package/src/config/surface.ts +1 -0
- package/src/core/composer-state.ts +61 -0
- package/src/core/conversation-rendering.ts +359 -0
- package/src/core/conversation.ts +551 -0
- package/src/core/history.ts +45 -0
- package/src/core/orchestrator.ts +7 -0
- package/src/core/system-message-router.ts +171 -0
- package/src/daemon/cli.ts +55 -0
- package/src/daemon/safe-serve.ts +61 -0
- package/src/input/agent-workspace.ts +428 -0
- package/src/input/autocomplete.ts +96 -0
- package/src/input/bookmark-modal.ts +115 -0
- package/src/input/command-args-hint.ts +36 -0
- package/src/input/command-registry.ts +329 -0
- package/src/input/commands/agent-externalized-tui.ts +73 -0
- package/src/input/commands/agent-workspace-runtime.ts +17 -0
- package/src/input/commands/branch-runtime.ts +72 -0
- package/src/input/commands/cloudflare-runtime.ts +370 -0
- package/src/input/commands/config.ts +18 -0
- package/src/input/commands/control-room-runtime.ts +255 -0
- package/src/input/commands/conversation-runtime.ts +207 -0
- package/src/input/commands/discovery-runtime.ts +52 -0
- package/src/input/commands/eval.ts +204 -0
- package/src/input/commands/experience-runtime.ts +278 -0
- package/src/input/commands/guidance-runtime.ts +106 -0
- package/src/input/commands/health-runtime.ts +434 -0
- package/src/input/commands/hooks-runtime.ts +148 -0
- package/src/input/commands/incident-runtime.ts +95 -0
- package/src/input/commands/integration-runtime.ts +394 -0
- package/src/input/commands/intelligence-runtime.ts +223 -0
- package/src/input/commands/knowledge.ts +531 -0
- package/src/input/commands/local-auth-runtime.ts +105 -0
- package/src/input/commands/local-provider-runtime.ts +170 -0
- package/src/input/commands/local-runtime.ts +392 -0
- package/src/input/commands/local-setup-review.ts +199 -0
- package/src/input/commands/local-setup-transfer.ts +135 -0
- package/src/input/commands/local-setup.ts +282 -0
- package/src/input/commands/managed-runtime.ts +209 -0
- package/src/input/commands/marketplace-runtime.ts +290 -0
- package/src/input/commands/mcp-runtime.ts +432 -0
- package/src/input/commands/memory-product-runtime.ts +111 -0
- package/src/input/commands/memory.ts +151 -0
- package/src/input/commands/notify-runtime.ts +83 -0
- package/src/input/commands/onboarding-runtime.ts +14 -0
- package/src/input/commands/operator-panel-runtime.ts +146 -0
- package/src/input/commands/operator-runtime.ts +392 -0
- package/src/input/commands/planning-runtime.ts +205 -0
- package/src/input/commands/platform-access-runtime.ts +422 -0
- package/src/input/commands/platform-services-runtime.ts +246 -0
- package/src/input/commands/policy-dispatch.ts +339 -0
- package/src/input/commands/policy.ts +17 -0
- package/src/input/commands/product-runtime.ts +351 -0
- package/src/input/commands/profile-sync-runtime.ts +99 -0
- package/src/input/commands/provider-accounts-runtime.ts +113 -0
- package/src/input/commands/provider.ts +363 -0
- package/src/input/commands/qrcode-runtime.ts +20 -0
- package/src/input/commands/quit-shared.ts +162 -0
- package/src/input/commands/recall-bundle.ts +132 -0
- package/src/input/commands/recall-capture.ts +152 -0
- package/src/input/commands/recall-query.ts +229 -0
- package/src/input/commands/recall-review.ts +98 -0
- package/src/input/commands/recall-shared.ts +22 -0
- package/src/input/commands/remote-runtime-pool.ts +106 -0
- package/src/input/commands/remote-runtime-setup.ts +199 -0
- package/src/input/commands/remote-runtime.ts +431 -0
- package/src/input/commands/replay-runtime.ts +18 -0
- package/src/input/commands/runtime-services.ts +291 -0
- package/src/input/commands/schedule-runtime.ts +91 -0
- package/src/input/commands/services-runtime.ts +209 -0
- package/src/input/commands/session-content.ts +408 -0
- package/src/input/commands/session-workflow.ts +464 -0
- package/src/input/commands/session.ts +375 -0
- package/src/input/commands/settings-sync-runtime.ts +174 -0
- package/src/input/commands/share-runtime.ts +119 -0
- package/src/input/commands/shell-core.ts +307 -0
- package/src/input/commands/skills-runtime.ts +221 -0
- package/src/input/commands/subscription-runtime.ts +434 -0
- package/src/input/commands/tasks-runtime.ts +230 -0
- package/src/input/commands/teamwork-runtime.ts +339 -0
- package/src/input/commands/teleport-runtime.ts +57 -0
- package/src/input/commands/tts-runtime.ts +29 -0
- package/src/input/commands/work-plan-runtime.ts +169 -0
- package/src/input/commands.ts +131 -0
- package/src/input/feed-context-factory.ts +254 -0
- package/src/input/file-picker.ts +192 -0
- package/src/input/handler-command-route.ts +180 -0
- package/src/input/handler-content-actions.ts +497 -0
- package/src/input/handler-feed-routes.ts +648 -0
- package/src/input/handler-feed.ts +452 -0
- package/src/input/handler-interactions.ts +281 -0
- package/src/input/handler-modal-routes.ts +418 -0
- package/src/input/handler-modal-stack.ts +263 -0
- package/src/input/handler-modal-token-routes.ts +329 -0
- package/src/input/handler-onboarding-cloudflare.ts +391 -0
- package/src/input/handler-onboarding.ts +620 -0
- package/src/input/handler-picker-routes.ts +472 -0
- package/src/input/handler-prompt-buffer.ts +320 -0
- package/src/input/handler-shortcuts.ts +213 -0
- package/src/input/handler-ui-state.ts +372 -0
- package/src/input/handler.ts +729 -0
- package/src/input/input-history.ts +297 -0
- package/src/input/keybindings.ts +292 -0
- package/src/input/mcp-workspace.ts +554 -0
- package/src/input/model-picker-provider-filter.ts +28 -0
- package/src/input/model-picker-types.ts +137 -0
- package/src/input/model-picker.ts +797 -0
- package/src/input/onboarding/handler-onboarding-routes.ts +125 -0
- package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +277 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +158 -0
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +130 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +762 -0
- package/src/input/onboarding/onboarding-wizard-helpers.ts +167 -0
- package/src/input/onboarding/onboarding-wizard-rules.ts +256 -0
- package/src/input/onboarding/onboarding-wizard-state.ts +365 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +798 -0
- package/src/input/onboarding/onboarding-wizard-types.ts +195 -0
- package/src/input/onboarding/onboarding-wizard.ts +711 -0
- package/src/input/panel-integration-actions.ts +78 -0
- package/src/input/profile-picker-modal.ts +222 -0
- package/src/input/search.ts +100 -0
- package/src/input/selection-modal.ts +163 -0
- package/src/input/selection.ts +135 -0
- package/src/input/session-picker-modal.ts +136 -0
- package/src/input/settings-modal-behavior.ts +37 -0
- package/src/input/settings-modal-secrets.ts +41 -0
- package/src/input/settings-modal-subscriptions.ts +95 -0
- package/src/input/settings-modal-types.ts +91 -0
- package/src/input/settings-modal.ts +793 -0
- package/src/input/submission-intent.ts +17 -0
- package/src/input/submission-router.ts +59 -0
- package/src/input/tts-settings-actions.ts +100 -0
- package/src/main.ts +792 -0
- package/src/mcp/runtime-reload.ts +81 -0
- package/src/panels/agent-inspector-panel.ts +521 -0
- package/src/panels/agent-inspector-shared.ts +94 -0
- package/src/panels/agent-logs-panel.ts +559 -0
- package/src/panels/agent-logs-shared.ts +129 -0
- package/src/panels/approval-panel.ts +150 -0
- package/src/panels/automation-control-panel.ts +212 -0
- package/src/panels/base-panel.ts +254 -0
- package/src/panels/builtin/agent.ts +117 -0
- package/src/panels/builtin/development.ts +31 -0
- package/src/panels/builtin/knowledge.ts +26 -0
- package/src/panels/builtin/operations.ts +349 -0
- package/src/panels/builtin/session.ts +129 -0
- package/src/panels/builtin/shared.ts +274 -0
- package/src/panels/builtin-panels.ts +23 -0
- package/src/panels/cockpit-panel.ts +183 -0
- package/src/panels/communication-panel.ts +153 -0
- package/src/panels/confirm-state.ts +61 -0
- package/src/panels/context-visualizer-panel.ts +204 -0
- package/src/panels/control-plane-panel.ts +211 -0
- package/src/panels/cost-tracker-panel.ts +444 -0
- package/src/panels/debug-panel.ts +432 -0
- package/src/panels/diff-panel.ts +520 -0
- package/src/panels/docs-panel.ts +283 -0
- package/src/panels/eval-panel.ts +399 -0
- package/src/panels/file-explorer-panel.ts +584 -0
- package/src/panels/file-preview-panel.ts +434 -0
- package/src/panels/forensics-panel.ts +364 -0
- package/src/panels/git-panel.ts +638 -0
- package/src/panels/hooks-panel.ts +239 -0
- package/src/panels/incident-review-panel.ts +197 -0
- package/src/panels/index.ts +46 -0
- package/src/panels/intelligence-panel.ts +176 -0
- package/src/panels/knowledge-panel.ts +345 -0
- package/src/panels/local-auth-panel.ts +130 -0
- package/src/panels/marketplace-panel.ts +212 -0
- package/src/panels/memory-panel.ts +225 -0
- package/src/panels/ops-control-panel.ts +150 -0
- package/src/panels/ops-strategy-panel.ts +235 -0
- package/src/panels/orchestration-panel.ts +273 -0
- package/src/panels/panel-list-panel.ts +509 -0
- package/src/panels/panel-manager.ts +570 -0
- package/src/panels/panel-picker.ts +106 -0
- package/src/panels/plan-dashboard-panel.ts +274 -0
- package/src/panels/plugins-panel.ts +178 -0
- package/src/panels/policy-panel.ts +308 -0
- package/src/panels/polish.ts +717 -0
- package/src/panels/project-planning-panel.ts +711 -0
- package/src/panels/provider-account-snapshot.ts +259 -0
- package/src/panels/provider-accounts-panel.ts +218 -0
- package/src/panels/provider-health-domains.ts +215 -0
- package/src/panels/provider-health-panel.ts +727 -0
- package/src/panels/provider-health-tracker.ts +115 -0
- package/src/panels/provider-stats-panel.ts +366 -0
- package/src/panels/qr-panel.ts +182 -0
- package/src/panels/remote-panel.ts +449 -0
- package/src/panels/routes-panel.ts +178 -0
- package/src/panels/sandbox-panel.ts +283 -0
- package/src/panels/schedule-panel.ts +329 -0
- package/src/panels/scrollable-list-panel.ts +491 -0
- package/src/panels/search-focus.ts +32 -0
- package/src/panels/security-panel.ts +295 -0
- package/src/panels/services-panel.ts +231 -0
- package/src/panels/session-browser-panel.ts +400 -0
- package/src/panels/session-maintenance.ts +125 -0
- package/src/panels/settings-sync-panel.ts +120 -0
- package/src/panels/skills-panel.ts +431 -0
- package/src/panels/subscription-panel.ts +263 -0
- package/src/panels/symbol-outline-panel.ts +486 -0
- package/src/panels/system-messages-panel.ts +230 -0
- package/src/panels/tasks-panel.ts +399 -0
- package/src/panels/thinking-panel.ts +304 -0
- package/src/panels/token-budget-panel.ts +475 -0
- package/src/panels/tool-inspector-panel.ts +429 -0
- package/src/panels/types.ts +54 -0
- package/src/panels/watchers-panel.ts +193 -0
- package/src/panels/work-plan-panel.ts +175 -0
- package/src/panels/worktree-panel.ts +182 -0
- package/src/panels/wrfc-panel.ts +609 -0
- package/src/permissions/prompt.ts +165 -0
- package/src/planning/project-planning-coordinator.ts +543 -0
- package/src/plugins/loader.ts +15 -0
- package/src/renderer/agent-detail-modal.ts +331 -0
- package/src/renderer/agent-workspace.ts +238 -0
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/autocomplete-overlay.ts +154 -0
- package/src/renderer/block-actions.ts +76 -0
- package/src/renderer/bookmark-modal.ts +101 -0
- package/src/renderer/bottom-bar.ts +58 -0
- package/src/renderer/buffer.ts +113 -0
- package/src/renderer/code-block.ts +373 -0
- package/src/renderer/compositor.ts +283 -0
- package/src/renderer/context-inspector.ts +219 -0
- package/src/renderer/conversation-layout.ts +67 -0
- package/src/renderer/conversation-overlays.ts +140 -0
- package/src/renderer/conversation-surface.ts +260 -0
- package/src/renderer/diff-view.ts +132 -0
- package/src/renderer/diff.ts +130 -0
- package/src/renderer/file-picker-overlay.ts +101 -0
- package/src/renderer/file-tree.ts +153 -0
- package/src/renderer/fullscreen-primitives.ts +130 -0
- package/src/renderer/fullscreen-workspace.ts +199 -0
- package/src/renderer/git-status.ts +89 -0
- package/src/renderer/help-overlay.ts +267 -0
- package/src/renderer/history-search-overlay.ts +73 -0
- package/src/renderer/layout-engine.ts +97 -0
- package/src/renderer/layout.ts +32 -0
- package/src/renderer/live-tail-modal.ts +156 -0
- package/src/renderer/markdown.ts +635 -0
- package/src/renderer/mcp-workspace.ts +237 -0
- package/src/renderer/modal-factory.ts +467 -0
- package/src/renderer/modal-utils.ts +24 -0
- package/src/renderer/model-picker-overlay.ts +473 -0
- package/src/renderer/model-workspace.ts +488 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +615 -0
- package/src/renderer/overlay-box.ts +146 -0
- package/src/renderer/overlay-viewport.ts +104 -0
- package/src/renderer/panel-composite.ts +158 -0
- package/src/renderer/panel-picker-overlay.ts +202 -0
- package/src/renderer/panel-tab-bar.ts +69 -0
- package/src/renderer/panel-workspace-bar.ts +42 -0
- package/src/renderer/process-indicator.ts +96 -0
- package/src/renderer/process-modal.ts +656 -0
- package/src/renderer/process-summary.ts +67 -0
- package/src/renderer/profile-picker-modal.ts +129 -0
- package/src/renderer/progress.ts +98 -0
- package/src/renderer/qr-renderer.ts +120 -0
- package/src/renderer/search-overlay.ts +54 -0
- package/src/renderer/selection-modal-overlay.ts +214 -0
- package/src/renderer/semantic-diff.ts +369 -0
- package/src/renderer/session-picker-modal.ts +127 -0
- package/src/renderer/settings-modal-helpers.ts +193 -0
- package/src/renderer/settings-modal.ts +537 -0
- package/src/renderer/shell-surface.ts +88 -0
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +67 -0
- package/src/renderer/surface-layout.ts +101 -0
- package/src/renderer/syntax-highlighter.ts +542 -0
- package/src/renderer/system-message.ts +83 -0
- package/src/renderer/tab-strip.ts +108 -0
- package/src/renderer/text-layout.ts +31 -0
- package/src/renderer/thinking.ts +17 -0
- package/src/renderer/tool-call.ts +234 -0
- package/src/renderer/ui-factory.ts +524 -0
- package/src/renderer/ui-primitives.ts +96 -0
- package/src/runtime/bootstrap-command-context.ts +278 -0
- package/src/runtime/bootstrap-command-parts.ts +386 -0
- package/src/runtime/bootstrap-core.ts +540 -0
- package/src/runtime/bootstrap-hook-bridge.ts +112 -0
- package/src/runtime/bootstrap-shell.ts +283 -0
- package/src/runtime/bootstrap.ts +575 -0
- package/src/runtime/cloudflare-control-plane.ts +349 -0
- package/src/runtime/context.ts +142 -0
- package/src/runtime/diagnostics/panels/index.ts +24 -0
- package/src/runtime/diagnostics/panels/ops.ts +156 -0
- package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
- package/src/runtime/diagnostics/panels/policy.ts +177 -0
- package/src/runtime/index.ts +662 -0
- package/src/runtime/onboarding/apply.ts +642 -0
- package/src/runtime/onboarding/derivation.ts +534 -0
- package/src/runtime/onboarding/index.ts +7 -0
- package/src/runtime/onboarding/markers.ts +148 -0
- package/src/runtime/onboarding/snapshot.ts +406 -0
- package/src/runtime/onboarding/state.ts +141 -0
- package/src/runtime/onboarding/types.ts +404 -0
- package/src/runtime/onboarding/verify.ts +171 -0
- package/src/runtime/operator-token-cleanup.ts +27 -0
- package/src/runtime/perf/panel-contracts.ts +32 -0
- package/src/runtime/perf/panel-health-monitor.ts +18 -0
- package/src/runtime/sandbox-public-gaps.ts +358 -0
- package/src/runtime/services.ts +670 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +15 -0
- package/src/runtime/store/domains/index.ts +222 -0
- package/src/runtime/store/domains/panels.ts +117 -0
- package/src/runtime/store/domains/ui-perf.ts +103 -0
- package/src/runtime/store/index.ts +305 -0
- package/src/runtime/store/selectors/index.ts +359 -0
- package/src/runtime/store/state.ts +145 -0
- package/src/runtime/surface-feature-flags.ts +65 -0
- package/src/runtime/terminal-output-guard.ts +228 -0
- package/src/runtime/ui/index.ts +39 -0
- package/src/runtime/ui/model-picker/data-provider.ts +182 -0
- package/src/runtime/ui/model-picker/health-enrichment.ts +228 -0
- package/src/runtime/ui/model-picker/index.ts +59 -0
- package/src/runtime/ui/model-picker/types.ts +149 -0
- package/src/runtime/ui/provider-health/data-provider.ts +244 -0
- package/src/runtime/ui/provider-health/fallback-visualizer.ts +71 -0
- package/src/runtime/ui/provider-health/index.ts +46 -0
- package/src/runtime/ui/provider-health/types.ts +146 -0
- package/src/runtime/ui-events.ts +1 -0
- package/src/runtime/ui-read-model-helpers.ts +1 -0
- package/src/runtime/ui-read-models-observability-maintenance.ts +1 -0
- package/src/runtime/ui-read-models-observability-options.ts +1 -0
- package/src/runtime/ui-read-models-observability-remote.ts +1 -0
- package/src/runtime/ui-read-models-observability-security.ts +1 -0
- package/src/runtime/ui-read-models-observability-system.ts +1 -0
- package/src/runtime/ui-read-models-observability.ts +1 -0
- package/src/runtime/ui-read-models.ts +61 -0
- package/src/runtime/ui-service-queries.ts +1 -0
- package/src/runtime/ui-services.ts +190 -0
- package/src/scripts/process-messages.ts +42 -0
- package/src/shell/blocking-input.ts +98 -0
- package/src/shell/service-settings-sync.ts +273 -0
- package/src/shell/ui-openers.ts +352 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/wrfc-agent-guard.ts +49 -0
- package/src/types/grid.ts +48 -0
- package/src/types/sql-js.d.ts +15 -0
- package/src/utils/clipboard.ts +22 -0
- package/src/utils/splash-lines.ts +46 -0
- package/src/utils/terminal-width.ts +185 -0
- package/src/verification/live-verifier.ts +430 -0
- package/src/verification/verification-ledger.ts +242 -0
- package/src/version.ts +17 -0
- package/src/widget/index.ts +2 -0
- package/src/widget/types.ts +9 -0
- package/src/widget/widget.ts +8 -0
- package/src/work-plans/work-plan-store.ts +374 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { type Line, type Cell, createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
2
|
+
import { UIFactory } from './ui-factory.ts';
|
|
3
|
+
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
|
+
import { LAYOUT } from './layout.ts';
|
|
5
|
+
import { SyntaxHighlighter, type SyntaxToken as HLToken } from './syntax-highlighter.ts';
|
|
6
|
+
|
|
7
|
+
// ─── Language Keyword Maps ───────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const TS_JS_KEYWORDS = new Set([
|
|
10
|
+
'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while',
|
|
11
|
+
'do', 'switch', 'case', 'break', 'continue', 'class', 'extends', 'import',
|
|
12
|
+
'export', 'default', 'from', 'new', 'this', 'super', 'typeof', 'instanceof',
|
|
13
|
+
'in', 'of', 'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
|
|
14
|
+
'null', 'undefined', 'true', 'false', 'void', 'delete', 'interface', 'type',
|
|
15
|
+
'enum', 'namespace', 'module', 'declare', 'abstract', 'implements', 'static',
|
|
16
|
+
'readonly', 'public', 'private', 'protected', 'as', 'satisfies',
|
|
17
|
+
]);
|
|
18
|
+
const TS_TYPES = new Set([
|
|
19
|
+
'string', 'number', 'boolean', 'any', 'unknown', 'never', 'object', 'symbol',
|
|
20
|
+
'bigint', 'void', 'Record', 'Array', 'Map', 'Set', 'Promise', 'Partial',
|
|
21
|
+
'Required', 'Readonly', 'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const PYTHON_KEYWORDS = new Set([
|
|
25
|
+
'def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'import',
|
|
26
|
+
'from', 'as', 'with', 'try', 'except', 'finally', 'raise', 'pass', 'break',
|
|
27
|
+
'continue', 'and', 'or', 'not', 'in', 'is', 'lambda', 'yield', 'global',
|
|
28
|
+
'nonlocal', 'del', 'assert', 'True', 'False', 'None', 'async', 'await',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const BASH_KEYWORDS = new Set([
|
|
32
|
+
'if', 'then', 'else', 'elif', 'fi', 'for', 'do', 'done', 'while', 'case',
|
|
33
|
+
'esac', 'function', 'return', 'exit', 'echo', 'export', 'local', 'readonly',
|
|
34
|
+
'source', 'set', 'unset', 'shift', 'trap', 'exec', 'eval', 'read',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// ─── Language Detection ──────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function detectLanguage(lang: string): 'ts' | 'python' | 'bash' | 'json' | 'yaml' | 'html' | 'css' | 'unknown' {
|
|
40
|
+
const l = lang.toLowerCase();
|
|
41
|
+
if (l === 'ts' || l === 'tsx' || l === 'js' || l === 'jsx' || l === 'typescript' || l === 'javascript') return 'ts';
|
|
42
|
+
if (l === 'py' || l === 'python') return 'python';
|
|
43
|
+
if (l === 'sh' || l === 'bash' || l === 'shell' || l === 'zsh') return 'bash';
|
|
44
|
+
if (l === 'json') return 'json';
|
|
45
|
+
if (l === 'yaml' || l === 'yml') return 'yaml';
|
|
46
|
+
if (l === 'html' || l === 'htm' || l === 'xml') return 'html';
|
|
47
|
+
if (l === 'css' || l === 'scss' || l === 'less') return 'css';
|
|
48
|
+
return 'unknown';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Token Types ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
type SyntaxToken = { text: string; fg: string; bold?: boolean; italic?: boolean };
|
|
54
|
+
|
|
55
|
+
// ─── Tokenizers ──────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function tokenizeTsJs(line: string): SyntaxToken[] {
|
|
58
|
+
const tokens: SyntaxToken[] = [];
|
|
59
|
+
let i = 0;
|
|
60
|
+
|
|
61
|
+
while (i < line.length) {
|
|
62
|
+
// Line comment
|
|
63
|
+
if (line.slice(i, i + 2) === '//') {
|
|
64
|
+
tokens.push({ text: line.slice(i), fg: '65', italic: true });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
// String (single, double, template)
|
|
68
|
+
if (line[i] === '"' || line[i] === "'" || line[i] === '`') {
|
|
69
|
+
const q = line[i];
|
|
70
|
+
let j = i + 1;
|
|
71
|
+
while (j < line.length && line[j] !== q) {
|
|
72
|
+
if (line[j] === '\\') j++;
|
|
73
|
+
j++;
|
|
74
|
+
}
|
|
75
|
+
tokens.push({ text: line.slice(i, j + 1), fg: '#ce9178' });
|
|
76
|
+
i = j + 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Number
|
|
80
|
+
if (/[0-9]/.test(line[i])) {
|
|
81
|
+
let j = i;
|
|
82
|
+
while (j < line.length && /[0-9._xXbBoO]/.test(line[j])) j++;
|
|
83
|
+
tokens.push({ text: line.slice(i, j), fg: '#b5cea8' });
|
|
84
|
+
i = j;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Identifier or keyword
|
|
88
|
+
if (/[a-zA-Z_$]/.test(line[i])) {
|
|
89
|
+
let j = i;
|
|
90
|
+
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
91
|
+
const word = line.slice(i, j);
|
|
92
|
+
if (TS_JS_KEYWORDS.has(word)) {
|
|
93
|
+
tokens.push({ text: word, fg: '#569cd6', bold: true });
|
|
94
|
+
} else if (TS_TYPES.has(word)) {
|
|
95
|
+
tokens.push({ text: word, fg: '#4ec9b0' });
|
|
96
|
+
} else if (line[j] === '(') {
|
|
97
|
+
tokens.push({ text: word, fg: '#dcdcaa' });
|
|
98
|
+
} else {
|
|
99
|
+
tokens.push({ text: word, fg: '' });
|
|
100
|
+
}
|
|
101
|
+
i = j;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Operators and punctuation
|
|
105
|
+
const ch = line[i];
|
|
106
|
+
const isOp = '=<>!&|+-*/%^~?:'.includes(ch);
|
|
107
|
+
tokens.push({ text: ch, fg: isOp ? '#d4d4d4' : '' });
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return tokens;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function tokenizePython(line: string): SyntaxToken[] {
|
|
115
|
+
const tokens: SyntaxToken[] = [];
|
|
116
|
+
let i = 0;
|
|
117
|
+
|
|
118
|
+
while (i < line.length) {
|
|
119
|
+
if (line[i] === '#') {
|
|
120
|
+
tokens.push({ text: line.slice(i), fg: '65', italic: true });
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (line[i] === '"' || line[i] === "'") {
|
|
124
|
+
const q = line[i];
|
|
125
|
+
let j = i + 1;
|
|
126
|
+
while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
|
|
127
|
+
tokens.push({ text: line.slice(i, j + 1), fg: '#ce9178' });
|
|
128
|
+
i = j + 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (/[0-9]/.test(line[i])) {
|
|
132
|
+
let j = i;
|
|
133
|
+
while (j < line.length && /[0-9._]/.test(line[j])) j++;
|
|
134
|
+
tokens.push({ text: line.slice(i, j), fg: '#b5cea8' });
|
|
135
|
+
i = j;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (/[a-zA-Z_]/.test(line[i])) {
|
|
139
|
+
let j = i;
|
|
140
|
+
while (j < line.length && /[\w]/.test(line[j])) j++;
|
|
141
|
+
const word = line.slice(i, j);
|
|
142
|
+
if (PYTHON_KEYWORDS.has(word)) {
|
|
143
|
+
tokens.push({ text: word, fg: '#569cd6', bold: true });
|
|
144
|
+
} else if (/^[A-Z]/.test(word)) {
|
|
145
|
+
tokens.push({ text: word, fg: '#4ec9b0' });
|
|
146
|
+
} else if (line[j] === '(') {
|
|
147
|
+
tokens.push({ text: word, fg: '#dcdcaa' });
|
|
148
|
+
} else {
|
|
149
|
+
tokens.push({ text: word, fg: '' });
|
|
150
|
+
}
|
|
151
|
+
i = j;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
tokens.push({ text: line[i], fg: '' });
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
return tokens;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function tokenizeBash(line: string): SyntaxToken[] {
|
|
161
|
+
const tokens: SyntaxToken[] = [];
|
|
162
|
+
let i = 0;
|
|
163
|
+
|
|
164
|
+
while (i < line.length) {
|
|
165
|
+
if (line[i] === '#') {
|
|
166
|
+
tokens.push({ text: line.slice(i), fg: '65', italic: true });
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (line[i] === '"' || line[i] === "'") {
|
|
170
|
+
const q = line[i];
|
|
171
|
+
let j = i + 1;
|
|
172
|
+
while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
|
|
173
|
+
tokens.push({ text: line.slice(i, j + 1), fg: '#ce9178' });
|
|
174
|
+
i = j + 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (line[i] === '$') {
|
|
178
|
+
let j = i + 1;
|
|
179
|
+
while (j < line.length && /[\w{}_]/.test(line[j])) j++;
|
|
180
|
+
tokens.push({ text: line.slice(i, j), fg: '#9cdcfe' });
|
|
181
|
+
i = j;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (/[a-zA-Z_]/.test(line[i])) {
|
|
185
|
+
let j = i;
|
|
186
|
+
while (j < line.length && /[\w-]/.test(line[j])) j++;
|
|
187
|
+
const word = line.slice(i, j);
|
|
188
|
+
if (BASH_KEYWORDS.has(word)) {
|
|
189
|
+
tokens.push({ text: word, fg: '#569cd6', bold: true });
|
|
190
|
+
} else {
|
|
191
|
+
tokens.push({ text: word, fg: '' });
|
|
192
|
+
}
|
|
193
|
+
i = j;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
tokens.push({ text: line[i], fg: '' });
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
return tokens;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function tokenizeJson(line: string): SyntaxToken[] {
|
|
203
|
+
const tokens: SyntaxToken[] = [];
|
|
204
|
+
let i = 0;
|
|
205
|
+
|
|
206
|
+
while (i < line.length) {
|
|
207
|
+
if (line[i] === '"') {
|
|
208
|
+
let j = i + 1;
|
|
209
|
+
while (j < line.length && line[j] !== '"') { if (line[j] === '\\') j++; j++; }
|
|
210
|
+
const str = line.slice(i, j + 1);
|
|
211
|
+
// JSON key: followed by :
|
|
212
|
+
const rest = line.slice(j + 1).trimStart();
|
|
213
|
+
if (rest.startsWith(':')) {
|
|
214
|
+
tokens.push({ text: str, fg: '#9cdcfe' });
|
|
215
|
+
} else {
|
|
216
|
+
tokens.push({ text: str, fg: '#ce9178' });
|
|
217
|
+
}
|
|
218
|
+
i = j + 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (/[0-9-]/.test(line[i])) {
|
|
222
|
+
let j = i;
|
|
223
|
+
while (j < line.length && /[0-9.eE+-]/.test(line[j])) j++;
|
|
224
|
+
tokens.push({ text: line.slice(i, j), fg: '#b5cea8' });
|
|
225
|
+
i = j;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const boolNull = ['true', 'false', 'null'].find(k => line.startsWith(k, i));
|
|
229
|
+
if (boolNull) {
|
|
230
|
+
tokens.push({ text: boolNull, fg: '#569cd6', bold: true });
|
|
231
|
+
i += boolNull.length;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
tokens.push({ text: line[i], fg: '244' });
|
|
235
|
+
i++;
|
|
236
|
+
}
|
|
237
|
+
return tokens;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function tokenizeYaml(line: string): SyntaxToken[] {
|
|
241
|
+
const tokens: SyntaxToken[] = [];
|
|
242
|
+
if (line.trimStart().startsWith('#')) {
|
|
243
|
+
return [{ text: line, fg: '65', italic: true }];
|
|
244
|
+
}
|
|
245
|
+
const keyMatch = line.match(/^(\s*)([^:]+)(:)(\s*.*)/);
|
|
246
|
+
if (keyMatch) {
|
|
247
|
+
if (keyMatch[1]) tokens.push({ text: keyMatch[1], fg: '' });
|
|
248
|
+
tokens.push({ text: keyMatch[2], fg: '#9cdcfe' });
|
|
249
|
+
tokens.push({ text: keyMatch[3], fg: '244' });
|
|
250
|
+
if (keyMatch[4]) {
|
|
251
|
+
const val = keyMatch[4];
|
|
252
|
+
const trimVal = val.trimStart();
|
|
253
|
+
// Differentiate YAML value types for syntax highlighting
|
|
254
|
+
const isStr = /^['"]/.test(trimVal);
|
|
255
|
+
const isBool = trimVal === 'true' || trimVal === 'false' || trimVal === 'null' || trimVal === 'yes' || trimVal === 'no';
|
|
256
|
+
const isNum = /^-?[0-9]/.test(trimVal);
|
|
257
|
+
const valFg = isStr ? '#ce9178' : isBool ? '#569cd6' : isNum ? '#b5cea8' : '';
|
|
258
|
+
tokens.push({ text: val, fg: valFg });
|
|
259
|
+
}
|
|
260
|
+
return tokens;
|
|
261
|
+
}
|
|
262
|
+
return [{ text: line, fg: '' }];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function tokenizePlain(line: string): SyntaxToken[] {
|
|
266
|
+
return [{ text: line, fg: '' }];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Main Renderer ───────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* renderCodeBlock - Render lines of code with syntax highlighting and line numbers.
|
|
273
|
+
* Returns Line[] for the cell-based pipeline.
|
|
274
|
+
*/
|
|
275
|
+
export function renderCodeBlock(
|
|
276
|
+
codeLines: string[],
|
|
277
|
+
lang: string,
|
|
278
|
+
width: number,
|
|
279
|
+
opts: { showLineNumbers?: boolean } = {},
|
|
280
|
+
): Line[] {
|
|
281
|
+
const lines: Line[] = [];
|
|
282
|
+
const language = detectLanguage(lang);
|
|
283
|
+
const leftMargin = LAYOUT.LEFT_MARGIN;
|
|
284
|
+
const showLineNumbers = opts.showLineNumbers ?? true;
|
|
285
|
+
const lineNumW = showLineNumbers ? String(codeLines.length).length + 1 : 0; // e.g. "10 "
|
|
286
|
+
const contentStartX = showLineNumbers ? leftMargin + lineNumW + 1 : leftMargin;
|
|
287
|
+
const BG = '#0d0d0d';
|
|
288
|
+
const LINE_NUM_FG = '238';
|
|
289
|
+
const effectiveWidth = width - LAYOUT.RIGHT_MARGIN;
|
|
290
|
+
|
|
291
|
+
// Try tree-sitter highlight cache first (populated asynchronously).
|
|
292
|
+
// Falls back to regex tokenizer when parser not yet ready or language unsupported.
|
|
293
|
+
const fullCode = codeLines.join('\n');
|
|
294
|
+
const hlLines = lang ? new SyntaxHighlighter().highlight(fullCode, lang) : null;
|
|
295
|
+
|
|
296
|
+
// Regex tokenizer fallback (used when tree-sitter not ready)
|
|
297
|
+
const regexTokenize = (line: string): SyntaxToken[] => {
|
|
298
|
+
switch (language) {
|
|
299
|
+
case 'ts': return tokenizeTsJs(line);
|
|
300
|
+
case 'python': return tokenizePython(line);
|
|
301
|
+
case 'bash': return tokenizeBash(line);
|
|
302
|
+
case 'json': return tokenizeJson(line);
|
|
303
|
+
case 'yaml': return tokenizeYaml(line);
|
|
304
|
+
default: return tokenizePlain(line);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Header bar: language label
|
|
309
|
+
const langLabel = lang ? ` ${lang} ` : ' code ';
|
|
310
|
+
const headerLine = createEmptyLine(width);
|
|
311
|
+
const headerStr = langLabel.padEnd(effectiveWidth - leftMargin);
|
|
312
|
+
let hx = leftMargin;
|
|
313
|
+
for (const ch of headerStr) {
|
|
314
|
+
if (hx >= effectiveWidth) break;
|
|
315
|
+
headerLine[hx] = createStyledCell(ch, { fg: '#1a1a1a', bg: '#4ec9b0', bold: true });
|
|
316
|
+
hx++;
|
|
317
|
+
}
|
|
318
|
+
lines.push(headerLine);
|
|
319
|
+
|
|
320
|
+
// Code lines
|
|
321
|
+
for (let i = 0; i < codeLines.length; i++) {
|
|
322
|
+
const rawLine = codeLines[i];
|
|
323
|
+
const lineNum = String(i + 1).padStart(lineNumW);
|
|
324
|
+
|
|
325
|
+
// Select token source: tree-sitter (accurate) or regex (fallback)
|
|
326
|
+
const tokens: SyntaxToken[] =
|
|
327
|
+
hlLines && i < hlLines.length && hlLines[i].length > 0
|
|
328
|
+
? (hlLines[i] as HLToken[])
|
|
329
|
+
: regexTokenize(rawLine);
|
|
330
|
+
|
|
331
|
+
const line: Cell[] = createEmptyLine(width);
|
|
332
|
+
// Paint only the code-block body band so body rows match the header/footer width.
|
|
333
|
+
for (let x = leftMargin; x < effectiveWidth; x++) {
|
|
334
|
+
line[x] = createStyledCell(' ', { bg: BG });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let cx = leftMargin;
|
|
338
|
+
if (showLineNumbers) {
|
|
339
|
+
for (const ch of lineNum) {
|
|
340
|
+
if (cx >= contentStartX) break;
|
|
341
|
+
line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg: BG, dim: true });
|
|
342
|
+
}
|
|
343
|
+
line[cx++] = createStyledCell(' ', { bg: BG });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Syntax tokens
|
|
347
|
+
for (const token of tokens) {
|
|
348
|
+
for (const ch of token.text) {
|
|
349
|
+
if (cx >= effectiveWidth) break;
|
|
350
|
+
const cw = getDisplayWidth(ch);
|
|
351
|
+
const code = ch.charCodeAt(0);
|
|
352
|
+
if (code < 32 || code === 127) {
|
|
353
|
+
cx++;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
line[cx] = createStyledCell(ch, { fg: token.fg, bg: BG, bold: token.bold, italic: token.italic });
|
|
357
|
+
if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
|
|
358
|
+
cx += cw;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
lines.push(line);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Footer line
|
|
366
|
+
const footerLine = createEmptyLine(width);
|
|
367
|
+
for (let fx = leftMargin; fx < effectiveWidth; fx++) {
|
|
368
|
+
footerLine[fx] = createStyledCell(' ', { bg: '#0d0d0d' });
|
|
369
|
+
}
|
|
370
|
+
lines.push(footerLine);
|
|
371
|
+
|
|
372
|
+
return lines;
|
|
373
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { TerminalBuffer } from './buffer.ts';
|
|
2
|
+
import { DiffEngine } from './diff.ts';
|
|
3
|
+
import { type Line, createEmptyCell, createStyledCell } from '../types/grid.ts';
|
|
4
|
+
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
5
|
+
import type { SearchManager } from '../input/search.ts';
|
|
6
|
+
import { allowTerminalWrite } from '../runtime/terminal-output-guard.ts';
|
|
7
|
+
|
|
8
|
+
export interface SelectionInfo {
|
|
9
|
+
isCellSelected: (col: number, absoluteRow: number) => boolean;
|
|
10
|
+
scrollTop: number;
|
|
11
|
+
lineCount: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SearchInfo {
|
|
15
|
+
manager: SearchManager;
|
|
16
|
+
scrollTop: number;
|
|
17
|
+
viewportStartY: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PanelCompositeData {
|
|
21
|
+
/** Workspace-level tab bar spanning all open panels. */
|
|
22
|
+
workspaceBar: Line;
|
|
23
|
+
/** Top pane: tab bar */
|
|
24
|
+
topTabBar?: Line;
|
|
25
|
+
/** Top pane: panel content lines */
|
|
26
|
+
topContent: Line[];
|
|
27
|
+
/** Whether the top pane is focused (affects separator color) */
|
|
28
|
+
topFocused: boolean;
|
|
29
|
+
/** Bottom pane tab bar. Undefined = no bottom pane. */
|
|
30
|
+
bottomTabBar?: Line;
|
|
31
|
+
/** Bottom pane content lines. Undefined = no bottom pane. */
|
|
32
|
+
bottomContent?: Line[];
|
|
33
|
+
/** Whether the bottom pane is focused */
|
|
34
|
+
bottomFocused?: boolean;
|
|
35
|
+
/** Separator between left and right panel area */
|
|
36
|
+
separator: boolean;
|
|
37
|
+
/** Ratio of panel height for the top pane (0–1). Only used when bottom pane is present. */
|
|
38
|
+
verticalSplitRatio: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CompositeRequest {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
header: Line[];
|
|
45
|
+
viewport: Line[];
|
|
46
|
+
footer: Line[];
|
|
47
|
+
selection?: SelectionInfo;
|
|
48
|
+
search?: SearchInfo;
|
|
49
|
+
panel?: PanelCompositeData;
|
|
50
|
+
panelWidth?: number; // width of the right panel area (0 = no panel)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compositor - Authoritative TUI layout engine with Selection Overlay.
|
|
55
|
+
* Decoupled from global state — all needed data is passed as parameters.
|
|
56
|
+
*/
|
|
57
|
+
export class Compositor {
|
|
58
|
+
/** Double-buffer reuse: back is written, front is the last-rendered reference. */
|
|
59
|
+
private frontBuffer: TerminalBuffer | null = null;
|
|
60
|
+
private backBuffer: TerminalBuffer | null = null;
|
|
61
|
+
private diffEngine = new DiffEngine();
|
|
62
|
+
|
|
63
|
+
constructor(private stdout: NodeJS.WriteStream) {}
|
|
64
|
+
|
|
65
|
+
/** Exposed for unit tests — returns the last composited buffer. */
|
|
66
|
+
public get lastBufferForTest(): TerminalBuffer | null {
|
|
67
|
+
return this.frontBuffer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public resetDiff(): void {
|
|
71
|
+
this.diffEngine.reset();
|
|
72
|
+
this.frontBuffer = null;
|
|
73
|
+
this.backBuffer = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public composite(params: CompositeRequest): void {
|
|
77
|
+
const { width, height, header, viewport, footer, selection, search, panel, panelWidth } = params;
|
|
78
|
+
// R3: Reuse back-buffer instead of allocating each frame
|
|
79
|
+
if (!this.backBuffer) {
|
|
80
|
+
this.backBuffer = new TerminalBuffer(width, height);
|
|
81
|
+
} else {
|
|
82
|
+
this.backBuffer.reset(width, height, this.frontBuffer);
|
|
83
|
+
}
|
|
84
|
+
const newBuffer = this.backBuffer;
|
|
85
|
+
|
|
86
|
+
const hasPanel = panel !== undefined && panelWidth !== undefined && panelWidth > 0;
|
|
87
|
+
const leftWidth = hasPanel ? Math.max(1, width - panelWidth - 1) : width;
|
|
88
|
+
const sepX = hasPanel ? leftWidth : -1;
|
|
89
|
+
|
|
90
|
+
// 1. Draw Header — always full width
|
|
91
|
+
header.forEach((line, i) => newBuffer.blitLine(i, line));
|
|
92
|
+
|
|
93
|
+
// 2. Draw Viewport directly after the supplied header.
|
|
94
|
+
const viewportStartY = header.length;
|
|
95
|
+
const vHeight = Math.max(0, height - header.length - footer.length);
|
|
96
|
+
|
|
97
|
+
// Calculate the offset for bottom-anchored short history
|
|
98
|
+
const lineCount = selection?.lineCount ?? 0;
|
|
99
|
+
const offset = Math.max(0, vHeight - lineCount);
|
|
100
|
+
|
|
101
|
+
// --- Pre-compute panel row layout when split pane is active ---
|
|
102
|
+
// When both top and bottom panes are visible, the panel area is split:
|
|
103
|
+
// row 0: workspace tab bar
|
|
104
|
+
// row 1: top tab bar
|
|
105
|
+
// rows 2..topH+1: top content
|
|
106
|
+
// row topH+2: horizontal separator (───)
|
|
107
|
+
// row topH+3: bottom tab bar
|
|
108
|
+
// rows topH+4..end: bottom content
|
|
109
|
+
const hasBottomPane = hasPanel && panel!.bottomTabBar !== undefined;
|
|
110
|
+
let topPaneHeight = 0; // number of content rows in top pane
|
|
111
|
+
let bottomPaneHeight = 0;
|
|
112
|
+
let hSepRow = -1; // viewport row of the horizontal separator
|
|
113
|
+
if (hasPanel && hasBottomPane) {
|
|
114
|
+
const panelAreaRows = Math.max(0, vHeight - 1); // subtract workspace tab bar
|
|
115
|
+
// top: 1 (tabbar) + topContent rows; bottom: 1 (sep) + 1 (tabbar) + bottomContent
|
|
116
|
+
const contentRows = Math.max(0, panelAreaRows - 3); // subtract top-tabbar + h-sep + bottom-tabbar
|
|
117
|
+
topPaneHeight = Math.max(1, Math.floor(contentRows * panel!.verticalSplitRatio));
|
|
118
|
+
bottomPaneHeight = Math.max(1, contentRows - topPaneHeight);
|
|
119
|
+
hSepRow = 2 + topPaneHeight; // workspace bar + top tab bar + top content rows
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sepFg = hasPanel && panel!.separator
|
|
123
|
+
? (panel!.topFocused || panel!.bottomFocused ? '244' : '238')
|
|
124
|
+
: '238';
|
|
125
|
+
|
|
126
|
+
viewport.forEach((line, i) => {
|
|
127
|
+
const screenY = viewportStartY + i;
|
|
128
|
+
if (screenY >= height) return;
|
|
129
|
+
|
|
130
|
+
if (!hasPanel) {
|
|
131
|
+
// No panel: existing fast path
|
|
132
|
+
newBuffer.blitLine(screenY, line);
|
|
133
|
+
} else {
|
|
134
|
+
// Panel active: write cells individually to support split layout
|
|
135
|
+
// Left side: viewport cells 0..leftWidth-1
|
|
136
|
+
for (let x = 0; x < leftWidth; x++) {
|
|
137
|
+
const cell = line[x];
|
|
138
|
+
if (cell !== undefined) {
|
|
139
|
+
// If this is a wide char (2-cell) at the last left-side column,
|
|
140
|
+
// it would bleed into the separator column visually.
|
|
141
|
+
// Replace with a space to keep the separator aligned.
|
|
142
|
+
if (x === leftWidth - 1 && cell.char && cell.char.length > 0 && getDisplayWidth(cell.char) > 1) {
|
|
143
|
+
newBuffer.setCell(x, screenY, { ...cell, char: ' ' });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
newBuffer.setCell(x, screenY, cell);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const p = panel!;
|
|
151
|
+
|
|
152
|
+
// Separator column (vertical bar between left and panel area)
|
|
153
|
+
if (p.separator) {
|
|
154
|
+
newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: sepFg }));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const panelStartX = sepX + 1;
|
|
158
|
+
const clearPanelRemainder = (fromX = 0) => {
|
|
159
|
+
for (let x = Math.max(0, fromX); x < panelWidth; x++) {
|
|
160
|
+
newBuffer.setCell(panelStartX + x, screenY, createEmptyCell());
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const drawPanelLine = (panelLine: Line | undefined) => {
|
|
164
|
+
if (panelLine === undefined) {
|
|
165
|
+
clearPanelRemainder();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const limit = Math.min(panelLine.length, panelWidth);
|
|
169
|
+
for (let x = 0; x < limit; x++) {
|
|
170
|
+
const cell = panelLine[x];
|
|
171
|
+
if (cell !== undefined) {
|
|
172
|
+
newBuffer.setCell(panelStartX + x, screenY, cell);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
clearPanelRemainder(limit);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (!hasBottomPane) {
|
|
179
|
+
// --- Single pane mode ---
|
|
180
|
+
// viewport row 0 → workspace bar, viewport rows 1+ → panel content
|
|
181
|
+
const panelLine = i === 0 ? p.workspaceBar : p.topContent[i - 1];
|
|
182
|
+
drawPanelLine(panelLine);
|
|
183
|
+
} else {
|
|
184
|
+
// --- Two pane mode ---
|
|
185
|
+
// Row layout (by viewport row i):
|
|
186
|
+
// i = 0: workspace tab bar
|
|
187
|
+
// i = 1: top tab bar
|
|
188
|
+
// 2 <= i <= topPaneHeight+1: top content[i-2]
|
|
189
|
+
// i = hSepRow: horizontal separator
|
|
190
|
+
// i = hSepRow+1: bottom tab bar
|
|
191
|
+
// i >= hSepRow+2: bottom content[i - (hSepRow+2)]
|
|
192
|
+
let panelLine: Line | undefined;
|
|
193
|
+
|
|
194
|
+
if (i === 0) {
|
|
195
|
+
panelLine = p.workspaceBar;
|
|
196
|
+
} else if (i === 1) {
|
|
197
|
+
panelLine = p.topTabBar;
|
|
198
|
+
} else if (i <= topPaneHeight + 1) {
|
|
199
|
+
panelLine = p.topContent[i - 2];
|
|
200
|
+
} else if (i === hSepRow) {
|
|
201
|
+
// Horizontal separator between the two panes
|
|
202
|
+
// Render ─ chars across the panel width
|
|
203
|
+
const focusFg = p.bottomFocused ? '36' : '238'; // cyan if bottom pane focused
|
|
204
|
+
for (let x = 0; x < panelWidth; x++) {
|
|
205
|
+
newBuffer.setCell(panelStartX + x, screenY, createStyledCell('─', { fg: focusFg }));
|
|
206
|
+
}
|
|
207
|
+
// Also update the separator column char to T-junction (├):
|
|
208
|
+
// ├ connects the vertical left-separator with the horizontal pane divider,
|
|
209
|
+
// forming a clean T-shaped joint at the split point.
|
|
210
|
+
if (p.separator) {
|
|
211
|
+
newBuffer.setCell(sepX, screenY, createStyledCell('├', { fg: focusFg }));
|
|
212
|
+
}
|
|
213
|
+
} else if (i === hSepRow + 1) {
|
|
214
|
+
panelLine = p.bottomTabBar;
|
|
215
|
+
} else {
|
|
216
|
+
panelLine = p.bottomContent?.[i - (hSepRow + 2)];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (i !== hSepRow) {
|
|
220
|
+
drawPanelLine(panelLine);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Apply Selection Highlighting Overlay (left side only)
|
|
226
|
+
// Only highlight rows that actually contain history (past the bottom-anchor offset)
|
|
227
|
+
if (selection && i >= offset) {
|
|
228
|
+
const absoluteRow = selection.scrollTop + (i - offset);
|
|
229
|
+
for (let x = 0; x < leftWidth; x++) {
|
|
230
|
+
if (selection.isCellSelected(x, absoluteRow)) {
|
|
231
|
+
newBuffer.setCell(x, screenY, { bg: '4', fg: '0', bold: false, dim: false });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Apply Search Match Highlighting Overlay (left side only)
|
|
237
|
+
if (search && search.manager.active && search.manager.query.length > 0 && i >= offset) {
|
|
238
|
+
const absoluteRow = search.scrollTop + (i - offset);
|
|
239
|
+
const lineMatches = search.manager.getMatchesOnLine(absoluteRow);
|
|
240
|
+
for (const match of lineMatches) {
|
|
241
|
+
const isCurrent = search.manager.isCurrentMatch(absoluteRow, match.col);
|
|
242
|
+
for (let x = match.col; x < match.col + match.length && x < leftWidth; x++) {
|
|
243
|
+
if (isCurrent) {
|
|
244
|
+
newBuffer.setCell(x, screenY, { bg: '#ffff00', fg: '#000000', bold: true, dim: false });
|
|
245
|
+
} else {
|
|
246
|
+
newBuffer.setCell(x, screenY, { bg: '#806600', fg: '#ffffff', bold: false, dim: false });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Draw separator on remaining viewport rows past content (when panel is active)
|
|
254
|
+
if (hasPanel && panel!.separator) {
|
|
255
|
+
for (let i = viewport.length; i < vHeight; i++) {
|
|
256
|
+
const screenY = viewportStartY + i;
|
|
257
|
+
if (screenY >= height) break;
|
|
258
|
+
newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: sepFg }));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 3. Draw Footer (Pinned to Bottom) — always full width
|
|
263
|
+
const footerStart = height - footer.length;
|
|
264
|
+
footer.forEach((line, i) => {
|
|
265
|
+
const screenY = footerStart + i;
|
|
266
|
+
if (screenY >= height) return;
|
|
267
|
+
newBuffer.blitLine(screenY, line);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// 4. Diff and Render
|
|
271
|
+
// R3: Diff against front-buffer (last-rendered), then swap front/back — no clone() needed
|
|
272
|
+
const diff = this.diffEngine.diff(this.frontBuffer, newBuffer);
|
|
273
|
+
if (diff) {
|
|
274
|
+
allowTerminalWrite(() => this.stdout.write(diff));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Swap: back (just written) becomes the new front reference; old front becomes the next back
|
|
278
|
+
const swap = this.frontBuffer;
|
|
279
|
+
this.frontBuffer = this.backBuffer;
|
|
280
|
+
this.frontBuffer.clearDirty();
|
|
281
|
+
this.backBuffer = swap;
|
|
282
|
+
}
|
|
283
|
+
}
|