@alumbwe/anvil 1.0.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/README.md +84 -0
- package/bin/anvil +2 -0
- package/bin/tree-sitter.wasm +0 -0
- package/package.json +67 -0
- package/src/__tests__/README.md +256 -0
- package/src/__tests__/bash-mode.test.ts +442 -0
- package/src/__tests__/cli-args.test.ts +181 -0
- package/src/__tests__/cli-harness.test.ts +52 -0
- package/src/__tests__/e2e/first-time-login.test.ts +173 -0
- package/src/__tests__/e2e/logout-relogin-flow.test.ts +110 -0
- package/src/__tests__/e2e/returning-user-auth.test.ts +113 -0
- package/src/__tests__/e2e-cli.test.ts +183 -0
- package/src/__tests__/helpers/launcher-disconnect-fixture.cjs +176 -0
- package/src/__tests__/helpers/mock-api-client.ts +64 -0
- package/src/__tests__/helpers/renderer-cleanup-fixture.tsx +89 -0
- package/src/__tests__/helpers/terminal-watchdog-fixture.ts +75 -0
- package/src/__tests__/home-directory-detection.test.ts +213 -0
- package/src/__tests__/integration/api-integration.test.ts +288 -0
- package/src/__tests__/integration/credentials-storage.test.ts +486 -0
- package/src/__tests__/integration/local-agents.test.ts +1187 -0
- package/src/__tests__/integration/login-flow-instrumentation.test.ts +134 -0
- package/src/__tests__/integration/login-polling-working.test.ts +285 -0
- package/src/__tests__/integration/queue-editing.test.tsx +261 -0
- package/src/__tests__/integration/usage-refresh-on-completion.test.ts +168 -0
- package/src/__tests__/integration-tmux.test.ts +188 -0
- package/src/__tests__/launcher-avx2-fallback.test.ts +630 -0
- package/src/__tests__/launcher-disconnect.test.ts +104 -0
- package/src/__tests__/mocks/hover-toggle-controller.ts +71 -0
- package/src/__tests__/open-tui-native-bundle.test.ts +378 -0
- package/src/__tests__/path-completion.test.ts +243 -0
- package/src/__tests__/project-files-chat-id.test.ts +34 -0
- package/src/__tests__/release/proxy-http-get.test.ts +667 -0
- package/src/__tests__/release/wrapper-safety.test.ts +412 -0
- package/src/__tests__/renderer-cleanup.test.ts +174 -0
- package/src/__tests__/rerender-perf.integration.test.ts +283 -0
- package/src/__tests__/terminal-reset-sequences.test.ts +36 -0
- package/src/__tests__/terminal-watchdog.test.ts +388 -0
- package/src/__tests__/test-utils.ts +271 -0
- package/src/__tests__/tmux-poc.ts +150 -0
- package/src/__tests__/unit/agent-mode-toggle.test.ts +137 -0
- package/src/__tests__/unit/copy-button.test.ts +157 -0
- package/src/__tests__/unit/create-run-config.test.ts +66 -0
- package/src/__tests__/unit/publish-confirmation.test.ts +70 -0
- package/src/__tests__/unit/segmented-control.test.ts +152 -0
- package/src/__tests__/utils/env.test.ts +169 -0
- package/src/__tests__/utils/project-picker.test.ts +39 -0
- package/src/agents/bundled-agents.generated.d.ts +14 -0
- package/src/agents/bundled-agents.generated.ts +3692 -0
- package/src/app.tsx +399 -0
- package/src/chat.tsx +1698 -0
- package/src/cli-args.ts +134 -0
- package/src/commands/__tests__/bash-command.test.ts +445 -0
- package/src/commands/__tests__/command-args.test.ts +349 -0
- package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
- package/src/commands/__tests__/copy-conversation.test.ts +226 -0
- package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
- package/src/commands/__tests__/freebuff-command-aliases.test.ts +56 -0
- package/src/commands/__tests__/image.test.ts +99 -0
- package/src/commands/__tests__/init.test.ts +456 -0
- package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
- package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
- package/src/commands/__tests__/prompt-builders.test.ts +28 -0
- package/src/commands/__tests__/router-input.test.ts +298 -0
- package/src/commands/ads.ts +44 -0
- package/src/commands/command-registry.ts +726 -0
- package/src/commands/copy-conversation.ts +392 -0
- package/src/commands/help.ts +13 -0
- package/src/commands/image.ts +20 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/process-diagnostics.ts +120 -0
- package/src/commands/prompt-builders.ts +100 -0
- package/src/commands/publish.ts +203 -0
- package/src/commands/router-utils.ts +80 -0
- package/src/commands/router.ts +487 -0
- package/src/commands/usage.ts +26 -0
- package/src/components/__tests__/ad-banner.test.tsx +83 -0
- package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
- package/src/components/__tests__/freebuff-landing-heading-row.test.tsx +91 -0
- package/src/components/__tests__/freebuff-model-selector.test.tsx +519 -0
- package/src/components/__tests__/freebuff-offer-invariants.test.ts +54 -0
- package/src/components/__tests__/freebuff-takeover-prompt.test.tsx +212 -0
- package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
- package/src/components/__tests__/grid-layout.test.tsx +1051 -0
- package/src/components/__tests__/message-block.completion.test.tsx +104 -0
- package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
- package/src/components/__tests__/message-with-agents.test.tsx +579 -0
- package/src/components/__tests__/multiline-input.test.tsx +1132 -0
- package/src/components/__tests__/queue-panel.test.tsx +398 -0
- package/src/components/__tests__/selectable-list.test.ts +232 -0
- package/src/components/__tests__/status-bar.test.tsx +44 -0
- package/src/components/__tests__/status-indicator.test.tsx +204 -0
- package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
- package/src/components/__tests__/user-error-banner.test.tsx +102 -0
- package/src/components/ad-banner.tsx +323 -0
- package/src/components/agent-checklist.tsx +350 -0
- package/src/components/agent-mode-toggle.tsx +241 -0
- package/src/components/anvil-mark.tsx +52 -0
- package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
- package/src/components/ask-user/__tests__/validation.test.ts +213 -0
- package/src/components/ask-user/components/accordion-question.tsx +153 -0
- package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
- package/src/components/ask-user/components/options-list.tsx +133 -0
- package/src/components/ask-user/components/question-header.tsx +80 -0
- package/src/components/ask-user/components/question-option.tsx +89 -0
- package/src/components/ask-user/constants.ts +38 -0
- package/src/components/ask-user/index.tsx +631 -0
- package/src/components/ask-user/utils/validation.ts +85 -0
- package/src/components/attachment-card.tsx +65 -0
- package/src/components/blocks/agent-block-grid.tsx +58 -0
- package/src/components/blocks/agent-branch-item.tsx +311 -0
- package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
- package/src/components/blocks/agent-list-branch.tsx +77 -0
- package/src/components/blocks/ask-user-branch.tsx +81 -0
- package/src/components/blocks/block-helpers.ts +11 -0
- package/src/components/blocks/blocks-renderer.tsx +289 -0
- package/src/components/blocks/content-with-markdown.tsx +33 -0
- package/src/components/blocks/image-block.tsx +128 -0
- package/src/components/blocks/implementor-row.tsx +429 -0
- package/src/components/blocks/single-block.tsx +201 -0
- package/src/components/blocks/thinking-block.tsx +67 -0
- package/src/components/blocks/tool-block-group.tsx +57 -0
- package/src/components/blocks/tool-branch.tsx +206 -0
- package/src/components/blocks/user-content-copy.tsx +157 -0
- package/src/components/bottom-banner.tsx +136 -0
- package/src/components/build-mode-buttons.tsx +110 -0
- package/src/components/button.tsx +66 -0
- package/src/components/chat-header.tsx +77 -0
- package/src/components/chat-history-screen.tsx +413 -0
- package/src/components/chat-input-bar.tsx +523 -0
- package/src/components/clickable-title-box.tsx +66 -0
- package/src/components/clickable.tsx +119 -0
- package/src/components/collapse-button.tsx +34 -0
- package/src/components/copy-button.tsx +203 -0
- package/src/components/elapsed-timer.tsx +55 -0
- package/src/components/error-boundary.tsx +55 -0
- package/src/components/feedback-container.tsx +181 -0
- package/src/components/feedback-icon-button.tsx +89 -0
- package/src/components/feedback-input-mode.tsx +344 -0
- package/src/components/file-attachment-card.tsx +98 -0
- package/src/components/freebuff-active-session-summary.tsx +63 -0
- package/src/components/freebuff-landing-screen.tsx +865 -0
- package/src/components/freebuff-model-selector.tsx +1087 -0
- package/src/components/freebuff-referral-banner.tsx +604 -0
- package/src/components/freebuff-superseded-screen.tsx +62 -0
- package/src/components/grid-layout.tsx +80 -0
- package/src/components/help-banner.tsx +127 -0
- package/src/components/highlighted-text.tsx +52 -0
- package/src/components/image-card.tsx +167 -0
- package/src/components/image-thumbnail.tsx +114 -0
- package/src/components/input-cursor.tsx +77 -0
- package/src/components/input-mode-banner.tsx +57 -0
- package/src/components/load-previous-button.tsx +48 -0
- package/src/components/login-modal.tsx +475 -0
- package/src/components/message-block.tsx +346 -0
- package/src/components/message-footer.tsx +258 -0
- package/src/components/message-with-agents.tsx +506 -0
- package/src/components/mode-divider.tsx +43 -0
- package/src/components/multiline-input.tsx +1241 -0
- package/src/components/out-of-credits-banner.tsx +160 -0
- package/src/components/pending-attachments-banner.tsx +116 -0
- package/src/components/pending-bash-message.tsx +59 -0
- package/src/components/progress-bar.tsx +81 -0
- package/src/components/project-picker-screen.tsx +485 -0
- package/src/components/publish-confirmation.tsx +493 -0
- package/src/components/publish-container.tsx +687 -0
- package/src/components/queue-panel.tsx +310 -0
- package/src/components/raised-pill.tsx +95 -0
- package/src/components/renderers/plan-box.tsx +59 -0
- package/src/components/review-screen.tsx +112 -0
- package/src/components/scroll-to-bottom-button.tsx +34 -0
- package/src/components/segmented-control.tsx +187 -0
- package/src/components/selectable-list.tsx +248 -0
- package/src/components/selected-chips.tsx +80 -0
- package/src/components/separator.tsx +40 -0
- package/src/components/session-ended-banner.tsx +226 -0
- package/src/components/shimmer-text.tsx +248 -0
- package/src/components/status-bar.tsx +313 -0
- package/src/components/subscription-limit-banner.tsx +206 -0
- package/src/components/suggested-prompts.tsx +184 -0
- package/src/components/suggestion-menu.tsx +215 -0
- package/src/components/terminal-command-display.tsx +151 -0
- package/src/components/terminal-link.tsx +97 -0
- package/src/components/terminal-thinking-orb.tsx +95 -0
- package/src/components/text-attachment-card.tsx +77 -0
- package/src/components/thinking.tsx +109 -0
- package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
- package/src/components/tools/__tests__/code-search.test.tsx +45 -0
- package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
- package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
- package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
- package/src/components/tools/apply-patch.tsx +95 -0
- package/src/components/tools/code-search.tsx +45 -0
- package/src/components/tools/composio.tsx +152 -0
- package/src/components/tools/diff-viewer.tsx +76 -0
- package/src/components/tools/glob.tsx +61 -0
- package/src/components/tools/gravity-index.tsx +77 -0
- package/src/components/tools/list-directory.tsx +61 -0
- package/src/components/tools/read-docs.tsx +37 -0
- package/src/components/tools/read-files.tsx +91 -0
- package/src/components/tools/read-subtree.tsx +44 -0
- package/src/components/tools/read-url.tsx +33 -0
- package/src/components/tools/registry.ts +126 -0
- package/src/components/tools/render-ui.tsx +122 -0
- package/src/components/tools/run-terminal-command.tsx +79 -0
- package/src/components/tools/skill.tsx +29 -0
- package/src/components/tools/str-replace.tsx +77 -0
- package/src/components/tools/suggest-followups.tsx +369 -0
- package/src/components/tools/task-completed.tsx +16 -0
- package/src/components/tools/tool-call-item.tsx +273 -0
- package/src/components/tools/types.ts +55 -0
- package/src/components/tools/web-search.tsx +33 -0
- package/src/components/tools/write-file.tsx +16 -0
- package/src/components/tools/write-todos.tsx +95 -0
- package/src/components/top-banner.tsx +178 -0
- package/src/components/usage-banner.tsx +244 -0
- package/src/components/user-error-banner.tsx +56 -0
- package/src/components/validation-error-popover.tsx +192 -0
- package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
- package/src/contexts/chat-runtime-context.tsx +240 -0
- package/src/data/slash-commands.test.ts +24 -0
- package/src/data/slash-commands.ts +249 -0
- package/src/entry.ts +12 -0
- package/src/hooks/__tests__/holds-live-freebuff-slot.test.ts +49 -0
- package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
- package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
- package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
- package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
- package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
- package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
- package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
- package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
- package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
- package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
- package/src/hooks/__tests__/use-input-history.test.ts +699 -0
- package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
- package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
- package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
- package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
- package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
- package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
- package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
- package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
- package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
- package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
- package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
- package/src/hooks/__tests__/use-timeout.test.ts +330 -0
- package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
- package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
- package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
- package/src/hooks/helpers/send-message.ts +654 -0
- package/src/hooks/stream-state.ts +83 -0
- package/src/hooks/use-activity-query.ts +668 -0
- package/src/hooks/use-agent-validation.ts +74 -0
- package/src/hooks/use-ask-user-bridge.ts +92 -0
- package/src/hooks/use-auth-query.ts +253 -0
- package/src/hooks/use-auth-state.ts +152 -0
- package/src/hooks/use-chat-input.ts +105 -0
- package/src/hooks/use-chat-keyboard.ts +321 -0
- package/src/hooks/use-chat-messages.ts +254 -0
- package/src/hooks/use-chat-state.ts +165 -0
- package/src/hooks/use-chat-streaming.ts +180 -0
- package/src/hooks/use-chat-ui.ts +131 -0
- package/src/hooks/use-clipboard.ts +151 -0
- package/src/hooks/use-connection-status.ts +142 -0
- package/src/hooks/use-directory-browser.ts +88 -0
- package/src/hooks/use-elapsed-time.ts +116 -0
- package/src/hooks/use-event.ts +34 -0
- package/src/hooks/use-exit-handler.ts +66 -0
- package/src/hooks/use-fetch-login-url.ts +65 -0
- package/src/hooks/use-fingerprint.ts +61 -0
- package/src/hooks/use-freebuff-ctrl-c-exit.ts +23 -0
- package/src/hooks/use-freebuff-session-progress.ts +34 -0
- package/src/hooks/use-freebuff-session.ts +806 -0
- package/src/hooks/use-freebuff-streak-query.ts +61 -0
- package/src/hooks/use-gravity-ad.ts +656 -0
- package/src/hooks/use-grid-layout.ts +73 -0
- package/src/hooks/use-input-history.ts +173 -0
- package/src/hooks/use-login-keyboard-handlers.ts +75 -0
- package/src/hooks/use-login-polling.ts +114 -0
- package/src/hooks/use-logo.tsx +161 -0
- package/src/hooks/use-message-queue.ts +387 -0
- package/src/hooks/use-now.ts +20 -0
- package/src/hooks/use-path-tab-completion.ts +88 -0
- package/src/hooks/use-publish-mutation.ts +60 -0
- package/src/hooks/use-queue-controls.ts +68 -0
- package/src/hooks/use-queue-ui.ts +74 -0
- package/src/hooks/use-scroll-management.ts +184 -0
- package/src/hooks/use-searchable-list.ts +98 -0
- package/src/hooks/use-send-message.ts +773 -0
- package/src/hooks/use-sheen-animation.tsx +88 -0
- package/src/hooks/use-subscription-query.ts +71 -0
- package/src/hooks/use-suggestion-engine.ts +783 -0
- package/src/hooks/use-terminal-breakpoints.ts +77 -0
- package/src/hooks/use-terminal-dimensions.ts +46 -0
- package/src/hooks/use-terminal-focus.ts +29 -0
- package/src/hooks/use-terminal-layout.ts +189 -0
- package/src/hooks/use-theme.tsx +140 -0
- package/src/hooks/use-timeout.ts +85 -0
- package/src/hooks/use-update-preference.ts +66 -0
- package/src/hooks/use-usage-monitor.ts +55 -0
- package/src/hooks/use-usage-query.ts +124 -0
- package/src/hooks/use-user-details-query.ts +93 -0
- package/src/hooks/use-why-did-you-update.ts +177 -0
- package/src/index.tsx +441 -0
- package/src/init/__tests__/init-direnv.test.ts +526 -0
- package/src/init/init-app.ts +34 -0
- package/src/init/init-direnv.ts +133 -0
- package/src/login/constants.ts +60 -0
- package/src/login/login-flow.ts +204 -0
- package/src/login/plain-login.ts +108 -0
- package/src/login/utils.ts +183 -0
- package/src/native/ripgrep.ts +80 -0
- package/src/polyfills/bun-strip-ansi.ts +12 -0
- package/src/pre-init/tree-sitter-wasm.ts +96 -0
- package/src/project-files.ts +102 -0
- package/src/smoke/terminal-command-broker.ts +439 -0
- package/src/state/__tests__/chat-store-focus.test.ts +22 -0
- package/src/state/__tests__/feedback-store.test.ts +257 -0
- package/src/state/chat-history-store.ts +30 -0
- package/src/state/chat-store.ts +544 -0
- package/src/state/feedback-store.ts +160 -0
- package/src/state/freebuff-model-store.ts +42 -0
- package/src/state/freebuff-session-store.ts +53 -0
- package/src/state/login-store.ts +150 -0
- package/src/state/message-block-store.ts +149 -0
- package/src/state/publish-store.ts +146 -0
- package/src/state/queue-panel-store.ts +26 -0
- package/src/state/review-store.ts +24 -0
- package/src/testing/env.ts +46 -0
- package/src/types/chat-state.ts +15 -0
- package/src/types/chat.ts +241 -0
- package/src/types/contracts/send-message.ts +12 -0
- package/src/types/env.ts +98 -0
- package/src/types/freebuff-session.ts +17 -0
- package/src/types/function-params.ts +28 -0
- package/src/types/react19-compat.d.ts +19 -0
- package/src/types/store.ts +111 -0
- package/src/types/theme-system.ts +144 -0
- package/src/types/utils.ts +3 -0
- package/src/utils/__tests__/active-run.test.ts +140 -0
- package/src/utils/__tests__/activity-tracker.test.ts +440 -0
- package/src/utils/__tests__/agent-display.test.ts +139 -0
- package/src/utils/__tests__/analytics-client.test.ts +262 -0
- package/src/utils/__tests__/anonymous-id.test.ts +74 -0
- package/src/utils/__tests__/anvil-api.test.ts +578 -0
- package/src/utils/__tests__/arrays.test.ts +264 -0
- package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
- package/src/utils/__tests__/block-processor.test.ts +810 -0
- package/src/utils/__tests__/chat-history.test.ts +203 -0
- package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
- package/src/utils/__tests__/chat-meta.test.ts +130 -0
- package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
- package/src/utils/__tests__/clipboard.test.ts +910 -0
- package/src/utils/__tests__/code-search-summary.test.ts +84 -0
- package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
- package/src/utils/__tests__/error-handling.test.ts +658 -0
- package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
- package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
- package/src/utils/__tests__/feedback-submission.test.ts +26 -0
- package/src/utils/__tests__/fetch-usage.test.ts +355 -0
- package/src/utils/__tests__/fingerprint.test.ts +144 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
- package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
- package/src/utils/__tests__/format-timeout.test.ts +87 -0
- package/src/utils/__tests__/freebuff-instance-owner.test.ts +69 -0
- package/src/utils/__tests__/freebuff-model-navigation.test.ts +102 -0
- package/src/utils/__tests__/freebuff-premium-reset.test.ts +79 -0
- package/src/utils/__tests__/freebuff-referral-cache.test.ts +116 -0
- package/src/utils/__tests__/freebuff-session-api.test.ts +215 -0
- package/src/utils/__tests__/freebuff-session-display.test.ts +23 -0
- package/src/utils/__tests__/freebuff-streak-line.test.ts +217 -0
- package/src/utils/__tests__/image-dimensions.test.ts +249 -0
- package/src/utils/__tests__/image-processor.test.ts +209 -0
- package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
- package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
- package/src/utils/__tests__/layout-helpers.test.ts +36 -0
- package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
- package/src/utils/__tests__/log-shipper.test.ts +53 -0
- package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
- package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
- package/src/utils/__tests__/message-updater.test.ts +645 -0
- package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
- package/src/utils/__tests__/pending-attachments.test.ts +277 -0
- package/src/utils/__tests__/polling-backoff.test.ts +51 -0
- package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
- package/src/utils/__tests__/run-state-storage.test.ts +919 -0
- package/src/utils/__tests__/safe-json.test.ts +77 -0
- package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
- package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
- package/src/utils/__tests__/send-message-timer.test.ts +64 -0
- package/src/utils/__tests__/settings.test.ts +99 -0
- package/src/utils/__tests__/strings.test.ts +192 -0
- package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
- package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
- package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
- package/src/utils/__tests__/terminal-io.test.ts +137 -0
- package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
- package/src/utils/__tests__/text-layout.test.ts +82 -0
- package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
- package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
- package/src/utils/__tests__/trace-writer.test.ts +223 -0
- package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
- package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
- package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
- package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
- package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
- package/src/utils/active-run.ts +131 -0
- package/src/utils/activity-tracker.ts +66 -0
- package/src/utils/agent-display.ts +87 -0
- package/src/utils/agent-helpers.ts +27 -0
- package/src/utils/agent-id-utils.ts +14 -0
- package/src/utils/ai-message-id.ts +12 -0
- package/src/utils/analytics.ts +361 -0
- package/src/utils/anonymous-id.ts +87 -0
- package/src/utils/anvil-api.ts +612 -0
- package/src/utils/anvil-client.ts +217 -0
- package/src/utils/arrays.ts +117 -0
- package/src/utils/auth.ts +258 -0
- package/src/utils/bash-context-processor.ts +47 -0
- package/src/utils/bash-messages.ts +108 -0
- package/src/utils/block-margins.ts +35 -0
- package/src/utils/block-operations.ts +523 -0
- package/src/utils/block-processor.ts +213 -0
- package/src/utils/chat-history.ts +253 -0
- package/src/utils/chat-input-key-intercept.ts +52 -0
- package/src/utils/chat-meta.ts +90 -0
- package/src/utils/chat-scroll-accel.ts +151 -0
- package/src/utils/clipboard-image.ts +588 -0
- package/src/utils/clipboard.ts +397 -0
- package/src/utils/code-search-summary.ts +70 -0
- package/src/utils/collapse-helpers.ts +256 -0
- package/src/utils/config-dir.ts +24 -0
- package/src/utils/constants.ts +190 -0
- package/src/utils/create-event-handler-state.ts +73 -0
- package/src/utils/create-run-config.ts +121 -0
- package/src/utils/detect-shell.ts +116 -0
- package/src/utils/directory-browser.ts +72 -0
- package/src/utils/engagement.ts +55 -0
- package/src/utils/env.ts +92 -0
- package/src/utils/error-handling.ts +255 -0
- package/src/utils/error-messages.ts +62 -0
- package/src/utils/exit-cleanly.ts +86 -0
- package/src/utils/feedback-helpers.ts +103 -0
- package/src/utils/feedback-submission.ts +22 -0
- package/src/utils/fetch-usage.ts +76 -0
- package/src/utils/fingerprint.ts +245 -0
- package/src/utils/format-elapsed-time.ts +33 -0
- package/src/utils/format-session-units.ts +6 -0
- package/src/utils/format-timeout.ts +28 -0
- package/src/utils/format-validation-errors-for-message.ts +84 -0
- package/src/utils/freebuff-agent-selection.ts +33 -0
- package/src/utils/freebuff-instance-owner.ts +66 -0
- package/src/utils/freebuff-model-navigation.ts +50 -0
- package/src/utils/freebuff-premium-reset.ts +55 -0
- package/src/utils/freebuff-referral-cache.ts +51 -0
- package/src/utils/freebuff-session-api.ts +243 -0
- package/src/utils/freebuff-session-display.ts +21 -0
- package/src/utils/freebuff-streak-line.ts +93 -0
- package/src/utils/git.ts +17 -0
- package/src/utils/helpers.ts +47 -0
- package/src/utils/image-display.ts +67 -0
- package/src/utils/image-handler.ts +325 -0
- package/src/utils/image-processor.ts +133 -0
- package/src/utils/image-thumbnail.ts +78 -0
- package/src/utils/implementor-helpers.ts +787 -0
- package/src/utils/input-modes.ts +174 -0
- package/src/utils/keyboard-actions.ts +392 -0
- package/src/utils/keypad-keys.ts +47 -0
- package/src/utils/layout-helpers.ts +33 -0
- package/src/utils/lazy-response-ads.ts +1 -0
- package/src/utils/local-agent-registry.ts +474 -0
- package/src/utils/log-shipper.ts +126 -0
- package/src/utils/logger.ts +327 -0
- package/src/utils/markdown-renderer.tsx +1090 -0
- package/src/utils/math.ts +3 -0
- package/src/utils/message-block-helpers.ts +684 -0
- package/src/utils/message-history.ts +143 -0
- package/src/utils/message-tree-utils.ts +67 -0
- package/src/utils/message-updater.ts +279 -0
- package/src/utils/open-file.ts +146 -0
- package/src/utils/open-url.ts +63 -0
- package/src/utils/path-completion.ts +111 -0
- package/src/utils/path-helpers.ts +38 -0
- package/src/utils/pending-attachments.ts +360 -0
- package/src/utils/polling-backoff.ts +59 -0
- package/src/utils/project-picker.ts +12 -0
- package/src/utils/queue-panel-actions.ts +70 -0
- package/src/utils/recent-projects.ts +156 -0
- package/src/utils/renderer-cleanup.ts +233 -0
- package/src/utils/response-ad-positions.ts +1 -0
- package/src/utils/run-state-storage.ts +603 -0
- package/src/utils/safe-json.ts +153 -0
- package/src/utils/sdk-event-handlers.ts +571 -0
- package/src/utils/send-message-helpers.ts +210 -0
- package/src/utils/send-message-timer.ts +110 -0
- package/src/utils/settings.ts +226 -0
- package/src/utils/skill-registry.ts +94 -0
- package/src/utils/spawn-agent-matcher.ts +55 -0
- package/src/utils/status-indicator-state.ts +107 -0
- package/src/utils/stream-chunk-processor.ts +57 -0
- package/src/utils/strings.ts +271 -0
- package/src/utils/subscription.ts +31 -0
- package/src/utils/syntax-highlighter.tsx +19 -0
- package/src/utils/terminal-color-detection.ts +474 -0
- package/src/utils/terminal-command-broker.ts +467 -0
- package/src/utils/terminal-enter-detection.ts +63 -0
- package/src/utils/terminal-images.ts +229 -0
- package/src/utils/terminal-io.ts +85 -0
- package/src/utils/terminal-protocol-controller.ts +173 -0
- package/src/utils/terminal-reset-sequences.ts +33 -0
- package/src/utils/terminal-title.ts +84 -0
- package/src/utils/terminal-watchdog.ts +366 -0
- package/src/utils/text-layout.ts +149 -0
- package/src/utils/theme-config.ts +141 -0
- package/src/utils/theme-system.ts +1206 -0
- package/src/utils/think-tag-parser.ts +95 -0
- package/src/utils/time-format.ts +21 -0
- package/src/utils/trace-writer.ts +162 -0
- package/src/utils/ui-constants.ts +69 -0
- package/src/utils/update-checker.ts +110 -0
- package/src/utils/usage-banner-state.ts +146 -0
- package/src/utils/validation-error-formatting.ts +86 -0
- package/src/utils/validation-error-helpers.ts +19 -0
- package/src/utils/windows-terminal-health.ts +124 -0
- package/src/utils/word-wrap-utils.ts +53 -0
- package/src/utils/write-file-atomic.ts +54 -0
- package/src/utils/yield-to-event-loop.ts +9 -0
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
import { TextAttributes } from '@opentui/core'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import remarkBreaks from 'remark-breaks'
|
|
4
|
+
import remarkGfm from 'remark-gfm'
|
|
5
|
+
import remarkParse from 'remark-parse'
|
|
6
|
+
import stringWidth from 'string-width'
|
|
7
|
+
import { unified } from 'unified'
|
|
8
|
+
|
|
9
|
+
import { logger } from './logger'
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
Blockquote,
|
|
13
|
+
Code,
|
|
14
|
+
Content,
|
|
15
|
+
Emphasis,
|
|
16
|
+
Heading,
|
|
17
|
+
InlineCode,
|
|
18
|
+
Link,
|
|
19
|
+
List,
|
|
20
|
+
ListItem,
|
|
21
|
+
Paragraph,
|
|
22
|
+
Root,
|
|
23
|
+
Strong,
|
|
24
|
+
Table,
|
|
25
|
+
TableCell,
|
|
26
|
+
TableRow,
|
|
27
|
+
Text,
|
|
28
|
+
} from 'mdast'
|
|
29
|
+
import type { ReactNode } from 'react'
|
|
30
|
+
|
|
31
|
+
// Helper component to work around TypeScript's Fragment key typing issue
|
|
32
|
+
const KeyedFragment = React.Fragment as React.FC<{ key?: string | number; children?: ReactNode }>
|
|
33
|
+
|
|
34
|
+
// Helper to wrap segments in KeyedFragments
|
|
35
|
+
const wrapSegmentsInFragments = (
|
|
36
|
+
segments: ReactNode[],
|
|
37
|
+
keyPrefix: string,
|
|
38
|
+
): ReactNode => {
|
|
39
|
+
return segments.map((segment, idx) => (
|
|
40
|
+
<KeyedFragment key={keyPrefix + '-' + idx}>{segment}</KeyedFragment>
|
|
41
|
+
))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MarkdownPalette {
|
|
45
|
+
inlineCodeFg: string
|
|
46
|
+
codeBackground: string
|
|
47
|
+
codeHeaderFg: string
|
|
48
|
+
headingFg: Record<number, string>
|
|
49
|
+
listBulletFg: string
|
|
50
|
+
blockquoteBorderFg: string
|
|
51
|
+
blockquoteTextFg: string
|
|
52
|
+
dividerFg: string
|
|
53
|
+
codeTextFg: string
|
|
54
|
+
codeMonochrome: boolean
|
|
55
|
+
linkFg: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MarkdownRenderOptions {
|
|
59
|
+
palette?: Partial<MarkdownPalette>
|
|
60
|
+
codeBlockWidth?: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const defaultPalette: MarkdownPalette = {
|
|
64
|
+
inlineCodeFg: '#86efac',
|
|
65
|
+
codeBackground: '#0d1117',
|
|
66
|
+
codeHeaderFg: '#666',
|
|
67
|
+
headingFg: {
|
|
68
|
+
1: 'magenta',
|
|
69
|
+
2: 'green',
|
|
70
|
+
3: 'green',
|
|
71
|
+
4: 'green',
|
|
72
|
+
5: 'green',
|
|
73
|
+
6: 'green',
|
|
74
|
+
},
|
|
75
|
+
listBulletFg: 'white',
|
|
76
|
+
blockquoteBorderFg: 'gray',
|
|
77
|
+
blockquoteTextFg: 'gray',
|
|
78
|
+
dividerFg: '#666',
|
|
79
|
+
codeTextFg: 'brightWhite',
|
|
80
|
+
codeMonochrome: false,
|
|
81
|
+
linkFg: '#3B82F6',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolvePalette = (
|
|
85
|
+
overrides?: Partial<MarkdownPalette>,
|
|
86
|
+
): MarkdownPalette => {
|
|
87
|
+
const palette: MarkdownPalette = {
|
|
88
|
+
...defaultPalette,
|
|
89
|
+
headingFg: { ...defaultPalette.headingFg },
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!overrides) {
|
|
93
|
+
return palette
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { headingFg, ...rest } = overrides
|
|
97
|
+
Object.assign(palette, rest)
|
|
98
|
+
|
|
99
|
+
if (headingFg) {
|
|
100
|
+
palette.headingFg = {
|
|
101
|
+
...palette.headingFg,
|
|
102
|
+
...headingFg,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return palette
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const processor = unified()
|
|
110
|
+
.use(remarkParse)
|
|
111
|
+
.use(remarkGfm)
|
|
112
|
+
.use(remarkBreaks)
|
|
113
|
+
|
|
114
|
+
type MarkdownNode = Content | Root
|
|
115
|
+
|
|
116
|
+
type InlineFallbackNode = Text | Strong | Emphasis
|
|
117
|
+
|
|
118
|
+
interface RenderState {
|
|
119
|
+
palette: MarkdownPalette
|
|
120
|
+
codeBlockWidth: number
|
|
121
|
+
nextKey: () => string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const createRenderState = (
|
|
125
|
+
palette: MarkdownPalette,
|
|
126
|
+
codeBlockWidth: number,
|
|
127
|
+
): RenderState => {
|
|
128
|
+
let counter = 0
|
|
129
|
+
return {
|
|
130
|
+
palette,
|
|
131
|
+
codeBlockWidth,
|
|
132
|
+
nextKey: () => {
|
|
133
|
+
counter += 1
|
|
134
|
+
return `markdown-${counter}`
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Unified trim helper with predicate
|
|
140
|
+
const trimTrailingNodes = (
|
|
141
|
+
nodes: ReactNode[],
|
|
142
|
+
predicate: (node: ReactNode) => boolean,
|
|
143
|
+
): ReactNode[] => {
|
|
144
|
+
let end = nodes.length
|
|
145
|
+
while (end > 0 && predicate(nodes[end - 1])) {
|
|
146
|
+
end -= 1
|
|
147
|
+
}
|
|
148
|
+
return end === nodes.length ? nodes : nodes.slice(0, end)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const trimTrailingWhitespaceNodes = (nodes: ReactNode[]): ReactNode[] => {
|
|
152
|
+
return trimTrailingNodes(
|
|
153
|
+
nodes,
|
|
154
|
+
(node) => typeof node === 'string' && node.trim().length === 0,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const trimTrailingBreaks = (nodes: ReactNode[]): ReactNode[] => {
|
|
159
|
+
return trimTrailingNodes(
|
|
160
|
+
nodes,
|
|
161
|
+
(node) => typeof node === 'string' && /^\n+$/.test(node),
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const splitNodesByNewline = (nodes: ReactNode[]): ReactNode[][] => {
|
|
166
|
+
const lines: ReactNode[][] = [[]]
|
|
167
|
+
nodes.forEach((node) => {
|
|
168
|
+
if (typeof node === 'string') {
|
|
169
|
+
const parts = node.split('\n')
|
|
170
|
+
parts.forEach((part, idx) => {
|
|
171
|
+
if (part.length > 0) {
|
|
172
|
+
lines[lines.length - 1].push(part)
|
|
173
|
+
}
|
|
174
|
+
if (idx < parts.length - 1) {
|
|
175
|
+
lines.push([])
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
} else {
|
|
179
|
+
lines[lines.length - 1].push(node)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
return lines
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const hasUnescapedMarker = (value: string): boolean => {
|
|
186
|
+
if (!value) {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
const markers = ['**', '*']
|
|
190
|
+
return markers.some((marker) => {
|
|
191
|
+
let idx = value.indexOf(marker)
|
|
192
|
+
while (idx !== -1) {
|
|
193
|
+
let backslashes = 0
|
|
194
|
+
for (let offset = idx - 1; offset >= 0 && value[offset] === '\\'; offset -= 1) {
|
|
195
|
+
backslashes += 1
|
|
196
|
+
}
|
|
197
|
+
if (backslashes % 2 === 0) {
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
idx = value.indexOf(marker, idx + marker.length)
|
|
201
|
+
}
|
|
202
|
+
return false
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const findClosingDelimiter = (
|
|
207
|
+
value: string,
|
|
208
|
+
start: number,
|
|
209
|
+
marker: string,
|
|
210
|
+
): number => {
|
|
211
|
+
let idx = start
|
|
212
|
+
while (idx < value.length) {
|
|
213
|
+
idx = value.indexOf(marker, idx)
|
|
214
|
+
if (idx === -1) {
|
|
215
|
+
return -1
|
|
216
|
+
}
|
|
217
|
+
let backslashes = 0
|
|
218
|
+
for (let offset = idx - 1; offset >= 0 && value[offset] === '\\'; offset -= 1) {
|
|
219
|
+
backslashes += 1
|
|
220
|
+
}
|
|
221
|
+
if (backslashes % 2 === 0) {
|
|
222
|
+
return idx
|
|
223
|
+
}
|
|
224
|
+
idx += marker.length
|
|
225
|
+
}
|
|
226
|
+
return -1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Remark follows CommonMark's emphasis rules, which ignore some practical
|
|
231
|
+
* patterns (e.g., `Other**.github/**`). This fallback splits leftover text
|
|
232
|
+
* nodes on emphasis markers so we still render inline styling in those cases.
|
|
233
|
+
*/
|
|
234
|
+
const parseInlineFallback = (value: string): InlineFallbackNode[] => {
|
|
235
|
+
if (!value || !hasUnescapedMarker(value)) {
|
|
236
|
+
return [{ type: 'text', value }]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const nodes: InlineFallbackNode[] = []
|
|
240
|
+
let buffer = ''
|
|
241
|
+
|
|
242
|
+
const flushBuffer = () => {
|
|
243
|
+
if (buffer.length > 0) {
|
|
244
|
+
nodes.push({ type: 'text', value: buffer })
|
|
245
|
+
buffer = ''
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let index = 0
|
|
250
|
+
while (index < value.length) {
|
|
251
|
+
const char = value[index]
|
|
252
|
+
|
|
253
|
+
if (char === '*') {
|
|
254
|
+
const markerChar = char
|
|
255
|
+
const isDouble =
|
|
256
|
+
index + 1 < value.length && value[index + 1] === markerChar
|
|
257
|
+
const marker = isDouble ? markerChar.repeat(2) : markerChar
|
|
258
|
+
const markerLength = marker.length
|
|
259
|
+
|
|
260
|
+
let backslashes = 0
|
|
261
|
+
for (
|
|
262
|
+
let offset = index - 1;
|
|
263
|
+
offset >= 0 && value[offset] === '\\';
|
|
264
|
+
offset -= 1
|
|
265
|
+
) {
|
|
266
|
+
backslashes += 1
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (backslashes % 2 === 1) {
|
|
270
|
+
buffer += marker
|
|
271
|
+
index += markerLength
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const closing = findClosingDelimiter(value, index + markerLength, marker)
|
|
276
|
+
if (closing === -1) {
|
|
277
|
+
buffer += marker
|
|
278
|
+
index += markerLength
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const inner = value.slice(index + markerLength, closing)
|
|
283
|
+
flushBuffer()
|
|
284
|
+
const children = parseInlineFallback(inner).filter(
|
|
285
|
+
(node) => !(node.type === 'text' && node.value.length === 0),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
const emphasisNode: InlineFallbackNode =
|
|
289
|
+
isDouble
|
|
290
|
+
? { type: 'strong', children }
|
|
291
|
+
: { type: 'emphasis', children }
|
|
292
|
+
|
|
293
|
+
nodes.push(emphasisNode)
|
|
294
|
+
index = closing + markerLength
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
buffer += char
|
|
299
|
+
index += 1
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
flushBuffer()
|
|
303
|
+
|
|
304
|
+
if (nodes.length === 0) {
|
|
305
|
+
return [{ type: 'text', value }]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return nodes
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const applyInlineFallbackFormatting = (node: MarkdownNode): void => {
|
|
312
|
+
if (!node || typeof node !== 'object') {
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const mutable = node as { children?: MarkdownNode[] }
|
|
317
|
+
if (!Array.isArray(mutable.children)) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const nextChildren: MarkdownNode[] = []
|
|
322
|
+
|
|
323
|
+
mutable.children.forEach((child) => {
|
|
324
|
+
if (child.type === 'text') {
|
|
325
|
+
const replacements = parseInlineFallback(child.value)
|
|
326
|
+
const hasChanges =
|
|
327
|
+
replacements.length !== 1 ||
|
|
328
|
+
replacements[0].type !== 'text' ||
|
|
329
|
+
replacements[0].value !== child.value
|
|
330
|
+
|
|
331
|
+
if (hasChanges) {
|
|
332
|
+
replacements.forEach((replacement) => {
|
|
333
|
+
if (replacement.type === 'text') {
|
|
334
|
+
nextChildren.push(replacement)
|
|
335
|
+
} else {
|
|
336
|
+
applyInlineFallbackFormatting(
|
|
337
|
+
replacement as unknown as MarkdownNode,
|
|
338
|
+
)
|
|
339
|
+
nextChildren.push(replacement as unknown as MarkdownNode)
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
applyInlineFallbackFormatting(child as MarkdownNode)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
nextChildren.push(child as MarkdownNode)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
mutable.children = nextChildren
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const getChildrenText = (children: MarkdownNode[]): string => {
|
|
355
|
+
return children.map(nodeToPlainText).join('')
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const nodeToPlainText = (node: MarkdownNode): string => {
|
|
359
|
+
switch (node.type) {
|
|
360
|
+
case 'root':
|
|
361
|
+
return getChildrenText((node as Root).children as MarkdownNode[])
|
|
362
|
+
|
|
363
|
+
case 'paragraph':
|
|
364
|
+
return getChildrenText((node as Paragraph).children as MarkdownNode[]) + '\n\n'
|
|
365
|
+
|
|
366
|
+
case 'text':
|
|
367
|
+
return (node as Text).value
|
|
368
|
+
|
|
369
|
+
case 'strong':
|
|
370
|
+
return getChildrenText((node as Strong).children as MarkdownNode[])
|
|
371
|
+
|
|
372
|
+
case 'emphasis':
|
|
373
|
+
return getChildrenText((node as Emphasis).children as MarkdownNode[])
|
|
374
|
+
|
|
375
|
+
case 'inlineCode':
|
|
376
|
+
return (node as InlineCode).value
|
|
377
|
+
|
|
378
|
+
case 'heading': {
|
|
379
|
+
const heading = node as Heading
|
|
380
|
+
const prefix = '#'.repeat(Math.max(1, Math.min(heading.depth, 6)))
|
|
381
|
+
const content = getChildrenText(heading.children as MarkdownNode[])
|
|
382
|
+
return `${prefix} ${content}\n\n`
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
case 'list': {
|
|
386
|
+
const list = node as List
|
|
387
|
+
return (
|
|
388
|
+
list.children
|
|
389
|
+
.map((item, idx) => {
|
|
390
|
+
const marker = list.ordered ? `${(list.start ?? 1) + idx}. ` : '- '
|
|
391
|
+
const text = getChildrenText((item as ListItem).children as MarkdownNode[]).trimEnd()
|
|
392
|
+
return marker + text
|
|
393
|
+
})
|
|
394
|
+
.join('\n') + '\n\n'
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
case 'listItem':
|
|
399
|
+
return getChildrenText((node as ListItem).children as MarkdownNode[])
|
|
400
|
+
|
|
401
|
+
case 'blockquote': {
|
|
402
|
+
const blockquote = node as Blockquote
|
|
403
|
+
const content = blockquote.children
|
|
404
|
+
.map((child) => nodeToPlainText(child).replace(/^/gm, '> '))
|
|
405
|
+
.join('')
|
|
406
|
+
return `${content}\n\n`
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case 'code': {
|
|
410
|
+
const code = node as Code
|
|
411
|
+
const header = code.lang ? `\`\`\`${code.lang}\n` : '```\n'
|
|
412
|
+
return `${header}${code.value}\n\`\`\`\n\n`
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
case 'break':
|
|
416
|
+
return '\n'
|
|
417
|
+
|
|
418
|
+
case 'thematicBreak':
|
|
419
|
+
return '---\n\n'
|
|
420
|
+
|
|
421
|
+
case 'link': {
|
|
422
|
+
const link = node as Link
|
|
423
|
+
const label = link.children.length > 0
|
|
424
|
+
? getChildrenText(link.children as MarkdownNode[])
|
|
425
|
+
: link.url
|
|
426
|
+
return label
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
case 'table': {
|
|
430
|
+
const table = node as Table
|
|
431
|
+
return table.children
|
|
432
|
+
.map((row) => {
|
|
433
|
+
const cells = (row as TableRow).children as TableCell[]
|
|
434
|
+
return cells.map((cell) => nodeToPlainText(cell)).join(' | ')
|
|
435
|
+
})
|
|
436
|
+
.join('\n') + '\n\n'
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
case 'tableRow':
|
|
440
|
+
return (node as TableRow).children.map(nodeToPlainText).join(' | ')
|
|
441
|
+
|
|
442
|
+
case 'tableCell':
|
|
443
|
+
return getChildrenText((node as TableCell).children as MarkdownNode[])
|
|
444
|
+
|
|
445
|
+
case 'delete': {
|
|
446
|
+
// Strikethrough - just return the text content
|
|
447
|
+
const deleteNode = node as any
|
|
448
|
+
if (Array.isArray(deleteNode.children)) {
|
|
449
|
+
return getChildrenText(deleteNode.children as MarkdownNode[])
|
|
450
|
+
}
|
|
451
|
+
return ''
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
default: {
|
|
455
|
+
const anyNode = node as any
|
|
456
|
+
if (Array.isArray(anyNode.children)) {
|
|
457
|
+
return getChildrenText(anyNode.children as MarkdownNode[])
|
|
458
|
+
}
|
|
459
|
+
return ''
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const renderNodes = (
|
|
465
|
+
children: MarkdownNode[],
|
|
466
|
+
state: RenderState,
|
|
467
|
+
parentType: MarkdownNode['type'],
|
|
468
|
+
): ReactNode[] => {
|
|
469
|
+
const results: ReactNode[] = []
|
|
470
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
471
|
+
const child = children[index]
|
|
472
|
+
const nextSibling = children[index + 1] as MarkdownNode | undefined
|
|
473
|
+
results.push(...renderNode(child, state, parentType, nextSibling))
|
|
474
|
+
}
|
|
475
|
+
return results
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const renderCodeBlock = (code: Code, state: RenderState): ReactNode[] => {
|
|
479
|
+
const { palette, nextKey } = state
|
|
480
|
+
const lines = code.value.split('\n')
|
|
481
|
+
const nodes: ReactNode[] = []
|
|
482
|
+
|
|
483
|
+
if (code.lang) {
|
|
484
|
+
nodes.push(
|
|
485
|
+
<span key={nextKey()} fg={palette.codeHeaderFg}>
|
|
486
|
+
{`// ${code.lang}`}
|
|
487
|
+
</span>,
|
|
488
|
+
'\n',
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
lines.forEach((line, index) => {
|
|
493
|
+
const displayLine = line === '' ? ' ' : line
|
|
494
|
+
nodes.push(
|
|
495
|
+
<span
|
|
496
|
+
key={nextKey()}
|
|
497
|
+
fg={palette.codeTextFg}
|
|
498
|
+
bg={palette.codeMonochrome ? undefined : palette.codeBackground}
|
|
499
|
+
>
|
|
500
|
+
{displayLine}
|
|
501
|
+
</span>,
|
|
502
|
+
)
|
|
503
|
+
if (index < lines.length - 1) {
|
|
504
|
+
nodes.push('\n')
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
nodes.push('\n\n')
|
|
509
|
+
return nodes
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const renderBlockquote = (
|
|
513
|
+
blockquote: Blockquote,
|
|
514
|
+
state: RenderState,
|
|
515
|
+
): ReactNode[] => {
|
|
516
|
+
const { palette, nextKey } = state
|
|
517
|
+
const childNodes = renderNodes(
|
|
518
|
+
blockquote.children as MarkdownNode[],
|
|
519
|
+
state,
|
|
520
|
+
blockquote.type,
|
|
521
|
+
)
|
|
522
|
+
const lines = splitNodesByNewline(childNodes)
|
|
523
|
+
const nodes: ReactNode[] = []
|
|
524
|
+
|
|
525
|
+
lines.forEach((line, index) => {
|
|
526
|
+
if (line.length === 0) {
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
nodes.push(
|
|
530
|
+
<span key={nextKey()} fg={palette.blockquoteBorderFg}>
|
|
531
|
+
{'> '}
|
|
532
|
+
</span>,
|
|
533
|
+
)
|
|
534
|
+
nodes.push(
|
|
535
|
+
<span key={nextKey()} fg={palette.blockquoteTextFg}>
|
|
536
|
+
{wrapSegmentsInFragments(line, nextKey())}
|
|
537
|
+
</span>,
|
|
538
|
+
)
|
|
539
|
+
if (index < lines.length - 1) {
|
|
540
|
+
nodes.push('\n')
|
|
541
|
+
}
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
nodes.push('\n\n')
|
|
545
|
+
return nodes
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const renderList = (list: List, state: RenderState): ReactNode[] => {
|
|
549
|
+
const { palette, nextKey } = state
|
|
550
|
+
const nodes: ReactNode[] = []
|
|
551
|
+
const start = list.start ?? 1
|
|
552
|
+
|
|
553
|
+
list.children.forEach((item, idx) => {
|
|
554
|
+
const listItem = item as ListItem
|
|
555
|
+
const marker =
|
|
556
|
+
listItem.checked === true
|
|
557
|
+
? '[x] '
|
|
558
|
+
: listItem.checked === false
|
|
559
|
+
? '[ ] '
|
|
560
|
+
: list.ordered
|
|
561
|
+
? `${start + idx}. `
|
|
562
|
+
: '- '
|
|
563
|
+
|
|
564
|
+
nodes.push(
|
|
565
|
+
<span key={nextKey()} fg={palette.listBulletFg}>
|
|
566
|
+
{marker}
|
|
567
|
+
</span>,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
const itemNodes = trimTrailingBreaks(
|
|
571
|
+
renderNodes(listItem.children as MarkdownNode[], state, listItem.type),
|
|
572
|
+
)
|
|
573
|
+
if (itemNodes.length === 0) {
|
|
574
|
+
nodes.push('\n')
|
|
575
|
+
} else {
|
|
576
|
+
nodes.push(
|
|
577
|
+
<KeyedFragment key={nextKey()}>
|
|
578
|
+
{wrapSegmentsInFragments(itemNodes, nextKey())}
|
|
579
|
+
</KeyedFragment>,
|
|
580
|
+
)
|
|
581
|
+
nodes.push('\n')
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
if (nodes.length > 0) {
|
|
586
|
+
nodes.push('\n')
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return nodes
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const renderHeading = (heading: Heading, state: RenderState): ReactNode[] => {
|
|
593
|
+
const { palette, nextKey } = state
|
|
594
|
+
const depth = Math.max(1, Math.min(heading.depth, 6))
|
|
595
|
+
const color = palette.headingFg[depth] ?? palette.headingFg[6]
|
|
596
|
+
const childNodes = renderNodes(
|
|
597
|
+
heading.children as MarkdownNode[],
|
|
598
|
+
state,
|
|
599
|
+
heading.type,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
return [
|
|
603
|
+
<span key={nextKey()} fg={color} attributes={TextAttributes.BOLD}>
|
|
604
|
+
{childNodes.map((segment, idx) => (
|
|
605
|
+
<KeyedFragment key={nextKey() + '-' + idx}>{segment}</KeyedFragment>
|
|
606
|
+
))}
|
|
607
|
+
</span>,
|
|
608
|
+
'\n\n',
|
|
609
|
+
]
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const renderInlineCode = (
|
|
613
|
+
inlineCode: InlineCode,
|
|
614
|
+
state: RenderState,
|
|
615
|
+
): ReactNode[] => {
|
|
616
|
+
const { palette, nextKey } = state
|
|
617
|
+
const content = inlineCode.value || ' '
|
|
618
|
+
return [
|
|
619
|
+
<span
|
|
620
|
+
key={nextKey()}
|
|
621
|
+
fg={palette.inlineCodeFg}
|
|
622
|
+
bg={palette.codeMonochrome ? undefined : palette.codeBackground}
|
|
623
|
+
attributes={TextAttributes.BOLD}
|
|
624
|
+
>
|
|
625
|
+
{` ${content} `}
|
|
626
|
+
</span>,
|
|
627
|
+
]
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const renderLink = (link: Link, state: RenderState): ReactNode[] => {
|
|
631
|
+
const { palette, nextKey } = state
|
|
632
|
+
const labelNodes = renderNodes(
|
|
633
|
+
link.children as MarkdownNode[],
|
|
634
|
+
state,
|
|
635
|
+
link.type,
|
|
636
|
+
)
|
|
637
|
+
const label = labelNodes.length > 0 ? labelNodes : [link.url]
|
|
638
|
+
|
|
639
|
+
return [
|
|
640
|
+
<span key={nextKey()} fg={palette.linkFg}>
|
|
641
|
+
{wrapSegmentsInFragments(label, nextKey())}
|
|
642
|
+
</span>,
|
|
643
|
+
]
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Wraps text to fit within a specified width, returning an array of lines.
|
|
648
|
+
* Uses stringWidth to properly measure Unicode and wide characters.
|
|
649
|
+
* Performs word-wrapping where possible, falling back to character-level
|
|
650
|
+
* breaking for words that exceed the column width.
|
|
651
|
+
*/
|
|
652
|
+
const wrapText = (text: string, maxWidth: number): string[] => {
|
|
653
|
+
if (maxWidth < 1) return ['']
|
|
654
|
+
if (!text) return ['']
|
|
655
|
+
const textWidth = stringWidth(text)
|
|
656
|
+
if (textWidth <= maxWidth) return [text]
|
|
657
|
+
|
|
658
|
+
const lines: string[] = []
|
|
659
|
+
let currentLine = ''
|
|
660
|
+
let currentWidth = 0
|
|
661
|
+
const tokens = text.split(/(\s+)/)
|
|
662
|
+
|
|
663
|
+
for (const token of tokens) {
|
|
664
|
+
if (!token) continue
|
|
665
|
+
const tokenWidth = stringWidth(token)
|
|
666
|
+
const isWhitespace = /^\s+$/.test(token)
|
|
667
|
+
|
|
668
|
+
// Skip leading whitespace on new lines
|
|
669
|
+
if (isWhitespace && currentWidth === 0) continue
|
|
670
|
+
|
|
671
|
+
if (tokenWidth > maxWidth && !isWhitespace) {
|
|
672
|
+
// Break long words character by character
|
|
673
|
+
for (const char of token) {
|
|
674
|
+
const charWidth = stringWidth(char)
|
|
675
|
+
if (currentWidth + charWidth > maxWidth) {
|
|
676
|
+
if (currentLine) lines.push(currentLine)
|
|
677
|
+
currentLine = char
|
|
678
|
+
currentWidth = charWidth
|
|
679
|
+
} else {
|
|
680
|
+
currentLine += char
|
|
681
|
+
currentWidth += charWidth
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
} else if (currentWidth + tokenWidth > maxWidth) {
|
|
685
|
+
if (currentLine) lines.push(currentLine.trimEnd())
|
|
686
|
+
currentLine = isWhitespace ? '' : token
|
|
687
|
+
currentWidth = isWhitespace ? 0 : tokenWidth
|
|
688
|
+
} else {
|
|
689
|
+
currentLine += token
|
|
690
|
+
currentWidth += tokenWidth
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (currentLine) lines.push(currentLine.trimEnd())
|
|
695
|
+
return lines.length > 0 ? lines : ['']
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Pads text to reach exact width using spaces.
|
|
700
|
+
*/
|
|
701
|
+
const padText = (text: string, targetWidth: number): string => {
|
|
702
|
+
const currentWidth = stringWidth(text)
|
|
703
|
+
if (currentWidth >= targetWidth) return text
|
|
704
|
+
return text + ' '.repeat(targetWidth - currentWidth)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const renderTable = (table: Table, state: RenderState): ReactNode[] => {
|
|
708
|
+
const { palette, nextKey, codeBlockWidth } = state
|
|
709
|
+
const nodes: ReactNode[] = []
|
|
710
|
+
|
|
711
|
+
// Extract all rows and their plain text content
|
|
712
|
+
const rows = table.children.map((row) => {
|
|
713
|
+
const cells = (row as TableRow).children as TableCell[]
|
|
714
|
+
return cells.map((cell) => nodeToPlainText(cell).trim())
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
if (rows.length === 0) return nodes
|
|
718
|
+
|
|
719
|
+
// Determine number of columns
|
|
720
|
+
const numCols = Math.max(...rows.map((r) => r.length))
|
|
721
|
+
if (numCols === 0) return nodes
|
|
722
|
+
|
|
723
|
+
// Calculate natural column widths (minimum 3 chars per column)
|
|
724
|
+
const naturalWidths: number[] = Array(numCols).fill(3)
|
|
725
|
+
for (const row of rows) {
|
|
726
|
+
for (let i = 0; i < row.length; i++) {
|
|
727
|
+
const cellWidth = stringWidth(row[i] || '')
|
|
728
|
+
naturalWidths[i] = Math.max(naturalWidths[i], cellWidth)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Calculate total width needed:
|
|
733
|
+
// Each column has its content width
|
|
734
|
+
// Separators: " │ " between columns (3 chars each), none at edges
|
|
735
|
+
const separatorWidth = 3 // ' │ '
|
|
736
|
+
const numSeparators = numCols - 1
|
|
737
|
+
const totalNaturalWidth =
|
|
738
|
+
naturalWidths.reduce((a, b) => a + b, 0) + numSeparators * separatorWidth
|
|
739
|
+
|
|
740
|
+
// Available width for the table (leave some margin)
|
|
741
|
+
const availableWidth = Math.max(20, codeBlockWidth - 2)
|
|
742
|
+
|
|
743
|
+
// Calculate final column widths
|
|
744
|
+
let columnWidths: number[]
|
|
745
|
+
if (totalNaturalWidth <= availableWidth) {
|
|
746
|
+
// Table fits - use natural widths
|
|
747
|
+
columnWidths = naturalWidths
|
|
748
|
+
} else {
|
|
749
|
+
// Table too wide - proportionally shrink columns
|
|
750
|
+
const availableForContent = availableWidth - numSeparators * separatorWidth
|
|
751
|
+
const totalNaturalContent = naturalWidths.reduce((a, b) => a + b, 0)
|
|
752
|
+
const scale = availableForContent / totalNaturalContent
|
|
753
|
+
|
|
754
|
+
columnWidths = naturalWidths.map((w) => {
|
|
755
|
+
// Minimum 3 chars, scale the rest
|
|
756
|
+
return Math.max(3, Math.floor(w * scale))
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
// Distribute any remaining width to columns that were clamped
|
|
760
|
+
let usedWidth = columnWidths.reduce((a, b) => a + b, 0)
|
|
761
|
+
let remaining = availableForContent - usedWidth
|
|
762
|
+
for (let i = 0; i < columnWidths.length && remaining > 0; i++) {
|
|
763
|
+
if (columnWidths[i] < naturalWidths[i]) {
|
|
764
|
+
const add = Math.min(remaining, naturalWidths[i] - columnWidths[i])
|
|
765
|
+
columnWidths[i] += add
|
|
766
|
+
remaining -= add
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Helper to render a horizontal separator line
|
|
772
|
+
const renderSeparator = (leftChar: string, midChar: string, rightChar: string): void => {
|
|
773
|
+
let line = leftChar
|
|
774
|
+
columnWidths.forEach((width, idx) => {
|
|
775
|
+
line += '─'.repeat(width + 2) // +2 for padding spaces
|
|
776
|
+
line += idx < columnWidths.length - 1 ? midChar : rightChar
|
|
777
|
+
})
|
|
778
|
+
nodes.push(
|
|
779
|
+
<span key={nextKey()} fg={palette.dividerFg}>
|
|
780
|
+
{line}
|
|
781
|
+
</span>,
|
|
782
|
+
)
|
|
783
|
+
nodes.push('\n')
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Pre-wrap all cell contents so we know the height of each row
|
|
787
|
+
const wrappedRows: string[][][] = rows.map((row) =>
|
|
788
|
+
Array.from({ length: numCols }, (_, i) => {
|
|
789
|
+
const cellText = row[i] || ''
|
|
790
|
+
return wrapText(cellText, columnWidths[i])
|
|
791
|
+
}),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
// Render top border
|
|
795
|
+
renderSeparator('┌', '┬', '┐')
|
|
796
|
+
|
|
797
|
+
// Render each row with word-wrapped cells
|
|
798
|
+
wrappedRows.forEach((wrappedCells, rowIdx) => {
|
|
799
|
+
const isHeader = rowIdx === 0
|
|
800
|
+
const rowHeight = Math.max(...wrappedCells.map((lines) => lines.length), 1)
|
|
801
|
+
|
|
802
|
+
// Render each visual line in the row
|
|
803
|
+
for (let lineIdx = 0; lineIdx < rowHeight; lineIdx++) {
|
|
804
|
+
for (let cellIdx = 0; cellIdx < numCols; cellIdx++) {
|
|
805
|
+
const colWidth = columnWidths[cellIdx]
|
|
806
|
+
const lineText = wrappedCells[cellIdx][lineIdx] || ''
|
|
807
|
+
const displayText = padText(lineText, colWidth)
|
|
808
|
+
|
|
809
|
+
// Left border for first cell
|
|
810
|
+
if (cellIdx === 0) {
|
|
811
|
+
nodes.push(
|
|
812
|
+
<span key={nextKey()} fg={palette.dividerFg}>
|
|
813
|
+
│
|
|
814
|
+
</span>,
|
|
815
|
+
)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Cell content with padding
|
|
819
|
+
nodes.push(
|
|
820
|
+
<span
|
|
821
|
+
key={nextKey()}
|
|
822
|
+
fg={isHeader ? palette.headingFg[3] : undefined}
|
|
823
|
+
attributes={isHeader ? TextAttributes.BOLD : undefined}
|
|
824
|
+
>
|
|
825
|
+
{' '}
|
|
826
|
+
{displayText}
|
|
827
|
+
{' '}
|
|
828
|
+
</span>,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
// Separator or right border
|
|
832
|
+
nodes.push(
|
|
833
|
+
<span key={nextKey()} fg={palette.dividerFg}>
|
|
834
|
+
│
|
|
835
|
+
</span>,
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
nodes.push('\n')
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Add separator line after header
|
|
842
|
+
if (isHeader) {
|
|
843
|
+
renderSeparator('├', '┼', '┤')
|
|
844
|
+
}
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
// Render bottom border
|
|
848
|
+
renderSeparator('└', '┴', '┘')
|
|
849
|
+
|
|
850
|
+
nodes.push('\n')
|
|
851
|
+
return nodes
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const renderNode = (
|
|
855
|
+
node: MarkdownNode,
|
|
856
|
+
state: RenderState,
|
|
857
|
+
parentType: MarkdownNode['type'],
|
|
858
|
+
nextSibling?: MarkdownNode,
|
|
859
|
+
): ReactNode[] => {
|
|
860
|
+
switch (node.type) {
|
|
861
|
+
case 'root':
|
|
862
|
+
return renderNodes(
|
|
863
|
+
(node as Root).children as MarkdownNode[],
|
|
864
|
+
state,
|
|
865
|
+
node.type,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
case 'paragraph': {
|
|
869
|
+
const children = renderNodes(
|
|
870
|
+
(node as Paragraph).children as MarkdownNode[],
|
|
871
|
+
state,
|
|
872
|
+
node.type,
|
|
873
|
+
)
|
|
874
|
+
const nodes = [...children]
|
|
875
|
+
if (parentType === 'listItem') {
|
|
876
|
+
nodes.push('\n')
|
|
877
|
+
} else if (parentType === 'blockquote') {
|
|
878
|
+
nodes.push('\n')
|
|
879
|
+
} else {
|
|
880
|
+
const isTightFollowup =
|
|
881
|
+
parentType === 'root' &&
|
|
882
|
+
nextSibling &&
|
|
883
|
+
(nextSibling.type === 'blockquote' || nextSibling.type === 'list')
|
|
884
|
+
nodes.push(isTightFollowup ? '\n' : '\n\n')
|
|
885
|
+
}
|
|
886
|
+
return nodes
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
case 'text':
|
|
890
|
+
return [(node as Text).value]
|
|
891
|
+
|
|
892
|
+
case 'strong': {
|
|
893
|
+
const children = renderNodes(
|
|
894
|
+
(node as Strong).children as MarkdownNode[],
|
|
895
|
+
state,
|
|
896
|
+
node.type,
|
|
897
|
+
)
|
|
898
|
+
return [
|
|
899
|
+
<span key={state.nextKey()} attributes={TextAttributes.BOLD}>
|
|
900
|
+
{wrapSegmentsInFragments(children, state.nextKey())}
|
|
901
|
+
</span>,
|
|
902
|
+
]
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
case 'emphasis': {
|
|
906
|
+
const children = renderNodes(
|
|
907
|
+
(node as Emphasis).children as MarkdownNode[],
|
|
908
|
+
state,
|
|
909
|
+
node.type,
|
|
910
|
+
)
|
|
911
|
+
return [
|
|
912
|
+
<span key={state.nextKey()} attributes={TextAttributes.ITALIC}>
|
|
913
|
+
{wrapSegmentsInFragments(children, state.nextKey())}
|
|
914
|
+
</span>,
|
|
915
|
+
]
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
case 'inlineCode':
|
|
919
|
+
return renderInlineCode(node as InlineCode, state)
|
|
920
|
+
|
|
921
|
+
case 'heading':
|
|
922
|
+
return renderHeading(node as Heading, state)
|
|
923
|
+
|
|
924
|
+
case 'list':
|
|
925
|
+
return renderList(node as List, state)
|
|
926
|
+
|
|
927
|
+
case 'listItem': {
|
|
928
|
+
return renderNodes(
|
|
929
|
+
(node as ListItem).children as MarkdownNode[],
|
|
930
|
+
state,
|
|
931
|
+
node.type,
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
case 'blockquote':
|
|
936
|
+
return renderBlockquote(node as Blockquote, state)
|
|
937
|
+
|
|
938
|
+
case 'code':
|
|
939
|
+
return renderCodeBlock(node as Code, state)
|
|
940
|
+
|
|
941
|
+
case 'break':
|
|
942
|
+
return ['\n']
|
|
943
|
+
|
|
944
|
+
case 'thematicBreak': {
|
|
945
|
+
const width = Math.max(10, Math.min(state.codeBlockWidth, 80))
|
|
946
|
+
const divider = '─'.repeat(width)
|
|
947
|
+
return [
|
|
948
|
+
<span key={state.nextKey()} fg={state.palette.dividerFg}>
|
|
949
|
+
{divider}
|
|
950
|
+
</span>,
|
|
951
|
+
'\n\n',
|
|
952
|
+
]
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
case 'link':
|
|
956
|
+
return renderLink(node as Link, state)
|
|
957
|
+
|
|
958
|
+
case 'table':
|
|
959
|
+
return renderTable(node as Table, state)
|
|
960
|
+
|
|
961
|
+
case 'delete': {
|
|
962
|
+
// Strikethrough from GFM
|
|
963
|
+
const anyNode = node as any
|
|
964
|
+
const children = renderNodes(
|
|
965
|
+
anyNode.children as MarkdownNode[],
|
|
966
|
+
state,
|
|
967
|
+
node.type,
|
|
968
|
+
)
|
|
969
|
+
return [
|
|
970
|
+
<span key={state.nextKey()} attributes={TextAttributes.DIM}>
|
|
971
|
+
{wrapSegmentsInFragments(children, state.nextKey())}
|
|
972
|
+
</span>,
|
|
973
|
+
]
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
default: {
|
|
977
|
+
const fallbackText = nodeToPlainText(node)
|
|
978
|
+
if (fallbackText) {
|
|
979
|
+
return [fallbackText]
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const anyNode = node as any
|
|
983
|
+
if (Array.isArray(anyNode.children)) {
|
|
984
|
+
return renderNodes(anyNode.children as MarkdownNode[], state, node.type)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return []
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const normalizeOutput = (nodes: ReactNode[]): ReactNode => {
|
|
993
|
+
const trimmed = trimTrailingWhitespaceNodes(nodes)
|
|
994
|
+
if (trimmed.length === 0) {
|
|
995
|
+
return ''
|
|
996
|
+
}
|
|
997
|
+
if (trimmed.length === 1) {
|
|
998
|
+
return trimmed[0]
|
|
999
|
+
}
|
|
1000
|
+
return (
|
|
1001
|
+
<>
|
|
1002
|
+
{trimmed.map((node, idx) => (
|
|
1003
|
+
<KeyedFragment key={`markdown-out-${idx}`}>{node}</KeyedFragment>
|
|
1004
|
+
))}
|
|
1005
|
+
</>
|
|
1006
|
+
)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
export function renderMarkdown(
|
|
1010
|
+
markdown: string,
|
|
1011
|
+
options: MarkdownRenderOptions = {},
|
|
1012
|
+
): ReactNode {
|
|
1013
|
+
try {
|
|
1014
|
+
const palette = resolvePalette(options.palette)
|
|
1015
|
+
const codeBlockWidth = options.codeBlockWidth ?? 80
|
|
1016
|
+
const state = createRenderState(palette, codeBlockWidth)
|
|
1017
|
+
const ast = processor.parse(markdown) as Root
|
|
1018
|
+
applyInlineFallbackFormatting(ast)
|
|
1019
|
+
const nodes = renderNode(ast, state, ast.type, undefined)
|
|
1020
|
+
return normalizeOutput(nodes)
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
logger.error(error, 'Failed to parse markdown')
|
|
1023
|
+
return markdown
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export function hasMarkdown(content: string): boolean {
|
|
1028
|
+
return /[*_`#>\-\+]|\[.*\]\(.*\)|```/.test(content)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export function hasIncompleteCodeFence(content: string): boolean {
|
|
1032
|
+
let fenceCount = 0
|
|
1033
|
+
const fenceRegex = /```/g
|
|
1034
|
+
while (fenceRegex.exec(content)) {
|
|
1035
|
+
fenceCount += 1
|
|
1036
|
+
}
|
|
1037
|
+
return fenceCount % 2 === 1
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const mergeStreamingSegments = (segments: ReactNode[]): ReactNode => {
|
|
1041
|
+
if (segments.length === 0) {
|
|
1042
|
+
return ''
|
|
1043
|
+
}
|
|
1044
|
+
if (segments.length === 1) {
|
|
1045
|
+
return segments[0]
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return (
|
|
1049
|
+
<>
|
|
1050
|
+
{segments.map((segment, idx) => (
|
|
1051
|
+
<KeyedFragment key={`stream-segment-${idx}`}>
|
|
1052
|
+
{segment}
|
|
1053
|
+
</KeyedFragment>
|
|
1054
|
+
))}
|
|
1055
|
+
</>
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export function renderStreamingMarkdown(
|
|
1060
|
+
content: string,
|
|
1061
|
+
options: MarkdownRenderOptions = {},
|
|
1062
|
+
): ReactNode {
|
|
1063
|
+
if (!hasMarkdown(content)) {
|
|
1064
|
+
return content
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (!hasIncompleteCodeFence(content)) {
|
|
1068
|
+
return renderMarkdown(content, options)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const lastFenceIndex = content.lastIndexOf('```')
|
|
1072
|
+
if (lastFenceIndex === -1) {
|
|
1073
|
+
return renderMarkdown(content, options)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const completeSection = content.slice(0, lastFenceIndex)
|
|
1077
|
+
const pendingSection = content.slice(lastFenceIndex)
|
|
1078
|
+
|
|
1079
|
+
const segments: ReactNode[] = []
|
|
1080
|
+
|
|
1081
|
+
if (completeSection.length > 0) {
|
|
1082
|
+
segments.push(renderMarkdown(completeSection, options))
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (pendingSection.length > 0) {
|
|
1086
|
+
segments.push(pendingSection)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return mergeStreamingSegments(segments)
|
|
1090
|
+
}
|