@alumbwe/anvil 1.0.19 → 1.0.20
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 +28 -30
- package/bin/anvil +2 -0
- package/package.json +71 -75
- package/src/agents/bundled-agents.generated.d.ts +14 -0
- package/src/agents/bundled-agents.generated.ts +3760 -0
- package/src/app.tsx +399 -0
- package/src/chat.tsx +1698 -0
- package/src/cli-args.ts +134 -0
- package/src/commands/__tests__/anvil-command-aliases.test.ts +56 -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__/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__/anvil-landing-heading-row.test.tsx +91 -0
- package/src/components/__tests__/anvil-model-selector.test.tsx +519 -0
- package/src/components/__tests__/anvil-offer-invariants.test.ts +54 -0
- package/src/components/__tests__/anvil-takeover-prompt.test.tsx +212 -0
- package/src/components/__tests__/chat-input-bar.test.tsx +130 -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-active-session-summary.tsx +63 -0
- package/src/components/anvil-landing-screen.tsx +865 -0
- package/src/components/anvil-mark.tsx +52 -0
- package/src/components/anvil-model-selector.tsx +1087 -0
- package/src/components/anvil-referral-banner.tsx +604 -0
- package/src/components/anvil-superseded-screen.tsx +62 -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/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-anvil-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-anvil-ctrl-c-exit.ts +23 -0
- package/src/hooks/use-anvil-session-progress.ts +34 -0
- package/src/hooks/use-anvil-session.ts +806 -0
- package/src/hooks/use-anvil-streak-query.ts +61 -0
- package/src/hooks/use-ask-user-bridge.ts +92 -0
- package/src/hooks/use-auth-query.ts +262 -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-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 +473 -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/setup/key-setup.ts +295 -0
- package/src/setup/load-env.ts +106 -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/anvil-model-store.ts +42 -0
- package/src/state/anvil-session-store.ts +53 -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/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/types/anvil-session.ts +17 -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/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__/anvil-instance-owner.test.ts +69 -0
- package/src/utils/__tests__/anvil-model-navigation.test.ts +102 -0
- package/src/utils/__tests__/anvil-premium-reset.test.ts +79 -0
- package/src/utils/__tests__/anvil-referral-cache.test.ts +116 -0
- package/src/utils/__tests__/anvil-session-api.test.ts +215 -0
- package/src/utils/__tests__/anvil-session-display.test.ts +23 -0
- package/src/utils/__tests__/anvil-streak-line.test.ts +217 -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__/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-agent-selection.ts +33 -0
- package/src/utils/anvil-api.ts +612 -0
- package/src/utils/anvil-client.ts +217 -0
- package/src/utils/anvil-instance-owner.ts +66 -0
- package/src/utils/anvil-model-navigation.ts +50 -0
- package/src/utils/anvil-premium-reset.ts +55 -0
- package/src/utils/anvil-referral-cache.ts +51 -0
- package/src/utils/anvil-session-api.ts +243 -0
- package/src/utils/anvil-session-display.ts +21 -0
- package/src/utils/anvil-streak-line.ts +93 -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/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
- package/bin/anvil.cjs +0 -40
- package/dist/bundled.js +0 -123508
- package/dist/bundled.js.map +0 -828
- package/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
package/src/chat.tsx
ADDED
|
@@ -0,0 +1,1698 @@
|
|
|
1
|
+
import { AnalyticsEvent } from '@anvil/common/constants/analytics-events'
|
|
2
|
+
import type { FeedbackCategory } from '@anvil/common/constants/feedback'
|
|
3
|
+
import { isDirectDeepSeekMode } from '@anvil/common/constants/deepseek-direct'
|
|
4
|
+
import { setFreeModeCapacityDeferralListener } from '@anvil/sdk'
|
|
5
|
+
import { safeOpen } from './utils/open-url'
|
|
6
|
+
import {
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react'
|
|
14
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
15
|
+
|
|
16
|
+
import { getAdsEnabled } from './commands/ads'
|
|
17
|
+
import { routeUserPrompt, addBashMessageToHistory } from './commands/router'
|
|
18
|
+
import { SingleAdBanner } from './components/ad-banner'
|
|
19
|
+
import { ChatInputBar } from './components/chat-input-bar'
|
|
20
|
+
import { ChatHeader } from './components/chat-header'
|
|
21
|
+
import { AnvilActiveSessionSummary } from './components/anvil-active-session-summary'
|
|
22
|
+
import { LoadPreviousButton } from './components/load-previous-button'
|
|
23
|
+
import { QueuePanel } from './components/queue-panel'
|
|
24
|
+
import { ReviewScreen } from './components/review-screen'
|
|
25
|
+
import { MessageWithAgents } from './components/message-with-agents'
|
|
26
|
+
import { areCreditsRestored } from './components/out-of-credits-banner'
|
|
27
|
+
import { PendingBashMessage } from './components/pending-bash-message'
|
|
28
|
+
import { SessionEndedBanner } from './components/session-ended-banner'
|
|
29
|
+
import { StatusBar } from './components/status-bar'
|
|
30
|
+
import {
|
|
31
|
+
SuggestedPrompts,
|
|
32
|
+
DEFAULT_SUGGESTED_PROMPTS,
|
|
33
|
+
type SuggestedPromptSelection,
|
|
34
|
+
} from './components/suggested-prompts'
|
|
35
|
+
import { TopBanner } from './components/top-banner'
|
|
36
|
+
import { useChatRuntime } from './contexts/chat-runtime-context'
|
|
37
|
+
import { getSlashCommandsWithSkills } from './data/slash-commands'
|
|
38
|
+
import { useAskUserBridge } from './hooks/use-ask-user-bridge'
|
|
39
|
+
import { useChatInput } from './hooks/use-chat-input'
|
|
40
|
+
import {
|
|
41
|
+
useChatKeyboard,
|
|
42
|
+
type ChatKeyboardHandlers,
|
|
43
|
+
} from './hooks/use-chat-keyboard'
|
|
44
|
+
import { useChatMessages } from './hooks/use-chat-messages'
|
|
45
|
+
import { useChatState } from './hooks/use-chat-state'
|
|
46
|
+
import { useChatStreaming } from './hooks/use-chat-streaming'
|
|
47
|
+
import { useChatUI } from './hooks/use-chat-ui'
|
|
48
|
+
import { useClipboard } from './hooks/use-clipboard'
|
|
49
|
+
import { useEvent } from './hooks/use-event'
|
|
50
|
+
import { useGravityAd } from './hooks/use-gravity-ad'
|
|
51
|
+
import { useInputHistory } from './hooks/use-input-history'
|
|
52
|
+
import { usePublishMutation } from './hooks/use-publish-mutation'
|
|
53
|
+
import { useSuggestionEngine } from './hooks/use-suggestion-engine'
|
|
54
|
+
import { useUsageMonitor } from './hooks/use-usage-monitor'
|
|
55
|
+
import { WEBSITE_URL } from './login/constants'
|
|
56
|
+
import { getProjectRoot } from './project-files'
|
|
57
|
+
import { useChatHistoryStore } from './state/chat-history-store'
|
|
58
|
+
import { useChatStore } from './state/chat-store'
|
|
59
|
+
import { useQueuePanelStore } from './state/queue-panel-store'
|
|
60
|
+
import { useReviewStore } from './state/review-store'
|
|
61
|
+
import { useFeedbackStore } from './state/feedback-store'
|
|
62
|
+
import { useMessageBlockStore } from './state/message-block-store'
|
|
63
|
+
import { usePublishStore } from './state/publish-store'
|
|
64
|
+
import { reportActivity } from './utils/activity-tracker'
|
|
65
|
+
import { stopActiveRun } from './utils/active-run'
|
|
66
|
+
import { trackEvent } from './utils/analytics'
|
|
67
|
+
import { showClipboardMessage } from './utils/clipboard'
|
|
68
|
+
import { readClipboardImage } from './utils/clipboard-image'
|
|
69
|
+
import { returnToAnvilLanding } from './hooks/use-anvil-session'
|
|
70
|
+
import { END_SESSION_MESSAGE, IS_ANVIL } from './utils/constants'
|
|
71
|
+
import { getSystemMessage } from './utils/message-history'
|
|
72
|
+
import { getInputModeConfig } from './utils/input-modes'
|
|
73
|
+
import {
|
|
74
|
+
hasSubmittedFirstPrompt,
|
|
75
|
+
markFirstPromptSubmitted,
|
|
76
|
+
} from './utils/settings'
|
|
77
|
+
|
|
78
|
+
import {
|
|
79
|
+
type ChatKeyboardState,
|
|
80
|
+
createDefaultChatKeyboardState,
|
|
81
|
+
} from './utils/keyboard-actions'
|
|
82
|
+
import { loadLocalAgents } from './utils/local-agent-registry'
|
|
83
|
+
import { logger } from './utils/logger'
|
|
84
|
+
import {
|
|
85
|
+
addClipboardPlaceholder,
|
|
86
|
+
addPendingFileFromPath,
|
|
87
|
+
addPendingImageFromFile,
|
|
88
|
+
validateAndAddImage,
|
|
89
|
+
} from './utils/pending-attachments'
|
|
90
|
+
import { getLoadedSkills } from './utils/skill-registry'
|
|
91
|
+
import {
|
|
92
|
+
getStatusIndicatorState,
|
|
93
|
+
type AuthStatus,
|
|
94
|
+
} from './utils/status-indicator-state'
|
|
95
|
+
import { createPasteHandler } from './utils/strings'
|
|
96
|
+
import { setTerminalTitle } from './utils/terminal-title'
|
|
97
|
+
import { computeInputLayoutMetrics } from './utils/text-layout'
|
|
98
|
+
|
|
99
|
+
import type { CommandResult } from './commands/command-registry'
|
|
100
|
+
import type { MultilineInputHandle } from './components/multiline-input'
|
|
101
|
+
import type { MatchedSlashCommand } from './hooks/use-suggestion-engine'
|
|
102
|
+
import type { AnvilSessionResponse } from './types/anvil-session'
|
|
103
|
+
import type { User } from './utils/auth'
|
|
104
|
+
import type { AgentMode } from './utils/constants'
|
|
105
|
+
import type { FileTreeNode } from '@anvil/common/util/file'
|
|
106
|
+
import type { BoxRenderable, ScrollBoxRenderable } from '@opentui/core'
|
|
107
|
+
import type { UseMutationResult } from '@tanstack/react-query'
|
|
108
|
+
import type { Dispatch, SetStateAction } from 'react'
|
|
109
|
+
|
|
110
|
+
export const Chat = ({
|
|
111
|
+
consumeInitialPrompt,
|
|
112
|
+
fileTree,
|
|
113
|
+
inputRef,
|
|
114
|
+
setIsAuthenticated,
|
|
115
|
+
setUser,
|
|
116
|
+
logoutMutation,
|
|
117
|
+
authStatus,
|
|
118
|
+
initialMode,
|
|
119
|
+
gitRoot,
|
|
120
|
+
onSwitchToGitRoot,
|
|
121
|
+
anvilSession,
|
|
122
|
+
}: {
|
|
123
|
+
consumeInitialPrompt: () => string | null
|
|
124
|
+
fileTree: FileTreeNode[]
|
|
125
|
+
inputRef: React.MutableRefObject<MultilineInputHandle | null>
|
|
126
|
+
setIsAuthenticated: Dispatch<SetStateAction<boolean | null>>
|
|
127
|
+
setUser: Dispatch<SetStateAction<User | null>>
|
|
128
|
+
logoutMutation: UseMutationResult<boolean, Error, void, unknown>
|
|
129
|
+
authStatus: AuthStatus
|
|
130
|
+
initialMode?: AgentMode
|
|
131
|
+
gitRoot?: string | null
|
|
132
|
+
onSwitchToGitRoot?: () => void
|
|
133
|
+
anvilSession: AnvilSessionResponse | null
|
|
134
|
+
}) => {
|
|
135
|
+
const [forceFileOnlyMentions, setForceFileOnlyMentions] = useState(false)
|
|
136
|
+
const headerRef = useRef<BoxRenderable | null>(null)
|
|
137
|
+
|
|
138
|
+
// First-time onboarding: show clickable starter prompts until the user
|
|
139
|
+
// submits their first prompt ever (persisted in settings). Anvil only.
|
|
140
|
+
const [showSuggestedPrompts, setShowSuggestedPrompts] = useState(
|
|
141
|
+
() => IS_ANVIL && !hasSubmittedFirstPrompt(),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Subscribe to ask_user bridge to trigger form display
|
|
145
|
+
useAskUserBridge()
|
|
146
|
+
|
|
147
|
+
// Monitor usage data and auto-show banner when thresholds are crossed
|
|
148
|
+
useUsageMonitor()
|
|
149
|
+
|
|
150
|
+
// Get chat state from extracted hook
|
|
151
|
+
const {
|
|
152
|
+
inputValue,
|
|
153
|
+
cursorPosition,
|
|
154
|
+
lastEditDueToNav,
|
|
155
|
+
setInputValue,
|
|
156
|
+
inputFocused,
|
|
157
|
+
setInputFocused,
|
|
158
|
+
slashSelectedIndex,
|
|
159
|
+
setSlashSelectedIndex,
|
|
160
|
+
agentSelectedIndex,
|
|
161
|
+
setAgentSelectedIndex,
|
|
162
|
+
focusedAgentId,
|
|
163
|
+
setFocusedAgentId,
|
|
164
|
+
messages,
|
|
165
|
+
setMessages,
|
|
166
|
+
agentMode,
|
|
167
|
+
setAgentMode,
|
|
168
|
+
toggleAgentMode,
|
|
169
|
+
isRetrying,
|
|
170
|
+
isCapacityWait,
|
|
171
|
+
pendingBashMessages,
|
|
172
|
+
} = useChatState()
|
|
173
|
+
|
|
174
|
+
const { statusMessage } = useClipboard()
|
|
175
|
+
const {
|
|
176
|
+
isChainInProgressRef,
|
|
177
|
+
sendMessage,
|
|
178
|
+
clearMessages,
|
|
179
|
+
subscriptionData,
|
|
180
|
+
registerScrollToLatest,
|
|
181
|
+
queuedMessages,
|
|
182
|
+
editQueuedMessage,
|
|
183
|
+
removeQueuedMessage,
|
|
184
|
+
moveQueuedMessage,
|
|
185
|
+
} = useChatRuntime()
|
|
186
|
+
const hasSubscription = subscriptionData?.hasSubscription ?? false
|
|
187
|
+
|
|
188
|
+
const {
|
|
189
|
+
ads,
|
|
190
|
+
responseAds,
|
|
191
|
+
requestResponseAds,
|
|
192
|
+
recordClick,
|
|
193
|
+
recordImpression,
|
|
194
|
+
} = useGravityAd({
|
|
195
|
+
enabled: IS_ANVIL || !hasSubscription,
|
|
196
|
+
provider: 'gravity',
|
|
197
|
+
inline: true,
|
|
198
|
+
surface: 'cli_chat',
|
|
199
|
+
// Lazily fill a four-ad pool, then repeat it for later transcript slots.
|
|
200
|
+
inlinePlacementId: 'CLI-Chat-Inline',
|
|
201
|
+
// Keep the rotating above-input slot separate for reporting continuity.
|
|
202
|
+
slotPlacementId: 'Single-Ad-Unit-1',
|
|
203
|
+
})
|
|
204
|
+
const showInlineAds = IS_ANVIL || getAdsEnabled()
|
|
205
|
+
|
|
206
|
+
// Stable identities so the message-block callbacks (set once) always call
|
|
207
|
+
// the latest recorder from the hook.
|
|
208
|
+
const handleAdClick = useEvent(recordClick)
|
|
209
|
+
const handleAdImpression = useEvent(recordImpression)
|
|
210
|
+
const handleResponseAdsNeeded = useEvent(requestResponseAds)
|
|
211
|
+
|
|
212
|
+
// Set initial mode from CLI flag on mount
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (initialMode) {
|
|
215
|
+
setAgentMode(initialMode)
|
|
216
|
+
}
|
|
217
|
+
}, [initialMode, setAgentMode])
|
|
218
|
+
|
|
219
|
+
// Use extracted chat messages hook for message tree and pagination
|
|
220
|
+
const {
|
|
221
|
+
messageTree,
|
|
222
|
+
visibleTopLevelMessages,
|
|
223
|
+
hiddenMessageCount,
|
|
224
|
+
handleCollapseToggle,
|
|
225
|
+
isUserCollapsing,
|
|
226
|
+
handleLoadPreviousMessages,
|
|
227
|
+
handleToggleAll,
|
|
228
|
+
} = useChatMessages({ messages, setMessages })
|
|
229
|
+
|
|
230
|
+
// Use extracted UI hook for scroll, terminal dimensions, and theme
|
|
231
|
+
const {
|
|
232
|
+
scrollRef,
|
|
233
|
+
scrollToLatest,
|
|
234
|
+
scrollUp,
|
|
235
|
+
scrollDown,
|
|
236
|
+
appliedScrollboxProps,
|
|
237
|
+
isAtBottom,
|
|
238
|
+
hasOverflow,
|
|
239
|
+
terminalWidth,
|
|
240
|
+
terminalHeight,
|
|
241
|
+
separatorWidth,
|
|
242
|
+
messageAvailableWidth,
|
|
243
|
+
isCompactHeight,
|
|
244
|
+
isNarrowWidth,
|
|
245
|
+
theme,
|
|
246
|
+
markdownPalette,
|
|
247
|
+
} = useChatUI({ messages, isUserCollapsing })
|
|
248
|
+
|
|
249
|
+
useEffect(
|
|
250
|
+
() => registerScrollToLatest(scrollToLatest),
|
|
251
|
+
[registerScrollToLatest, scrollToLatest],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
// Surface server capacity deferrals (free-mode tier shedding under peak
|
|
255
|
+
// demand): the SDK's retry loop absorbs the 429s silently, so without this
|
|
256
|
+
// the user just sees a longer unexplained "thinking". The stream-chunk
|
|
257
|
+
// handlers clear the flag (via setIsRetrying(false)) as soon as real
|
|
258
|
+
// output resumes.
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
setFreeModeCapacityDeferralListener(() => {
|
|
261
|
+
useChatStore.getState().noteCapacityDeferral()
|
|
262
|
+
})
|
|
263
|
+
return () => setFreeModeCapacityDeferralListener(null)
|
|
264
|
+
}, [])
|
|
265
|
+
|
|
266
|
+
const localAgents = useMemo(() => loadLocalAgents(agentMode), [agentMode])
|
|
267
|
+
const inputMode = useChatStore((state) => state.inputMode)
|
|
268
|
+
const setInputMode = useChatStore((state) => state.setInputMode)
|
|
269
|
+
const askUserState = useChatStore((state) => state.askUserState)
|
|
270
|
+
|
|
271
|
+
// Get loaded skills for slash commands
|
|
272
|
+
const loadedSkills = useMemo(() => getLoadedSkills(), [])
|
|
273
|
+
|
|
274
|
+
// Filter slash commands based on current ads state - only show the option that changes state
|
|
275
|
+
// Hide both ads commands entirely for subscribers
|
|
276
|
+
// Also merge in skill commands
|
|
277
|
+
const filteredSlashCommands = useMemo(() => {
|
|
278
|
+
const adsEnabled = getAdsEnabled()
|
|
279
|
+
const allCommands = getSlashCommandsWithSkills(loadedSkills)
|
|
280
|
+
return allCommands.filter((cmd) => {
|
|
281
|
+
if (cmd.id === 'ads:enable') return !hasSubscription && !adsEnabled
|
|
282
|
+
if (cmd.id === 'ads:disable') return !hasSubscription && adsEnabled
|
|
283
|
+
return true
|
|
284
|
+
})
|
|
285
|
+
}, [inputValue, loadedSkills, hasSubscription]) // Re-evaluate when input changes (user may have just toggled)
|
|
286
|
+
|
|
287
|
+
const {
|
|
288
|
+
slashContext,
|
|
289
|
+
mentionContext,
|
|
290
|
+
slashMatches,
|
|
291
|
+
agentMatches,
|
|
292
|
+
fileMatches,
|
|
293
|
+
slashSuggestionItems,
|
|
294
|
+
agentSuggestionItems,
|
|
295
|
+
fileSuggestionItems,
|
|
296
|
+
} = useSuggestionEngine({
|
|
297
|
+
disableAgentSuggestions: forceFileOnlyMentions || inputMode !== 'default',
|
|
298
|
+
inputValue: inputMode === 'bash' ? '' : inputValue,
|
|
299
|
+
cursorPosition,
|
|
300
|
+
slashCommands: filteredSlashCommands,
|
|
301
|
+
localAgents,
|
|
302
|
+
fileTree,
|
|
303
|
+
currentAgentMode: agentMode,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!mentionContext.active) {
|
|
308
|
+
setForceFileOnlyMentions(false)
|
|
309
|
+
}
|
|
310
|
+
}, [mentionContext.active])
|
|
311
|
+
|
|
312
|
+
// Track when slash menu is activated
|
|
313
|
+
const prevSlashActiveRef = useRef(false)
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (slashContext.active && !prevSlashActiveRef.current) {
|
|
316
|
+
trackEvent(AnalyticsEvent.SLASH_MENU_ACTIVATED, {
|
|
317
|
+
queryLength: slashContext.query.length,
|
|
318
|
+
matchCount: slashMatches.length,
|
|
319
|
+
inputLength: inputValue.length,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
prevSlashActiveRef.current = slashContext.active
|
|
323
|
+
}, [
|
|
324
|
+
slashContext.active,
|
|
325
|
+
slashContext.query,
|
|
326
|
+
slashMatches.length,
|
|
327
|
+
inputValue.length,
|
|
328
|
+
])
|
|
329
|
+
|
|
330
|
+
// Reset suggestion menu indexes when context changes
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (!slashContext.active) {
|
|
333
|
+
setSlashSelectedIndex(0)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
setSlashSelectedIndex(0)
|
|
337
|
+
}, [slashContext.active, slashContext.query, setSlashSelectedIndex])
|
|
338
|
+
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
if (slashMatches.length > 0 && slashSelectedIndex >= slashMatches.length) {
|
|
341
|
+
setSlashSelectedIndex(slashMatches.length - 1)
|
|
342
|
+
}
|
|
343
|
+
if (slashMatches.length === 0 && slashSelectedIndex !== 0) {
|
|
344
|
+
setSlashSelectedIndex(0)
|
|
345
|
+
}
|
|
346
|
+
}, [slashMatches.length, slashSelectedIndex, setSlashSelectedIndex])
|
|
347
|
+
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
if (!mentionContext.active) {
|
|
350
|
+
setAgentSelectedIndex(0)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
setAgentSelectedIndex(0)
|
|
354
|
+
}, [mentionContext.active, mentionContext.query, setAgentSelectedIndex])
|
|
355
|
+
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
const totalMatches = agentMatches.length + fileMatches.length
|
|
358
|
+
if (totalMatches > 0 && agentSelectedIndex >= totalMatches) {
|
|
359
|
+
setAgentSelectedIndex(totalMatches - 1)
|
|
360
|
+
}
|
|
361
|
+
if (totalMatches === 0 && agentSelectedIndex !== 0) {
|
|
362
|
+
setAgentSelectedIndex(0)
|
|
363
|
+
}
|
|
364
|
+
}, [
|
|
365
|
+
agentMatches.length,
|
|
366
|
+
fileMatches.length,
|
|
367
|
+
agentSelectedIndex,
|
|
368
|
+
setAgentSelectedIndex,
|
|
369
|
+
])
|
|
370
|
+
|
|
371
|
+
const openFileMenuWithTab = useCallback(() => {
|
|
372
|
+
const safeCursor = Math.max(0, Math.min(cursorPosition, inputValue.length))
|
|
373
|
+
|
|
374
|
+
let wordStart = safeCursor
|
|
375
|
+
while (wordStart > 0 && !/\s/.test(inputValue[wordStart - 1])) {
|
|
376
|
+
wordStart--
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const before = inputValue.slice(0, wordStart)
|
|
380
|
+
const wordAtCursor = inputValue.slice(wordStart, safeCursor)
|
|
381
|
+
const after = inputValue.slice(safeCursor)
|
|
382
|
+
const mentionWord = wordAtCursor.startsWith('@')
|
|
383
|
+
? wordAtCursor
|
|
384
|
+
: `@${wordAtCursor}`
|
|
385
|
+
|
|
386
|
+
const text = `${before}${mentionWord}${after}`
|
|
387
|
+
const nextCursor = before.length + mentionWord.length
|
|
388
|
+
|
|
389
|
+
setInputValue({
|
|
390
|
+
text,
|
|
391
|
+
cursorPosition: nextCursor,
|
|
392
|
+
lastEditDueToNav: false,
|
|
393
|
+
})
|
|
394
|
+
setForceFileOnlyMentions(true)
|
|
395
|
+
}, [cursorPosition, inputValue, setInputValue])
|
|
396
|
+
|
|
397
|
+
const { saveToHistory, navigateUp, navigateDown, resetHistoryNavigation } =
|
|
398
|
+
useInputHistory(inputValue, setInputValue, { inputMode, setInputMode })
|
|
399
|
+
|
|
400
|
+
// Use extracted streaming hook for connection, timer, queue, and exit handling
|
|
401
|
+
const {
|
|
402
|
+
isConnected,
|
|
403
|
+
showReconnectionMessage,
|
|
404
|
+
timerStartTime,
|
|
405
|
+
streamStatus,
|
|
406
|
+
isWaitingForResponse,
|
|
407
|
+
isStreaming,
|
|
408
|
+
queuePaused,
|
|
409
|
+
streamMessageIdRef,
|
|
410
|
+
addToQueue,
|
|
411
|
+
setCanProcessQueue,
|
|
412
|
+
clearQueue,
|
|
413
|
+
queuedCount,
|
|
414
|
+
shouldShowQueuePreview,
|
|
415
|
+
inputBoxTitle,
|
|
416
|
+
inputPlaceholder,
|
|
417
|
+
handleCtrlC,
|
|
418
|
+
ensureQueueActiveBeforeSubmit,
|
|
419
|
+
nextCtrlCWillExit,
|
|
420
|
+
} = useChatStreaming({
|
|
421
|
+
inputValue,
|
|
422
|
+
setInputValue,
|
|
423
|
+
terminalWidth,
|
|
424
|
+
separatorWidth,
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// When streaming completes, flush any pending bash commands into history (ghost mode only)
|
|
428
|
+
// Non-ghost mode commands are already in history and will be cleared when user sends next message
|
|
429
|
+
useEffect(() => {
|
|
430
|
+
if (
|
|
431
|
+
!isStreaming &&
|
|
432
|
+
!streamMessageIdRef.current &&
|
|
433
|
+
!isChainInProgressRef.current &&
|
|
434
|
+
pendingBashMessages.length > 0
|
|
435
|
+
) {
|
|
436
|
+
// Only flush ghost mode commands (those not already added to history) to UI
|
|
437
|
+
const ghostModeMessages = pendingBashMessages.filter(
|
|
438
|
+
(msg) => !msg.isRunning && !msg.addedToHistory,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
// Add ghost mode messages to UI history
|
|
442
|
+
for (const msg of ghostModeMessages) {
|
|
443
|
+
addBashMessageToHistory({
|
|
444
|
+
command: msg.command,
|
|
445
|
+
stdout: msg.stdout,
|
|
446
|
+
stderr: msg.stderr ?? null,
|
|
447
|
+
exitCode: msg.exitCode,
|
|
448
|
+
cwd: msg.cwd || process.cwd(),
|
|
449
|
+
setMessages,
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Mark ghost mode messages as added to history (so they don't show as ghost UI)
|
|
454
|
+
// but keep them in pendingBashMessages so they get sent to LLM with next user message
|
|
455
|
+
if (ghostModeMessages.length > 0) {
|
|
456
|
+
const ghostIds = new Set(ghostModeMessages.map((m) => m.id))
|
|
457
|
+
useChatStore.setState((state) => ({
|
|
458
|
+
pendingBashMessages: state.pendingBashMessages.map((m) =>
|
|
459
|
+
ghostIds.has(m.id) ? { ...m, addedToHistory: true } : m,
|
|
460
|
+
),
|
|
461
|
+
}))
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}, [isStreaming, pendingBashMessages, setMessages])
|
|
465
|
+
|
|
466
|
+
const onSubmitPrompt = useEvent(
|
|
467
|
+
async (
|
|
468
|
+
content: string,
|
|
469
|
+
mode: AgentMode,
|
|
470
|
+
options?: { preserveInputValue?: boolean },
|
|
471
|
+
) => {
|
|
472
|
+
ensureQueueActiveBeforeSubmit()
|
|
473
|
+
|
|
474
|
+
const preserveInput = options?.preserveInputValue === true
|
|
475
|
+
const previousInputValue = preserveInput
|
|
476
|
+
? (() => {
|
|
477
|
+
const {
|
|
478
|
+
inputValue: text,
|
|
479
|
+
cursorPosition,
|
|
480
|
+
lastEditDueToNav,
|
|
481
|
+
} = useChatStore.getState()
|
|
482
|
+
return { text, cursorPosition, lastEditDueToNav }
|
|
483
|
+
})()
|
|
484
|
+
: null
|
|
485
|
+
|
|
486
|
+
// Preserve attachments if needed (inline logic to avoid abstraction overhead)
|
|
487
|
+
const preservedAttachments = preserveInput
|
|
488
|
+
? (() => {
|
|
489
|
+
const items = useChatStore.getState().pendingAttachments
|
|
490
|
+
if (items.length > 0) {
|
|
491
|
+
useChatStore.getState().clearPendingAttachments()
|
|
492
|
+
return [...items]
|
|
493
|
+
}
|
|
494
|
+
return null
|
|
495
|
+
})()
|
|
496
|
+
: null
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const result = await routeUserPrompt({
|
|
500
|
+
agentMode: mode,
|
|
501
|
+
inputRef,
|
|
502
|
+
inputValue: content,
|
|
503
|
+
isChainInProgressRef,
|
|
504
|
+
isStreaming,
|
|
505
|
+
logoutMutation,
|
|
506
|
+
streamMessageIdRef,
|
|
507
|
+
addToQueue,
|
|
508
|
+
clearMessages,
|
|
509
|
+
saveToHistory,
|
|
510
|
+
scrollToLatest,
|
|
511
|
+
sendMessage,
|
|
512
|
+
setCanProcessQueue,
|
|
513
|
+
setInputFocused,
|
|
514
|
+
setInputValue,
|
|
515
|
+
setIsAuthenticated,
|
|
516
|
+
setMessages,
|
|
517
|
+
setUser,
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
return result
|
|
521
|
+
} finally {
|
|
522
|
+
if (previousInputValue) {
|
|
523
|
+
setInputValue({
|
|
524
|
+
text: previousInputValue.text,
|
|
525
|
+
cursorPosition: previousInputValue.cursorPosition,
|
|
526
|
+
lastEditDueToNav: previousInputValue.lastEditDueToNav,
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Restore attachments if they were preserved and none have been added since
|
|
531
|
+
if (
|
|
532
|
+
preservedAttachments &&
|
|
533
|
+
useChatStore.getState().pendingAttachments.length === 0
|
|
534
|
+
) {
|
|
535
|
+
useChatStore.setState((state) => {
|
|
536
|
+
state.pendingAttachments = preservedAttachments
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
// Retire onboarding suggested prompts once the user submits anything
|
|
544
|
+
// (typed or clicked), persisting so they don't return on future launches.
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
if (showSuggestedPrompts && messages.length > 0) {
|
|
547
|
+
markFirstPromptSubmitted()
|
|
548
|
+
setShowSuggestedPrompts(false)
|
|
549
|
+
}
|
|
550
|
+
}, [showSuggestedPrompts, messages.length])
|
|
551
|
+
|
|
552
|
+
// Submit a suggested onboarding prompt as if the user had typed and sent it
|
|
553
|
+
const handleSelectSuggestedPrompt = useEvent(
|
|
554
|
+
(prompt: string, selection: SuggestedPromptSelection) => {
|
|
555
|
+
trackEvent(AnalyticsEvent.SUGGESTED_PROMPT_CLICKED, {
|
|
556
|
+
label: selection.label,
|
|
557
|
+
index: selection.index,
|
|
558
|
+
promptLength: prompt.length,
|
|
559
|
+
agentMode,
|
|
560
|
+
})
|
|
561
|
+
onSubmitPrompt(prompt, agentMode).catch((error) => {
|
|
562
|
+
logger.error({ error }, '[suggested-prompt] Failed to submit prompt')
|
|
563
|
+
showClipboardMessage('Failed to send prompt', { durationMs: 3000 })
|
|
564
|
+
})
|
|
565
|
+
},
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
// Handle followup suggestion clicks
|
|
569
|
+
useEffect(() => {
|
|
570
|
+
const handleFollowupClick = (event: Event) => {
|
|
571
|
+
const customEvent = event as CustomEvent<{
|
|
572
|
+
prompt: string
|
|
573
|
+
index: number
|
|
574
|
+
toolCallId: string
|
|
575
|
+
}>
|
|
576
|
+
const { prompt, index, toolCallId } = customEvent.detail
|
|
577
|
+
|
|
578
|
+
logger.info(
|
|
579
|
+
{ promptLength: prompt.length, index, toolCallId, agentMode },
|
|
580
|
+
'[followup-click] Followup clicked',
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
// Track analytics event
|
|
584
|
+
trackEvent(AnalyticsEvent.FOLLOWUP_CLICKED, {
|
|
585
|
+
promptLength: prompt.length,
|
|
586
|
+
index,
|
|
587
|
+
agentMode,
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
// Mark this followup as clicked (persisted per toolCallId)
|
|
591
|
+
useChatStore.getState().markFollowupClicked(toolCallId, index)
|
|
592
|
+
|
|
593
|
+
// Send the followup prompt directly, preserving the user's current input
|
|
594
|
+
onSubmitPrompt(prompt, agentMode, {
|
|
595
|
+
preserveInputValue: true,
|
|
596
|
+
})
|
|
597
|
+
.then((result) => {
|
|
598
|
+
logger.info(
|
|
599
|
+
{ hasResult: !!result },
|
|
600
|
+
'[followup-click] onSubmitPrompt completed',
|
|
601
|
+
)
|
|
602
|
+
})
|
|
603
|
+
.catch((error) => {
|
|
604
|
+
logger.error(
|
|
605
|
+
{ error },
|
|
606
|
+
'[followup-click] onSubmitPrompt failed with error',
|
|
607
|
+
)
|
|
608
|
+
showClipboardMessage('Failed to send followup', { durationMs: 3000 })
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
globalThis.addEventListener('anvil:send-followup', handleFollowupClick)
|
|
613
|
+
return () => {
|
|
614
|
+
globalThis.removeEventListener(
|
|
615
|
+
'anvil:send-followup',
|
|
616
|
+
handleFollowupClick,
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
}, [onSubmitPrompt, agentMode])
|
|
620
|
+
|
|
621
|
+
// handleSlashItemClick is defined later after feedback/publish stores are available
|
|
622
|
+
|
|
623
|
+
const handleMentionItemClick = useCallback(
|
|
624
|
+
(index: number) => {
|
|
625
|
+
if (mentionContext.startIndex < 0) return
|
|
626
|
+
|
|
627
|
+
let replacement: string
|
|
628
|
+
if (index < agentMatches.length) {
|
|
629
|
+
const selected = agentMatches[index]
|
|
630
|
+
if (!selected) return
|
|
631
|
+
replacement = `@${selected.id} `
|
|
632
|
+
} else {
|
|
633
|
+
const fileIndex = index - agentMatches.length
|
|
634
|
+
const selectedFile = fileMatches[fileIndex]
|
|
635
|
+
if (!selectedFile) return
|
|
636
|
+
replacement = `@${selectedFile.filePath} `
|
|
637
|
+
}
|
|
638
|
+
const before = inputValue.slice(0, mentionContext.startIndex)
|
|
639
|
+
const after = inputValue.slice(
|
|
640
|
+
mentionContext.startIndex + 1 + mentionContext.query.length,
|
|
641
|
+
)
|
|
642
|
+
setInputValue({
|
|
643
|
+
text: before + replacement + after,
|
|
644
|
+
cursorPosition: before.length + replacement.length,
|
|
645
|
+
lastEditDueToNav: false,
|
|
646
|
+
})
|
|
647
|
+
setAgentSelectedIndex(0)
|
|
648
|
+
},
|
|
649
|
+
[
|
|
650
|
+
mentionContext,
|
|
651
|
+
agentMatches,
|
|
652
|
+
fileMatches,
|
|
653
|
+
inputValue,
|
|
654
|
+
setInputValue,
|
|
655
|
+
setAgentSelectedIndex,
|
|
656
|
+
],
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
const { inputWidth, handleBuildFast, handleBuildMax, handleBuildLite } =
|
|
660
|
+
useChatInput({
|
|
661
|
+
setInputValue,
|
|
662
|
+
agentMode,
|
|
663
|
+
setAgentMode,
|
|
664
|
+
separatorWidth,
|
|
665
|
+
consumeInitialPrompt,
|
|
666
|
+
onSubmitPrompt,
|
|
667
|
+
isCompactHeight,
|
|
668
|
+
isNarrowWidth,
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
const {
|
|
672
|
+
feedbackMode,
|
|
673
|
+
feedbackText,
|
|
674
|
+
openFeedbackForMessage,
|
|
675
|
+
closeFeedback,
|
|
676
|
+
saveCurrentInput,
|
|
677
|
+
restoreSavedInput,
|
|
678
|
+
setFeedbackText,
|
|
679
|
+
} = useFeedbackStore(
|
|
680
|
+
useShallow((state) => ({
|
|
681
|
+
feedbackMode: state.feedbackMode,
|
|
682
|
+
feedbackText: state.feedbackText,
|
|
683
|
+
openFeedbackForMessage: state.openFeedbackForMessage,
|
|
684
|
+
closeFeedback: state.closeFeedback,
|
|
685
|
+
saveCurrentInput: state.saveCurrentInput,
|
|
686
|
+
restoreSavedInput: state.restoreSavedInput,
|
|
687
|
+
setFeedbackText: state.setFeedbackText,
|
|
688
|
+
})),
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
const { publishMode, openPublishMode, closePublish, preSelectAgents } =
|
|
692
|
+
usePublishStore(
|
|
693
|
+
useShallow((state) => ({
|
|
694
|
+
publishMode: state.publishMode,
|
|
695
|
+
openPublishMode: state.openPublishMode,
|
|
696
|
+
closePublish: state.closePublish,
|
|
697
|
+
preSelectAgents: state.preSelectAgents,
|
|
698
|
+
})),
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
const { reviewMode, closeReviewScreen } = useReviewStore(
|
|
702
|
+
useShallow((state) => ({
|
|
703
|
+
reviewMode: state.reviewMode,
|
|
704
|
+
closeReviewScreen: state.closeReviewScreen,
|
|
705
|
+
})),
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
const { queuePanelOpen, openQueuePanel, closeQueuePanel } =
|
|
709
|
+
useQueuePanelStore(
|
|
710
|
+
useShallow((state) => ({
|
|
711
|
+
queuePanelOpen: state.queuePanelOpen,
|
|
712
|
+
openQueuePanel: state.openQueuePanel,
|
|
713
|
+
closeQueuePanel: state.closeQueuePanel,
|
|
714
|
+
})),
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
// Review and ask_user take the composer's place too. Leaving the panel
|
|
718
|
+
// flagged open behind them would keep chat's keyboard disabled with nothing
|
|
719
|
+
// rendered to handle keys, so hand the surface back for real.
|
|
720
|
+
useEffect(() => {
|
|
721
|
+
if (queuePanelOpen && (reviewMode || askUserState !== null)) {
|
|
722
|
+
closeQueuePanel()
|
|
723
|
+
}
|
|
724
|
+
}, [queuePanelOpen, reviewMode, askUserState, closeQueuePanel])
|
|
725
|
+
|
|
726
|
+
// The panel store outlives this component and a Anvil session can end on
|
|
727
|
+
// its own, unmounting chat mid-edit. Without this, the next session would
|
|
728
|
+
// open onto a panel for a queue that no longer exists.
|
|
729
|
+
useEffect(() => () => useQueuePanelStore.getState().closeQueuePanel(), [])
|
|
730
|
+
|
|
731
|
+
const publishMutation = usePublishMutation()
|
|
732
|
+
|
|
733
|
+
const handleCommandResult = useCallback(
|
|
734
|
+
(result?: CommandResult) => {
|
|
735
|
+
if (!result) return
|
|
736
|
+
|
|
737
|
+
if (result.openFeedbackMode) {
|
|
738
|
+
// Save the feedback text that was set by the command handler before opening feedback mode
|
|
739
|
+
const { feedbackText, feedbackCursor } = useFeedbackStore.getState()
|
|
740
|
+
saveCurrentInput('', 0)
|
|
741
|
+
openFeedbackForMessage(null)
|
|
742
|
+
// Restore the prefilled text after openFeedbackForMessage resets it
|
|
743
|
+
if (feedbackText) {
|
|
744
|
+
useFeedbackStore.getState().setFeedbackText(feedbackText)
|
|
745
|
+
useFeedbackStore.getState().setFeedbackCursor(feedbackCursor)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (result.openPublishMode) {
|
|
750
|
+
if (result.preSelectAgents && result.preSelectAgents.length > 0) {
|
|
751
|
+
// preSelectAgents already sets publishMode: true, so don't call openPublishMode
|
|
752
|
+
// which would reset the selectedAgentIds
|
|
753
|
+
preSelectAgents(result.preSelectAgents)
|
|
754
|
+
} else {
|
|
755
|
+
openPublishMode()
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (result.openChatHistory) {
|
|
760
|
+
useChatHistoryStore.getState().openChatHistory()
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (result.openReviewScreen) {
|
|
764
|
+
useReviewStore.getState().openReviewScreen()
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (result.openQueuePanel) {
|
|
768
|
+
// The panel closes itself once the queue drains, so opening an empty
|
|
769
|
+
// one would just flash. Say so instead.
|
|
770
|
+
if (queuedCount > 0) useQueuePanelStore.getState().openQueuePanel()
|
|
771
|
+
else setMessages((prev) => [...prev, getSystemMessage('Nothing queued.')])
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
[
|
|
775
|
+
saveCurrentInput,
|
|
776
|
+
openFeedbackForMessage,
|
|
777
|
+
openPublishMode,
|
|
778
|
+
preSelectAgents,
|
|
779
|
+
queuedCount,
|
|
780
|
+
setMessages,
|
|
781
|
+
],
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
// Helper to apply insertText for slash commands - returns true if handled
|
|
785
|
+
const applySlashInsertText = useCallback(
|
|
786
|
+
(selected: MatchedSlashCommand): boolean => {
|
|
787
|
+
if (selected.insertText != null && slashContext.startIndex >= 0) {
|
|
788
|
+
const before = inputValue.slice(0, slashContext.startIndex)
|
|
789
|
+
const after = inputValue.slice(
|
|
790
|
+
slashContext.startIndex + 1 + slashContext.query.length,
|
|
791
|
+
)
|
|
792
|
+
setInputValue({
|
|
793
|
+
text: before + selected.insertText + after,
|
|
794
|
+
cursorPosition: before.length + selected.insertText.length,
|
|
795
|
+
lastEditDueToNav: false,
|
|
796
|
+
})
|
|
797
|
+
setSlashSelectedIndex(0)
|
|
798
|
+
return true
|
|
799
|
+
}
|
|
800
|
+
return false
|
|
801
|
+
},
|
|
802
|
+
[slashContext, inputValue, setInputValue, setSlashSelectedIndex],
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
// Click handler for slash menu items - executes command or inserts text
|
|
806
|
+
const handleSlashItemClick = useCallback(
|
|
807
|
+
async (index: number) => {
|
|
808
|
+
const selected = slashMatches[index]
|
|
809
|
+
if (!selected) return
|
|
810
|
+
|
|
811
|
+
// If the command has insertText, insert it instead of executing
|
|
812
|
+
if (applySlashInsertText(selected)) return
|
|
813
|
+
|
|
814
|
+
// Execute the selected slash command immediately
|
|
815
|
+
const commandString = `/${selected.id}`
|
|
816
|
+
setSlashSelectedIndex(0)
|
|
817
|
+
|
|
818
|
+
const result = await onSubmitPrompt(commandString, agentMode)
|
|
819
|
+
handleCommandResult(result)
|
|
820
|
+
},
|
|
821
|
+
[
|
|
822
|
+
slashMatches,
|
|
823
|
+
applySlashInsertText,
|
|
824
|
+
setSlashSelectedIndex,
|
|
825
|
+
onSubmitPrompt,
|
|
826
|
+
agentMode,
|
|
827
|
+
handleCommandResult,
|
|
828
|
+
],
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
const inputValueRef = useRef(inputValue)
|
|
832
|
+
const cursorPositionRef = useRef(cursorPosition)
|
|
833
|
+
useEffect(() => {
|
|
834
|
+
inputValueRef.current = inputValue
|
|
835
|
+
}, [inputValue])
|
|
836
|
+
|
|
837
|
+
// Report activity on input changes for ad rotation (debounced via separate effect)
|
|
838
|
+
const lastReportedActivityRef = useRef<number>(0)
|
|
839
|
+
useEffect(() => {
|
|
840
|
+
const now = Date.now()
|
|
841
|
+
// Throttle to max once per second to avoid excessive calls
|
|
842
|
+
if (now - lastReportedActivityRef.current > 1000) {
|
|
843
|
+
lastReportedActivityRef.current = now
|
|
844
|
+
reportActivity()
|
|
845
|
+
}
|
|
846
|
+
}, [inputValue])
|
|
847
|
+
useEffect(() => {
|
|
848
|
+
cursorPositionRef.current = cursorPosition
|
|
849
|
+
}, [cursorPosition])
|
|
850
|
+
|
|
851
|
+
const handleOpenFeedbackForMessage = useCallback(
|
|
852
|
+
(
|
|
853
|
+
id: string | null,
|
|
854
|
+
options?: {
|
|
855
|
+
category?: FeedbackCategory
|
|
856
|
+
footerMessage?: string
|
|
857
|
+
errors?: Array<{ id: string; message: string }>
|
|
858
|
+
},
|
|
859
|
+
) => {
|
|
860
|
+
saveCurrentInput(inputValueRef.current, cursorPositionRef.current)
|
|
861
|
+
openFeedbackForMessage(id, options)
|
|
862
|
+
},
|
|
863
|
+
[saveCurrentInput, openFeedbackForMessage],
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
const handleMessageFeedback = useCallback(
|
|
867
|
+
(
|
|
868
|
+
id: string,
|
|
869
|
+
options?: {
|
|
870
|
+
category?: FeedbackCategory
|
|
871
|
+
footerMessage?: string
|
|
872
|
+
errors?: Array<{ id: string; message: string }>
|
|
873
|
+
},
|
|
874
|
+
) => {
|
|
875
|
+
handleOpenFeedbackForMessage(id, options)
|
|
876
|
+
},
|
|
877
|
+
[handleOpenFeedbackForMessage],
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
const handleExitFeedback = useCallback(() => {
|
|
881
|
+
const { value, cursor } = restoreSavedInput()
|
|
882
|
+
setInputValue({
|
|
883
|
+
text: value,
|
|
884
|
+
cursorPosition: cursor,
|
|
885
|
+
lastEditDueToNav: false,
|
|
886
|
+
})
|
|
887
|
+
setInputFocused(true)
|
|
888
|
+
resetHistoryNavigation()
|
|
889
|
+
}, [
|
|
890
|
+
restoreSavedInput,
|
|
891
|
+
setInputValue,
|
|
892
|
+
setInputFocused,
|
|
893
|
+
resetHistoryNavigation,
|
|
894
|
+
])
|
|
895
|
+
|
|
896
|
+
const handleCloseFeedback = useCallback(() => {
|
|
897
|
+
closeFeedback()
|
|
898
|
+
handleExitFeedback()
|
|
899
|
+
}, [closeFeedback, handleExitFeedback])
|
|
900
|
+
|
|
901
|
+
const handleExitPublish = useCallback(() => {
|
|
902
|
+
closePublish()
|
|
903
|
+
setInputFocused(true)
|
|
904
|
+
}, [closePublish, setInputFocused])
|
|
905
|
+
|
|
906
|
+
const handleReviewOptionSelect = useCallback(
|
|
907
|
+
(reviewText: string) => {
|
|
908
|
+
closeReviewScreen()
|
|
909
|
+
setInputFocused(true)
|
|
910
|
+
// Submit the review request
|
|
911
|
+
onSubmitPrompt(reviewText, agentMode)
|
|
912
|
+
.then((result) => handleCommandResult(result))
|
|
913
|
+
.catch((error) => {
|
|
914
|
+
logger.error({ error }, '[review] Failed to submit review prompt')
|
|
915
|
+
showClipboardMessage('Failed to send review request', {
|
|
916
|
+
durationMs: 3000,
|
|
917
|
+
})
|
|
918
|
+
})
|
|
919
|
+
},
|
|
920
|
+
[
|
|
921
|
+
closeReviewScreen,
|
|
922
|
+
setInputFocused,
|
|
923
|
+
onSubmitPrompt,
|
|
924
|
+
agentMode,
|
|
925
|
+
handleCommandResult,
|
|
926
|
+
],
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
const handleCloseReviewScreen = useCallback(() => {
|
|
930
|
+
closeReviewScreen()
|
|
931
|
+
setInputFocused(true)
|
|
932
|
+
}, [closeReviewScreen, setInputFocused])
|
|
933
|
+
|
|
934
|
+
// The panel took the composer's place, so give the keyboard back to it the
|
|
935
|
+
// same way the review screen does.
|
|
936
|
+
const handleCloseQueuePanel = useCallback(() => {
|
|
937
|
+
closeQueuePanel()
|
|
938
|
+
setInputFocused(true)
|
|
939
|
+
inputRef.current?.focus()
|
|
940
|
+
}, [closeQueuePanel, setInputFocused, inputRef])
|
|
941
|
+
|
|
942
|
+
const handleReviewCustom = useCallback(() => {
|
|
943
|
+
closeReviewScreen()
|
|
944
|
+
setInputMode('review')
|
|
945
|
+
setInputFocused(true)
|
|
946
|
+
}, [closeReviewScreen, setInputMode, setInputFocused])
|
|
947
|
+
|
|
948
|
+
const handlePublish = useCallback(
|
|
949
|
+
async (agentIds: string[]) => {
|
|
950
|
+
await publishMutation.mutateAsync(agentIds)
|
|
951
|
+
},
|
|
952
|
+
[publishMutation],
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
// Ensure bracketed paste events target the active chat input
|
|
956
|
+
useEffect(() => {
|
|
957
|
+
if (feedbackMode) {
|
|
958
|
+
inputRef.current?.focus()
|
|
959
|
+
return
|
|
960
|
+
}
|
|
961
|
+
if (!askUserState) {
|
|
962
|
+
inputRef.current?.focus()
|
|
963
|
+
}
|
|
964
|
+
}, [feedbackMode, askUserState, inputRef])
|
|
965
|
+
|
|
966
|
+
const handleSubmit = useCallback(async () => {
|
|
967
|
+
// Report activity for ad rotation
|
|
968
|
+
reportActivity()
|
|
969
|
+
// Update terminal title with truncated user input
|
|
970
|
+
if (inputValue.trim()) {
|
|
971
|
+
setTerminalTitle(inputValue)
|
|
972
|
+
}
|
|
973
|
+
const result = await onSubmitPrompt(inputValue, agentMode)
|
|
974
|
+
handleCommandResult(result)
|
|
975
|
+
}, [onSubmitPrompt, inputValue, agentMode, handleCommandResult])
|
|
976
|
+
|
|
977
|
+
const totalMentionMatches = agentMatches.length + fileMatches.length
|
|
978
|
+
const historyNavUpEnabled =
|
|
979
|
+
lastEditDueToNav ||
|
|
980
|
+
(cursorPosition === 0 &&
|
|
981
|
+
((slashContext.active && slashSelectedIndex === 0) ||
|
|
982
|
+
(mentionContext.active && agentSelectedIndex === 0) ||
|
|
983
|
+
(!slashContext.active && !mentionContext.active)))
|
|
984
|
+
const historyNavDownEnabled =
|
|
985
|
+
lastEditDueToNav ||
|
|
986
|
+
(cursorPosition === inputValue.length &&
|
|
987
|
+
((slashContext.active &&
|
|
988
|
+
slashSelectedIndex === slashMatches.length - 1) ||
|
|
989
|
+
(mentionContext.active &&
|
|
990
|
+
agentSelectedIndex === totalMentionMatches - 1) ||
|
|
991
|
+
(!slashContext.active && !mentionContext.active)))
|
|
992
|
+
|
|
993
|
+
// Build keyboard state from store values
|
|
994
|
+
const chatKeyboardState: ChatKeyboardState = useMemo(
|
|
995
|
+
() => ({
|
|
996
|
+
...createDefaultChatKeyboardState(),
|
|
997
|
+
inputMode,
|
|
998
|
+
inputValue: feedbackMode ? feedbackText : inputValue,
|
|
999
|
+
cursorPosition,
|
|
1000
|
+
isStreaming,
|
|
1001
|
+
isWaitingForResponse,
|
|
1002
|
+
feedbackMode,
|
|
1003
|
+
focusedAgentId,
|
|
1004
|
+
slashMenuActive: slashContext.active,
|
|
1005
|
+
mentionMenuActive: mentionContext.active,
|
|
1006
|
+
slashSelectedIndex,
|
|
1007
|
+
agentSelectedIndex,
|
|
1008
|
+
slashMatchesLength: slashMatches.length,
|
|
1009
|
+
totalMentionMatches: agentMatches.length + fileMatches.length,
|
|
1010
|
+
disableSlashSuggestions:
|
|
1011
|
+
getInputModeConfig(inputMode).disableSlashSuggestions,
|
|
1012
|
+
historyNavUpEnabled,
|
|
1013
|
+
historyNavDownEnabled,
|
|
1014
|
+
nextCtrlCWillExit,
|
|
1015
|
+
queuePaused,
|
|
1016
|
+
queuedCount,
|
|
1017
|
+
}),
|
|
1018
|
+
[
|
|
1019
|
+
inputMode,
|
|
1020
|
+
inputValue,
|
|
1021
|
+
feedbackText,
|
|
1022
|
+
cursorPosition,
|
|
1023
|
+
isStreaming,
|
|
1024
|
+
isWaitingForResponse,
|
|
1025
|
+
feedbackMode,
|
|
1026
|
+
focusedAgentId,
|
|
1027
|
+
slashContext.active,
|
|
1028
|
+
mentionContext.active,
|
|
1029
|
+
slashSelectedIndex,
|
|
1030
|
+
agentSelectedIndex,
|
|
1031
|
+
slashMatches.length,
|
|
1032
|
+
agentMatches.length,
|
|
1033
|
+
fileMatches.length,
|
|
1034
|
+
historyNavUpEnabled,
|
|
1035
|
+
historyNavDownEnabled,
|
|
1036
|
+
nextCtrlCWillExit,
|
|
1037
|
+
queuePaused,
|
|
1038
|
+
queuedCount,
|
|
1039
|
+
],
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
// Keyboard handlers
|
|
1043
|
+
const chatKeyboardHandlers: ChatKeyboardHandlers = useMemo(
|
|
1044
|
+
() => ({
|
|
1045
|
+
onExitInputMode: () => setInputMode('default'),
|
|
1046
|
+
onExitFeedbackMode: handleCloseFeedback,
|
|
1047
|
+
onClearFeedbackInput: () => {
|
|
1048
|
+
setFeedbackText('')
|
|
1049
|
+
useFeedbackStore.getState().setFeedbackCursor(0)
|
|
1050
|
+
},
|
|
1051
|
+
onClearInput: () =>
|
|
1052
|
+
setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false }),
|
|
1053
|
+
onBackspaceExitMode: () => setInputMode('default'),
|
|
1054
|
+
onInterruptStream: () => {
|
|
1055
|
+
stopActiveRun('user-interrupt')
|
|
1056
|
+
},
|
|
1057
|
+
onSlashMenuDown: () => setSlashSelectedIndex((prev) => prev + 1),
|
|
1058
|
+
onSlashMenuUp: () => setSlashSelectedIndex((prev) => prev - 1),
|
|
1059
|
+
onSlashMenuSelect: async () => {
|
|
1060
|
+
const selected = slashMatches[slashSelectedIndex] || slashMatches[0]
|
|
1061
|
+
if (!selected) return
|
|
1062
|
+
|
|
1063
|
+
// If the command has insertText, insert it instead of executing
|
|
1064
|
+
if (applySlashInsertText(selected)) return
|
|
1065
|
+
|
|
1066
|
+
// Execute the selected slash command immediately
|
|
1067
|
+
const commandString = `/${selected.id}`
|
|
1068
|
+
setSlashSelectedIndex(0)
|
|
1069
|
+
|
|
1070
|
+
const result = await onSubmitPrompt(commandString, agentMode)
|
|
1071
|
+
|
|
1072
|
+
handleCommandResult(result)
|
|
1073
|
+
},
|
|
1074
|
+
onSlashMenuComplete: () => {
|
|
1075
|
+
// Complete the word without executing - same as clicking on the item
|
|
1076
|
+
const selected = slashMatches[slashSelectedIndex] || slashMatches[0]
|
|
1077
|
+
if (!selected || slashContext.startIndex < 0) return
|
|
1078
|
+
|
|
1079
|
+
// If the command has insertText, insert it instead of the command
|
|
1080
|
+
if (applySlashInsertText(selected)) return
|
|
1081
|
+
|
|
1082
|
+
const before = inputValue.slice(0, slashContext.startIndex)
|
|
1083
|
+
const after = inputValue.slice(
|
|
1084
|
+
slashContext.startIndex + 1 + slashContext.query.length,
|
|
1085
|
+
)
|
|
1086
|
+
const replacement = `/${selected.id} `
|
|
1087
|
+
setInputValue({
|
|
1088
|
+
text: before + replacement + after,
|
|
1089
|
+
cursorPosition: before.length + replacement.length,
|
|
1090
|
+
lastEditDueToNav: false,
|
|
1091
|
+
})
|
|
1092
|
+
setSlashSelectedIndex(0)
|
|
1093
|
+
},
|
|
1094
|
+
onMentionMenuDown: () => setAgentSelectedIndex((prev) => prev + 1),
|
|
1095
|
+
onMentionMenuUp: () => setAgentSelectedIndex((prev) => prev - 1),
|
|
1096
|
+
onMentionMenuTab: () => {
|
|
1097
|
+
const totalMatches = agentMatches.length + fileMatches.length
|
|
1098
|
+
setAgentSelectedIndex((prev) => (prev + 1) % totalMatches)
|
|
1099
|
+
},
|
|
1100
|
+
onMentionMenuShiftTab: () => {
|
|
1101
|
+
const totalMatches = agentMatches.length + fileMatches.length
|
|
1102
|
+
setAgentSelectedIndex(
|
|
1103
|
+
(prev) => (totalMatches + prev - 1) % totalMatches,
|
|
1104
|
+
)
|
|
1105
|
+
},
|
|
1106
|
+
onMentionMenuSelect: () => {
|
|
1107
|
+
if (mentionContext.startIndex < 0) return
|
|
1108
|
+
|
|
1109
|
+
const trySelectAtIndex = (index: number): boolean => {
|
|
1110
|
+
let replacement: string
|
|
1111
|
+
if (index < agentMatches.length) {
|
|
1112
|
+
const selected = agentMatches[index]
|
|
1113
|
+
if (!selected) return false
|
|
1114
|
+
replacement = `@${selected.id} `
|
|
1115
|
+
} else {
|
|
1116
|
+
const fileIndex = index - agentMatches.length
|
|
1117
|
+
const selectedFile = fileMatches[fileIndex]
|
|
1118
|
+
if (!selectedFile) return false
|
|
1119
|
+
replacement = `@${selectedFile.filePath} `
|
|
1120
|
+
}
|
|
1121
|
+
const before = inputValue.slice(0, mentionContext.startIndex)
|
|
1122
|
+
const after = inputValue.slice(
|
|
1123
|
+
mentionContext.startIndex + 1 + mentionContext.query.length,
|
|
1124
|
+
)
|
|
1125
|
+
setInputValue({
|
|
1126
|
+
text: before + replacement + after,
|
|
1127
|
+
cursorPosition: before.length + replacement.length,
|
|
1128
|
+
lastEditDueToNav: false,
|
|
1129
|
+
})
|
|
1130
|
+
setAgentSelectedIndex(0)
|
|
1131
|
+
return true
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Try current selection, fall back to first item
|
|
1135
|
+
trySelectAtIndex(agentSelectedIndex) || trySelectAtIndex(0)
|
|
1136
|
+
},
|
|
1137
|
+
onMentionMenuComplete: () => {
|
|
1138
|
+
// Complete the word without executing - same as select for mentions
|
|
1139
|
+
if (mentionContext.startIndex < 0) return
|
|
1140
|
+
|
|
1141
|
+
let replacement: string
|
|
1142
|
+
const index = agentSelectedIndex
|
|
1143
|
+
if (index < agentMatches.length) {
|
|
1144
|
+
const selected =
|
|
1145
|
+
agentMatches.length > 0
|
|
1146
|
+
? agentMatches[index] || agentMatches[0]
|
|
1147
|
+
: undefined
|
|
1148
|
+
if (!selected) return
|
|
1149
|
+
replacement = `@${selected.id} `
|
|
1150
|
+
} else {
|
|
1151
|
+
const fileIndex = index - agentMatches.length
|
|
1152
|
+
const selectedFile =
|
|
1153
|
+
fileMatches.length > 0
|
|
1154
|
+
? fileMatches[fileIndex] || fileMatches[0]
|
|
1155
|
+
: undefined
|
|
1156
|
+
if (!selectedFile) return
|
|
1157
|
+
replacement = `@${selectedFile.filePath} `
|
|
1158
|
+
}
|
|
1159
|
+
const before = inputValue.slice(0, mentionContext.startIndex)
|
|
1160
|
+
const after = inputValue.slice(
|
|
1161
|
+
mentionContext.startIndex + 1 + mentionContext.query.length,
|
|
1162
|
+
)
|
|
1163
|
+
setInputValue({
|
|
1164
|
+
text: before + replacement + after,
|
|
1165
|
+
cursorPosition: before.length + replacement.length,
|
|
1166
|
+
lastEditDueToNav: false,
|
|
1167
|
+
})
|
|
1168
|
+
setAgentSelectedIndex(0)
|
|
1169
|
+
},
|
|
1170
|
+
onOpenFileMenuWithTab: () => {
|
|
1171
|
+
const safeCursor = Math.max(
|
|
1172
|
+
0,
|
|
1173
|
+
Math.min(cursorPosition, inputValue.length),
|
|
1174
|
+
)
|
|
1175
|
+
let wordStart = safeCursor
|
|
1176
|
+
while (wordStart > 0 && !/\s/.test(inputValue[wordStart - 1]!)) {
|
|
1177
|
+
wordStart--
|
|
1178
|
+
}
|
|
1179
|
+
if (wordStart < safeCursor) {
|
|
1180
|
+
openFileMenuWithTab()
|
|
1181
|
+
return true
|
|
1182
|
+
}
|
|
1183
|
+
return false
|
|
1184
|
+
},
|
|
1185
|
+
onHistoryUp: navigateUp,
|
|
1186
|
+
onHistoryDown: navigateDown,
|
|
1187
|
+
onToggleAgentMode: toggleAgentMode,
|
|
1188
|
+
onUnfocusAgent: () => {
|
|
1189
|
+
setFocusedAgentId(null)
|
|
1190
|
+
setInputFocused(true)
|
|
1191
|
+
inputRef.current?.focus()
|
|
1192
|
+
},
|
|
1193
|
+
onClearQueue: clearQueue,
|
|
1194
|
+
onOpenQueuePanel: openQueuePanel,
|
|
1195
|
+
onExitAppWarning: () => handleCtrlC(),
|
|
1196
|
+
onExitApp: () => handleCtrlC(),
|
|
1197
|
+
onBashHistoryUp: navigateUp,
|
|
1198
|
+
onBashHistoryDown: navigateDown,
|
|
1199
|
+
onPasteImage: () => {
|
|
1200
|
+
const placeholderPath = addClipboardPlaceholder()
|
|
1201
|
+
|
|
1202
|
+
// Process the image in the background
|
|
1203
|
+
setTimeout(() => {
|
|
1204
|
+
const result = readClipboardImage()
|
|
1205
|
+
if (!result.success || !result.imagePath) {
|
|
1206
|
+
useChatStore.getState().removePendingImage(placeholderPath)
|
|
1207
|
+
showClipboardMessage(result.error || 'Failed to paste image', {
|
|
1208
|
+
durationMs: 3000,
|
|
1209
|
+
})
|
|
1210
|
+
return
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const cwd = getProjectRoot() ?? process.cwd()
|
|
1214
|
+
addPendingImageFromFile(result.imagePath, cwd, placeholderPath).catch(
|
|
1215
|
+
(error) => {
|
|
1216
|
+
logger.error({ error }, 'Failed to add pending image from file')
|
|
1217
|
+
showClipboardMessage('Failed to add image', { durationMs: 3000 })
|
|
1218
|
+
},
|
|
1219
|
+
)
|
|
1220
|
+
}, 0)
|
|
1221
|
+
},
|
|
1222
|
+
onPasteImagePath: (imagePath: string) => {
|
|
1223
|
+
const cwd = getProjectRoot() ?? process.cwd()
|
|
1224
|
+
validateAndAddImage(imagePath, cwd).catch((error) => {
|
|
1225
|
+
logger.error({ error, imagePath }, 'Failed to validate and add image')
|
|
1226
|
+
showClipboardMessage('Failed to add image', { durationMs: 3000 })
|
|
1227
|
+
})
|
|
1228
|
+
},
|
|
1229
|
+
onPasteFilePath: (filePath: string, isDirectory: boolean) => {
|
|
1230
|
+
addPendingFileFromPath(filePath, isDirectory)
|
|
1231
|
+
},
|
|
1232
|
+
onPasteText: (text: string) => {
|
|
1233
|
+
setInputValue((prev) => {
|
|
1234
|
+
const before = prev.text.slice(0, prev.cursorPosition)
|
|
1235
|
+
const after = prev.text.slice(prev.cursorPosition)
|
|
1236
|
+
return {
|
|
1237
|
+
text: before + text + after,
|
|
1238
|
+
cursorPosition: before.length + text.length,
|
|
1239
|
+
lastEditDueToNav: false,
|
|
1240
|
+
}
|
|
1241
|
+
})
|
|
1242
|
+
},
|
|
1243
|
+
onScrollUp: scrollUp,
|
|
1244
|
+
onScrollDown: scrollDown,
|
|
1245
|
+
onToggleAll: handleToggleAll,
|
|
1246
|
+
onOpenBuyCredits: () => {
|
|
1247
|
+
// If credits have been restored, just return to default mode
|
|
1248
|
+
if (areCreditsRestored()) {
|
|
1249
|
+
setInputMode('default')
|
|
1250
|
+
return
|
|
1251
|
+
}
|
|
1252
|
+
// Direct-to-DeepSeek mode has no Anvil credits to buy; the provider's
|
|
1253
|
+
// own balance page is the correct destination, so fall through to the
|
|
1254
|
+
// default input rather than opening the Anvil billing page.
|
|
1255
|
+
if (isDirectDeepSeekMode()) {
|
|
1256
|
+
setInputMode('default')
|
|
1257
|
+
return
|
|
1258
|
+
}
|
|
1259
|
+
// Otherwise open the buy credits page
|
|
1260
|
+
safeOpen(WEBSITE_URL + '/usage')
|
|
1261
|
+
},
|
|
1262
|
+
}),
|
|
1263
|
+
[
|
|
1264
|
+
setInputMode,
|
|
1265
|
+
handleCloseFeedback,
|
|
1266
|
+
setFeedbackText,
|
|
1267
|
+
setInputValue,
|
|
1268
|
+
setSlashSelectedIndex,
|
|
1269
|
+
slashMatches,
|
|
1270
|
+
slashSelectedIndex,
|
|
1271
|
+
slashContext,
|
|
1272
|
+
inputValue,
|
|
1273
|
+
applySlashInsertText,
|
|
1274
|
+
onSubmitPrompt,
|
|
1275
|
+
agentMode,
|
|
1276
|
+
handleCommandResult,
|
|
1277
|
+
setAgentSelectedIndex,
|
|
1278
|
+
agentMatches,
|
|
1279
|
+
fileMatches,
|
|
1280
|
+
agentSelectedIndex,
|
|
1281
|
+
mentionContext,
|
|
1282
|
+
cursorPosition,
|
|
1283
|
+
openFileMenuWithTab,
|
|
1284
|
+
navigateUp,
|
|
1285
|
+
navigateDown,
|
|
1286
|
+
toggleAgentMode,
|
|
1287
|
+
setFocusedAgentId,
|
|
1288
|
+
setInputFocused,
|
|
1289
|
+
inputRef,
|
|
1290
|
+
handleCtrlC,
|
|
1291
|
+
clearQueue,
|
|
1292
|
+
openQueuePanel,
|
|
1293
|
+
scrollUp,
|
|
1294
|
+
scrollDown,
|
|
1295
|
+
handleToggleAll,
|
|
1296
|
+
],
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
// Use the chat keyboard hook
|
|
1300
|
+
useChatKeyboard({
|
|
1301
|
+
state: chatKeyboardState,
|
|
1302
|
+
handlers: chatKeyboardHandlers,
|
|
1303
|
+
disabled: askUserState !== null || reviewMode || queuePanelOpen,
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
// Sync message block context to zustand store for child components
|
|
1307
|
+
const setMessageBlockContext = useMessageBlockStore(
|
|
1308
|
+
(state) => state.setContext,
|
|
1309
|
+
)
|
|
1310
|
+
const setMessageBlockCallbacks = useMessageBlockStore(
|
|
1311
|
+
(state) => state.setCallbacks,
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
// Update context when values change - useLayoutEffect ensures synchronous updates
|
|
1315
|
+
// to prevent message loss during rapid streaming (race condition fix)
|
|
1316
|
+
useLayoutEffect(() => {
|
|
1317
|
+
setMessageBlockContext({
|
|
1318
|
+
theme,
|
|
1319
|
+
markdownPalette,
|
|
1320
|
+
messageTree,
|
|
1321
|
+
isWaitingForResponse,
|
|
1322
|
+
timerStartTime,
|
|
1323
|
+
availableWidth: messageAvailableWidth,
|
|
1324
|
+
responseAds: showInlineAds ? responseAds : {},
|
|
1325
|
+
})
|
|
1326
|
+
}, [
|
|
1327
|
+
theme,
|
|
1328
|
+
markdownPalette,
|
|
1329
|
+
messageTree,
|
|
1330
|
+
isWaitingForResponse,
|
|
1331
|
+
timerStartTime,
|
|
1332
|
+
messageAvailableWidth,
|
|
1333
|
+
responseAds,
|
|
1334
|
+
showInlineAds,
|
|
1335
|
+
setMessageBlockContext,
|
|
1336
|
+
])
|
|
1337
|
+
|
|
1338
|
+
// Update callbacks once (they're stable)
|
|
1339
|
+
useEffect(() => {
|
|
1340
|
+
setMessageBlockCallbacks({
|
|
1341
|
+
onToggleCollapsed: handleCollapseToggle,
|
|
1342
|
+
onBuildFast: handleBuildFast,
|
|
1343
|
+
onBuildMax: handleBuildMax,
|
|
1344
|
+
onBuildLite: handleBuildLite,
|
|
1345
|
+
onFeedback: handleMessageFeedback,
|
|
1346
|
+
onCloseFeedback: handleCloseFeedback,
|
|
1347
|
+
onAdClick: handleAdClick,
|
|
1348
|
+
onAdImpression: handleAdImpression,
|
|
1349
|
+
onResponseAdsNeeded: handleResponseAdsNeeded,
|
|
1350
|
+
})
|
|
1351
|
+
}, [
|
|
1352
|
+
handleCollapseToggle,
|
|
1353
|
+
handleBuildFast,
|
|
1354
|
+
handleBuildMax,
|
|
1355
|
+
handleBuildLite,
|
|
1356
|
+
handleMessageFeedback,
|
|
1357
|
+
handleCloseFeedback,
|
|
1358
|
+
handleAdClick,
|
|
1359
|
+
handleAdImpression,
|
|
1360
|
+
handleResponseAdsNeeded,
|
|
1361
|
+
setMessageBlockCallbacks,
|
|
1362
|
+
])
|
|
1363
|
+
|
|
1364
|
+
const modeConfig = getInputModeConfig(inputMode)
|
|
1365
|
+
const hasSlashSuggestions =
|
|
1366
|
+
slashContext.active &&
|
|
1367
|
+
slashSuggestionItems.length > 0 &&
|
|
1368
|
+
!modeConfig.disableSlashSuggestions
|
|
1369
|
+
const hasMentionSuggestions =
|
|
1370
|
+
!slashContext.active &&
|
|
1371
|
+
mentionContext.active &&
|
|
1372
|
+
(agentSuggestionItems.length > 0 || fileSuggestionItems.length > 0)
|
|
1373
|
+
const hasSuggestionMenu = hasSlashSuggestions || hasMentionSuggestions
|
|
1374
|
+
|
|
1375
|
+
// Show first-time onboarding starter prompts only on a pristine, idle,
|
|
1376
|
+
// empty-input default-mode chat — and never while a menu/overlay is up.
|
|
1377
|
+
const showOnboardingPrompts =
|
|
1378
|
+
showSuggestedPrompts &&
|
|
1379
|
+
messages.length === 0 &&
|
|
1380
|
+
inputValue.length === 0 &&
|
|
1381
|
+
inputMode === 'default' &&
|
|
1382
|
+
!hasSuggestionMenu &&
|
|
1383
|
+
!isStreaming &&
|
|
1384
|
+
!isWaitingForResponse &&
|
|
1385
|
+
!feedbackMode &&
|
|
1386
|
+
!publishMode &&
|
|
1387
|
+
!reviewMode &&
|
|
1388
|
+
askUserState === null
|
|
1389
|
+
|
|
1390
|
+
// Fire a one-time impression so we can measure onboarding-prompt usage
|
|
1391
|
+
// (click-through = SUGGESTED_PROMPT_CLICKED / SUGGESTED_PROMPT_SHOWN).
|
|
1392
|
+
const suggestedPromptsShownRef = useRef(false)
|
|
1393
|
+
useEffect(() => {
|
|
1394
|
+
if (showOnboardingPrompts && !suggestedPromptsShownRef.current) {
|
|
1395
|
+
suggestedPromptsShownRef.current = true
|
|
1396
|
+
trackEvent(AnalyticsEvent.SUGGESTED_PROMPT_SHOWN, {
|
|
1397
|
+
count: isCompactHeight
|
|
1398
|
+
? Math.min(2, DEFAULT_SUGGESTED_PROMPTS.length)
|
|
1399
|
+
: DEFAULT_SUGGESTED_PROMPTS.length,
|
|
1400
|
+
isCompactHeight,
|
|
1401
|
+
})
|
|
1402
|
+
}
|
|
1403
|
+
}, [showOnboardingPrompts, isCompactHeight])
|
|
1404
|
+
|
|
1405
|
+
const inputLayoutMetrics = useMemo(() => {
|
|
1406
|
+
// In bash mode, layout is based on the actual input (no ! prefix needed)
|
|
1407
|
+
const text = inputValue ?? ''
|
|
1408
|
+
const layoutContent = text.length > 0 ? text : ' '
|
|
1409
|
+
const safeCursor = Math.max(
|
|
1410
|
+
0,
|
|
1411
|
+
Math.min(cursorPosition, layoutContent.length),
|
|
1412
|
+
)
|
|
1413
|
+
const cursorProbe =
|
|
1414
|
+
safeCursor >= layoutContent.length
|
|
1415
|
+
? layoutContent
|
|
1416
|
+
: layoutContent.slice(0, safeCursor)
|
|
1417
|
+
const cols = Math.max(1, inputWidth)
|
|
1418
|
+
return computeInputLayoutMetrics({
|
|
1419
|
+
layoutContent,
|
|
1420
|
+
cursorProbe,
|
|
1421
|
+
cols,
|
|
1422
|
+
maxHeight: Math.floor(terminalHeight / 2),
|
|
1423
|
+
})
|
|
1424
|
+
}, [inputValue, cursorPosition, inputWidth, terminalHeight])
|
|
1425
|
+
const isMultilineInput = inputLayoutMetrics.heightLines > 1
|
|
1426
|
+
const shouldCenterInputVertically = !hasSuggestionMenu && !isMultilineInput
|
|
1427
|
+
const statusIndicatorState = getStatusIndicatorState({
|
|
1428
|
+
statusMessage,
|
|
1429
|
+
streamStatus,
|
|
1430
|
+
nextCtrlCWillExit,
|
|
1431
|
+
isConnected,
|
|
1432
|
+
authStatus,
|
|
1433
|
+
showReconnectionMessage,
|
|
1434
|
+
isRetrying,
|
|
1435
|
+
isCapacityWait,
|
|
1436
|
+
isAskUserActive: askUserState !== null,
|
|
1437
|
+
})
|
|
1438
|
+
const hasStatusIndicatorContent = statusIndicatorState.kind !== 'idle'
|
|
1439
|
+
|
|
1440
|
+
// Auto-show subscription limit banner when rate limit becomes active
|
|
1441
|
+
const subscriptionLimitShownRef = useRef(false)
|
|
1442
|
+
const subscriptionRateLimit = subscriptionData?.hasSubscription
|
|
1443
|
+
? subscriptionData.rateLimit
|
|
1444
|
+
: undefined
|
|
1445
|
+
const fallbackToALaCarte = subscriptionData?.fallbackToALaCarte ?? false
|
|
1446
|
+
useEffect(() => {
|
|
1447
|
+
const isLimited = subscriptionRateLimit?.limited === true
|
|
1448
|
+
if (isLimited && !subscriptionLimitShownRef.current) {
|
|
1449
|
+
subscriptionLimitShownRef.current = true
|
|
1450
|
+
// Skip showing the banner if user prefers to always fall back to a-la-carte
|
|
1451
|
+
if (!fallbackToALaCarte) {
|
|
1452
|
+
useChatStore.getState().setInputMode('subscriptionLimit')
|
|
1453
|
+
}
|
|
1454
|
+
} else if (!isLimited) {
|
|
1455
|
+
subscriptionLimitShownRef.current = false
|
|
1456
|
+
if (useChatStore.getState().inputMode === 'subscriptionLimit') {
|
|
1457
|
+
useChatStore.getState().setInputMode('default')
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}, [subscriptionRateLimit?.limited, fallbackToALaCarte])
|
|
1461
|
+
|
|
1462
|
+
const hasActiveAnvilSession =
|
|
1463
|
+
IS_ANVIL && anvilSession?.status === 'active'
|
|
1464
|
+
const isAnvilSessionOver =
|
|
1465
|
+
IS_ANVIL && anvilSession?.status === 'ended'
|
|
1466
|
+
const shouldShowStatusLine =
|
|
1467
|
+
!feedbackMode &&
|
|
1468
|
+
(hasStatusIndicatorContent ||
|
|
1469
|
+
shouldShowQueuePreview ||
|
|
1470
|
+
!isAtBottom ||
|
|
1471
|
+
hasActiveAnvilSession)
|
|
1472
|
+
|
|
1473
|
+
// Track mouse movement for ad activity (throttled)
|
|
1474
|
+
const lastMouseActivityRef = useRef<number>(0)
|
|
1475
|
+
const handleMouseActivity = useCallback(() => {
|
|
1476
|
+
const now = Date.now()
|
|
1477
|
+
// Throttle to max once per second
|
|
1478
|
+
if (now - lastMouseActivityRef.current > 1000) {
|
|
1479
|
+
lastMouseActivityRef.current = now
|
|
1480
|
+
reportActivity()
|
|
1481
|
+
}
|
|
1482
|
+
}, [])
|
|
1483
|
+
|
|
1484
|
+
return (
|
|
1485
|
+
<box
|
|
1486
|
+
onMouseMove={handleMouseActivity}
|
|
1487
|
+
style={{
|
|
1488
|
+
flexDirection: 'column',
|
|
1489
|
+
gap: 0,
|
|
1490
|
+
flexGrow: 1,
|
|
1491
|
+
}}
|
|
1492
|
+
>
|
|
1493
|
+
<scrollbox
|
|
1494
|
+
ref={scrollRef as React.Ref<ScrollBoxRenderable>}
|
|
1495
|
+
stickyScroll
|
|
1496
|
+
stickyStart="bottom"
|
|
1497
|
+
scrollX={false}
|
|
1498
|
+
scrollbarOptions={{ visible: false }}
|
|
1499
|
+
verticalScrollbarOptions={{
|
|
1500
|
+
visible: !isStreaming && !isWaitingForResponse && hasOverflow,
|
|
1501
|
+
trackOptions: { width: 1 },
|
|
1502
|
+
}}
|
|
1503
|
+
{...appliedScrollboxProps}
|
|
1504
|
+
style={{
|
|
1505
|
+
flexGrow: 1,
|
|
1506
|
+
rootOptions: {
|
|
1507
|
+
flexGrow: 1,
|
|
1508
|
+
padding: 0,
|
|
1509
|
+
gap: 0,
|
|
1510
|
+
flexDirection: 'row',
|
|
1511
|
+
shouldFill: true,
|
|
1512
|
+
backgroundColor: 'transparent',
|
|
1513
|
+
},
|
|
1514
|
+
wrapperOptions: {
|
|
1515
|
+
flexGrow: 1,
|
|
1516
|
+
border: false,
|
|
1517
|
+
shouldFill: true,
|
|
1518
|
+
backgroundColor: 'transparent',
|
|
1519
|
+
flexDirection: 'column',
|
|
1520
|
+
},
|
|
1521
|
+
contentOptions: {
|
|
1522
|
+
flexDirection: 'column',
|
|
1523
|
+
gap: 0,
|
|
1524
|
+
shouldFill: true,
|
|
1525
|
+
justifyContent: 'flex-end',
|
|
1526
|
+
backgroundColor: 'transparent',
|
|
1527
|
+
paddingLeft: 1,
|
|
1528
|
+
paddingRight: 2,
|
|
1529
|
+
},
|
|
1530
|
+
}}
|
|
1531
|
+
>
|
|
1532
|
+
<TopBanner gitRoot={gitRoot} onSwitchToGitRoot={onSwitchToGitRoot} />
|
|
1533
|
+
|
|
1534
|
+
<box
|
|
1535
|
+
ref={headerRef as React.Ref<BoxRenderable>}
|
|
1536
|
+
style={{ flexDirection: 'column' }}
|
|
1537
|
+
>
|
|
1538
|
+
<ChatHeader projectRoot={getProjectRoot()} animationEnabled={true} />
|
|
1539
|
+
</box>
|
|
1540
|
+
{IS_ANVIL && (
|
|
1541
|
+
<AnvilActiveSessionSummary session={anvilSession} />
|
|
1542
|
+
)}
|
|
1543
|
+
{hiddenMessageCount > 0 && (
|
|
1544
|
+
<LoadPreviousButton
|
|
1545
|
+
hiddenCount={hiddenMessageCount}
|
|
1546
|
+
onLoadMore={handleLoadPreviousMessages}
|
|
1547
|
+
/>
|
|
1548
|
+
)}
|
|
1549
|
+
{visibleTopLevelMessages.map((message, idx) => (
|
|
1550
|
+
<MessageWithAgents
|
|
1551
|
+
key={message.id}
|
|
1552
|
+
message={message}
|
|
1553
|
+
depth={0}
|
|
1554
|
+
isLastMessage={idx === visibleTopLevelMessages.length - 1}
|
|
1555
|
+
availableWidth={messageAvailableWidth}
|
|
1556
|
+
/>
|
|
1557
|
+
))}
|
|
1558
|
+
{/* Pending bash messages as ghost messages (only show those not already in history) */}
|
|
1559
|
+
{pendingBashMessages
|
|
1560
|
+
.filter((msg) => !msg.addedToHistory)
|
|
1561
|
+
.map((msg) => (
|
|
1562
|
+
<PendingBashMessage key={`pending-bash-${msg.id}`} message={msg} />
|
|
1563
|
+
))}
|
|
1564
|
+
</scrollbox>
|
|
1565
|
+
|
|
1566
|
+
<box
|
|
1567
|
+
style={{
|
|
1568
|
+
flexShrink: 0,
|
|
1569
|
+
backgroundColor: 'transparent',
|
|
1570
|
+
}}
|
|
1571
|
+
>
|
|
1572
|
+
{showOnboardingPrompts && !reviewMode && !isAnvilSessionOver && (
|
|
1573
|
+
<SuggestedPrompts
|
|
1574
|
+
onSelect={handleSelectSuggestedPrompt}
|
|
1575
|
+
maxItems={isCompactHeight ? 2 : undefined}
|
|
1576
|
+
/>
|
|
1577
|
+
)}
|
|
1578
|
+
|
|
1579
|
+
{shouldShowStatusLine && (
|
|
1580
|
+
<StatusBar
|
|
1581
|
+
timerStartTime={timerStartTime}
|
|
1582
|
+
isAtBottom={isAtBottom}
|
|
1583
|
+
scrollToLatest={scrollToLatest}
|
|
1584
|
+
statusIndicatorState={statusIndicatorState}
|
|
1585
|
+
onStop={chatKeyboardHandlers.onInterruptStream}
|
|
1586
|
+
onEndSession={() => {
|
|
1587
|
+
setMessages((prev) => [
|
|
1588
|
+
...prev,
|
|
1589
|
+
getSystemMessage(END_SESSION_MESSAGE),
|
|
1590
|
+
])
|
|
1591
|
+
returnToAnvilLanding({ resetChat: true }).catch(() => {})
|
|
1592
|
+
}}
|
|
1593
|
+
anvilSession={anvilSession}
|
|
1594
|
+
/>
|
|
1595
|
+
)}
|
|
1596
|
+
|
|
1597
|
+
{ads?.[0] && showInlineAds && (
|
|
1598
|
+
<SingleAdBanner
|
|
1599
|
+
ad={ads[0]}
|
|
1600
|
+
onClick={recordClick}
|
|
1601
|
+
onImpression={recordImpression}
|
|
1602
|
+
/>
|
|
1603
|
+
)}
|
|
1604
|
+
|
|
1605
|
+
{reviewMode ? (
|
|
1606
|
+
// Review and ask_user take precedence over the session-ended banner:
|
|
1607
|
+
// during the grace window the agent may still be asking to run tools
|
|
1608
|
+
// or asking the user a question, and those approvals/answers must be
|
|
1609
|
+
// reachable for the run to finish — otherwise the agent hangs
|
|
1610
|
+
// waiting for input that can never be given.
|
|
1611
|
+
<ReviewScreen
|
|
1612
|
+
onSelectOption={handleReviewOptionSelect}
|
|
1613
|
+
onCustom={handleReviewCustom}
|
|
1614
|
+
onCancel={handleCloseReviewScreen}
|
|
1615
|
+
/>
|
|
1616
|
+
) : queuePanelOpen && !askUserState ? (
|
|
1617
|
+
<QueuePanel
|
|
1618
|
+
queuedMessages={queuedMessages}
|
|
1619
|
+
onEdit={editQueuedMessage}
|
|
1620
|
+
onDelete={removeQueuedMessage}
|
|
1621
|
+
onMove={moveQueuedMessage}
|
|
1622
|
+
onClose={handleCloseQueuePanel}
|
|
1623
|
+
width={separatorWidth}
|
|
1624
|
+
maxVisibleRows={isCompactHeight ? 4 : 8}
|
|
1625
|
+
/>
|
|
1626
|
+
) : isAnvilSessionOver && !askUserState ? (
|
|
1627
|
+
<SessionEndedBanner
|
|
1628
|
+
isStreaming={isStreaming || isWaitingForResponse}
|
|
1629
|
+
/>
|
|
1630
|
+
) : (
|
|
1631
|
+
<>
|
|
1632
|
+
<ChatInputBar
|
|
1633
|
+
inputValue={inputValue}
|
|
1634
|
+
cursorPosition={cursorPosition}
|
|
1635
|
+
setInputValue={setInputValue}
|
|
1636
|
+
inputFocused={inputFocused}
|
|
1637
|
+
inputRef={inputRef}
|
|
1638
|
+
inputPlaceholder={inputPlaceholder}
|
|
1639
|
+
lastEditDueToNav={lastEditDueToNav}
|
|
1640
|
+
agentMode={agentMode}
|
|
1641
|
+
toggleAgentMode={toggleAgentMode}
|
|
1642
|
+
setAgentMode={setAgentMode}
|
|
1643
|
+
hasSlashSuggestions={hasSlashSuggestions}
|
|
1644
|
+
hasMentionSuggestions={hasMentionSuggestions}
|
|
1645
|
+
hasSuggestionMenu={hasSuggestionMenu}
|
|
1646
|
+
slashSuggestionItems={slashSuggestionItems}
|
|
1647
|
+
agentSuggestionItems={agentSuggestionItems}
|
|
1648
|
+
fileSuggestionItems={fileSuggestionItems}
|
|
1649
|
+
slashSelectedIndex={slashSelectedIndex}
|
|
1650
|
+
agentSelectedIndex={agentSelectedIndex}
|
|
1651
|
+
onSlashItemClick={handleSlashItemClick}
|
|
1652
|
+
onMentionItemClick={handleMentionItemClick}
|
|
1653
|
+
theme={theme}
|
|
1654
|
+
terminalHeight={terminalHeight}
|
|
1655
|
+
separatorWidth={separatorWidth}
|
|
1656
|
+
shouldCenterInputVertically={shouldCenterInputVertically}
|
|
1657
|
+
inputBoxTitle={inputBoxTitle}
|
|
1658
|
+
onQueuePreviewClick={openQueuePanel}
|
|
1659
|
+
isCompactHeight={isCompactHeight}
|
|
1660
|
+
isNarrowWidth={isNarrowWidth}
|
|
1661
|
+
feedbackMode={feedbackMode}
|
|
1662
|
+
handleExitFeedback={handleExitFeedback}
|
|
1663
|
+
publishMode={publishMode}
|
|
1664
|
+
handleExitPublish={handleExitPublish}
|
|
1665
|
+
handlePublish={handlePublish}
|
|
1666
|
+
handleSubmit={handleSubmit}
|
|
1667
|
+
onPaste={createPasteHandler({
|
|
1668
|
+
text: inputValue,
|
|
1669
|
+
cursorPosition,
|
|
1670
|
+
onChange: setInputValue,
|
|
1671
|
+
onPasteImage: chatKeyboardHandlers.onPasteImage,
|
|
1672
|
+
onPasteImagePath: chatKeyboardHandlers.onPasteImagePath,
|
|
1673
|
+
onPasteFilePath: chatKeyboardHandlers.onPasteFilePath,
|
|
1674
|
+
onPasteLongText: (pastedText) => {
|
|
1675
|
+
const id = crypto.randomUUID()
|
|
1676
|
+
const preview = pastedText.slice(0, 100).replace(/\n/g, ' ')
|
|
1677
|
+
useChatStore.getState().addPendingTextAttachment({
|
|
1678
|
+
id,
|
|
1679
|
+
content: pastedText,
|
|
1680
|
+
preview,
|
|
1681
|
+
charCount: pastedText.length,
|
|
1682
|
+
})
|
|
1683
|
+
// Show temporary status message
|
|
1684
|
+
showClipboardMessage(
|
|
1685
|
+
`📋 Pasted text (${pastedText.length.toLocaleString()} chars)`,
|
|
1686
|
+
{ durationMs: 5000 },
|
|
1687
|
+
)
|
|
1688
|
+
},
|
|
1689
|
+
cwd: getProjectRoot() ?? process.cwd(),
|
|
1690
|
+
})}
|
|
1691
|
+
onInterruptStream={chatKeyboardHandlers.onInterruptStream}
|
|
1692
|
+
/>
|
|
1693
|
+
</>
|
|
1694
|
+
)}
|
|
1695
|
+
</box>
|
|
1696
|
+
</box>
|
|
1697
|
+
)
|
|
1698
|
+
}
|