@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,635 @@
|
|
|
1
|
+
import { type Line, type Cell, createStyledCell } from '../types/grid.ts';
|
|
2
|
+
import { UIFactory } from './ui-factory.ts';
|
|
3
|
+
import { renderCodeBlock } from './code-block.ts';
|
|
4
|
+
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
5
|
+
import { LAYOUT } from './layout.ts';
|
|
6
|
+
|
|
7
|
+
export interface MarkdownRenderOptions {
|
|
8
|
+
codeBlockLineNumbers?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Module-level set of inline markdown special characters (hoisted out of hot loop). */
|
|
12
|
+
const INLINE_SPECIAL_CHARS = new Set(['[', '`', '*', '_', '~']);
|
|
13
|
+
|
|
14
|
+
function splitTableCells(row: string): string[] {
|
|
15
|
+
const cells = row.trim().split('|').map((c) => c.trim());
|
|
16
|
+
if (cells.length > 0 && cells[0] === '') cells.shift();
|
|
17
|
+
if (cells.length > 0 && cells[cells.length - 1] === '') cells.pop();
|
|
18
|
+
return cells;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isLikelyTableSeparatorRow(row: string): boolean {
|
|
22
|
+
if (!row.includes('|')) return false;
|
|
23
|
+
const cells = splitTableCells(row);
|
|
24
|
+
if (cells.length === 0) return false;
|
|
25
|
+
return cells.every((cell) => /:?-{3,}:?/.test(cell));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isLikelyTableHeaderRow(row: string): boolean {
|
|
29
|
+
return splitTableCells(row).length >= 2;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* renderMarkdown - Parse markdown text into styled Line[].
|
|
34
|
+
* Thin wrapper over renderMarkdownTracked for callers that don't need code-block metadata.
|
|
35
|
+
*/
|
|
36
|
+
export function renderMarkdown(text: string, width: number, options: MarkdownRenderOptions = {}): Line[] {
|
|
37
|
+
return renderMarkdownTracked(text, width, options).lines;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CodeBlockSpan {
|
|
41
|
+
/** Line offset from the start of renderMarkdown output where this block begins. */
|
|
42
|
+
startOffset: number;
|
|
43
|
+
/** Number of rendered lines occupied by this code block. */
|
|
44
|
+
lineCount: number;
|
|
45
|
+
/** Raw source lines inside the fence (no fence markers). */
|
|
46
|
+
rawContent: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* renderMarkdownTracked - Same as renderMarkdown but also returns metadata
|
|
51
|
+
* about every code block encountered, keyed by their line offset in the output.
|
|
52
|
+
* Used by ConversationManager to register code blocks in the blockRegistry.
|
|
53
|
+
*/
|
|
54
|
+
export function renderMarkdownTracked(
|
|
55
|
+
text: string,
|
|
56
|
+
width: number,
|
|
57
|
+
options: MarkdownRenderOptions = {},
|
|
58
|
+
): { lines: ReturnType<typeof renderMarkdown>; codeBlocks: CodeBlockSpan[] } {
|
|
59
|
+
const lines: ReturnType<typeof renderMarkdown> = [];
|
|
60
|
+
const codeBlocks: CodeBlockSpan[] = [];
|
|
61
|
+
const rawLines = text.split('\n');
|
|
62
|
+
|
|
63
|
+
let inCodeBlock = false;
|
|
64
|
+
let codeBlockLang = '';
|
|
65
|
+
let codeBlockLines: string[] = [];
|
|
66
|
+
const indent = LAYOUT.LEFT_MARGIN;
|
|
67
|
+
const contentWidth = LAYOUT.contentWidth(width);
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
70
|
+
const raw = rawLines[i];
|
|
71
|
+
|
|
72
|
+
const fenceMatch = raw.match(/^```(\w*)/);
|
|
73
|
+
if (fenceMatch && !inCodeBlock) {
|
|
74
|
+
inCodeBlock = true;
|
|
75
|
+
codeBlockLang = fenceMatch[1] || '';
|
|
76
|
+
codeBlockLines = [];
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (inCodeBlock) {
|
|
80
|
+
if (raw.trimStart().startsWith('```')) {
|
|
81
|
+
const blockStart = lines.length;
|
|
82
|
+
const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
|
|
83
|
+
showLineNumbers: options.codeBlockLineNumbers ?? true,
|
|
84
|
+
});
|
|
85
|
+
codeBlocks.push({
|
|
86
|
+
startOffset: blockStart,
|
|
87
|
+
lineCount: rendered.length,
|
|
88
|
+
rawContent: codeBlockLines.join('\n'),
|
|
89
|
+
});
|
|
90
|
+
lines.push(...rendered);
|
|
91
|
+
inCodeBlock = false;
|
|
92
|
+
codeBlockLang = '';
|
|
93
|
+
codeBlockLines = [];
|
|
94
|
+
} else {
|
|
95
|
+
codeBlockLines.push(raw);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (raw.trim() === '') {
|
|
101
|
+
lines.push(UIFactory.stringToLine('', width));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const h3 = raw.match(/^### (.+)/);
|
|
106
|
+
const h2 = raw.match(/^## (.+)/);
|
|
107
|
+
const h1 = raw.match(/^# (.+)/);
|
|
108
|
+
if (h1) {
|
|
109
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h1[1].toUpperCase(), width, { fg: '#00ffff', bold: true }));
|
|
110
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + '━'.repeat(Math.min(getDisplayWidth(h1[1]), contentWidth)), width, { fg: '244' }));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (h2) {
|
|
114
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h2[1], width, { fg: '#00ffff', bold: true }));
|
|
115
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(Math.min(getDisplayWidth(h2[1]), contentWidth)), width, { fg: '240' }));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (h3) {
|
|
119
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h3[1], width, { fg: '111', bold: true }));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const taskMatch = raw.match(/^(\s*)[-*] \[([ xX])\] (.+)/);
|
|
124
|
+
if (taskMatch) {
|
|
125
|
+
const listIndent = Math.floor(taskMatch[1].length / 2);
|
|
126
|
+
const checked = taskMatch[2] !== ' ';
|
|
127
|
+
const bulletX = indent + listIndent * 2;
|
|
128
|
+
const textStartX = bulletX + 4;
|
|
129
|
+
const checkbox = checked ? '\u2611 ' : '\u2610 ';
|
|
130
|
+
const rendered = renderInlineMarkdown(taskMatch[3]);
|
|
131
|
+
const prefix = ' '.repeat(bulletX) + checkbox;
|
|
132
|
+
const style = checked ? { fg: '244', strikethrough: true } : {};
|
|
133
|
+
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: checked ? '#22c55e' : '252', ...style }, textStartX));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ulMatch = raw.match(/^(\s*)[-*] (.+)/);
|
|
138
|
+
if (ulMatch) {
|
|
139
|
+
const listIndent = Math.floor(ulMatch[1].length / 2);
|
|
140
|
+
const bulletX = indent + listIndent * 2;
|
|
141
|
+
const textStartX = bulletX + 2;
|
|
142
|
+
const rendered = renderInlineMarkdown(ulMatch[2]);
|
|
143
|
+
const prefix = ' '.repeat(bulletX) + '• ';
|
|
144
|
+
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '135', bold: false }, textStartX));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const olMatch = raw.match(/^(\s*)(\d+)\. (.+)/);
|
|
149
|
+
if (olMatch) {
|
|
150
|
+
const listIndent = Math.floor(olMatch[1].length / 2);
|
|
151
|
+
const numStr = olMatch[2] + '. ';
|
|
152
|
+
const bulletX = indent + listIndent * 2;
|
|
153
|
+
const textStartX = bulletX + numStr.length;
|
|
154
|
+
const rendered = renderInlineMarkdown(olMatch[3]);
|
|
155
|
+
const prefix = ' '.repeat(bulletX) + numStr;
|
|
156
|
+
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '135', bold: false }, textStartX));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (/^[-*_]{3,}$/.test(raw.trim())) {
|
|
161
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(contentWidth), width, { fg: '240' }));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const bqMatch = raw.match(/^> (.*)/);
|
|
166
|
+
if (bqMatch) {
|
|
167
|
+
const rendered = renderInlineMarkdown(bqMatch[1]);
|
|
168
|
+
const prefix = ' '.repeat(indent) + '┃ ';
|
|
169
|
+
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '244', italic: true }, indent + 3));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (raw.includes('|') && i + 1 < rawLines.length && isLikelyTableHeaderRow(raw) && isLikelyTableSeparatorRow(rawLines[i + 1])) {
|
|
174
|
+
const tableRows: string[] = [];
|
|
175
|
+
let j = i;
|
|
176
|
+
while (j < rawLines.length && rawLines[j].includes('|')) {
|
|
177
|
+
tableRows.push(rawLines[j]);
|
|
178
|
+
j++;
|
|
179
|
+
}
|
|
180
|
+
i = j - 1;
|
|
181
|
+
lines.push(...renderTable(tableRows, width, indent));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const rendered = renderInlineMarkdown(raw);
|
|
186
|
+
lines.push(...compositeInlineLine(' '.repeat(indent), rendered, width, {}, indent));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (inCodeBlock && codeBlockLines.length > 0) {
|
|
190
|
+
const blockStart = lines.length;
|
|
191
|
+
const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
|
|
192
|
+
showLineNumbers: options.codeBlockLineNumbers ?? true,
|
|
193
|
+
});
|
|
194
|
+
codeBlocks.push({
|
|
195
|
+
startOffset: blockStart,
|
|
196
|
+
lineCount: rendered.length,
|
|
197
|
+
rawContent: codeBlockLines.join('\n'),
|
|
198
|
+
});
|
|
199
|
+
lines.push(...rendered);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { lines, codeBlocks };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Strip markdown formatting from text for width measurement.
|
|
207
|
+
* Removes **, *, `, ~~ markers but keeps the inner text.
|
|
208
|
+
*/
|
|
209
|
+
function stripMarkdown(text: string): string {
|
|
210
|
+
return text
|
|
211
|
+
.replace(/\*\*(.+?)\*\*/g, '$1') // bold
|
|
212
|
+
.replace(/\*(.+?)\*/g, '$1') // italic
|
|
213
|
+
.replace(/~~(.+?)~~/g, '$1') // strikethrough
|
|
214
|
+
.replace(/`(.+?)`/g, '$1'); // inline code
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Render a markdown table with box-drawing borders.
|
|
219
|
+
* Parses inline markdown in cells, fits to terminal width,
|
|
220
|
+
* truncates long content, and renders with proper styling.
|
|
221
|
+
*/
|
|
222
|
+
function renderTable(rows: string[], width: number, indent: number): Line[] {
|
|
223
|
+
const lines: Line[] = [];
|
|
224
|
+
|
|
225
|
+
// Parse rows into cells, skip separator
|
|
226
|
+
const parsedRows: string[][] = [];
|
|
227
|
+
let hasSeparator = false;
|
|
228
|
+
for (const row of rows) {
|
|
229
|
+
const trimmed = row.trim();
|
|
230
|
+
if (isLikelyTableSeparatorRow(trimmed)) {
|
|
231
|
+
hasSeparator = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const cells = splitTableCells(trimmed);
|
|
235
|
+
if (cells.length > 0) parsedRows.push(cells);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (parsedRows.length === 0) return lines;
|
|
239
|
+
|
|
240
|
+
const colCount = Math.max(...parsedRows.map(r => r.length));
|
|
241
|
+
const availW = width - indent;
|
|
242
|
+
|
|
243
|
+
// Measure column widths using stripped text (no markdown markers)
|
|
244
|
+
const naturalWidths: number[] = new Array(colCount).fill(0);
|
|
245
|
+
for (const row of parsedRows) {
|
|
246
|
+
for (let c = 0; c < row.length; c++) {
|
|
247
|
+
naturalWidths[c] = Math.max(naturalWidths[c], getDisplayWidth(stripMarkdown(row[c])));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Budget: availW minus borders (colCount+1) minus padding (2 per col)
|
|
252
|
+
const overhead = (colCount + 1) + (colCount * 2);
|
|
253
|
+
const contentBudget = Math.max(colCount, availW - overhead);
|
|
254
|
+
const totalNatural = naturalWidths.reduce((a, b) => a + b, 0);
|
|
255
|
+
|
|
256
|
+
// Compute final column widths
|
|
257
|
+
const colWidths: number[] = new Array(colCount).fill(0);
|
|
258
|
+
if (totalNatural <= contentBudget) {
|
|
259
|
+
// Everything fits
|
|
260
|
+
for (let c = 0; c < colCount; c++) colWidths[c] = naturalWidths[c];
|
|
261
|
+
} else {
|
|
262
|
+
// Proportionally shrink, with minimum of 4 chars per column
|
|
263
|
+
const minW = 4;
|
|
264
|
+
for (let c = 0; c < colCount; c++) {
|
|
265
|
+
colWidths[c] = Math.max(minW, Math.floor((naturalWidths[c] / totalNatural) * contentBudget));
|
|
266
|
+
}
|
|
267
|
+
// Distribute leftover from rounding
|
|
268
|
+
let used = colWidths.reduce((a, b) => a + b, 0);
|
|
269
|
+
for (let c = 0; c < colCount && used < contentBudget; c++) {
|
|
270
|
+
colWidths[c]++;
|
|
271
|
+
used++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const bc = '240'; // border color
|
|
276
|
+
|
|
277
|
+
// Helper: build a border line
|
|
278
|
+
const makeBorder = (left: string, mid: string, right: string, horiz: string): Line => {
|
|
279
|
+
let s = ' '.repeat(indent) + left;
|
|
280
|
+
for (let c = 0; c < colCount; c++) {
|
|
281
|
+
s += horiz.repeat(colWidths[c] + 2) + (c < colCount - 1 ? mid : right);
|
|
282
|
+
}
|
|
283
|
+
return UIFactory.stringToLine(s, width, { fg: bc });
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Helper: render a cell's content with inline markdown into Cell[]
|
|
287
|
+
const renderCellContent = (raw: string, maxW: number, isHdr: boolean): Cell[] => {
|
|
288
|
+
const cells: Cell[] = [];
|
|
289
|
+
const tokens = renderInlineMarkdown(raw);
|
|
290
|
+
let w = 0;
|
|
291
|
+
|
|
292
|
+
for (const token of tokens) {
|
|
293
|
+
const text = token.text;
|
|
294
|
+
for (const ch of text) {
|
|
295
|
+
const cw = getDisplayWidth(ch);
|
|
296
|
+
if (w + cw > maxW) {
|
|
297
|
+
// Truncate with ellipsis
|
|
298
|
+
if (cells.length > 0) cells[cells.length - 1] = createStyledCell('\u2026', cells[cells.length - 1]);
|
|
299
|
+
return cells;
|
|
300
|
+
}
|
|
301
|
+
let style: Partial<Cell> = {};
|
|
302
|
+
if (token.type === 'code') {
|
|
303
|
+
style = { fg: '#ffcc00', bg: '#1a1a1a' };
|
|
304
|
+
} else if (token.type === 'link') {
|
|
305
|
+
style = { fg: '#00aaff', underline: true };
|
|
306
|
+
} else {
|
|
307
|
+
style = { ...token.style };
|
|
308
|
+
}
|
|
309
|
+
if (isHdr) {
|
|
310
|
+
style.fg = style.fg || '#00ffff';
|
|
311
|
+
style.bold = true;
|
|
312
|
+
} else {
|
|
313
|
+
style.fg = style.fg || '252';
|
|
314
|
+
}
|
|
315
|
+
cells.push(createStyledCell(ch, style));
|
|
316
|
+
if (cw === 2) cells.push(createStyledCell('', style)); // wide char placeholder
|
|
317
|
+
w += cw;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Pad remaining space
|
|
322
|
+
while (w < maxW) {
|
|
323
|
+
cells.push(createStyledCell(' ', isHdr ? { fg: '#00ffff' } : { fg: '252' }));
|
|
324
|
+
w++;
|
|
325
|
+
}
|
|
326
|
+
return cells;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Top border
|
|
330
|
+
lines.push(makeBorder('\u250c', '\u252c', '\u2510', '\u2500'));
|
|
331
|
+
|
|
332
|
+
// Rows
|
|
333
|
+
for (let r = 0; r < parsedRows.length; r++) {
|
|
334
|
+
const row = parsedRows[r];
|
|
335
|
+
const isHeader = hasSeparator && r === 0;
|
|
336
|
+
|
|
337
|
+
// Build the row line cell-by-cell
|
|
338
|
+
const line = new Array(width).fill(null).map(() => createStyledCell(' ')) as Cell[];
|
|
339
|
+
let x = indent;
|
|
340
|
+
|
|
341
|
+
// Left border
|
|
342
|
+
if (x < width) line[x] = createStyledCell('\u2502', { fg: bc });
|
|
343
|
+
x++;
|
|
344
|
+
|
|
345
|
+
for (let c = 0; c < colCount; c++) {
|
|
346
|
+
// Space before content
|
|
347
|
+
if (x < width) line[x] = createStyledCell(' ');
|
|
348
|
+
x++;
|
|
349
|
+
|
|
350
|
+
// Cell content
|
|
351
|
+
const raw = c < row.length ? row[c] : '';
|
|
352
|
+
const cellContent = renderCellContent(raw, colWidths[c], isHeader);
|
|
353
|
+
for (const cell of cellContent) {
|
|
354
|
+
if (x < width) line[x] = cell;
|
|
355
|
+
x++;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Space after content
|
|
359
|
+
if (x < width) line[x] = createStyledCell(' ');
|
|
360
|
+
x++;
|
|
361
|
+
|
|
362
|
+
// Column separator
|
|
363
|
+
if (x < width) line[x] = createStyledCell('\u2502', { fg: bc });
|
|
364
|
+
x++;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
lines.push(line);
|
|
368
|
+
|
|
369
|
+
// Header separator
|
|
370
|
+
if (isHeader) {
|
|
371
|
+
lines.push(makeBorder('\u251c', '\u253c', '\u2524', '\u2500'));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Bottom border
|
|
376
|
+
lines.push(makeBorder('\u2514', '\u2534', '\u2518', '\u2500'));
|
|
377
|
+
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Inline markdown token types.
|
|
383
|
+
*/
|
|
384
|
+
type InlineToken =
|
|
385
|
+
| { type: 'text'; text: string; style: Partial<Cell> }
|
|
386
|
+
| { type: 'code'; text: string }
|
|
387
|
+
| { type: 'link'; text: string; url: string };
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* renderInlineMarkdown - Parse inline markdown (bold, italic, inline code, links)
|
|
391
|
+
* and return a flat array of tokens with style info.
|
|
392
|
+
*/
|
|
393
|
+
export function renderInlineMarkdown(text: string): InlineToken[] {
|
|
394
|
+
const tokens: InlineToken[] = [];
|
|
395
|
+
let i = 0;
|
|
396
|
+
|
|
397
|
+
while (i < text.length) {
|
|
398
|
+
// Link: [text](url)
|
|
399
|
+
if (text[i] === '[') {
|
|
400
|
+
const closeB = text.indexOf(']', i);
|
|
401
|
+
if (closeB !== -1 && text[closeB + 1] === '(') {
|
|
402
|
+
const closeP = text.indexOf(')', closeB + 2);
|
|
403
|
+
if (closeP !== -1) {
|
|
404
|
+
const linkText = text.slice(i + 1, closeB);
|
|
405
|
+
const url = text.slice(closeB + 2, closeP);
|
|
406
|
+
tokens.push({ type: 'link', text: linkText, url });
|
|
407
|
+
i = closeP + 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Inline code: `code`
|
|
414
|
+
if (text[i] === '`') {
|
|
415
|
+
const end = text.indexOf('`', i + 1);
|
|
416
|
+
if (end !== -1) {
|
|
417
|
+
tokens.push({ type: 'code', text: text.slice(i + 1, end) });
|
|
418
|
+
i = end + 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Bold+italic: ***text***
|
|
424
|
+
if (text.slice(i, i + 3) === '***') {
|
|
425
|
+
const end = text.indexOf('***', i + 3);
|
|
426
|
+
if (end !== -1) {
|
|
427
|
+
tokens.push({ type: 'text', text: text.slice(i + 3, end), style: { bold: true, italic: true } });
|
|
428
|
+
i = end + 3;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// No closing *** found — emit the leading * as plain text so the ** bold
|
|
432
|
+
// check can handle the remaining ** on the next iteration.
|
|
433
|
+
tokens.push({ type: 'text', text: '*', style: {} });
|
|
434
|
+
i += 1;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Bold: **text**
|
|
439
|
+
if (text.slice(i, i + 2) === '**') {
|
|
440
|
+
const end = text.indexOf('**', i + 2);
|
|
441
|
+
if (end !== -1) {
|
|
442
|
+
tokens.push({ type: 'text', text: text.slice(i + 2, end), style: { bold: true } });
|
|
443
|
+
i = end + 2;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Italic: *text* or _text_
|
|
449
|
+
// Guard: text[i - 1] !== '*' prevents the second * of ** from starting italic.
|
|
450
|
+
// Guard: text[i + 1] !== text[i] prevents the first * of ** (or _ of __) from
|
|
451
|
+
// starting italic when bold/underscore-bold detection failed (e.g. unclosed **).
|
|
452
|
+
if (
|
|
453
|
+
(text[i] === '*' || text[i] === '_') &&
|
|
454
|
+
text[i - 1] !== text[i] &&
|
|
455
|
+
text[i + 1] !== text[i]
|
|
456
|
+
) {
|
|
457
|
+
const closer = text[i];
|
|
458
|
+
const end = text.indexOf(closer, i + 1);
|
|
459
|
+
if (end !== -1 && end > i + 1) {
|
|
460
|
+
tokens.push({ type: 'text', text: text.slice(i + 1, end), style: { italic: true } });
|
|
461
|
+
i = end + 1;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Strikethrough: ~~text~~
|
|
467
|
+
if (text.slice(i, i + 2) === '~~') {
|
|
468
|
+
const end = text.indexOf('~~', i + 2);
|
|
469
|
+
if (end !== -1) {
|
|
470
|
+
tokens.push({ type: 'text', text: text.slice(i + 2, end), style: { strikethrough: true, fg: '244' } });
|
|
471
|
+
i = end + 2;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Plain text — accumulate until next special char, detect bare URLs and file paths
|
|
477
|
+
let end = i + 1;
|
|
478
|
+
while (end < text.length && !INLINE_SPECIAL_CHARS.has(text[end])) {
|
|
479
|
+
const code = text.charCodeAt(end);
|
|
480
|
+
end += (code >= 0xD800 && code <= 0xDBFF) ? 2 : 1;
|
|
481
|
+
}
|
|
482
|
+
const plainText = text.slice(i, end);
|
|
483
|
+
|
|
484
|
+
// Detect http/https URLs in plain text
|
|
485
|
+
const urlMatch = plainText.match(/^(https?:\/\/[^\s,)>"]+)/);
|
|
486
|
+
if (urlMatch) {
|
|
487
|
+
const url = urlMatch[1];
|
|
488
|
+
tokens.push({ type: 'link', text: url, url });
|
|
489
|
+
i += url.length;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Detect absolute file paths in plain text (require at least one directory separator)
|
|
494
|
+
const fileMatch = plainText.match(/^(\/[^\s,)>"]+\/[^\s,)>"]+(?:\.[a-zA-Z0-9]+)?)/);
|
|
495
|
+
if (fileMatch) {
|
|
496
|
+
const filePath = fileMatch[1];
|
|
497
|
+
const fileUrl = `file://${filePath}`;
|
|
498
|
+
tokens.push({ type: 'link', text: filePath, url: fileUrl });
|
|
499
|
+
i += filePath.length;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
tokens.push({ type: 'text', text: plainText, style: {} });
|
|
504
|
+
i = end;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return tokens;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* compositeInlineLine - Convert a prefix + InlineTokens into Line[], applying word wrap.
|
|
512
|
+
* Builds cells directly from token styles.
|
|
513
|
+
*/
|
|
514
|
+
function compositeInlineLine(
|
|
515
|
+
prefix: string,
|
|
516
|
+
tokens: InlineToken[],
|
|
517
|
+
width: number,
|
|
518
|
+
prefixStyle: Partial<Cell>,
|
|
519
|
+
textStartX: number
|
|
520
|
+
): Line[] {
|
|
521
|
+
const lines: Line[] = [];
|
|
522
|
+
|
|
523
|
+
// Flatten tokens to [char, style] pairs
|
|
524
|
+
type StyledChar = { char: string; style: Partial<Cell> };
|
|
525
|
+
const chars: StyledChar[] = [];
|
|
526
|
+
|
|
527
|
+
for (const token of tokens) {
|
|
528
|
+
if (token.type === 'text') {
|
|
529
|
+
for (const ch of token.text) chars.push({ char: ch, style: token.style });
|
|
530
|
+
} else if (token.type === 'code') {
|
|
531
|
+
for (const ch of token.text) chars.push({ char: ch, style: { fg: '#ffcc00', bg: '#1a1a1a' } });
|
|
532
|
+
} else if (token.type === 'link') {
|
|
533
|
+
// Resolve URL: if url is empty or relative, treat as text; if it's a file path, use file:// protocol
|
|
534
|
+
let resolvedUrl = token.url;
|
|
535
|
+
if (resolvedUrl && !resolvedUrl.startsWith('http') && !resolvedUrl.startsWith('file://') && resolvedUrl.startsWith('/')) {
|
|
536
|
+
resolvedUrl = `file://${resolvedUrl}`;
|
|
537
|
+
}
|
|
538
|
+
for (const ch of token.text) chars.push({ char: ch, style: { fg: '#00aaff', underline: true, link: resolvedUrl || undefined } });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Render with simple line-breaking at width
|
|
543
|
+
const availW = width - textStartX;
|
|
544
|
+
if (availW <= 0) return lines;
|
|
545
|
+
|
|
546
|
+
let lineChars: StyledChar[] = [];
|
|
547
|
+
let lineW = 0;
|
|
548
|
+
|
|
549
|
+
const flushLine = (isFirst: boolean) => {
|
|
550
|
+
const line = new Array(width).fill(null).map(() => createStyledCell(' ')) as Cell[];
|
|
551
|
+
// Write prefix on first line
|
|
552
|
+
if (isFirst) {
|
|
553
|
+
let px = 0;
|
|
554
|
+
for (const ch of prefix) {
|
|
555
|
+
if (px >= width) break;
|
|
556
|
+
const cw = getDisplayWidth(ch);
|
|
557
|
+
line[px] = createStyledCell(ch, { fg: prefixStyle.fg, bg: prefixStyle.bg, bold: prefixStyle.bold, dim: prefixStyle.dim, underline: prefixStyle.underline, italic: prefixStyle.italic, strikethrough: prefixStyle.strikethrough });
|
|
558
|
+
if (cw === 2 && px + 1 < width) line[px + 1] = { ...line[px], char: '' };
|
|
559
|
+
px += cw;
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
// indent-only for continuation lines
|
|
563
|
+
for (let x = 0; x < textStartX && x < width; x++) {
|
|
564
|
+
line[x] = createStyledCell(' ');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Write content chars
|
|
568
|
+
let cx = textStartX;
|
|
569
|
+
for (const sc of lineChars) {
|
|
570
|
+
if (cx >= width) break;
|
|
571
|
+
const cw = getDisplayWidth(sc.char);
|
|
572
|
+
line[cx] = createStyledCell(sc.char, sc.style);
|
|
573
|
+
if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
|
|
574
|
+
cx += cw;
|
|
575
|
+
}
|
|
576
|
+
lines.push(line);
|
|
577
|
+
lineChars = [];
|
|
578
|
+
lineW = 0;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// Word-aware line breaking: accumulate words, break at spaces
|
|
582
|
+
let isFirstLine = true;
|
|
583
|
+
let wordChars: StyledChar[] = [];
|
|
584
|
+
let wordW = 0;
|
|
585
|
+
|
|
586
|
+
const flushWord = () => {
|
|
587
|
+
// If the word doesn't fit on the current line, wrap first
|
|
588
|
+
if (lineW > 0 && lineW + wordW > availW) {
|
|
589
|
+
flushLine(isFirstLine);
|
|
590
|
+
isFirstLine = false;
|
|
591
|
+
}
|
|
592
|
+
// If a single word is wider than availW, force-break it character by character
|
|
593
|
+
if (wordW > availW) {
|
|
594
|
+
for (const sc of wordChars) {
|
|
595
|
+
const cw = getDisplayWidth(sc.char);
|
|
596
|
+
if (lineW + cw > availW && lineW > 0) {
|
|
597
|
+
flushLine(isFirstLine);
|
|
598
|
+
isFirstLine = false;
|
|
599
|
+
}
|
|
600
|
+
lineChars.push(sc);
|
|
601
|
+
lineW += cw;
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
lineChars.push(...wordChars);
|
|
605
|
+
lineW += wordW;
|
|
606
|
+
}
|
|
607
|
+
wordChars = [];
|
|
608
|
+
wordW = 0;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
for (const sc of chars) {
|
|
612
|
+
const cw = getDisplayWidth(sc.char);
|
|
613
|
+
if (sc.char === ' ') {
|
|
614
|
+
// Space: flush current word, then add the space
|
|
615
|
+
flushWord();
|
|
616
|
+
if (lineW + cw > availW && lineW > 0) {
|
|
617
|
+
flushLine(isFirstLine);
|
|
618
|
+
isFirstLine = false;
|
|
619
|
+
}
|
|
620
|
+
lineChars.push(sc);
|
|
621
|
+
lineW += cw;
|
|
622
|
+
} else {
|
|
623
|
+
// Non-space: accumulate into current word
|
|
624
|
+
wordChars.push(sc);
|
|
625
|
+
wordW += cw;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Flush remaining word
|
|
629
|
+
if (wordChars.length > 0) flushWord();
|
|
630
|
+
if (lineChars.length > 0 || isFirstLine) {
|
|
631
|
+
flushLine(isFirstLine);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return lines;
|
|
635
|
+
}
|