@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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { AskUserBridge } from '@anvil/common/utils/ask-user-bridge'
|
|
2
|
+
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { useChatStore } from '../../state/chat-store'
|
|
6
|
+
|
|
7
|
+
describe('useAskUserBridge', () => {
|
|
8
|
+
const submitAnswers = (
|
|
9
|
+
answers: Array<{
|
|
10
|
+
questionIndex: number
|
|
11
|
+
selectedOption?: string
|
|
12
|
+
selectedOptions?: string[]
|
|
13
|
+
otherText?: string
|
|
14
|
+
}>
|
|
15
|
+
) => {
|
|
16
|
+
AskUserBridge.submit({ answers })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const skip = () => {
|
|
20
|
+
AskUserBridge.submit({ skipped: true })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let submitSpy: ReturnType<typeof spyOn>
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
// Mock AskUserBridge.submit to track calls
|
|
27
|
+
submitSpy = spyOn(AskUserBridge, 'submit')
|
|
28
|
+
|
|
29
|
+
// Reset the chat store to a known state with some input
|
|
30
|
+
useChatStore.setState({
|
|
31
|
+
inputValue: 'user input that should be preserved',
|
|
32
|
+
cursorPosition: 35,
|
|
33
|
+
lastEditDueToNav: false,
|
|
34
|
+
askUserState: null,
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
submitSpy.mockRestore()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('submitAnswers', () => {
|
|
43
|
+
test('calls AskUserBridge.submit with the provided answers', () => {
|
|
44
|
+
const answers = [
|
|
45
|
+
{ questionIndex: 0, selectedOption: 'Option A' },
|
|
46
|
+
{ questionIndex: 1, selectedOptions: ['Option B', 'Option C'] },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
submitAnswers(answers)
|
|
50
|
+
|
|
51
|
+
expect(submitSpy).toHaveBeenCalledTimes(1)
|
|
52
|
+
expect(submitSpy).toHaveBeenCalledWith({ answers })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('does NOT modify the input value in the store', () => {
|
|
56
|
+
const originalInputValue = useChatStore.getState().inputValue
|
|
57
|
+
const originalCursorPosition = useChatStore.getState().cursorPosition
|
|
58
|
+
|
|
59
|
+
submitAnswers([{ questionIndex: 0, selectedOption: 'Test' }])
|
|
60
|
+
|
|
61
|
+
// Verify input value was NOT changed
|
|
62
|
+
const currentState = useChatStore.getState()
|
|
63
|
+
expect(currentState.inputValue).toBe(originalInputValue)
|
|
64
|
+
expect(currentState.cursorPosition).toBe(originalCursorPosition)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('preserves input value with empty answers array', () => {
|
|
68
|
+
const originalInputValue = useChatStore.getState().inputValue
|
|
69
|
+
|
|
70
|
+
submitAnswers([])
|
|
71
|
+
|
|
72
|
+
expect(useChatStore.getState().inputValue).toBe(originalInputValue)
|
|
73
|
+
expect(submitSpy).toHaveBeenCalledWith({ answers: [] })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('preserves input value with multiple question answers', () => {
|
|
77
|
+
const originalInputValue = useChatStore.getState().inputValue
|
|
78
|
+
|
|
79
|
+
const answers = [
|
|
80
|
+
{ questionIndex: 0, selectedOption: 'First answer' },
|
|
81
|
+
{ questionIndex: 1, selectedOptions: ['Multi 1', 'Multi 2'] },
|
|
82
|
+
{ questionIndex: 2, otherText: 'Custom text input' },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
submitAnswers(answers)
|
|
86
|
+
|
|
87
|
+
expect(useChatStore.getState().inputValue).toBe(originalInputValue)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('skip', () => {
|
|
92
|
+
test('calls AskUserBridge.submit with skipped: true', () => {
|
|
93
|
+
skip()
|
|
94
|
+
|
|
95
|
+
expect(submitSpy).toHaveBeenCalledTimes(1)
|
|
96
|
+
expect(submitSpy).toHaveBeenCalledWith({ skipped: true })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('does NOT modify the input value in the store', () => {
|
|
100
|
+
const originalInputValue = useChatStore.getState().inputValue
|
|
101
|
+
const originalCursorPosition = useChatStore.getState().cursorPosition
|
|
102
|
+
|
|
103
|
+
skip()
|
|
104
|
+
|
|
105
|
+
// Verify input value was NOT changed
|
|
106
|
+
const currentState = useChatStore.getState()
|
|
107
|
+
expect(currentState.inputValue).toBe(originalInputValue)
|
|
108
|
+
expect(currentState.cursorPosition).toBe(originalCursorPosition)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('input preservation regression tests', () => {
|
|
113
|
+
test('input with special characters is preserved after submitAnswers', () => {
|
|
114
|
+
useChatStore.setState({
|
|
115
|
+
inputValue: 'Input with "quotes" and `backticks` and @mentions',
|
|
116
|
+
cursorPosition: 48,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const originalInputValue = useChatStore.getState().inputValue
|
|
120
|
+
|
|
121
|
+
submitAnswers([{ questionIndex: 0, selectedOption: 'Test' }])
|
|
122
|
+
|
|
123
|
+
expect(useChatStore.getState().inputValue).toBe(originalInputValue)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('input with special characters is preserved after skip', () => {
|
|
127
|
+
useChatStore.setState({
|
|
128
|
+
inputValue: "Don't lose this apostrophe or @file-picker mention",
|
|
129
|
+
cursorPosition: 51,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const originalInputValue = useChatStore.getState().inputValue
|
|
133
|
+
|
|
134
|
+
skip()
|
|
135
|
+
|
|
136
|
+
expect(useChatStore.getState().inputValue).toBe(originalInputValue)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('multiline input is preserved after submitAnswers', () => {
|
|
140
|
+
useChatStore.setState({
|
|
141
|
+
inputValue: 'Line 1\nLine 2\nLine 3',
|
|
142
|
+
cursorPosition: 20,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const originalInputValue = useChatStore.getState().inputValue
|
|
146
|
+
|
|
147
|
+
submitAnswers([{ questionIndex: 0, selectedOption: 'Test' }])
|
|
148
|
+
|
|
149
|
+
expect(useChatStore.getState().inputValue).toBe(originalInputValue)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('empty input remains empty after submitAnswers', () => {
|
|
153
|
+
useChatStore.setState({
|
|
154
|
+
inputValue: '',
|
|
155
|
+
cursorPosition: 0,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
submitAnswers([{ questionIndex: 0, selectedOption: 'Test' }])
|
|
159
|
+
|
|
160
|
+
expect(useChatStore.getState().inputValue).toBe('')
|
|
161
|
+
expect(useChatStore.getState().cursorPosition).toBe(0)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('empty input remains empty after skip', () => {
|
|
165
|
+
useChatStore.setState({
|
|
166
|
+
inputValue: '',
|
|
167
|
+
cursorPosition: 0,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
skip()
|
|
171
|
+
|
|
172
|
+
expect(useChatStore.getState().inputValue).toBe('')
|
|
173
|
+
expect(useChatStore.getState().cursorPosition).toBe(0)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { authQueryKeys } from '../use-auth-query'
|
|
4
|
+
|
|
5
|
+
describe('authQueryKeys.validation', () => {
|
|
6
|
+
test('changes when api key changes', () => {
|
|
7
|
+
const firstKey = authQueryKeys.validation('token-1')
|
|
8
|
+
const secondKey = authQueryKeys.validation('token-2')
|
|
9
|
+
|
|
10
|
+
expect(firstKey).not.toEqual(secondKey)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('does not include the raw api key', () => {
|
|
14
|
+
const token = 'secret-token-123'
|
|
15
|
+
const key = authQueryKeys.validation(token)
|
|
16
|
+
const [, , apiKeyHash] = key
|
|
17
|
+
|
|
18
|
+
expect(key).not.toContain(token)
|
|
19
|
+
expect(apiKeyHash).toMatch(/^[0-9a-f]{64}$/)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { createTestRenderer } from '@opentui/core/testing'
|
|
3
|
+
import { createRoot, flushSync } from '@opentui/react'
|
|
4
|
+
import React, { useCallback, useRef, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import { useChatInput } from '../use-chat-input'
|
|
7
|
+
|
|
8
|
+
test('initial prompt is submitted only once across Chat remounts', async () => {
|
|
9
|
+
const submissions: string[] = []
|
|
10
|
+
let setShowHistory: ((show: boolean) => void) | undefined
|
|
11
|
+
|
|
12
|
+
const ChatView = ({
|
|
13
|
+
consumeInitialPrompt,
|
|
14
|
+
}: {
|
|
15
|
+
consumeInitialPrompt: () => string | null
|
|
16
|
+
}) => {
|
|
17
|
+
useChatInput({
|
|
18
|
+
setInputValue: () => {},
|
|
19
|
+
agentMode: 'DEFAULT',
|
|
20
|
+
setAgentMode: () => {},
|
|
21
|
+
separatorWidth: 80,
|
|
22
|
+
consumeInitialPrompt,
|
|
23
|
+
onSubmitPrompt: (content) => {
|
|
24
|
+
submissions.push(content)
|
|
25
|
+
},
|
|
26
|
+
isCompactHeight: false,
|
|
27
|
+
isNarrowWidth: false,
|
|
28
|
+
})
|
|
29
|
+
return <text>chat</text>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AppLifetime = () => {
|
|
33
|
+
const initialPromptConsumedRef = useRef(false)
|
|
34
|
+
const consumeInitialPrompt = useCallback(() => {
|
|
35
|
+
if (initialPromptConsumedRef.current) return null
|
|
36
|
+
initialPromptConsumedRef.current = true
|
|
37
|
+
return 'launch prompt'
|
|
38
|
+
}, [])
|
|
39
|
+
const [showHistory, setHistory] = useState(false)
|
|
40
|
+
setShowHistory = setHistory
|
|
41
|
+
|
|
42
|
+
return showHistory ? (
|
|
43
|
+
<text>history</text>
|
|
44
|
+
) : (
|
|
45
|
+
<ChatView consumeInitialPrompt={consumeInitialPrompt} />
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const setup = await createTestRenderer({ width: 80, height: 3 })
|
|
50
|
+
const root = createRoot(setup.renderer)
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
flushSync(() => root.render(<AppLifetime />))
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
55
|
+
expect(submissions).toEqual(['launch prompt'])
|
|
56
|
+
|
|
57
|
+
flushSync(() => setShowHistory!(true))
|
|
58
|
+
flushSync(() => setShowHistory!(false))
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
60
|
+
|
|
61
|
+
expect(submissions).toEqual(['launch prompt'])
|
|
62
|
+
} finally {
|
|
63
|
+
flushSync(() => root.unmount())
|
|
64
|
+
setup.renderer.destroy()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createTestRenderer } from '@opentui/core/testing'
|
|
2
|
+
import { createRoot, flushSync } from '@opentui/react'
|
|
3
|
+
import { afterEach, describe, expect, spyOn, test } from 'bun:test'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
|
|
6
|
+
import { useClipboard } from '../use-clipboard'
|
|
7
|
+
import * as clipboard from '../../utils/clipboard'
|
|
8
|
+
|
|
9
|
+
import type { CopyToClipboardOptions } from '../../utils/clipboard'
|
|
10
|
+
|
|
11
|
+
const waitForSelectionCopy = () =>
|
|
12
|
+
new Promise((resolve) => setTimeout(resolve, 300))
|
|
13
|
+
|
|
14
|
+
describe('useClipboard selection copying', () => {
|
|
15
|
+
let dispose: (() => void) | undefined
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
dispose?.()
|
|
19
|
+
dispose = undefined
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('a newer selection aborts the in-flight copy before starting its own', async () => {
|
|
23
|
+
const calls: Array<{
|
|
24
|
+
text: string
|
|
25
|
+
signal: AbortSignal
|
|
26
|
+
resolve: () => void
|
|
27
|
+
}> = []
|
|
28
|
+
const copySpy = spyOn(clipboard, 'copyTextToClipboard').mockImplementation(
|
|
29
|
+
(text: string, options: CopyToClipboardOptions = {}) =>
|
|
30
|
+
new Promise<void>((resolve, reject) => {
|
|
31
|
+
const signal = options.signal!
|
|
32
|
+
signal.addEventListener('abort', () => reject(signal.reason), {
|
|
33
|
+
once: true,
|
|
34
|
+
})
|
|
35
|
+
calls.push({ text, signal, resolve })
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const Harness = () => {
|
|
40
|
+
useClipboard()
|
|
41
|
+
return <text>clipboard</text>
|
|
42
|
+
}
|
|
43
|
+
const setup = await createTestRenderer({ width: 20, height: 2 })
|
|
44
|
+
const root = createRoot(setup.renderer)
|
|
45
|
+
flushSync(() => root.render(<Harness />))
|
|
46
|
+
await setup.renderOnce()
|
|
47
|
+
dispose = () => {
|
|
48
|
+
copySpy.mockRestore()
|
|
49
|
+
flushSync(() => root.unmount())
|
|
50
|
+
setup.renderer.destroy()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setup.renderer.emit('selection', {
|
|
54
|
+
getSelectedText: () => 'first selection',
|
|
55
|
+
} as never)
|
|
56
|
+
await waitForSelectionCopy()
|
|
57
|
+
expect(calls).toHaveLength(1)
|
|
58
|
+
expect(calls[0]?.signal.aborted).toBe(false)
|
|
59
|
+
|
|
60
|
+
setup.renderer.emit('selection', {
|
|
61
|
+
getSelectedText: () => 'second selection',
|
|
62
|
+
} as never)
|
|
63
|
+
expect(calls[0]?.signal.aborted).toBe(true)
|
|
64
|
+
await waitForSelectionCopy()
|
|
65
|
+
|
|
66
|
+
expect(calls.map(({ text }) => text)).toEqual([
|
|
67
|
+
'first selection',
|
|
68
|
+
'second selection',
|
|
69
|
+
])
|
|
70
|
+
expect(calls[1]?.signal.aborted).toBe(false)
|
|
71
|
+
calls[1]?.resolve()
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { getNextInterval } from '../use-connection-status'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for the adaptive health check interval logic.
|
|
7
|
+
*
|
|
8
|
+
* These tests verify the core exponential backoff algorithm that determines
|
|
9
|
+
* how frequently health checks should run based on consecutive successful checks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
describe('useConnectionStatus - adaptive interval logic', () => {
|
|
13
|
+
describe('getNextInterval', () => {
|
|
14
|
+
test('returns 10s for 0-2 consecutive successes', () => {
|
|
15
|
+
expect(getNextInterval(0)).toBe(10_000)
|
|
16
|
+
expect(getNextInterval(1)).toBe(10_000)
|
|
17
|
+
expect(getNextInterval(2)).toBe(10_000)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('returns 30s for 3-5 consecutive successes', () => {
|
|
21
|
+
expect(getNextInterval(3)).toBe(30_000)
|
|
22
|
+
expect(getNextInterval(4)).toBe(30_000)
|
|
23
|
+
expect(getNextInterval(5)).toBe(30_000)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('returns 60s for 6-9 consecutive successes', () => {
|
|
27
|
+
expect(getNextInterval(6)).toBe(60_000)
|
|
28
|
+
expect(getNextInterval(7)).toBe(60_000)
|
|
29
|
+
expect(getNextInterval(8)).toBe(60_000)
|
|
30
|
+
expect(getNextInterval(9)).toBe(60_000)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('returns 2min for 10-14 consecutive successes', () => {
|
|
34
|
+
expect(getNextInterval(10)).toBe(120_000)
|
|
35
|
+
expect(getNextInterval(11)).toBe(120_000)
|
|
36
|
+
expect(getNextInterval(14)).toBe(120_000)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('returns 5min for 15-19 consecutive successes', () => {
|
|
40
|
+
expect(getNextInterval(15)).toBe(300_000)
|
|
41
|
+
expect(getNextInterval(16)).toBe(300_000)
|
|
42
|
+
expect(getNextInterval(19)).toBe(300_000)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('returns 10min (max) for 20+ consecutive successes', () => {
|
|
46
|
+
expect(getNextInterval(20)).toBe(600_000)
|
|
47
|
+
expect(getNextInterval(25)).toBe(600_000)
|
|
48
|
+
expect(getNextInterval(100)).toBe(600_000)
|
|
49
|
+
expect(getNextInterval(1000)).toBe(600_000)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('interval progression', () => {
|
|
54
|
+
test('progresses through all thresholds in order', () => {
|
|
55
|
+
const progression = [
|
|
56
|
+
{ successes: 0, expectedInterval: 10_000 },
|
|
57
|
+
{ successes: 3, expectedInterval: 30_000 },
|
|
58
|
+
{ successes: 6, expectedInterval: 60_000 },
|
|
59
|
+
{ successes: 10, expectedInterval: 120_000 },
|
|
60
|
+
{ successes: 15, expectedInterval: 300_000 },
|
|
61
|
+
{ successes: 20, expectedInterval: 600_000 },
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
for (const { successes, expectedInterval } of progression) {
|
|
65
|
+
expect(getNextInterval(successes)).toBe(expectedInterval)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('intervals increase monotonically', () => {
|
|
70
|
+
let previousInterval = 0
|
|
71
|
+
// Test first occurrence of each threshold
|
|
72
|
+
const testPoints = [0, 3, 6, 10, 15, 20, 50]
|
|
73
|
+
|
|
74
|
+
for (const successes of testPoints) {
|
|
75
|
+
const interval = getNextInterval(successes)
|
|
76
|
+
expect(interval).toBeGreaterThanOrEqual(previousInterval)
|
|
77
|
+
previousInterval = interval
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('edge cases', () => {
|
|
83
|
+
test('handles negative values (treated as 0)', () => {
|
|
84
|
+
// In practice, consecutive successes should never be negative,
|
|
85
|
+
// but the function should handle it gracefully
|
|
86
|
+
expect(getNextInterval(-1)).toBe(10_000)
|
|
87
|
+
expect(getNextInterval(-100)).toBe(10_000)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('handles very large values', () => {
|
|
91
|
+
expect(getNextInterval(Number.MAX_SAFE_INTEGER)).toBe(600_000)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('boundary conditions', () => {
|
|
96
|
+
test('uses correct interval at exact threshold boundaries', () => {
|
|
97
|
+
// At threshold - 1: should use previous interval
|
|
98
|
+
expect(getNextInterval(2)).toBe(10_000) // Just before 3
|
|
99
|
+
expect(getNextInterval(5)).toBe(30_000) // Just before 6
|
|
100
|
+
expect(getNextInterval(9)).toBe(60_000) // Just before 10
|
|
101
|
+
expect(getNextInterval(14)).toBe(120_000) // Just before 15
|
|
102
|
+
expect(getNextInterval(19)).toBe(300_000) // Just before 20
|
|
103
|
+
|
|
104
|
+
// At threshold: should use new interval
|
|
105
|
+
expect(getNextInterval(3)).toBe(30_000)
|
|
106
|
+
expect(getNextInterval(6)).toBe(60_000)
|
|
107
|
+
expect(getNextInterval(10)).toBe(120_000)
|
|
108
|
+
expect(getNextInterval(15)).toBe(300_000)
|
|
109
|
+
expect(getNextInterval(20)).toBe(600_000)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'fs'
|
|
9
|
+
import os from 'os'
|
|
10
|
+
import path from 'path'
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tests for useDirectoryBrowser hook logic.
|
|
16
|
+
*
|
|
17
|
+
* These tests focus on the pure logic aspects of the hook:
|
|
18
|
+
* - expandPath: Expands ~ to home directory
|
|
19
|
+
* - Path validation and navigation logic
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Extract the pure expandPath logic for testing
|
|
23
|
+
const expandPath = (inputPath: string): string => {
|
|
24
|
+
if (inputPath.startsWith('~')) {
|
|
25
|
+
return path.join(os.homedir(), inputPath.slice(1))
|
|
26
|
+
}
|
|
27
|
+
return inputPath
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract the path validation logic for testing
|
|
31
|
+
const isValidDirectoryPath = (inputPath: string): boolean => {
|
|
32
|
+
const expandedPath = expandPath(inputPath.trim())
|
|
33
|
+
try {
|
|
34
|
+
return existsSync(expandedPath) && statSync(expandedPath).isDirectory()
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('useDirectoryBrowser - expandPath', () => {
|
|
41
|
+
const homeDir = os.homedir()
|
|
42
|
+
|
|
43
|
+
describe('tilde expansion', () => {
|
|
44
|
+
test('expands ~ to home directory', () => {
|
|
45
|
+
const result = expandPath('~')
|
|
46
|
+
expect(result).toBe(homeDir)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('expands ~/path to home directory + path', () => {
|
|
50
|
+
const result = expandPath('~/Documents')
|
|
51
|
+
expect(result).toBe(path.join(homeDir, 'Documents'))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('expands ~/nested/path correctly', () => {
|
|
55
|
+
const result = expandPath('~/nested/path/to/file')
|
|
56
|
+
expect(result).toBe(path.join(homeDir, 'nested/path/to/file'))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('expands ~/ (with trailing slash) correctly', () => {
|
|
60
|
+
const result = expandPath('~/')
|
|
61
|
+
expect(result).toBe(path.join(homeDir, '/'))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('handles ~ with special characters in path', () => {
|
|
65
|
+
const result = expandPath('~/path with spaces')
|
|
66
|
+
expect(result).toBe(path.join(homeDir, 'path with spaces'))
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('non-tilde paths', () => {
|
|
71
|
+
test('returns absolute path unchanged', () => {
|
|
72
|
+
const result = expandPath('/usr/local/bin')
|
|
73
|
+
expect(result).toBe('/usr/local/bin')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('returns relative path unchanged', () => {
|
|
77
|
+
const result = expandPath('relative/path')
|
|
78
|
+
expect(result).toBe('relative/path')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('returns empty string unchanged', () => {
|
|
82
|
+
const result = expandPath('')
|
|
83
|
+
expect(result).toBe('')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('returns dot path unchanged', () => {
|
|
87
|
+
const result = expandPath('.')
|
|
88
|
+
expect(result).toBe('.')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('returns double dot path unchanged', () => {
|
|
92
|
+
const result = expandPath('..')
|
|
93
|
+
expect(result).toBe('..')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('does not expand ~ in middle of path', () => {
|
|
97
|
+
const result = expandPath('/some/~/path')
|
|
98
|
+
expect(result).toBe('/some/~/path')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('does not expand ~ at end of path', () => {
|
|
102
|
+
const result = expandPath('/some/path~')
|
|
103
|
+
expect(result).toBe('/some/path~')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('edge cases', () => {
|
|
108
|
+
test('handles ~user syntax (does not expand, returns as-is)', () => {
|
|
109
|
+
// In this implementation, ~user is not specially handled
|
|
110
|
+
// It would expand to homedir + 'user' which may not be intended
|
|
111
|
+
// but that's the current behavior
|
|
112
|
+
const result = expandPath('~user')
|
|
113
|
+
expect(result).toBe(path.join(homeDir, 'user'))
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('handles path with only tilde character', () => {
|
|
117
|
+
const result = expandPath('~')
|
|
118
|
+
expect(result).toBe(homeDir)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('useDirectoryBrowser - path validation', () => {
|
|
124
|
+
let tempDir: string
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
tempDir = mkdtempSync(path.join(os.tmpdir(), 'dir-browser-test-'))
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('isValidDirectoryPath', () => {
|
|
135
|
+
test('returns true for existing directory', () => {
|
|
136
|
+
expect(isValidDirectoryPath(tempDir)).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('returns true for existing nested directory', () => {
|
|
140
|
+
const nestedDir = path.join(tempDir, 'nested')
|
|
141
|
+
mkdirSync(nestedDir)
|
|
142
|
+
expect(isValidDirectoryPath(nestedDir)).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('returns false for non-existent path', () => {
|
|
146
|
+
expect(isValidDirectoryPath('/this/path/does/not/exist')).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('returns false for file (not directory)', () => {
|
|
150
|
+
const filePath = path.join(tempDir, 'test.txt')
|
|
151
|
+
writeFileSync(filePath, 'test')
|
|
152
|
+
expect(isValidDirectoryPath(filePath)).toBe(false)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('handles whitespace in path by trimming', () => {
|
|
156
|
+
expect(isValidDirectoryPath(` ${tempDir} `)).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('returns false for empty string', () => {
|
|
160
|
+
expect(isValidDirectoryPath('')).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('handles tilde paths correctly', () => {
|
|
164
|
+
// Home directory should exist
|
|
165
|
+
expect(isValidDirectoryPath('~')).toBe(true)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('useDirectoryBrowser - directory entry conversion', () => {
|
|
171
|
+
// Test the logic for converting directory entries to list items
|
|
172
|
+
const convertToListItem = (entry: { name: string; path: string; isParent: boolean }) => ({
|
|
173
|
+
id: entry.path,
|
|
174
|
+
label: entry.name,
|
|
175
|
+
icon: entry.isParent ? '📂' : '📁',
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('converts parent directory entry correctly', () => {
|
|
179
|
+
const entry = { name: '..', path: '/home', isParent: true }
|
|
180
|
+
const result = convertToListItem(entry)
|
|
181
|
+
|
|
182
|
+
expect(result.id).toBe('/home')
|
|
183
|
+
expect(result.label).toBe('..')
|
|
184
|
+
expect(result.icon).toBe('📂')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('converts regular directory entry correctly', () => {
|
|
188
|
+
const entry = { name: 'Documents', path: '/home/user/Documents', isParent: false }
|
|
189
|
+
const result = convertToListItem(entry)
|
|
190
|
+
|
|
191
|
+
expect(result.id).toBe('/home/user/Documents')
|
|
192
|
+
expect(result.label).toBe('Documents')
|
|
193
|
+
expect(result.icon).toBe('📁')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('handles special characters in directory names', () => {
|
|
197
|
+
const entry = { name: 'My Folder (2024)', path: '/home/My Folder (2024)', isParent: false }
|
|
198
|
+
const result = convertToListItem(entry)
|
|
199
|
+
|
|
200
|
+
expect(result.id).toBe('/home/My Folder (2024)')
|
|
201
|
+
expect(result.label).toBe('My Folder (2024)')
|
|
202
|
+
})
|
|
203
|
+
})
|