@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,146 @@
|
|
|
1
|
+
import { createStyledCell, type Line } from '../types/grid.ts';
|
|
2
|
+
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
3
|
+
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
4
|
+
import { fillWidth, makeLine, writeText } from './fullscreen-primitives.ts';
|
|
5
|
+
|
|
6
|
+
export interface OverlayBoxPalette {
|
|
7
|
+
readonly borderFg: string;
|
|
8
|
+
readonly titleFg: string;
|
|
9
|
+
readonly bodyFg: string;
|
|
10
|
+
readonly mutedFg: string;
|
|
11
|
+
readonly selectedBg: string;
|
|
12
|
+
readonly titleBg: string;
|
|
13
|
+
readonly sectionBg: string;
|
|
14
|
+
readonly inputBg: string;
|
|
15
|
+
readonly bodyBg: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_OVERLAY_PALETTE: Readonly<OverlayBoxPalette> = {
|
|
19
|
+
borderFg: UI_TONES.fg.secondary,
|
|
20
|
+
titleFg: UI_TONES.fg.primary,
|
|
21
|
+
bodyFg: UI_TONES.fg.primary,
|
|
22
|
+
mutedFg: UI_TONES.fg.dim,
|
|
23
|
+
selectedBg: UI_TONES.bg.selected,
|
|
24
|
+
titleBg: UI_TONES.bg.title,
|
|
25
|
+
sectionBg: UI_TONES.bg.section,
|
|
26
|
+
inputBg: UI_TONES.bg.input,
|
|
27
|
+
bodyBg: UI_TONES.bg.surface,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export interface OverlayBoxLayout {
|
|
31
|
+
readonly margin: number;
|
|
32
|
+
readonly width: number;
|
|
33
|
+
readonly contentWidth: number;
|
|
34
|
+
readonly innerWidth: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OverlayTextStyle {
|
|
38
|
+
fg: string;
|
|
39
|
+
bg?: string;
|
|
40
|
+
bold?: boolean;
|
|
41
|
+
dim?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createOverlayBoxLayout(
|
|
45
|
+
terminalWidth: number,
|
|
46
|
+
margin: number,
|
|
47
|
+
maxWidth: number,
|
|
48
|
+
): OverlayBoxLayout {
|
|
49
|
+
const resolvedMaxWidth = getOverlayMaxWidth(terminalWidth, margin, maxWidth);
|
|
50
|
+
const width = Math.max(20, Math.min(terminalWidth - margin * 2, resolvedMaxWidth));
|
|
51
|
+
const contentWidth = width - 2;
|
|
52
|
+
const innerWidth = contentWidth - 2;
|
|
53
|
+
return { margin, width, contentWidth, innerWidth };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function putOverlayText(
|
|
57
|
+
line: Line,
|
|
58
|
+
startX: number,
|
|
59
|
+
maxWidth: number,
|
|
60
|
+
text: string,
|
|
61
|
+
style: OverlayTextStyle,
|
|
62
|
+
): void {
|
|
63
|
+
writeText(line, startX, maxWidth, text, {
|
|
64
|
+
fg: style.fg,
|
|
65
|
+
bg: style.bg ?? '',
|
|
66
|
+
bold: style.bold ?? false,
|
|
67
|
+
dim: style.dim ?? false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createOverlayBorderLine(
|
|
72
|
+
terminalWidth: number,
|
|
73
|
+
layout: OverlayBoxLayout,
|
|
74
|
+
left: string,
|
|
75
|
+
fill: string,
|
|
76
|
+
right: string,
|
|
77
|
+
fg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
|
|
78
|
+
): Line {
|
|
79
|
+
const line = makeLine(terminalWidth);
|
|
80
|
+
const leftX = layout.margin;
|
|
81
|
+
const rightX = layout.margin + layout.width - 1;
|
|
82
|
+
line[leftX] = createStyledCell(left, { fg });
|
|
83
|
+
for (let x = leftX + 1; x < rightX; x++) {
|
|
84
|
+
line[x] = createStyledCell(fill, { fg });
|
|
85
|
+
}
|
|
86
|
+
line[rightX] = createStyledCell(right, { fg });
|
|
87
|
+
return line;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createOverlayContentLine(
|
|
91
|
+
terminalWidth: number,
|
|
92
|
+
layout: OverlayBoxLayout,
|
|
93
|
+
borderFg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
|
|
94
|
+
bg = '',
|
|
95
|
+
): Line {
|
|
96
|
+
const line = makeLine(terminalWidth);
|
|
97
|
+
const leftX = layout.margin;
|
|
98
|
+
const rightX = layout.margin + layout.width - 1;
|
|
99
|
+
line[leftX] = createStyledCell('│', { fg: borderFg });
|
|
100
|
+
fillWidth(line, leftX + 1, rightX - leftX - 1, bg);
|
|
101
|
+
line[rightX] = createStyledCell('│', { fg: borderFg });
|
|
102
|
+
return line;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createOverlayFilledBorderLine(
|
|
106
|
+
terminalWidth: number,
|
|
107
|
+
layout: OverlayBoxLayout,
|
|
108
|
+
left: string,
|
|
109
|
+
fill: string,
|
|
110
|
+
right: string,
|
|
111
|
+
fg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
|
|
112
|
+
bg = '',
|
|
113
|
+
): Line {
|
|
114
|
+
const line = makeLine(terminalWidth);
|
|
115
|
+
const leftX = layout.margin;
|
|
116
|
+
const rightX = layout.margin + layout.width - 1;
|
|
117
|
+
line[leftX] = createStyledCell(left, { fg, bg });
|
|
118
|
+
for (let x = leftX + 1; x < rightX; x++) {
|
|
119
|
+
line[x] = createStyledCell(fill, { fg, bg });
|
|
120
|
+
}
|
|
121
|
+
line[rightX] = createStyledCell(right, { fg, bg });
|
|
122
|
+
return line;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createOverlayFrameLine(
|
|
126
|
+
terminalWidth: number,
|
|
127
|
+
layout: OverlayBoxLayout,
|
|
128
|
+
bg = DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
129
|
+
): Line {
|
|
130
|
+
return createOverlayContentLine(terminalWidth, layout, DEFAULT_OVERLAY_PALETTE.borderFg, bg);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const OVERLAY_GLYPHS = {
|
|
134
|
+
topLeft: GLYPHS.frame.topLeft,
|
|
135
|
+
topRight: GLYPHS.frame.topRight,
|
|
136
|
+
bottomLeft: GLYPHS.frame.bottomLeft,
|
|
137
|
+
bottomRight: GLYPHS.frame.bottomRight,
|
|
138
|
+
teeLeft: GLYPHS.frame.teeLeft,
|
|
139
|
+
teeRight: GLYPHS.frame.teeRight,
|
|
140
|
+
horizontal: GLYPHS.frame.horizontal,
|
|
141
|
+
selected: GLYPHS.navigation.selected,
|
|
142
|
+
expanded: GLYPHS.navigation.expanded,
|
|
143
|
+
cursor: GLYPHS.surface.cursor,
|
|
144
|
+
moreAbove: GLYPHS.navigation.moreAbove,
|
|
145
|
+
moreBelow: GLYPHS.navigation.moreBelow,
|
|
146
|
+
} as const;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { getSurfaceContentRows } from './surface-layout.ts';
|
|
2
|
+
|
|
3
|
+
export type OverlayWidthClass = 'narrow' | 'medium' | 'wide';
|
|
4
|
+
|
|
5
|
+
export function getOverlayWidthClass(viewportWidth: number): OverlayWidthClass {
|
|
6
|
+
if (viewportWidth < 88) return 'narrow';
|
|
7
|
+
if (viewportWidth < 120) return 'medium';
|
|
8
|
+
return 'wide';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OverlayViewportBudgetOptions {
|
|
12
|
+
readonly chromeRows: number;
|
|
13
|
+
readonly minContentRows?: number;
|
|
14
|
+
readonly maxContentRows?: number;
|
|
15
|
+
readonly targetRatio?: number;
|
|
16
|
+
readonly minTotalRows?: number;
|
|
17
|
+
readonly maxTotalRows?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getOverlayContentBudget(
|
|
21
|
+
viewportHeight: number,
|
|
22
|
+
options: OverlayViewportBudgetOptions,
|
|
23
|
+
): number {
|
|
24
|
+
const safeViewportHeight = Math.max(8, viewportHeight);
|
|
25
|
+
const defaultTargetTotalRows = Math.max(12, Math.min(Math.max(12, safeViewportHeight - 3), Math.round(safeViewportHeight * (options.targetRatio ?? 0.45))));
|
|
26
|
+
const minTotalRows = options.minTotalRows ?? 12;
|
|
27
|
+
const maxTotalRows = options.maxTotalRows ?? Math.max(minTotalRows, Math.min(22, safeViewportHeight - 2));
|
|
28
|
+
const targetTotalRows = Math.max(
|
|
29
|
+
minTotalRows,
|
|
30
|
+
Math.min(maxTotalRows, defaultTargetTotalRows),
|
|
31
|
+
);
|
|
32
|
+
return getSurfaceContentRows({
|
|
33
|
+
viewportHeight,
|
|
34
|
+
chromeRows: options.chromeRows,
|
|
35
|
+
minContentRows: options.minContentRows,
|
|
36
|
+
maxContentRows: options.maxContentRows,
|
|
37
|
+
targetRatio: 1,
|
|
38
|
+
minTotalRows: targetTotalRows,
|
|
39
|
+
maxTotalRows: targetTotalRows,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getOverlayMaxWidth(
|
|
44
|
+
terminalWidth: number,
|
|
45
|
+
margin: number,
|
|
46
|
+
requestedMaxWidth: number,
|
|
47
|
+
): number {
|
|
48
|
+
const safeRequested = Math.max(24, requestedMaxWidth);
|
|
49
|
+
const reservedSideSpace = margin * 2;
|
|
50
|
+
const viewportBounded = Math.max(24, terminalWidth - reservedSideSpace);
|
|
51
|
+
return Math.min(safeRequested, viewportBounded);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface OverlaySurfaceMetricsOptions {
|
|
55
|
+
readonly margin?: number;
|
|
56
|
+
readonly maxWidth?: number;
|
|
57
|
+
readonly chromeRows: number;
|
|
58
|
+
readonly minContentRows?: number;
|
|
59
|
+
readonly maxContentRows?: number;
|
|
60
|
+
readonly targetRatio?: number;
|
|
61
|
+
readonly minTotalRows?: number;
|
|
62
|
+
readonly maxTotalRows?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface OverlaySurfaceMetrics {
|
|
66
|
+
readonly margin: number;
|
|
67
|
+
readonly boxWidth: number;
|
|
68
|
+
readonly contentWidth: number;
|
|
69
|
+
readonly contentRows: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getStableOverlayContentRows(
|
|
73
|
+
contentRows: number,
|
|
74
|
+
minRows = 10,
|
|
75
|
+
): number {
|
|
76
|
+
return Math.max(contentRows, minRows);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getOverlaySurfaceMetrics(
|
|
80
|
+
viewportWidth: number,
|
|
81
|
+
viewportHeight: number,
|
|
82
|
+
options: OverlaySurfaceMetricsOptions,
|
|
83
|
+
): OverlaySurfaceMetrics {
|
|
84
|
+
// Keep overlays comfortably inset from shell panels and avoid wide, low-information modals.
|
|
85
|
+
const widthClass = getOverlayWidthClass(viewportWidth);
|
|
86
|
+
const margin = options.margin ?? (widthClass === 'wide' ? 6 : 4);
|
|
87
|
+
const defaultMaxWidth = widthClass === 'wide' ? 84 : widthClass === 'medium' ? 78 : 72;
|
|
88
|
+
const boxWidth = getOverlayMaxWidth(viewportWidth, margin, options.maxWidth ?? defaultMaxWidth);
|
|
89
|
+
const contentRows = getOverlayContentBudget(viewportHeight, {
|
|
90
|
+
chromeRows: options.chromeRows,
|
|
91
|
+
minContentRows: options.minContentRows ?? 8,
|
|
92
|
+
maxContentRows: options.maxContentRows ?? 14,
|
|
93
|
+
targetRatio: options.targetRatio ?? 0.45,
|
|
94
|
+
minTotalRows: options.minTotalRows,
|
|
95
|
+
maxTotalRows: options.maxTotalRows,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
margin,
|
|
100
|
+
boxWidth,
|
|
101
|
+
contentWidth: Math.max(1, boxWidth - 4),
|
|
102
|
+
contentRows,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import type { Panel } from '../panels/types.ts';
|
|
3
|
+
import type { InputHandler } from '../input/handler.ts';
|
|
4
|
+
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
5
|
+
import type { PanelCompositeData } from './compositor.ts';
|
|
6
|
+
import { createSplitPaneLayout } from './layout-engine.ts';
|
|
7
|
+
import { renderPanelTabBar } from './panel-tab-bar.ts';
|
|
8
|
+
import { renderPanelWorkspaceBar } from './panel-workspace-bar.ts';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* R2: Per-panel render cache for dirty-flag skipping.
|
|
12
|
+
*
|
|
13
|
+
* Maintainability hazard — mid-render invalidation race:
|
|
14
|
+
*
|
|
15
|
+
* If an event listener (e.g. a runtime-bus subscriber) fires DURING a panel's
|
|
16
|
+
* `render()` call and mutates state such that `needsRender = true`, the
|
|
17
|
+
* trailing `panel.markRendered()` below will clobber that invalidation back
|
|
18
|
+
* to `false`, causing the next frame to serve stale cached output until
|
|
19
|
+
* something else invalidates the panel.
|
|
20
|
+
*
|
|
21
|
+
* In practice this is rare because `render()` is synchronous and short, and
|
|
22
|
+
* runtime-bus listeners that mutate panel state typically run outside the
|
|
23
|
+
* render pass. Panels that DO expect to mutate from within their own render
|
|
24
|
+
* path (e.g. deferred lazy-load) should call `this.invalidate()` AFTER the
|
|
25
|
+
* trailing work so the next frame picks it up.
|
|
26
|
+
*
|
|
27
|
+
* The fix (now applied): snapshot `needsRender` before calling `render()` and
|
|
28
|
+
* only call `markRendered()` when no concurrent invalidation occurred during
|
|
29
|
+
* the render pass — preserving any mid-render invalidation.
|
|
30
|
+
*/
|
|
31
|
+
interface PanelRenderCache {
|
|
32
|
+
lines: Line[];
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
}
|
|
36
|
+
const panelRenderCache = new WeakMap<Panel, PanelRenderCache>();
|
|
37
|
+
/**
|
|
38
|
+
* Per-panel render-generation counter. Incremented whenever invalidate() fires
|
|
39
|
+
* on the panel (tracked here externally since Panel does not expose a version).
|
|
40
|
+
* We monkey-patch each panel the first time it enters renderPanel() by wrapping
|
|
41
|
+
* its invalidate() to bump the counter stored in this map.
|
|
42
|
+
*
|
|
43
|
+
* Race-guard: snapshot the generation before render(), compare after. If it
|
|
44
|
+
* changed, a mid-render invalidation occurred — leave needsRender=true.
|
|
45
|
+
*/
|
|
46
|
+
const panelRenderGen = new WeakMap<Panel, { gen: number }>();
|
|
47
|
+
|
|
48
|
+
function getRenderGenState(panel: Panel): { gen: number } {
|
|
49
|
+
let state = panelRenderGen.get(panel);
|
|
50
|
+
if (!state) {
|
|
51
|
+
state = { gen: 0 };
|
|
52
|
+
panelRenderGen.set(panel, state);
|
|
53
|
+
// Wrap invalidate() to bump generation counter.
|
|
54
|
+
const origInvalidate = panel.invalidate.bind(panel);
|
|
55
|
+
panel.invalidate = () => {
|
|
56
|
+
state!.gen++;
|
|
57
|
+
origInvalidate();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** R2: Render a panel, skipping if nothing changed. Returns cached lines on a skip. */
|
|
64
|
+
export function renderPanel(panel: Panel, width: number, height: number): Line[] {
|
|
65
|
+
const cached = panelRenderCache.get(panel);
|
|
66
|
+
if (cached && !panel.needsRender && cached.width === width && cached.height === height) {
|
|
67
|
+
return cached.lines;
|
|
68
|
+
}
|
|
69
|
+
// Snapshot render-generation counter BEFORE calling render(). If an event
|
|
70
|
+
// listener fires during render() and calls panel.invalidate(), the generation
|
|
71
|
+
// counter will be bumped. We compare after render to detect mid-render races.
|
|
72
|
+
const genState = getRenderGenState(panel);
|
|
73
|
+
const genBefore = genState.gen;
|
|
74
|
+
const lines = panel.render(width, height);
|
|
75
|
+
// Only call markRendered() when no mid-render invalidation occurred.
|
|
76
|
+
// If the generation changed during render(), a concurrent invalidate() fired —
|
|
77
|
+
// leave needsRender=true so the next frame re-renders with the new state.
|
|
78
|
+
if (genState.gen === genBefore) {
|
|
79
|
+
panel.markRendered();
|
|
80
|
+
}
|
|
81
|
+
// If gen changed, needsRender is already true (invalidate() set it); do not
|
|
82
|
+
// call markRendered() — the next frame will pick it up.
|
|
83
|
+
panelRenderCache.set(panel, { lines, width, height });
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PanelCompositeBuildResult {
|
|
88
|
+
readonly panelData?: PanelCompositeData;
|
|
89
|
+
readonly panelWidth?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildPanelCompositeData(
|
|
93
|
+
panelManager: PanelManager,
|
|
94
|
+
input: InputHandler,
|
|
95
|
+
panelWidth: number,
|
|
96
|
+
panelHeight: number,
|
|
97
|
+
): PanelCompositeBuildResult {
|
|
98
|
+
if (!panelManager.isVisible() || panelManager.getAllOpen().length === 0 || panelWidth <= 0) {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const topPane = panelManager.getTopPane();
|
|
103
|
+
const bottomPane = panelManager.getBottomPane();
|
|
104
|
+
const focusedPane = panelManager.getFocusedPane();
|
|
105
|
+
const workspaceTabs = panelManager.getWorkspaceTabs();
|
|
106
|
+
const verticalSplitRatio = panelManager.getVerticalSplitRatio();
|
|
107
|
+
const hasBottom = panelManager.isBottomPaneVisible() && bottomPane.panels.length > 0;
|
|
108
|
+
const workspaceBar = renderPanelWorkspaceBar(workspaceTabs, panelWidth, input.panelFocused);
|
|
109
|
+
|
|
110
|
+
let topContent: Line[];
|
|
111
|
+
let topTabBar: Line | undefined;
|
|
112
|
+
let bottomTabBar: Line | undefined;
|
|
113
|
+
let bottomContent: Line[] | undefined;
|
|
114
|
+
|
|
115
|
+
const topActivePanel = topPane.panels[topPane.activeIndex] ?? null;
|
|
116
|
+
|
|
117
|
+
if (hasBottom) {
|
|
118
|
+
topTabBar = renderPanelTabBar(
|
|
119
|
+
topPane.panels,
|
|
120
|
+
topPane.activeIndex,
|
|
121
|
+
panelWidth,
|
|
122
|
+
input.panelFocused && focusedPane === 'top',
|
|
123
|
+
'top',
|
|
124
|
+
);
|
|
125
|
+
const paneLayout = createSplitPaneLayout(Math.max(0, panelHeight - 1), verticalSplitRatio);
|
|
126
|
+
const topH = paneLayout.topContentRows;
|
|
127
|
+
const bottomH = paneLayout.bottomContentRows;
|
|
128
|
+
topContent = topActivePanel ? renderPanel(topActivePanel, panelWidth, topH) : [];
|
|
129
|
+
|
|
130
|
+
const bottomActivePanel = bottomPane.panels[bottomPane.activeIndex] ?? null;
|
|
131
|
+
bottomTabBar = renderPanelTabBar(
|
|
132
|
+
bottomPane.panels,
|
|
133
|
+
bottomPane.activeIndex,
|
|
134
|
+
panelWidth,
|
|
135
|
+
input.panelFocused && focusedPane === 'bottom',
|
|
136
|
+
'bottom',
|
|
137
|
+
);
|
|
138
|
+
bottomContent = bottomActivePanel ? renderPanel(bottomActivePanel, panelWidth, bottomH) : [];
|
|
139
|
+
} else {
|
|
140
|
+
const topH = Math.max(0, panelHeight - 1);
|
|
141
|
+
topContent = topActivePanel ? renderPanel(topActivePanel, panelWidth, topH) : [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
panelData: {
|
|
146
|
+
workspaceBar,
|
|
147
|
+
topTabBar,
|
|
148
|
+
topContent,
|
|
149
|
+
topFocused: input.panelFocused && focusedPane === 'top',
|
|
150
|
+
bottomTabBar,
|
|
151
|
+
bottomContent,
|
|
152
|
+
bottomFocused: input.panelFocused && focusedPane === 'bottom',
|
|
153
|
+
separator: true,
|
|
154
|
+
verticalSplitRatio,
|
|
155
|
+
},
|
|
156
|
+
panelWidth,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { type Line } from '../types/grid.ts';
|
|
2
|
+
import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
+
import type { PanelPicker } from '../panels/panel-picker.ts';
|
|
4
|
+
import { CATEGORY_LABELS, CATEGORY_ORDER } from '../panels/panel-picker.ts';
|
|
5
|
+
import type { PanelCategory, PanelRegistration } from '../panels/types.ts';
|
|
6
|
+
import {
|
|
7
|
+
createOverlayBorderLine,
|
|
8
|
+
createOverlayBoxLayout,
|
|
9
|
+
createOverlayContentLine,
|
|
10
|
+
DEFAULT_OVERLAY_PALETTE,
|
|
11
|
+
putOverlayText,
|
|
12
|
+
} from './overlay-box.ts';
|
|
13
|
+
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
14
|
+
|
|
15
|
+
const TITLE_FG = '#cbd5e1';
|
|
16
|
+
const CATEGORY_FG = '#94a3b8';
|
|
17
|
+
const SELECTED_FG = '#e2e8f0';
|
|
18
|
+
const SELECTED_BG = '#1e293b';
|
|
19
|
+
const BODY_FG = '252';
|
|
20
|
+
const BORDER_FG = DEFAULT_OVERLAY_PALETTE.borderFg;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render the panel picker modal as Line[] for overlay in the viewport.
|
|
24
|
+
* Panels are grouped by category. Category headers are inserted between groups.
|
|
25
|
+
* When a search query is active a search-bar row is shown beneath the title.
|
|
26
|
+
*/
|
|
27
|
+
export function renderPanelPickerOverlay(
|
|
28
|
+
picker: PanelPicker,
|
|
29
|
+
width: number,
|
|
30
|
+
viewportHeight = 24,
|
|
31
|
+
): Line[] {
|
|
32
|
+
if (!picker.active) return [];
|
|
33
|
+
|
|
34
|
+
const lines: Line[] = [];
|
|
35
|
+
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
36
|
+
margin: 4,
|
|
37
|
+
maxWidth: 72,
|
|
38
|
+
chromeRows: picker.searchQuery.length > 0 ? 7 : 6,
|
|
39
|
+
minContentRows: 6,
|
|
40
|
+
maxContentRows: 10,
|
|
41
|
+
});
|
|
42
|
+
const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
|
|
43
|
+
const contentW = layout.innerWidth;
|
|
44
|
+
const titleFg = TITLE_FG;
|
|
45
|
+
const borderFg = BORDER_FG;
|
|
46
|
+
|
|
47
|
+
// ── Title bar ──────────────────────────────────────────────────────────────
|
|
48
|
+
const titleLine = createOverlayBorderLine(width, layout, '┌', '─', '┐', borderFg);
|
|
49
|
+
putOverlayText(titleLine, layout.margin + 2, layout.width - 4, 'Open Panel Workspace', { fg: titleFg, bold: true });
|
|
50
|
+
lines.push(titleLine);
|
|
51
|
+
|
|
52
|
+
// ── Search bar (shown when query is non-empty) ──────────────────────────────
|
|
53
|
+
if (picker.searchQuery.length > 0) {
|
|
54
|
+
const searchLine = createOverlayContentLine(width, layout, borderFg);
|
|
55
|
+
const searchLabel = '\u2315 ';
|
|
56
|
+
const queryAvail = contentW - getDisplayWidth(searchLabel);
|
|
57
|
+
const queryText = fitDisplay(picker.searchQuery, queryAvail);
|
|
58
|
+
putOverlayText(searchLine, layout.margin + 2, getDisplayWidth(searchLabel), searchLabel, { fg: titleFg });
|
|
59
|
+
putOverlayText(searchLine, layout.margin + 2 + getDisplayWidth(searchLabel), queryAvail, queryText, { fg: titleFg });
|
|
60
|
+
lines.push(searchLine);
|
|
61
|
+
|
|
62
|
+
lines.push(createOverlayBorderLine(width, layout, '├', '─', '┤', borderFg));
|
|
63
|
+
} else {
|
|
64
|
+
const placeholder = ' Browse by category or start typing to filter by panel name, purpose, or category.';
|
|
65
|
+
const row = createOverlayContentLine(width, layout, borderFg);
|
|
66
|
+
putOverlayText(row, layout.margin + 2, contentW, fitDisplay(placeholder, contentW), { fg: '244', dim: true });
|
|
67
|
+
lines.push(row);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const visible = picker.getVisible();
|
|
71
|
+
|
|
72
|
+
if (visible.length === 0) {
|
|
73
|
+
const noResults = 'No panels match your search';
|
|
74
|
+
const noRow = createOverlayContentLine(width, layout, borderFg);
|
|
75
|
+
putOverlayText(noRow, layout.margin + 2, contentW, fitDisplay(noResults, contentW), { fg: '244', dim: true });
|
|
76
|
+
lines.push(noRow);
|
|
77
|
+
} else {
|
|
78
|
+
// Build a flat render list of { type: 'header' | 'item', ... } entries
|
|
79
|
+
// so we can compute the correct scroll window over all rows.
|
|
80
|
+
type HeaderEntry = { type: 'header'; category: PanelCategory };
|
|
81
|
+
type ItemEntry = { type: 'item'; reg: PanelRegistration; flatIndex: number };
|
|
82
|
+
type RenderEntry = HeaderEntry | ItemEntry;
|
|
83
|
+
|
|
84
|
+
const renderEntries: RenderEntry[] = [];
|
|
85
|
+
|
|
86
|
+
// When no search query, group by category; when searching, show flat list
|
|
87
|
+
// with a single "Results" header.
|
|
88
|
+
if (picker.searchQuery.length === 0) {
|
|
89
|
+
const byCategory = new Map<PanelCategory, PanelRegistration[]>();
|
|
90
|
+
for (const reg of visible) {
|
|
91
|
+
const group = byCategory.get(reg.category) ?? [];
|
|
92
|
+
group.push(reg);
|
|
93
|
+
byCategory.set(reg.category, group);
|
|
94
|
+
}
|
|
95
|
+
let flatIndex = 0;
|
|
96
|
+
for (const cat of CATEGORY_ORDER) {
|
|
97
|
+
const group = byCategory.get(cat);
|
|
98
|
+
if (!group || group.length === 0) continue;
|
|
99
|
+
renderEntries.push({ type: 'header', category: cat });
|
|
100
|
+
for (const reg of group) {
|
|
101
|
+
renderEntries.push({ type: 'item', reg, flatIndex });
|
|
102
|
+
flatIndex++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
for (let i = 0; i < visible.length; i++) {
|
|
107
|
+
renderEntries.push({ type: 'item', reg: visible[i], flatIndex: i });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Compute scroll window: ensure the selected item row stays visible.
|
|
112
|
+
// Find the render-entry index of the selected item.
|
|
113
|
+
const selectedEntryIdx = renderEntries.findIndex(
|
|
114
|
+
e => e.type === 'item' && e.flatIndex === picker.selectedIndex,
|
|
115
|
+
);
|
|
116
|
+
const total = renderEntries.length;
|
|
117
|
+
const maxVisible = metrics.contentRows;
|
|
118
|
+
let startEntry = 0;
|
|
119
|
+
if (total > maxVisible && selectedEntryIdx >= 0) {
|
|
120
|
+
startEntry = Math.max(
|
|
121
|
+
0,
|
|
122
|
+
Math.min(
|
|
123
|
+
selectedEntryIdx - Math.floor(maxVisible / 2),
|
|
124
|
+
total - maxVisible,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const endEntry = Math.min(startEntry + maxVisible, total);
|
|
129
|
+
|
|
130
|
+
for (let i = startEntry; i < endEntry; i++) {
|
|
131
|
+
const entry = renderEntries[i];
|
|
132
|
+
|
|
133
|
+
if (entry.type === 'header') {
|
|
134
|
+
// Category header row
|
|
135
|
+
const label = CATEGORY_LABELS[entry.category].toUpperCase();
|
|
136
|
+
const headerRow = createOverlayContentLine(width, layout, borderFg);
|
|
137
|
+
putOverlayText(headerRow, layout.margin + 2, contentW, fitDisplay(` ${label}`, contentW), { fg: CATEGORY_FG, dim: true });
|
|
138
|
+
lines.push(headerRow);
|
|
139
|
+
} else {
|
|
140
|
+
const { reg, flatIndex } = entry;
|
|
141
|
+
const isSelected = flatIndex === picker.selectedIndex;
|
|
142
|
+
const indicator = isSelected ? '\u25b6 ' : ' ';
|
|
143
|
+
|
|
144
|
+
// icon (1 char) + 2 spaces + name
|
|
145
|
+
const iconStr = reg.icon + ' ';
|
|
146
|
+
const iconW = getDisplayWidth(iconStr);
|
|
147
|
+
|
|
148
|
+
// separator between name and description: ' \u2014 '
|
|
149
|
+
const sep = ' \u2014 ';
|
|
150
|
+
const sepW = getDisplayWidth(sep);
|
|
151
|
+
|
|
152
|
+
// Allocate space: indicator(2) + icon(iconW) + name + sep + desc fills contentW
|
|
153
|
+
const nameMaxW = Math.max(8, Math.floor((contentW - 2 - iconW - sepW) * 0.35));
|
|
154
|
+
const descMaxW = contentW - 2 - iconW - nameMaxW - sepW;
|
|
155
|
+
|
|
156
|
+
const nameRaw = reg.name;
|
|
157
|
+
const nameStr = fitDisplay(nameRaw, nameMaxW);
|
|
158
|
+
|
|
159
|
+
const descRaw = reg.description;
|
|
160
|
+
const descStr = fitDisplay(descRaw, descMaxW);
|
|
161
|
+
|
|
162
|
+
const row = createOverlayContentLine(width, layout, borderFg, isSelected ? SELECTED_BG : '');
|
|
163
|
+
const rowText = indicator + iconStr + nameStr + sep + descStr;
|
|
164
|
+
putOverlayText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), {
|
|
165
|
+
fg: isSelected ? SELECTED_FG : BODY_FG,
|
|
166
|
+
bg: isSelected ? SELECTED_BG : '',
|
|
167
|
+
bold: isSelected,
|
|
168
|
+
});
|
|
169
|
+
lines.push(row);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const selected = picker.getSelected();
|
|
174
|
+
if (selected) {
|
|
175
|
+
lines.push(createOverlayBorderLine(width, layout, '├', '─', '┤', borderFg));
|
|
176
|
+
const categoryLabel = CATEGORY_LABELS[selected.category].toUpperCase();
|
|
177
|
+
const selectedLine = createOverlayContentLine(width, layout, borderFg);
|
|
178
|
+
putOverlayText(selectedLine, layout.margin + 2, contentW, fitDisplay(`${selected.icon} ${selected.name} [${categoryLabel}]`, contentW), { fg: SELECTED_FG });
|
|
179
|
+
lines.push(selectedLine);
|
|
180
|
+
const desc = fitDisplay(truncateDisplay(selected.description, contentW), contentW);
|
|
181
|
+
const descRow = createOverlayContentLine(width, layout, borderFg);
|
|
182
|
+
putOverlayText(descRow, layout.margin + 2, contentW, desc, { fg: '244', dim: true });
|
|
183
|
+
lines.push(descRow);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Scroll indicator ───────────────────────────────────────────────────
|
|
187
|
+
if (total > maxVisible) {
|
|
188
|
+
const scrollInfo = `${picker.selectedIndex + 1}/${visible.length}`;
|
|
189
|
+
const scrollRow = createOverlayContentLine(width, layout, borderFg);
|
|
190
|
+
putOverlayText(scrollRow, layout.margin + 2 + Math.max(0, contentW - getDisplayWidth(scrollInfo)), getDisplayWidth(scrollInfo), scrollInfo, { fg: '240', dim: true });
|
|
191
|
+
lines.push(scrollRow);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Bottom border with hints ───────────────────────────────────────────────
|
|
196
|
+
const hints = '[Up/Down] Navigate [Enter] Open [/] Filter [Esc] Cancel';
|
|
197
|
+
const bottomLine = createOverlayBorderLine(width, layout, '└', '─', '┘', borderFg);
|
|
198
|
+
putOverlayText(bottomLine, layout.margin + 2, layout.width - 4, truncateDisplay(hints, layout.width - 4), { fg: '240', dim: true });
|
|
199
|
+
lines.push(bottomLine);
|
|
200
|
+
|
|
201
|
+
return lines;
|
|
202
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Line } from '../types/grid.ts';
|
|
2
|
+
import type { Panel } from '../panels/types.ts';
|
|
3
|
+
import { renderTabStrip } from './tab-strip.ts';
|
|
4
|
+
|
|
5
|
+
const ACTIVE_FG = '#e2e8f0';
|
|
6
|
+
const ACTIVE_FG_UNFOCUSED = '#94a3b8';
|
|
7
|
+
const ACTIVE_BG = '#1e293b';
|
|
8
|
+
const INACTIVE_FG = '244';
|
|
9
|
+
const SEPARATOR_FG = '238';
|
|
10
|
+
const CLOSE_FG = '238';
|
|
11
|
+
const LABEL_FG = '#cbd5e1';
|
|
12
|
+
const LABEL_BG = '#0f172a';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Render the panel tab bar.
|
|
16
|
+
*
|
|
17
|
+
* Shows open panel tabs with the active one highlighted.
|
|
18
|
+
* Format: │ icon Name │ icon Name │ ...
|
|
19
|
+
*
|
|
20
|
+
* Active tab: slate-white, bold, bg #1e293b
|
|
21
|
+
* Inactive tab: grey (244), no background
|
|
22
|
+
* Separators: │ in dim grey (238)
|
|
23
|
+
* Overflow: > right scroll indicator (stateless - shows when tabs extend beyond width)
|
|
24
|
+
* Close button: x at far right for active tab
|
|
25
|
+
*/
|
|
26
|
+
export function renderPanelTabBar(
|
|
27
|
+
panels: Panel[],
|
|
28
|
+
activeIndex: number,
|
|
29
|
+
width: number,
|
|
30
|
+
focused: boolean = true,
|
|
31
|
+
paneLabel?: string,
|
|
32
|
+
): Line {
|
|
33
|
+
if (panels.length === 0) return renderTabStrip({ width, tabs: [], style: {
|
|
34
|
+
activeFg: ACTIVE_FG,
|
|
35
|
+
activeBg: ACTIVE_BG,
|
|
36
|
+
activeBold: focused,
|
|
37
|
+
inactiveFg: INACTIVE_FG,
|
|
38
|
+
separatorFg: SEPARATOR_FG,
|
|
39
|
+
labelFg: LABEL_FG,
|
|
40
|
+
labelBg: focused ? LABEL_BG : '',
|
|
41
|
+
labelBold: focused,
|
|
42
|
+
overflowFg: SEPARATOR_FG,
|
|
43
|
+
trailingFg: focused ? ACTIVE_FG_UNFOCUSED : INACTIVE_FG,
|
|
44
|
+
} });
|
|
45
|
+
|
|
46
|
+
const tabs = panels.map((panel, index) => ({
|
|
47
|
+
label: `${panel.icon} ${panel.name}`,
|
|
48
|
+
active: index === activeIndex,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
return renderTabStrip({
|
|
52
|
+
width,
|
|
53
|
+
tabs,
|
|
54
|
+
prefixLabel: paneLabel ? ` ${paneLabel.toUpperCase()} ` : undefined,
|
|
55
|
+
suffixLabel: ` ${panels.length} x `,
|
|
56
|
+
style: {
|
|
57
|
+
activeFg: focused ? ACTIVE_FG : ACTIVE_FG_UNFOCUSED,
|
|
58
|
+
activeBg: ACTIVE_BG,
|
|
59
|
+
activeBold: focused,
|
|
60
|
+
inactiveFg: INACTIVE_FG,
|
|
61
|
+
separatorFg: SEPARATOR_FG,
|
|
62
|
+
labelFg: LABEL_FG,
|
|
63
|
+
labelBg: focused ? LABEL_BG : '',
|
|
64
|
+
labelBold: focused,
|
|
65
|
+
overflowFg: SEPARATOR_FG,
|
|
66
|
+
trailingFg: CLOSE_FG,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|