@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,1206 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync, watch } from 'fs'
|
|
2
|
+
import { homedir } from 'os'
|
|
3
|
+
import { dirname, join } from 'path'
|
|
4
|
+
|
|
5
|
+
import { getCliEnv } from './env'
|
|
6
|
+
|
|
7
|
+
import type { MarkdownPalette } from './markdown-renderer'
|
|
8
|
+
import type { CliEnv } from '../types/env'
|
|
9
|
+
import type {
|
|
10
|
+
ChatTheme,
|
|
11
|
+
MarkdownHeadingLevel,
|
|
12
|
+
MarkdownThemeOverrides,
|
|
13
|
+
ThemeName,
|
|
14
|
+
} from '../types/theme-system'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if the terminal supports truecolor (24-bit color).
|
|
18
|
+
* Terminals like macOS Terminal.app only support 256 colors and cannot
|
|
19
|
+
* render hex colors properly - they need ANSI color name fallbacks.
|
|
20
|
+
*/
|
|
21
|
+
// Cache the truecolor support result since it won't change during runtime
|
|
22
|
+
let _truecolorSupport: boolean | null = null
|
|
23
|
+
|
|
24
|
+
export function supportsTruecolor(env: CliEnv = getCliEnv()): boolean {
|
|
25
|
+
if (_truecolorSupport !== null) {
|
|
26
|
+
return _truecolorSupport
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? ''
|
|
30
|
+
|
|
31
|
+
// Terminal.app (Apple_Terminal) does NOT support truecolor - only 256 colors
|
|
32
|
+
if (termProgram === 'apple_terminal') {
|
|
33
|
+
_truecolorSupport = false
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const colorterm = env.COLORTERM?.toLowerCase()
|
|
38
|
+
if (colorterm === 'truecolor' || colorterm === '24bit') {
|
|
39
|
+
_truecolorSupport = true
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Some terminals that are known to support truecolor
|
|
44
|
+
const truecolorTerminals = [
|
|
45
|
+
'iterm.app',
|
|
46
|
+
'hyper',
|
|
47
|
+
'wezterm',
|
|
48
|
+
'alacritty',
|
|
49
|
+
'kitty',
|
|
50
|
+
'ghostty',
|
|
51
|
+
'vscode',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
if (truecolorTerminals.some(t => termProgram.includes(t))) {
|
|
55
|
+
_truecolorSupport = true
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check TERM for known truecolor-capable values
|
|
60
|
+
const term = env.TERM?.toLowerCase() ?? ''
|
|
61
|
+
if (term.includes('truecolor') || term.includes('24bit')) {
|
|
62
|
+
_truecolorSupport = true
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// xterm-kitty, alacritty, etc.
|
|
67
|
+
if (term === 'xterm-kitty' || term === 'alacritty' || term.includes('ghostty')) {
|
|
68
|
+
_truecolorSupport = true
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_truecolorSupport = false
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the block color for the logo based on theme and terminal capabilities.
|
|
80
|
+
* In dark mode: white (#ffffff or 'white')
|
|
81
|
+
* In light mode: black (#000000 or 'black')
|
|
82
|
+
*/
|
|
83
|
+
export function getLogoBlockColor(
|
|
84
|
+
themeName: ThemeName,
|
|
85
|
+
env: CliEnv = getCliEnv(),
|
|
86
|
+
): string {
|
|
87
|
+
const isTruecolor = supportsTruecolor(env)
|
|
88
|
+
if (themeName === 'dark') {
|
|
89
|
+
return isTruecolor ? '#ffffff' : 'white'
|
|
90
|
+
}
|
|
91
|
+
return isTruecolor ? '#000000' : 'black'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the accent color for the logo based on theme and terminal capabilities.
|
|
96
|
+
* Returns the primary violet color with appropriate fallback.
|
|
97
|
+
*/
|
|
98
|
+
export function getLogoAccentColor(
|
|
99
|
+
themeName: ThemeName,
|
|
100
|
+
env: CliEnv = getCliEnv(),
|
|
101
|
+
): string {
|
|
102
|
+
const isTruecolor = supportsTruecolor(env)
|
|
103
|
+
// The primary accent color - violet/iris. 'magenta' is the closest CSS
|
|
104
|
+
// named color for 256-color terminals.
|
|
105
|
+
if (themeName === 'dark') {
|
|
106
|
+
return isTruecolor ? '#A78BFA' : 'magenta'
|
|
107
|
+
}
|
|
108
|
+
return isTruecolor ? '#7C3AED' : 'magenta'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const IDE_THEME_INFERENCE = {
|
|
112
|
+
dark: [
|
|
113
|
+
'dark',
|
|
114
|
+
'midnight',
|
|
115
|
+
'night',
|
|
116
|
+
'noir',
|
|
117
|
+
'black',
|
|
118
|
+
'charcoal',
|
|
119
|
+
'dim',
|
|
120
|
+
'dracula',
|
|
121
|
+
'darcula',
|
|
122
|
+
'moon',
|
|
123
|
+
'nebula',
|
|
124
|
+
'obsidian',
|
|
125
|
+
'shadow',
|
|
126
|
+
'storm',
|
|
127
|
+
'monokai',
|
|
128
|
+
'ayu mirage',
|
|
129
|
+
'material darker',
|
|
130
|
+
'tokyo',
|
|
131
|
+
'abyss',
|
|
132
|
+
'zed dark',
|
|
133
|
+
'vs dark',
|
|
134
|
+
],
|
|
135
|
+
light: [
|
|
136
|
+
'light',
|
|
137
|
+
'day',
|
|
138
|
+
'dawn',
|
|
139
|
+
'bright',
|
|
140
|
+
'paper',
|
|
141
|
+
'sun',
|
|
142
|
+
'snow',
|
|
143
|
+
'cloud',
|
|
144
|
+
'white',
|
|
145
|
+
'solarized light',
|
|
146
|
+
'pastel',
|
|
147
|
+
'cream',
|
|
148
|
+
'zed light',
|
|
149
|
+
'vs light',
|
|
150
|
+
],
|
|
151
|
+
} as const
|
|
152
|
+
|
|
153
|
+
const VS_CODE_PRODUCT_DIRS = [
|
|
154
|
+
'Code',
|
|
155
|
+
'Code - Insiders',
|
|
156
|
+
'Code - OSS',
|
|
157
|
+
'VSCodium',
|
|
158
|
+
'VSCodium - Insiders',
|
|
159
|
+
'Cursor',
|
|
160
|
+
] as const
|
|
161
|
+
|
|
162
|
+
const normalizeThemeName = (themeName: string): string =>
|
|
163
|
+
themeName.trim().toLowerCase()
|
|
164
|
+
|
|
165
|
+
const inferThemeFromName = (themeName: string): ThemeName | null => {
|
|
166
|
+
const normalized = normalizeThemeName(themeName)
|
|
167
|
+
|
|
168
|
+
for (const hint of IDE_THEME_INFERENCE.dark) {
|
|
169
|
+
if (normalized.includes(hint)) {
|
|
170
|
+
return 'dark'
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const hint of IDE_THEME_INFERENCE.light) {
|
|
175
|
+
if (normalized.includes(hint)) {
|
|
176
|
+
return 'light'
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const stripJsonStyleComments = (raw: string): string =>
|
|
184
|
+
raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
|
185
|
+
|
|
186
|
+
const safeReadFile = (filePath: string): string | null => {
|
|
187
|
+
try {
|
|
188
|
+
return readFileSync(filePath, 'utf8')
|
|
189
|
+
} catch {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const collectExistingPaths = (candidates: string[]): string[] => {
|
|
195
|
+
const seen = new Set<string>()
|
|
196
|
+
for (const candidate of candidates) {
|
|
197
|
+
if (!candidate) continue
|
|
198
|
+
try {
|
|
199
|
+
if (existsSync(candidate)) {
|
|
200
|
+
seen.add(candidate)
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Ignore filesystem errors when probing paths
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return [...seen]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const resolveVSCodeSettingsPaths = (
|
|
210
|
+
env: CliEnv = getCliEnv(),
|
|
211
|
+
): string[] => {
|
|
212
|
+
const settings: string[] = []
|
|
213
|
+
const home = homedir()
|
|
214
|
+
|
|
215
|
+
if (process.platform === 'darwin') {
|
|
216
|
+
const base = join(home, 'Library', 'Application Support')
|
|
217
|
+
for (const product of VS_CODE_PRODUCT_DIRS) {
|
|
218
|
+
settings.push(join(base, product, 'User', 'settings.json'))
|
|
219
|
+
}
|
|
220
|
+
} else if (process.platform === 'win32') {
|
|
221
|
+
const appData = env.APPDATA
|
|
222
|
+
if (appData) {
|
|
223
|
+
for (const product of VS_CODE_PRODUCT_DIRS) {
|
|
224
|
+
settings.push(join(appData, product, 'User', 'settings.json'))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
const configDir = env.XDG_CONFIG_HOME ?? join(home, '.config')
|
|
229
|
+
for (const product of VS_CODE_PRODUCT_DIRS) {
|
|
230
|
+
settings.push(join(configDir, product, 'User', 'settings.json'))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return settings
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const resolveJetBrainsLafPaths = (
|
|
238
|
+
env: CliEnv = getCliEnv(),
|
|
239
|
+
): string[] => {
|
|
240
|
+
const candidates: string[] = []
|
|
241
|
+
|
|
242
|
+
// Check IDE config dirs
|
|
243
|
+
if (env.IDE_CONFIG_DIR) {
|
|
244
|
+
candidates.push(join(env.IDE_CONFIG_DIR, 'options', 'laf.xml'))
|
|
245
|
+
}
|
|
246
|
+
if (env.JB_IDE_CONFIG_DIR) {
|
|
247
|
+
candidates.push(join(env.JB_IDE_CONFIG_DIR, 'options', 'laf.xml'))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const home = homedir()
|
|
251
|
+
|
|
252
|
+
const baseDirs: string[] = []
|
|
253
|
+
if (process.platform === 'darwin') {
|
|
254
|
+
baseDirs.push(join(home, 'Library', 'Application Support', 'JetBrains'))
|
|
255
|
+
} else if (process.platform === 'win32') {
|
|
256
|
+
const appData = env.APPDATA
|
|
257
|
+
if (appData) {
|
|
258
|
+
baseDirs.push(join(appData, 'JetBrains'))
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
baseDirs.push(join(home, '.config', 'JetBrains'))
|
|
262
|
+
baseDirs.push(join(home, '.local', 'share', 'JetBrains'))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (const base of baseDirs) {
|
|
266
|
+
try {
|
|
267
|
+
if (!existsSync(base)) continue
|
|
268
|
+
const entries = readdirSync(base)
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const dirPath = join(base, entry)
|
|
271
|
+
try {
|
|
272
|
+
if (!statSync(dirPath).isDirectory()) continue
|
|
273
|
+
} catch {
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
candidates.push(join(dirPath, 'options', 'laf.xml'))
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// Ignore unreadable directories
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return candidates
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const resolveZedSettingsPaths = (
|
|
288
|
+
env: CliEnv = getCliEnv(),
|
|
289
|
+
): string[] => {
|
|
290
|
+
const home = homedir()
|
|
291
|
+
const paths: string[] = []
|
|
292
|
+
|
|
293
|
+
const configDirs = new Set<string>()
|
|
294
|
+
|
|
295
|
+
const xdgConfig = env.XDG_CONFIG_HOME ?? join(home, '.config')
|
|
296
|
+
configDirs.add(join(xdgConfig, 'zed'))
|
|
297
|
+
configDirs.add(join(xdgConfig, 'dev.zed.Zed'))
|
|
298
|
+
|
|
299
|
+
if (process.platform === 'darwin') {
|
|
300
|
+
configDirs.add(join(home, 'Library', 'Application Support', 'Zed'))
|
|
301
|
+
configDirs.add(join(home, 'Library', 'Application Support', 'dev.zed.Zed'))
|
|
302
|
+
} else if (process.platform === 'win32') {
|
|
303
|
+
const appData = env.APPDATA
|
|
304
|
+
if (appData) {
|
|
305
|
+
configDirs.add(join(appData, 'Zed'))
|
|
306
|
+
configDirs.add(join(appData, 'dev.zed.Zed'))
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
configDirs.add(join(home, '.config', 'zed'))
|
|
310
|
+
configDirs.add(join(home, '.config', 'dev.zed.Zed'))
|
|
311
|
+
configDirs.add(join(home, '.local', 'share', 'zed'))
|
|
312
|
+
configDirs.add(join(home, '.local', 'share', 'dev.zed.Zed'))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const legacyConfig = join(home, '.zed')
|
|
316
|
+
configDirs.add(legacyConfig)
|
|
317
|
+
|
|
318
|
+
for (const dir of configDirs) {
|
|
319
|
+
paths.push(join(dir, 'settings.json'))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return paths
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const extractVSCodeTheme = (content: string): ThemeName | null => {
|
|
326
|
+
// Try standard colorTheme setting
|
|
327
|
+
const colorThemeMatch = content.match(
|
|
328
|
+
/"workbench\.colorTheme"\s*:\s*"([^"]+)"/i,
|
|
329
|
+
)
|
|
330
|
+
if (colorThemeMatch) {
|
|
331
|
+
const inferred = inferThemeFromName(colorThemeMatch[1])
|
|
332
|
+
if (inferred) return inferred
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Check if auto-detect is enabled and try preferred themes
|
|
336
|
+
const autoDetectMatch = content.match(
|
|
337
|
+
/"window\.autoDetectColorScheme"\s*:\s*(true|false)/i,
|
|
338
|
+
)
|
|
339
|
+
const autoDetectEnabled = autoDetectMatch?.[1]?.toLowerCase() === 'true'
|
|
340
|
+
|
|
341
|
+
if (autoDetectEnabled) {
|
|
342
|
+
// Try to extract both preferred themes and infer from their names
|
|
343
|
+
const preferredDarkMatch = content.match(
|
|
344
|
+
/"workbench\.preferredDarkColorTheme"\s*:\s*"([^"]+)"/i,
|
|
345
|
+
)
|
|
346
|
+
if (preferredDarkMatch) {
|
|
347
|
+
const inferred = inferThemeFromName(preferredDarkMatch[1])
|
|
348
|
+
if (inferred) return inferred
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const preferredLightMatch = content.match(
|
|
352
|
+
/"workbench\.preferredLightColorTheme"\s*:\s*"([^"]+)"/i,
|
|
353
|
+
)
|
|
354
|
+
if (preferredLightMatch) {
|
|
355
|
+
const inferred = inferThemeFromName(preferredLightMatch[1])
|
|
356
|
+
if (inferred) return inferred
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return null
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const extractJetBrainsTheme = (content: string): ThemeName | null => {
|
|
364
|
+
// Check if autodetect is enabled (Sync with OS setting)
|
|
365
|
+
const autodetectMatch = content.match(
|
|
366
|
+
/<component[^>]+name="LafManager"[^>]+autodetect="(true|false)"/i,
|
|
367
|
+
)
|
|
368
|
+
if (autodetectMatch?.[1]?.toLowerCase() === 'true') {
|
|
369
|
+
// When syncing with OS, return null to trigger platform detection
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const normalized = content.toLowerCase()
|
|
374
|
+
if (normalized.includes('darcula') || normalized.includes('dark')) {
|
|
375
|
+
return 'dark'
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (normalized.includes('light')) {
|
|
379
|
+
return 'light'
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return null
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const isVSCodeFamilyTerminal = (
|
|
386
|
+
env: CliEnv = getCliEnv(),
|
|
387
|
+
): boolean => {
|
|
388
|
+
if (env.TERM_PROGRAM?.toLowerCase() === 'vscode') {
|
|
389
|
+
return true
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check VS Code family env keys
|
|
393
|
+
if (
|
|
394
|
+
env.VSCODE_GIT_IPC_HANDLE ||
|
|
395
|
+
env.VSCODE_PID ||
|
|
396
|
+
env.VSCODE_CWD ||
|
|
397
|
+
env.VSCODE_NLS_CONFIG ||
|
|
398
|
+
env.CURSOR_PORT ||
|
|
399
|
+
env.CURSOR
|
|
400
|
+
) {
|
|
401
|
+
return true
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const isJetBrainsTerminal = (
|
|
408
|
+
env: CliEnv = getCliEnv(),
|
|
409
|
+
): boolean => {
|
|
410
|
+
if (env.TERMINAL_EMULATOR?.toLowerCase().includes('jetbrains')) {
|
|
411
|
+
return true
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Check JetBrains env keys
|
|
415
|
+
if (
|
|
416
|
+
env.JETBRAINS_REMOTE_RUN ||
|
|
417
|
+
env.IDEA_INITIAL_DIRECTORY ||
|
|
418
|
+
env.IDE_CONFIG_DIR ||
|
|
419
|
+
env.JB_IDE_CONFIG_DIR
|
|
420
|
+
) {
|
|
421
|
+
return true
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const isZedTerminal = (
|
|
428
|
+
env: CliEnv = getCliEnv(),
|
|
429
|
+
): boolean => {
|
|
430
|
+
const termProgram = env.TERM_PROGRAM?.toLowerCase()
|
|
431
|
+
return termProgram === 'zed' || false
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const detectVSCodeTheme = (
|
|
435
|
+
env: CliEnv = getCliEnv(),
|
|
436
|
+
): ThemeName | null => {
|
|
437
|
+
if (!isVSCodeFamilyTerminal(env)) {
|
|
438
|
+
return null
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const settingsPaths = collectExistingPaths(resolveVSCodeSettingsPaths(env))
|
|
442
|
+
|
|
443
|
+
for (const settingsPath of settingsPaths) {
|
|
444
|
+
const content = safeReadFile(settingsPath)
|
|
445
|
+
if (!content) continue
|
|
446
|
+
const theme = extractVSCodeTheme(content)
|
|
447
|
+
if (theme) {
|
|
448
|
+
return theme
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// If extractVSCodeTheme returned null but auto-detect is enabled,
|
|
452
|
+
// use platform theme as fallback
|
|
453
|
+
const autoDetectMatch = content.match(
|
|
454
|
+
/"window\.autoDetectColorScheme"\s*:\s*(true|false)/i,
|
|
455
|
+
)
|
|
456
|
+
if (autoDetectMatch?.[1]?.toLowerCase() === 'true') {
|
|
457
|
+
return detectPlatformTheme()
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const themeKindEnv =
|
|
462
|
+
env.VSCODE_THEME_KIND ?? env.VSCODE_COLOR_THEME_KIND
|
|
463
|
+
if (themeKindEnv) {
|
|
464
|
+
const normalized = themeKindEnv.trim().toLowerCase()
|
|
465
|
+
if (normalized === 'dark' || normalized === 'hc') return 'dark'
|
|
466
|
+
if (normalized === 'light') return 'light'
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return null
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const detectJetBrainsTheme = (
|
|
473
|
+
env: CliEnv = getCliEnv(),
|
|
474
|
+
): ThemeName | null => {
|
|
475
|
+
if (!isJetBrainsTerminal(env)) {
|
|
476
|
+
return null
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const lafPaths = collectExistingPaths(resolveJetBrainsLafPaths(env))
|
|
480
|
+
|
|
481
|
+
for (const lafPath of lafPaths) {
|
|
482
|
+
const content = safeReadFile(lafPath)
|
|
483
|
+
if (!content) continue
|
|
484
|
+
const theme = extractJetBrainsTheme(content)
|
|
485
|
+
if (theme) {
|
|
486
|
+
return theme
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// If extractJetBrainsTheme returned null, check if autodetect is enabled
|
|
490
|
+
// and fall back to platform detection
|
|
491
|
+
const autodetectMatch = content.match(
|
|
492
|
+
/<component[^>]+name="LafManager"[^>]+autodetect="(true|false)"/i,
|
|
493
|
+
)
|
|
494
|
+
if (autodetectMatch?.[1]?.toLowerCase() === 'true') {
|
|
495
|
+
return detectPlatformTheme()
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return null
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const extractZedTheme = (content: string): ThemeName | null => {
|
|
503
|
+
try {
|
|
504
|
+
const sanitized = stripJsonStyleComments(content)
|
|
505
|
+
const parsed = JSON.parse(sanitized) as Record<string, unknown>
|
|
506
|
+
const candidates: unknown[] = []
|
|
507
|
+
|
|
508
|
+
const themeSetting = parsed.theme
|
|
509
|
+
if (typeof themeSetting === 'string') {
|
|
510
|
+
candidates.push(themeSetting)
|
|
511
|
+
} else if (themeSetting && typeof themeSetting === 'object') {
|
|
512
|
+
const themeConfig = themeSetting as Record<string, unknown>
|
|
513
|
+
const modeRaw = themeConfig.mode
|
|
514
|
+
if (typeof modeRaw === 'string') {
|
|
515
|
+
const mode = modeRaw.toLowerCase()
|
|
516
|
+
// If mode is 'system', return null to trigger platform detection
|
|
517
|
+
if (mode === 'system') {
|
|
518
|
+
return null
|
|
519
|
+
}
|
|
520
|
+
if (mode === 'dark' || mode === 'light') {
|
|
521
|
+
candidates.push(mode)
|
|
522
|
+
const modeTheme = themeConfig[mode]
|
|
523
|
+
if (typeof modeTheme === 'string') {
|
|
524
|
+
candidates.push(modeTheme)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const darkTheme = themeConfig.dark
|
|
530
|
+
if (typeof darkTheme === 'string') {
|
|
531
|
+
candidates.push(darkTheme)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const lightTheme = themeConfig.light
|
|
535
|
+
if (typeof lightTheme === 'string') {
|
|
536
|
+
candidates.push(lightTheme)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const appearance = parsed.appearance
|
|
541
|
+
if (appearance && typeof appearance === 'object') {
|
|
542
|
+
const appearanceTheme = (appearance as Record<string, unknown>).theme
|
|
543
|
+
if (typeof appearanceTheme === 'string') {
|
|
544
|
+
candidates.push(appearanceTheme)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const preference = (appearance as Record<string, unknown>)
|
|
548
|
+
.theme_preference
|
|
549
|
+
if (typeof preference === 'string') {
|
|
550
|
+
candidates.push(preference)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const ui = parsed.ui
|
|
555
|
+
if (ui && typeof ui === 'object') {
|
|
556
|
+
const uiTheme = (ui as Record<string, unknown>).theme
|
|
557
|
+
if (typeof uiTheme === 'string') {
|
|
558
|
+
candidates.push(uiTheme)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
for (const candidate of candidates) {
|
|
563
|
+
if (typeof candidate !== 'string') continue
|
|
564
|
+
|
|
565
|
+
const inferred = inferThemeFromName(candidate)
|
|
566
|
+
if (inferred) {
|
|
567
|
+
return inferred
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} catch {
|
|
571
|
+
// Ignore malformed or partially written files
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return null
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const detectZedTheme = (
|
|
578
|
+
env: CliEnv = getCliEnv(),
|
|
579
|
+
): ThemeName | null => {
|
|
580
|
+
if (!isZedTerminal(env)) {
|
|
581
|
+
return null
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const settingsPaths = collectExistingPaths(resolveZedSettingsPaths(env))
|
|
585
|
+
for (const settingsPath of settingsPaths) {
|
|
586
|
+
const content = safeReadFile(settingsPath)
|
|
587
|
+
if (!content) continue
|
|
588
|
+
|
|
589
|
+
const theme = extractZedTheme(content)
|
|
590
|
+
if (theme) {
|
|
591
|
+
return theme
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// If extractZedTheme returned null, check if theme mode is 'system'
|
|
595
|
+
// and fall back to platform detection
|
|
596
|
+
try {
|
|
597
|
+
const sanitized = stripJsonStyleComments(content)
|
|
598
|
+
const parsed = JSON.parse(sanitized) as Record<string, unknown>
|
|
599
|
+
const themeSetting = parsed.theme
|
|
600
|
+
if (themeSetting && typeof themeSetting === 'object') {
|
|
601
|
+
const themeConfig = themeSetting as Record<string, unknown>
|
|
602
|
+
const modeRaw = themeConfig.mode
|
|
603
|
+
if (typeof modeRaw === 'string' && modeRaw.toLowerCase() === 'system') {
|
|
604
|
+
return detectPlatformTheme()
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
// Ignore parsing errors
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return null
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export const detectIDETheme = (
|
|
616
|
+
env: CliEnv = getCliEnv(),
|
|
617
|
+
): ThemeName | null => {
|
|
618
|
+
const theme = detectVSCodeTheme(env)
|
|
619
|
+
if (theme) return theme
|
|
620
|
+
|
|
621
|
+
const jbTheme = detectJetBrainsTheme(env)
|
|
622
|
+
if (jbTheme) return jbTheme
|
|
623
|
+
|
|
624
|
+
const zedTheme = detectZedTheme(env)
|
|
625
|
+
if (zedTheme) return zedTheme
|
|
626
|
+
|
|
627
|
+
return null
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export const getIDEThemeConfigPaths = (
|
|
631
|
+
env: CliEnv = getCliEnv(),
|
|
632
|
+
): string[] => {
|
|
633
|
+
const paths = new Set<string>()
|
|
634
|
+
for (const path of resolveVSCodeSettingsPaths(env)) {
|
|
635
|
+
paths.add(path)
|
|
636
|
+
}
|
|
637
|
+
for (const path of resolveJetBrainsLafPaths(env)) {
|
|
638
|
+
paths.add(path)
|
|
639
|
+
}
|
|
640
|
+
for (const path of resolveZedSettingsPaths(env)) {
|
|
641
|
+
paths.add(path)
|
|
642
|
+
}
|
|
643
|
+
return [...paths]
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
type ChatThemeOverrides = Partial<Omit<ChatTheme, 'markdown'>> & {
|
|
647
|
+
markdown?: MarkdownThemeOverrides
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
type ThemeOverrideConfig = Partial<Record<ThemeName, ChatThemeOverrides>> & {
|
|
651
|
+
all?: ChatThemeOverrides
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const mergeMarkdownOverrides = (
|
|
655
|
+
base: MarkdownThemeOverrides | undefined,
|
|
656
|
+
override: MarkdownThemeOverrides | undefined,
|
|
657
|
+
): MarkdownThemeOverrides | undefined => {
|
|
658
|
+
if (!base && !override) return undefined
|
|
659
|
+
if (!override)
|
|
660
|
+
return base
|
|
661
|
+
? {
|
|
662
|
+
...base,
|
|
663
|
+
headingFg: base.headingFg ? { ...base.headingFg } : undefined,
|
|
664
|
+
}
|
|
665
|
+
: undefined
|
|
666
|
+
|
|
667
|
+
const mergedHeading = {
|
|
668
|
+
...(base?.headingFg ?? {}),
|
|
669
|
+
...(override.headingFg ?? {}),
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
...(base ?? {}),
|
|
674
|
+
...override,
|
|
675
|
+
headingFg:
|
|
676
|
+
Object.keys(mergedHeading).length > 0
|
|
677
|
+
? (mergedHeading as Partial<Record<MarkdownHeadingLevel, string>>)
|
|
678
|
+
: undefined,
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const mergeTheme = (
|
|
683
|
+
base: ChatTheme,
|
|
684
|
+
override?: ChatThemeOverrides,
|
|
685
|
+
): ChatTheme => {
|
|
686
|
+
if (!override) {
|
|
687
|
+
return {
|
|
688
|
+
...base,
|
|
689
|
+
markdown: base.markdown
|
|
690
|
+
? {
|
|
691
|
+
...base.markdown,
|
|
692
|
+
headingFg: base.markdown.headingFg
|
|
693
|
+
? { ...base.markdown.headingFg }
|
|
694
|
+
: undefined,
|
|
695
|
+
}
|
|
696
|
+
: undefined,
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
...base,
|
|
702
|
+
...override,
|
|
703
|
+
markdown: mergeMarkdownOverrides(base.markdown, override.markdown),
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export const parseThemeOverrides = (
|
|
708
|
+
raw: string,
|
|
709
|
+
): Partial<Record<ThemeName, ChatThemeOverrides>> => {
|
|
710
|
+
try {
|
|
711
|
+
const parsed = JSON.parse(raw) as ThemeOverrideConfig
|
|
712
|
+
if (!parsed || typeof parsed !== 'object') return {}
|
|
713
|
+
|
|
714
|
+
const result: Partial<Record<ThemeName, ChatThemeOverrides>> = {}
|
|
715
|
+
const common =
|
|
716
|
+
typeof parsed.all === 'object' && parsed.all ? parsed.all : undefined
|
|
717
|
+
|
|
718
|
+
for (const themeName of ['dark', 'light'] as ThemeName[]) {
|
|
719
|
+
const specific =
|
|
720
|
+
typeof parsed?.[themeName] === 'object' && parsed?.[themeName]
|
|
721
|
+
? parsed?.[themeName]
|
|
722
|
+
: undefined
|
|
723
|
+
|
|
724
|
+
const mergedOverrides =
|
|
725
|
+
common || specific
|
|
726
|
+
? {
|
|
727
|
+
...(common ?? {}),
|
|
728
|
+
...(specific ?? {}),
|
|
729
|
+
markdown: mergeMarkdownOverrides(
|
|
730
|
+
common?.markdown,
|
|
731
|
+
specific?.markdown,
|
|
732
|
+
),
|
|
733
|
+
}
|
|
734
|
+
: undefined
|
|
735
|
+
|
|
736
|
+
if (mergedOverrides) {
|
|
737
|
+
result[themeName] = mergedOverrides
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return result
|
|
742
|
+
} catch {
|
|
743
|
+
return {}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const textDecoder = new TextDecoder()
|
|
748
|
+
|
|
749
|
+
const readSpawnOutput = (output: unknown): string => {
|
|
750
|
+
if (!output) return ''
|
|
751
|
+
if (typeof output === 'string') return output.trim()
|
|
752
|
+
if (output instanceof Uint8Array) return textDecoder.decode(output).trim()
|
|
753
|
+
return ''
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const runSystemCommand = (command: string[]): string | null => {
|
|
757
|
+
if (typeof Bun === 'undefined') return null
|
|
758
|
+
if (command.length === 0) return null
|
|
759
|
+
|
|
760
|
+
const [binary] = command
|
|
761
|
+
if (!binary) return null
|
|
762
|
+
|
|
763
|
+
const resolvedBinary =
|
|
764
|
+
Bun.which(binary) ??
|
|
765
|
+
(process.platform === 'win32' ? Bun.which(`${binary}.exe`) : null)
|
|
766
|
+
if (!resolvedBinary) return null
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const result = Bun.spawnSync({
|
|
770
|
+
cmd: [resolvedBinary, ...command.slice(1)],
|
|
771
|
+
stdout: 'pipe',
|
|
772
|
+
stderr: 'pipe',
|
|
773
|
+
})
|
|
774
|
+
if (result.exitCode !== 0) return null
|
|
775
|
+
return readSpawnOutput(result.stdout)
|
|
776
|
+
} catch {
|
|
777
|
+
return null
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export const detectTerminalOverrides = (): ThemeName | null => {
|
|
782
|
+
return null
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export function detectPlatformTheme(): ThemeName {
|
|
786
|
+
if (typeof Bun !== 'undefined') {
|
|
787
|
+
if (process.platform === 'darwin') {
|
|
788
|
+
const value = runSystemCommand([
|
|
789
|
+
'defaults',
|
|
790
|
+
'read',
|
|
791
|
+
'-g',
|
|
792
|
+
'AppleInterfaceStyle',
|
|
793
|
+
])
|
|
794
|
+
if (value?.toLowerCase() === 'dark') return 'dark'
|
|
795
|
+
return 'light'
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// No win32 branch on purpose: it ran two PowerShell commands that Defender
|
|
799
|
+
// flagged on real user machines, so Windows falls through to 'dark'.
|
|
800
|
+
//
|
|
801
|
+
// Be clear about what that costs. OSC 11 does NOT cover Windows today —
|
|
802
|
+
// index.tsx skips detectTerminalTheme() when platform === 'win32' — so
|
|
803
|
+
// outside VS Code and JetBrains (which resolve earlier from their own
|
|
804
|
+
// settings/env) a Windows user now always gets 'dark'. The signal actually
|
|
805
|
+
// lost is the HKCU AppsUseLightTheme read. The (Get-Host).UI.RawUI probe in
|
|
806
|
+
// front of it was reading a *separate* powershell.exe spawned with piped
|
|
807
|
+
// stdout, so it reported that child host's background rather than the
|
|
808
|
+
// user's terminal — whatever it returned, it was not measuring the thing
|
|
809
|
+
// the name suggests.
|
|
810
|
+
//
|
|
811
|
+
// The fix is to enable OSC on Windows, not to bring the subprocesses back:
|
|
812
|
+
// sendOscQuery already targets 'CON' for win32 and is timeout-bounded. It
|
|
813
|
+
// stays off here only because a legacy conhost without VT processing would
|
|
814
|
+
// echo the query bytes as visible garbage, and that needs verifying on a
|
|
815
|
+
// real Windows box first. See theme-platform-detection.test.ts.
|
|
816
|
+
|
|
817
|
+
if (process.platform === 'linux') {
|
|
818
|
+
const value = runSystemCommand([
|
|
819
|
+
'gsettings',
|
|
820
|
+
'get',
|
|
821
|
+
'org.gnome.desktop.interface',
|
|
822
|
+
'color-scheme',
|
|
823
|
+
])
|
|
824
|
+
if (value?.toLowerCase().includes('dark')) return 'dark'
|
|
825
|
+
if (value?.toLowerCase().includes('light')) return 'light'
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return 'dark'
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const DEFAULT_CHAT_THEMES: Record<ThemeName, ChatTheme> = {
|
|
833
|
+
dark: {
|
|
834
|
+
name: 'dark',
|
|
835
|
+
// Core semantic colors
|
|
836
|
+
primary: '#A78BFA',
|
|
837
|
+
secondary: '#a3aed0',
|
|
838
|
+
success: '#22c55e',
|
|
839
|
+
error: '#ef4444',
|
|
840
|
+
warning: '#FFA500',
|
|
841
|
+
info: '#A78BFA',
|
|
842
|
+
link: '#818CF8',
|
|
843
|
+
directory: '#9CA3AF',
|
|
844
|
+
|
|
845
|
+
// Neutral scale
|
|
846
|
+
foreground: '#f1f5f9',
|
|
847
|
+
background: 'transparent',
|
|
848
|
+
muted: '#acb3bf',
|
|
849
|
+
border: '#536175',
|
|
850
|
+
surface: '#202327',
|
|
851
|
+
surfaceHover: '#334155',
|
|
852
|
+
|
|
853
|
+
// Context-specific
|
|
854
|
+
aiLine: '#6b7280',
|
|
855
|
+
userLine: '#A78BFA',
|
|
856
|
+
|
|
857
|
+
// Agent backgrounds
|
|
858
|
+
agentToggleHeaderBg: '#f97316',
|
|
859
|
+
agentToggleExpandedBg: '#1d4ed8',
|
|
860
|
+
agentFocusedBg: '#334155',
|
|
861
|
+
agentContentBg: '#000000',
|
|
862
|
+
inputFg: '#f5f5f5',
|
|
863
|
+
inputFocusedFg: '#ffffff',
|
|
864
|
+
|
|
865
|
+
// Mode toggles
|
|
866
|
+
modeFastBg: '#f97316',
|
|
867
|
+
modeFastText: '#f97316',
|
|
868
|
+
modeMaxBg: '#dc2626',
|
|
869
|
+
modeMaxText: '#dc2626',
|
|
870
|
+
modePlanBg: '#1e40af',
|
|
871
|
+
modePlanText: '#1e40af',
|
|
872
|
+
|
|
873
|
+
// Image card
|
|
874
|
+
imageCardBorder: '#6B7280',
|
|
875
|
+
|
|
876
|
+
// Markdown
|
|
877
|
+
markdown: {
|
|
878
|
+
// Dark mode: slightly darker gray for less brightness
|
|
879
|
+
codeBackground: '#374151',
|
|
880
|
+
codeHeaderFg: '#5b647a',
|
|
881
|
+
inlineCodeFg: '#FF8534',
|
|
882
|
+
codeTextFg: '#f1f5f9',
|
|
883
|
+
headingFg: {
|
|
884
|
+
1: '#facc15',
|
|
885
|
+
2: '#facc15',
|
|
886
|
+
3: '#facc15',
|
|
887
|
+
4: '#facc15',
|
|
888
|
+
5: '#facc15',
|
|
889
|
+
6: '#facc15',
|
|
890
|
+
},
|
|
891
|
+
listBulletFg: '#a3aed0',
|
|
892
|
+
blockquoteBorderFg: '#334155',
|
|
893
|
+
blockquoteTextFg: '#e2e8f0',
|
|
894
|
+
dividerFg: '#283042',
|
|
895
|
+
codeMonochrome: false,
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
light: {
|
|
899
|
+
name: 'light',
|
|
900
|
+
// Core semantic colors
|
|
901
|
+
primary: '#7C3AED',
|
|
902
|
+
secondary: '#6b7280',
|
|
903
|
+
success: '#059669',
|
|
904
|
+
error: '#ef4444',
|
|
905
|
+
warning: '#F59E0B',
|
|
906
|
+
info: '#7C3AED',
|
|
907
|
+
link: '#2563EB',
|
|
908
|
+
directory: '#6B7280',
|
|
909
|
+
|
|
910
|
+
// Neutral scale
|
|
911
|
+
foreground: '#111827',
|
|
912
|
+
background: 'transparent',
|
|
913
|
+
muted: '#6b7280',
|
|
914
|
+
border: '#d1d5db',
|
|
915
|
+
surface: '#f3f4f6',
|
|
916
|
+
surfaceHover: '#e5e7eb',
|
|
917
|
+
|
|
918
|
+
// AI/User context
|
|
919
|
+
aiLine: '#6b7280',
|
|
920
|
+
userLine: '#7C3AED',
|
|
921
|
+
|
|
922
|
+
// Agent context
|
|
923
|
+
agentToggleHeaderBg: '#ea580c',
|
|
924
|
+
agentToggleExpandedBg: '#1d4ed8',
|
|
925
|
+
agentFocusedBg: '#f3f4f6',
|
|
926
|
+
agentContentBg: '#ffffff',
|
|
927
|
+
inputFg: '#111827',
|
|
928
|
+
inputFocusedFg: '#000000',
|
|
929
|
+
|
|
930
|
+
// Mode toggles
|
|
931
|
+
modeFastBg: '#f97316',
|
|
932
|
+
modeFastText: '#f97316',
|
|
933
|
+
modeMaxBg: '#dc2626',
|
|
934
|
+
modeMaxText: '#dc2626',
|
|
935
|
+
modePlanBg: '#1e40af',
|
|
936
|
+
modePlanText: '#1e40af',
|
|
937
|
+
|
|
938
|
+
// Image card
|
|
939
|
+
imageCardBorder: '#6B7280',
|
|
940
|
+
|
|
941
|
+
// Markdown
|
|
942
|
+
markdown: {
|
|
943
|
+
// Light mode: lighter gray background so inline code feels airy
|
|
944
|
+
codeBackground: '#f3f4f6',
|
|
945
|
+
codeHeaderFg: '#6b7280',
|
|
946
|
+
inlineCodeFg: '#C45A00',
|
|
947
|
+
codeTextFg: '#111827',
|
|
948
|
+
headingFg: {
|
|
949
|
+
1: '#dc2626',
|
|
950
|
+
2: '#dc2626',
|
|
951
|
+
3: '#dc2626',
|
|
952
|
+
4: '#dc2626',
|
|
953
|
+
5: '#dc2626',
|
|
954
|
+
6: '#dc2626',
|
|
955
|
+
},
|
|
956
|
+
listBulletFg: '#6b7280',
|
|
957
|
+
blockquoteBorderFg: '#d1d5db',
|
|
958
|
+
blockquoteTextFg: '#374151',
|
|
959
|
+
dividerFg: '#e5e7eb',
|
|
960
|
+
codeMonochrome: false,
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export const chatThemes = {
|
|
966
|
+
dark: DEFAULT_CHAT_THEMES.dark,
|
|
967
|
+
light: DEFAULT_CHAT_THEMES.light,
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export const createMarkdownPalette = (theme: ChatTheme): MarkdownPalette => {
|
|
971
|
+
const headingDefaults: Record<MarkdownHeadingLevel, string> = {
|
|
972
|
+
1: theme.primary,
|
|
973
|
+
2: theme.primary,
|
|
974
|
+
3: theme.primary,
|
|
975
|
+
4: theme.primary,
|
|
976
|
+
5: theme.primary,
|
|
977
|
+
6: theme.primary,
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const overrides = theme.markdown?.headingFg ?? {}
|
|
981
|
+
|
|
982
|
+
return {
|
|
983
|
+
inlineCodeFg: theme.markdown?.inlineCodeFg ?? theme.foreground,
|
|
984
|
+
codeBackground: theme.markdown?.codeBackground ?? theme.background,
|
|
985
|
+
codeHeaderFg: theme.markdown?.codeHeaderFg ?? theme.secondary,
|
|
986
|
+
headingFg: {
|
|
987
|
+
...headingDefaults,
|
|
988
|
+
...overrides,
|
|
989
|
+
},
|
|
990
|
+
listBulletFg: theme.markdown?.listBulletFg ?? theme.secondary,
|
|
991
|
+
blockquoteBorderFg: theme.markdown?.blockquoteBorderFg ?? theme.secondary,
|
|
992
|
+
blockquoteTextFg: theme.markdown?.blockquoteTextFg ?? theme.foreground,
|
|
993
|
+
dividerFg: theme.markdown?.dividerFg ?? theme.secondary,
|
|
994
|
+
codeTextFg: theme.markdown?.codeTextFg ?? theme.foreground,
|
|
995
|
+
codeMonochrome: theme.markdown?.codeMonochrome ?? true,
|
|
996
|
+
linkFg: theme.markdown?.linkFg ?? theme.link,
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Exported utilities for theme system
|
|
1002
|
+
*/
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Merge theme overrides with a base theme
|
|
1006
|
+
* Alias for mergeTheme to match our hook API
|
|
1007
|
+
*/
|
|
1008
|
+
export const mergeThemeOverrides = mergeTheme
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Clone a ChatTheme object to avoid mutations
|
|
1012
|
+
* Properly handles nested markdown configuration
|
|
1013
|
+
*/
|
|
1014
|
+
export const cloneChatTheme = (input: ChatTheme): ChatTheme => ({
|
|
1015
|
+
...input,
|
|
1016
|
+
markdown: input.markdown
|
|
1017
|
+
? {
|
|
1018
|
+
...input.markdown,
|
|
1019
|
+
headingFg: input.markdown.headingFg
|
|
1020
|
+
? { ...input.markdown.headingFg }
|
|
1021
|
+
: undefined,
|
|
1022
|
+
}
|
|
1023
|
+
: undefined,
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Resolve a theme color value with optional fallback
|
|
1028
|
+
* Returns undefined for 'default' values or empty strings
|
|
1029
|
+
*/
|
|
1030
|
+
export const resolveThemeColor = (
|
|
1031
|
+
color?: string,
|
|
1032
|
+
fallback?: string,
|
|
1033
|
+
): string | undefined => {
|
|
1034
|
+
if (typeof color === 'string') {
|
|
1035
|
+
const normalized = color.trim().toLowerCase()
|
|
1036
|
+
if (normalized.length > 0 && normalized !== 'default') {
|
|
1037
|
+
return color
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (fallback !== undefined) {
|
|
1042
|
+
return resolveThemeColor(fallback)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return undefined
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Reactive Theme Detection
|
|
1050
|
+
* Watches for system theme changes and updates zustand store
|
|
1051
|
+
*/
|
|
1052
|
+
|
|
1053
|
+
// Debounce timing for file watcher events
|
|
1054
|
+
const FILE_WATCHER_DEBOUNCE_MS = 250
|
|
1055
|
+
|
|
1056
|
+
let themeStoreUpdater: ((name: ThemeName) => void) | null = null
|
|
1057
|
+
// OSC detections happen asynchronously and at most once.
|
|
1058
|
+
// We cache the resolved value so synchronous theme code can read it later
|
|
1059
|
+
// without triggering terminal I/O.
|
|
1060
|
+
let oscDetectedTheme: ThemeName | null = null
|
|
1061
|
+
let pendingRecomputeTimer: NodeJS.Timeout | null = null
|
|
1062
|
+
let themeResolver: (() => ThemeName) | null = null
|
|
1063
|
+
|
|
1064
|
+
export const getOscDetectedTheme = (): ThemeName | null => oscDetectedTheme
|
|
1065
|
+
export const setOscDetectedTheme = (theme: ThemeName | null): void => {
|
|
1066
|
+
oscDetectedTheme = theme
|
|
1067
|
+
}
|
|
1068
|
+
export const setThemeResolver = (resolver: () => ThemeName) => {
|
|
1069
|
+
themeResolver = resolver
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Initialize theme store updater
|
|
1074
|
+
* Called by theme-store on initialization to enable reactive updates
|
|
1075
|
+
* @param setter - Function to call when theme changes
|
|
1076
|
+
*/
|
|
1077
|
+
export const initializeThemeWatcher = (setter: (name: ThemeName) => void) => {
|
|
1078
|
+
themeStoreUpdater = setter
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Recompute system theme and update store if it changed
|
|
1083
|
+
*/
|
|
1084
|
+
const recomputeSystemTheme = () => {
|
|
1085
|
+
const env = getCliEnv()
|
|
1086
|
+
// Only recompute if theme is auto-detected (not explicitly set)
|
|
1087
|
+
const envPreference = env.OPEN_TUI_THEME ?? env.OPENTUI_THEME
|
|
1088
|
+
if (envPreference && envPreference.toLowerCase() !== 'opposite') {
|
|
1089
|
+
// User explicitly set theme, don't react to system changes
|
|
1090
|
+
return
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (!themeResolver) {
|
|
1094
|
+
return
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const newTheme = themeResolver()
|
|
1098
|
+
|
|
1099
|
+
// Always call the updater and let it decide if an update is needed
|
|
1100
|
+
if (themeStoreUpdater) {
|
|
1101
|
+
themeStoreUpdater(newTheme)
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Debounced version of recomputeSystemTheme for file watcher events
|
|
1107
|
+
* Prevents excessive recomputations when files change rapidly
|
|
1108
|
+
*/
|
|
1109
|
+
const debouncedRecomputeSystemTheme = () => {
|
|
1110
|
+
if (pendingRecomputeTimer) {
|
|
1111
|
+
clearTimeout(pendingRecomputeTimer)
|
|
1112
|
+
}
|
|
1113
|
+
pendingRecomputeTimer = setTimeout(() => {
|
|
1114
|
+
pendingRecomputeTimer = null
|
|
1115
|
+
recomputeSystemTheme()
|
|
1116
|
+
}, FILE_WATCHER_DEBOUNCE_MS)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
let lastDetectedTheme: ThemeName | null = null
|
|
1120
|
+
export function setLastDetectedTheme(theme: ThemeName) {
|
|
1121
|
+
lastDetectedTheme = theme
|
|
1122
|
+
}
|
|
1123
|
+
export function getLastDetectedTheme(): ThemeName | null {
|
|
1124
|
+
return lastDetectedTheme
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Setup file watchers for theme changes
|
|
1129
|
+
* Watches parent directories which reliably catches all file modifications
|
|
1130
|
+
*/
|
|
1131
|
+
export const setupFileWatchers = () => {
|
|
1132
|
+
const watchTargets: string[] = []
|
|
1133
|
+
const watchedDirs = new Set<string>()
|
|
1134
|
+
|
|
1135
|
+
// macOS system preferences
|
|
1136
|
+
if (process.platform === 'darwin') {
|
|
1137
|
+
watchTargets.push(
|
|
1138
|
+
join(homedir(), 'Library/Preferences/.GlobalPreferences.plist'),
|
|
1139
|
+
join(homedir(), 'Library/Preferences/com.apple.Terminal.plist'),
|
|
1140
|
+
)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// IDE config files - only watch for the active IDE terminal
|
|
1144
|
+
if (isVSCodeFamilyTerminal()) {
|
|
1145
|
+
watchTargets.push(...resolveVSCodeSettingsPaths())
|
|
1146
|
+
}
|
|
1147
|
+
if (isJetBrainsTerminal()) {
|
|
1148
|
+
watchTargets.push(...resolveJetBrainsLafPaths())
|
|
1149
|
+
}
|
|
1150
|
+
if (isZedTerminal()) {
|
|
1151
|
+
watchTargets.push(...resolveZedSettingsPaths())
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Watch parent directories instead of individual files
|
|
1155
|
+
// Directory watches are more reliable for catching all modifications including plist key deletions
|
|
1156
|
+
for (const target of watchTargets) {
|
|
1157
|
+
if (existsSync(target)) {
|
|
1158
|
+
const parentDir = dirname(target)
|
|
1159
|
+
|
|
1160
|
+
// Only watch each directory once
|
|
1161
|
+
if (watchedDirs.has(parentDir)) continue
|
|
1162
|
+
watchedDirs.add(parentDir)
|
|
1163
|
+
|
|
1164
|
+
try {
|
|
1165
|
+
// Watch the directory - catches all file modifications
|
|
1166
|
+
const watcher = watch(
|
|
1167
|
+
parentDir,
|
|
1168
|
+
{ persistent: false },
|
|
1169
|
+
(eventType, filename) => {
|
|
1170
|
+
// Only respond to changes affecting our target files
|
|
1171
|
+
if (filename && watchTargets.some((t) => t.endsWith(filename))) {
|
|
1172
|
+
debouncedRecomputeSystemTheme()
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
watcher.on('error', () => {
|
|
1178
|
+
// Silently ignore watcher errors
|
|
1179
|
+
})
|
|
1180
|
+
} catch {
|
|
1181
|
+
// Silently ignore if we can't watch
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* SIGUSR2 signal handler for manual theme refresh
|
|
1189
|
+
* Users can send `kill -USR2 <pid>` to force theme recomputation
|
|
1190
|
+
*/
|
|
1191
|
+
export function enableManualThemeRefresh() {
|
|
1192
|
+
process.on('SIGUSR2', () => {
|
|
1193
|
+
recomputeSystemTheme()
|
|
1194
|
+
})
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* OSC Terminal Theme Detection
|
|
1199
|
+
*
|
|
1200
|
+
* OSC detection is now run synchronously at app startup in index.tsx,
|
|
1201
|
+
* BEFORE OpenTUI is initialized. This avoids stdin conflicts since
|
|
1202
|
+
* OpenTUI hasn't attached its listeners yet.
|
|
1203
|
+
*
|
|
1204
|
+
* The detected theme is stored via setOscDetectedTheme() and retrieved
|
|
1205
|
+
* via getOscDetectedTheme() when building the theme.
|
|
1206
|
+
*/
|