@alumbwe/anvil 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/bin/anvil +2 -0
- package/bin/tree-sitter.wasm +0 -0
- package/package.json +67 -0
- package/src/__tests__/README.md +256 -0
- package/src/__tests__/bash-mode.test.ts +442 -0
- package/src/__tests__/cli-args.test.ts +181 -0
- package/src/__tests__/cli-harness.test.ts +52 -0
- package/src/__tests__/e2e/first-time-login.test.ts +173 -0
- package/src/__tests__/e2e/logout-relogin-flow.test.ts +110 -0
- package/src/__tests__/e2e/returning-user-auth.test.ts +113 -0
- package/src/__tests__/e2e-cli.test.ts +183 -0
- package/src/__tests__/helpers/launcher-disconnect-fixture.cjs +176 -0
- package/src/__tests__/helpers/mock-api-client.ts +64 -0
- package/src/__tests__/helpers/renderer-cleanup-fixture.tsx +89 -0
- package/src/__tests__/helpers/terminal-watchdog-fixture.ts +75 -0
- package/src/__tests__/home-directory-detection.test.ts +213 -0
- package/src/__tests__/integration/api-integration.test.ts +288 -0
- package/src/__tests__/integration/credentials-storage.test.ts +486 -0
- package/src/__tests__/integration/local-agents.test.ts +1187 -0
- package/src/__tests__/integration/login-flow-instrumentation.test.ts +134 -0
- package/src/__tests__/integration/login-polling-working.test.ts +285 -0
- package/src/__tests__/integration/queue-editing.test.tsx +261 -0
- package/src/__tests__/integration/usage-refresh-on-completion.test.ts +168 -0
- package/src/__tests__/integration-tmux.test.ts +188 -0
- package/src/__tests__/launcher-avx2-fallback.test.ts +630 -0
- package/src/__tests__/launcher-disconnect.test.ts +104 -0
- package/src/__tests__/mocks/hover-toggle-controller.ts +71 -0
- package/src/__tests__/open-tui-native-bundle.test.ts +378 -0
- package/src/__tests__/path-completion.test.ts +243 -0
- package/src/__tests__/project-files-chat-id.test.ts +34 -0
- package/src/__tests__/release/proxy-http-get.test.ts +667 -0
- package/src/__tests__/release/wrapper-safety.test.ts +412 -0
- package/src/__tests__/renderer-cleanup.test.ts +174 -0
- package/src/__tests__/rerender-perf.integration.test.ts +283 -0
- package/src/__tests__/terminal-reset-sequences.test.ts +36 -0
- package/src/__tests__/terminal-watchdog.test.ts +388 -0
- package/src/__tests__/test-utils.ts +271 -0
- package/src/__tests__/tmux-poc.ts +150 -0
- package/src/__tests__/unit/agent-mode-toggle.test.ts +137 -0
- package/src/__tests__/unit/copy-button.test.ts +157 -0
- package/src/__tests__/unit/create-run-config.test.ts +66 -0
- package/src/__tests__/unit/publish-confirmation.test.ts +70 -0
- package/src/__tests__/unit/segmented-control.test.ts +152 -0
- package/src/__tests__/utils/env.test.ts +169 -0
- package/src/__tests__/utils/project-picker.test.ts +39 -0
- package/src/agents/bundled-agents.generated.d.ts +14 -0
- package/src/agents/bundled-agents.generated.ts +3692 -0
- package/src/app.tsx +399 -0
- package/src/chat.tsx +1698 -0
- package/src/cli-args.ts +134 -0
- package/src/commands/__tests__/bash-command.test.ts +445 -0
- package/src/commands/__tests__/command-args.test.ts +349 -0
- package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
- package/src/commands/__tests__/copy-conversation.test.ts +226 -0
- package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
- package/src/commands/__tests__/freebuff-command-aliases.test.ts +56 -0
- package/src/commands/__tests__/image.test.ts +99 -0
- package/src/commands/__tests__/init.test.ts +456 -0
- package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
- package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
- package/src/commands/__tests__/prompt-builders.test.ts +28 -0
- package/src/commands/__tests__/router-input.test.ts +298 -0
- package/src/commands/ads.ts +44 -0
- package/src/commands/command-registry.ts +726 -0
- package/src/commands/copy-conversation.ts +392 -0
- package/src/commands/help.ts +13 -0
- package/src/commands/image.ts +20 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/process-diagnostics.ts +120 -0
- package/src/commands/prompt-builders.ts +100 -0
- package/src/commands/publish.ts +203 -0
- package/src/commands/router-utils.ts +80 -0
- package/src/commands/router.ts +487 -0
- package/src/commands/usage.ts +26 -0
- package/src/components/__tests__/ad-banner.test.tsx +83 -0
- package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
- package/src/components/__tests__/freebuff-landing-heading-row.test.tsx +91 -0
- package/src/components/__tests__/freebuff-model-selector.test.tsx +519 -0
- package/src/components/__tests__/freebuff-offer-invariants.test.ts +54 -0
- package/src/components/__tests__/freebuff-takeover-prompt.test.tsx +212 -0
- package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
- package/src/components/__tests__/grid-layout.test.tsx +1051 -0
- package/src/components/__tests__/message-block.completion.test.tsx +104 -0
- package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
- package/src/components/__tests__/message-with-agents.test.tsx +579 -0
- package/src/components/__tests__/multiline-input.test.tsx +1132 -0
- package/src/components/__tests__/queue-panel.test.tsx +398 -0
- package/src/components/__tests__/selectable-list.test.ts +232 -0
- package/src/components/__tests__/status-bar.test.tsx +44 -0
- package/src/components/__tests__/status-indicator.test.tsx +204 -0
- package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
- package/src/components/__tests__/user-error-banner.test.tsx +102 -0
- package/src/components/ad-banner.tsx +323 -0
- package/src/components/agent-checklist.tsx +350 -0
- package/src/components/agent-mode-toggle.tsx +241 -0
- package/src/components/anvil-mark.tsx +52 -0
- package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
- package/src/components/ask-user/__tests__/validation.test.ts +213 -0
- package/src/components/ask-user/components/accordion-question.tsx +153 -0
- package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
- package/src/components/ask-user/components/options-list.tsx +133 -0
- package/src/components/ask-user/components/question-header.tsx +80 -0
- package/src/components/ask-user/components/question-option.tsx +89 -0
- package/src/components/ask-user/constants.ts +38 -0
- package/src/components/ask-user/index.tsx +631 -0
- package/src/components/ask-user/utils/validation.ts +85 -0
- package/src/components/attachment-card.tsx +65 -0
- package/src/components/blocks/agent-block-grid.tsx +58 -0
- package/src/components/blocks/agent-branch-item.tsx +311 -0
- package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
- package/src/components/blocks/agent-list-branch.tsx +77 -0
- package/src/components/blocks/ask-user-branch.tsx +81 -0
- package/src/components/blocks/block-helpers.ts +11 -0
- package/src/components/blocks/blocks-renderer.tsx +289 -0
- package/src/components/blocks/content-with-markdown.tsx +33 -0
- package/src/components/blocks/image-block.tsx +128 -0
- package/src/components/blocks/implementor-row.tsx +429 -0
- package/src/components/blocks/single-block.tsx +201 -0
- package/src/components/blocks/thinking-block.tsx +67 -0
- package/src/components/blocks/tool-block-group.tsx +57 -0
- package/src/components/blocks/tool-branch.tsx +206 -0
- package/src/components/blocks/user-content-copy.tsx +157 -0
- package/src/components/bottom-banner.tsx +136 -0
- package/src/components/build-mode-buttons.tsx +110 -0
- package/src/components/button.tsx +66 -0
- package/src/components/chat-header.tsx +77 -0
- package/src/components/chat-history-screen.tsx +413 -0
- package/src/components/chat-input-bar.tsx +523 -0
- package/src/components/clickable-title-box.tsx +66 -0
- package/src/components/clickable.tsx +119 -0
- package/src/components/collapse-button.tsx +34 -0
- package/src/components/copy-button.tsx +203 -0
- package/src/components/elapsed-timer.tsx +55 -0
- package/src/components/error-boundary.tsx +55 -0
- package/src/components/feedback-container.tsx +181 -0
- package/src/components/feedback-icon-button.tsx +89 -0
- package/src/components/feedback-input-mode.tsx +344 -0
- package/src/components/file-attachment-card.tsx +98 -0
- package/src/components/freebuff-active-session-summary.tsx +63 -0
- package/src/components/freebuff-landing-screen.tsx +865 -0
- package/src/components/freebuff-model-selector.tsx +1087 -0
- package/src/components/freebuff-referral-banner.tsx +604 -0
- package/src/components/freebuff-superseded-screen.tsx +62 -0
- package/src/components/grid-layout.tsx +80 -0
- package/src/components/help-banner.tsx +127 -0
- package/src/components/highlighted-text.tsx +52 -0
- package/src/components/image-card.tsx +167 -0
- package/src/components/image-thumbnail.tsx +114 -0
- package/src/components/input-cursor.tsx +77 -0
- package/src/components/input-mode-banner.tsx +57 -0
- package/src/components/load-previous-button.tsx +48 -0
- package/src/components/login-modal.tsx +475 -0
- package/src/components/message-block.tsx +346 -0
- package/src/components/message-footer.tsx +258 -0
- package/src/components/message-with-agents.tsx +506 -0
- package/src/components/mode-divider.tsx +43 -0
- package/src/components/multiline-input.tsx +1241 -0
- package/src/components/out-of-credits-banner.tsx +160 -0
- package/src/components/pending-attachments-banner.tsx +116 -0
- package/src/components/pending-bash-message.tsx +59 -0
- package/src/components/progress-bar.tsx +81 -0
- package/src/components/project-picker-screen.tsx +485 -0
- package/src/components/publish-confirmation.tsx +493 -0
- package/src/components/publish-container.tsx +687 -0
- package/src/components/queue-panel.tsx +310 -0
- package/src/components/raised-pill.tsx +95 -0
- package/src/components/renderers/plan-box.tsx +59 -0
- package/src/components/review-screen.tsx +112 -0
- package/src/components/scroll-to-bottom-button.tsx +34 -0
- package/src/components/segmented-control.tsx +187 -0
- package/src/components/selectable-list.tsx +248 -0
- package/src/components/selected-chips.tsx +80 -0
- package/src/components/separator.tsx +40 -0
- package/src/components/session-ended-banner.tsx +226 -0
- package/src/components/shimmer-text.tsx +248 -0
- package/src/components/status-bar.tsx +313 -0
- package/src/components/subscription-limit-banner.tsx +206 -0
- package/src/components/suggested-prompts.tsx +184 -0
- package/src/components/suggestion-menu.tsx +215 -0
- package/src/components/terminal-command-display.tsx +151 -0
- package/src/components/terminal-link.tsx +97 -0
- package/src/components/terminal-thinking-orb.tsx +95 -0
- package/src/components/text-attachment-card.tsx +77 -0
- package/src/components/thinking.tsx +109 -0
- package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
- package/src/components/tools/__tests__/code-search.test.tsx +45 -0
- package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
- package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
- package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
- package/src/components/tools/apply-patch.tsx +95 -0
- package/src/components/tools/code-search.tsx +45 -0
- package/src/components/tools/composio.tsx +152 -0
- package/src/components/tools/diff-viewer.tsx +76 -0
- package/src/components/tools/glob.tsx +61 -0
- package/src/components/tools/gravity-index.tsx +77 -0
- package/src/components/tools/list-directory.tsx +61 -0
- package/src/components/tools/read-docs.tsx +37 -0
- package/src/components/tools/read-files.tsx +91 -0
- package/src/components/tools/read-subtree.tsx +44 -0
- package/src/components/tools/read-url.tsx +33 -0
- package/src/components/tools/registry.ts +126 -0
- package/src/components/tools/render-ui.tsx +122 -0
- package/src/components/tools/run-terminal-command.tsx +79 -0
- package/src/components/tools/skill.tsx +29 -0
- package/src/components/tools/str-replace.tsx +77 -0
- package/src/components/tools/suggest-followups.tsx +369 -0
- package/src/components/tools/task-completed.tsx +16 -0
- package/src/components/tools/tool-call-item.tsx +273 -0
- package/src/components/tools/types.ts +55 -0
- package/src/components/tools/web-search.tsx +33 -0
- package/src/components/tools/write-file.tsx +16 -0
- package/src/components/tools/write-todos.tsx +95 -0
- package/src/components/top-banner.tsx +178 -0
- package/src/components/usage-banner.tsx +244 -0
- package/src/components/user-error-banner.tsx +56 -0
- package/src/components/validation-error-popover.tsx +192 -0
- package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
- package/src/contexts/chat-runtime-context.tsx +240 -0
- package/src/data/slash-commands.test.ts +24 -0
- package/src/data/slash-commands.ts +249 -0
- package/src/entry.ts +12 -0
- package/src/hooks/__tests__/holds-live-freebuff-slot.test.ts +49 -0
- package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
- package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
- package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
- package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
- package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
- package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
- package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
- package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
- package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
- package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
- package/src/hooks/__tests__/use-input-history.test.ts +699 -0
- package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
- package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
- package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
- package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
- package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
- package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
- package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
- package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
- package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
- package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
- package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
- package/src/hooks/__tests__/use-timeout.test.ts +330 -0
- package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
- package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
- package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
- package/src/hooks/helpers/send-message.ts +654 -0
- package/src/hooks/stream-state.ts +83 -0
- package/src/hooks/use-activity-query.ts +668 -0
- package/src/hooks/use-agent-validation.ts +74 -0
- package/src/hooks/use-ask-user-bridge.ts +92 -0
- package/src/hooks/use-auth-query.ts +253 -0
- package/src/hooks/use-auth-state.ts +152 -0
- package/src/hooks/use-chat-input.ts +105 -0
- package/src/hooks/use-chat-keyboard.ts +321 -0
- package/src/hooks/use-chat-messages.ts +254 -0
- package/src/hooks/use-chat-state.ts +165 -0
- package/src/hooks/use-chat-streaming.ts +180 -0
- package/src/hooks/use-chat-ui.ts +131 -0
- package/src/hooks/use-clipboard.ts +151 -0
- package/src/hooks/use-connection-status.ts +142 -0
- package/src/hooks/use-directory-browser.ts +88 -0
- package/src/hooks/use-elapsed-time.ts +116 -0
- package/src/hooks/use-event.ts +34 -0
- package/src/hooks/use-exit-handler.ts +66 -0
- package/src/hooks/use-fetch-login-url.ts +65 -0
- package/src/hooks/use-fingerprint.ts +61 -0
- package/src/hooks/use-freebuff-ctrl-c-exit.ts +23 -0
- package/src/hooks/use-freebuff-session-progress.ts +34 -0
- package/src/hooks/use-freebuff-session.ts +806 -0
- package/src/hooks/use-freebuff-streak-query.ts +61 -0
- package/src/hooks/use-gravity-ad.ts +656 -0
- package/src/hooks/use-grid-layout.ts +73 -0
- package/src/hooks/use-input-history.ts +173 -0
- package/src/hooks/use-login-keyboard-handlers.ts +75 -0
- package/src/hooks/use-login-polling.ts +114 -0
- package/src/hooks/use-logo.tsx +161 -0
- package/src/hooks/use-message-queue.ts +387 -0
- package/src/hooks/use-now.ts +20 -0
- package/src/hooks/use-path-tab-completion.ts +88 -0
- package/src/hooks/use-publish-mutation.ts +60 -0
- package/src/hooks/use-queue-controls.ts +68 -0
- package/src/hooks/use-queue-ui.ts +74 -0
- package/src/hooks/use-scroll-management.ts +184 -0
- package/src/hooks/use-searchable-list.ts +98 -0
- package/src/hooks/use-send-message.ts +773 -0
- package/src/hooks/use-sheen-animation.tsx +88 -0
- package/src/hooks/use-subscription-query.ts +71 -0
- package/src/hooks/use-suggestion-engine.ts +783 -0
- package/src/hooks/use-terminal-breakpoints.ts +77 -0
- package/src/hooks/use-terminal-dimensions.ts +46 -0
- package/src/hooks/use-terminal-focus.ts +29 -0
- package/src/hooks/use-terminal-layout.ts +189 -0
- package/src/hooks/use-theme.tsx +140 -0
- package/src/hooks/use-timeout.ts +85 -0
- package/src/hooks/use-update-preference.ts +66 -0
- package/src/hooks/use-usage-monitor.ts +55 -0
- package/src/hooks/use-usage-query.ts +124 -0
- package/src/hooks/use-user-details-query.ts +93 -0
- package/src/hooks/use-why-did-you-update.ts +177 -0
- package/src/index.tsx +441 -0
- package/src/init/__tests__/init-direnv.test.ts +526 -0
- package/src/init/init-app.ts +34 -0
- package/src/init/init-direnv.ts +133 -0
- package/src/login/constants.ts +60 -0
- package/src/login/login-flow.ts +204 -0
- package/src/login/plain-login.ts +108 -0
- package/src/login/utils.ts +183 -0
- package/src/native/ripgrep.ts +80 -0
- package/src/polyfills/bun-strip-ansi.ts +12 -0
- package/src/pre-init/tree-sitter-wasm.ts +96 -0
- package/src/project-files.ts +102 -0
- package/src/smoke/terminal-command-broker.ts +439 -0
- package/src/state/__tests__/chat-store-focus.test.ts +22 -0
- package/src/state/__tests__/feedback-store.test.ts +257 -0
- package/src/state/chat-history-store.ts +30 -0
- package/src/state/chat-store.ts +544 -0
- package/src/state/feedback-store.ts +160 -0
- package/src/state/freebuff-model-store.ts +42 -0
- package/src/state/freebuff-session-store.ts +53 -0
- package/src/state/login-store.ts +150 -0
- package/src/state/message-block-store.ts +149 -0
- package/src/state/publish-store.ts +146 -0
- package/src/state/queue-panel-store.ts +26 -0
- package/src/state/review-store.ts +24 -0
- package/src/testing/env.ts +46 -0
- package/src/types/chat-state.ts +15 -0
- package/src/types/chat.ts +241 -0
- package/src/types/contracts/send-message.ts +12 -0
- package/src/types/env.ts +98 -0
- package/src/types/freebuff-session.ts +17 -0
- package/src/types/function-params.ts +28 -0
- package/src/types/react19-compat.d.ts +19 -0
- package/src/types/store.ts +111 -0
- package/src/types/theme-system.ts +144 -0
- package/src/types/utils.ts +3 -0
- package/src/utils/__tests__/active-run.test.ts +140 -0
- package/src/utils/__tests__/activity-tracker.test.ts +440 -0
- package/src/utils/__tests__/agent-display.test.ts +139 -0
- package/src/utils/__tests__/analytics-client.test.ts +262 -0
- package/src/utils/__tests__/anonymous-id.test.ts +74 -0
- package/src/utils/__tests__/anvil-api.test.ts +578 -0
- package/src/utils/__tests__/arrays.test.ts +264 -0
- package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
- package/src/utils/__tests__/block-processor.test.ts +810 -0
- package/src/utils/__tests__/chat-history.test.ts +203 -0
- package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
- package/src/utils/__tests__/chat-meta.test.ts +130 -0
- package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
- package/src/utils/__tests__/clipboard.test.ts +910 -0
- package/src/utils/__tests__/code-search-summary.test.ts +84 -0
- package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
- package/src/utils/__tests__/error-handling.test.ts +658 -0
- package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
- package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
- package/src/utils/__tests__/feedback-submission.test.ts +26 -0
- package/src/utils/__tests__/fetch-usage.test.ts +355 -0
- package/src/utils/__tests__/fingerprint.test.ts +144 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
- package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
- package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
- package/src/utils/__tests__/format-timeout.test.ts +87 -0
- package/src/utils/__tests__/freebuff-instance-owner.test.ts +69 -0
- package/src/utils/__tests__/freebuff-model-navigation.test.ts +102 -0
- package/src/utils/__tests__/freebuff-premium-reset.test.ts +79 -0
- package/src/utils/__tests__/freebuff-referral-cache.test.ts +116 -0
- package/src/utils/__tests__/freebuff-session-api.test.ts +215 -0
- package/src/utils/__tests__/freebuff-session-display.test.ts +23 -0
- package/src/utils/__tests__/freebuff-streak-line.test.ts +217 -0
- package/src/utils/__tests__/image-dimensions.test.ts +249 -0
- package/src/utils/__tests__/image-processor.test.ts +209 -0
- package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
- package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
- package/src/utils/__tests__/layout-helpers.test.ts +36 -0
- package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
- package/src/utils/__tests__/log-shipper.test.ts +53 -0
- package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
- package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
- package/src/utils/__tests__/message-updater.test.ts +645 -0
- package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
- package/src/utils/__tests__/pending-attachments.test.ts +277 -0
- package/src/utils/__tests__/polling-backoff.test.ts +51 -0
- package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
- package/src/utils/__tests__/run-state-storage.test.ts +919 -0
- package/src/utils/__tests__/safe-json.test.ts +77 -0
- package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
- package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
- package/src/utils/__tests__/send-message-timer.test.ts +64 -0
- package/src/utils/__tests__/settings.test.ts +99 -0
- package/src/utils/__tests__/strings.test.ts +192 -0
- package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
- package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
- package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
- package/src/utils/__tests__/terminal-io.test.ts +137 -0
- package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
- package/src/utils/__tests__/text-layout.test.ts +82 -0
- package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
- package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
- package/src/utils/__tests__/trace-writer.test.ts +223 -0
- package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
- package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
- package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
- package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
- package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
- package/src/utils/active-run.ts +131 -0
- package/src/utils/activity-tracker.ts +66 -0
- package/src/utils/agent-display.ts +87 -0
- package/src/utils/agent-helpers.ts +27 -0
- package/src/utils/agent-id-utils.ts +14 -0
- package/src/utils/ai-message-id.ts +12 -0
- package/src/utils/analytics.ts +361 -0
- package/src/utils/anonymous-id.ts +87 -0
- package/src/utils/anvil-api.ts +612 -0
- package/src/utils/anvil-client.ts +217 -0
- package/src/utils/arrays.ts +117 -0
- package/src/utils/auth.ts +258 -0
- package/src/utils/bash-context-processor.ts +47 -0
- package/src/utils/bash-messages.ts +108 -0
- package/src/utils/block-margins.ts +35 -0
- package/src/utils/block-operations.ts +523 -0
- package/src/utils/block-processor.ts +213 -0
- package/src/utils/chat-history.ts +253 -0
- package/src/utils/chat-input-key-intercept.ts +52 -0
- package/src/utils/chat-meta.ts +90 -0
- package/src/utils/chat-scroll-accel.ts +151 -0
- package/src/utils/clipboard-image.ts +588 -0
- package/src/utils/clipboard.ts +397 -0
- package/src/utils/code-search-summary.ts +70 -0
- package/src/utils/collapse-helpers.ts +256 -0
- package/src/utils/config-dir.ts +24 -0
- package/src/utils/constants.ts +190 -0
- package/src/utils/create-event-handler-state.ts +73 -0
- package/src/utils/create-run-config.ts +121 -0
- package/src/utils/detect-shell.ts +116 -0
- package/src/utils/directory-browser.ts +72 -0
- package/src/utils/engagement.ts +55 -0
- package/src/utils/env.ts +92 -0
- package/src/utils/error-handling.ts +255 -0
- package/src/utils/error-messages.ts +62 -0
- package/src/utils/exit-cleanly.ts +86 -0
- package/src/utils/feedback-helpers.ts +103 -0
- package/src/utils/feedback-submission.ts +22 -0
- package/src/utils/fetch-usage.ts +76 -0
- package/src/utils/fingerprint.ts +245 -0
- package/src/utils/format-elapsed-time.ts +33 -0
- package/src/utils/format-session-units.ts +6 -0
- package/src/utils/format-timeout.ts +28 -0
- package/src/utils/format-validation-errors-for-message.ts +84 -0
- package/src/utils/freebuff-agent-selection.ts +33 -0
- package/src/utils/freebuff-instance-owner.ts +66 -0
- package/src/utils/freebuff-model-navigation.ts +50 -0
- package/src/utils/freebuff-premium-reset.ts +55 -0
- package/src/utils/freebuff-referral-cache.ts +51 -0
- package/src/utils/freebuff-session-api.ts +243 -0
- package/src/utils/freebuff-session-display.ts +21 -0
- package/src/utils/freebuff-streak-line.ts +93 -0
- package/src/utils/git.ts +17 -0
- package/src/utils/helpers.ts +47 -0
- package/src/utils/image-display.ts +67 -0
- package/src/utils/image-handler.ts +325 -0
- package/src/utils/image-processor.ts +133 -0
- package/src/utils/image-thumbnail.ts +78 -0
- package/src/utils/implementor-helpers.ts +787 -0
- package/src/utils/input-modes.ts +174 -0
- package/src/utils/keyboard-actions.ts +392 -0
- package/src/utils/keypad-keys.ts +47 -0
- package/src/utils/layout-helpers.ts +33 -0
- package/src/utils/lazy-response-ads.ts +1 -0
- package/src/utils/local-agent-registry.ts +474 -0
- package/src/utils/log-shipper.ts +126 -0
- package/src/utils/logger.ts +327 -0
- package/src/utils/markdown-renderer.tsx +1090 -0
- package/src/utils/math.ts +3 -0
- package/src/utils/message-block-helpers.ts +684 -0
- package/src/utils/message-history.ts +143 -0
- package/src/utils/message-tree-utils.ts +67 -0
- package/src/utils/message-updater.ts +279 -0
- package/src/utils/open-file.ts +146 -0
- package/src/utils/open-url.ts +63 -0
- package/src/utils/path-completion.ts +111 -0
- package/src/utils/path-helpers.ts +38 -0
- package/src/utils/pending-attachments.ts +360 -0
- package/src/utils/polling-backoff.ts +59 -0
- package/src/utils/project-picker.ts +12 -0
- package/src/utils/queue-panel-actions.ts +70 -0
- package/src/utils/recent-projects.ts +156 -0
- package/src/utils/renderer-cleanup.ts +233 -0
- package/src/utils/response-ad-positions.ts +1 -0
- package/src/utils/run-state-storage.ts +603 -0
- package/src/utils/safe-json.ts +153 -0
- package/src/utils/sdk-event-handlers.ts +571 -0
- package/src/utils/send-message-helpers.ts +210 -0
- package/src/utils/send-message-timer.ts +110 -0
- package/src/utils/settings.ts +226 -0
- package/src/utils/skill-registry.ts +94 -0
- package/src/utils/spawn-agent-matcher.ts +55 -0
- package/src/utils/status-indicator-state.ts +107 -0
- package/src/utils/stream-chunk-processor.ts +57 -0
- package/src/utils/strings.ts +271 -0
- package/src/utils/subscription.ts +31 -0
- package/src/utils/syntax-highlighter.tsx +19 -0
- package/src/utils/terminal-color-detection.ts +474 -0
- package/src/utils/terminal-command-broker.ts +467 -0
- package/src/utils/terminal-enter-detection.ts +63 -0
- package/src/utils/terminal-images.ts +229 -0
- package/src/utils/terminal-io.ts +85 -0
- package/src/utils/terminal-protocol-controller.ts +173 -0
- package/src/utils/terminal-reset-sequences.ts +33 -0
- package/src/utils/terminal-title.ts +84 -0
- package/src/utils/terminal-watchdog.ts +366 -0
- package/src/utils/text-layout.ts +149 -0
- package/src/utils/theme-config.ts +141 -0
- package/src/utils/theme-system.ts +1206 -0
- package/src/utils/think-tag-parser.ts +95 -0
- package/src/utils/time-format.ts +21 -0
- package/src/utils/trace-writer.ts +162 -0
- package/src/utils/ui-constants.ts +69 -0
- package/src/utils/update-checker.ts +110 -0
- package/src/utils/usage-banner-state.ts +146 -0
- package/src/utils/validation-error-formatting.ts +86 -0
- package/src/utils/validation-error-helpers.ts +19 -0
- package/src/utils/windows-terminal-health.ts +124 -0
- package/src/utils/word-wrap-utils.ts +53 -0
- package/src/utils/write-file-atomic.ts +54 -0
- package/src/utils/yield-to-event-loop.ts +9 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createTestRenderer } from '@opentui/core/testing'
|
|
3
|
+
import { createRoot, flushSync } from '@opentui/react'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
|
|
6
|
+
import { FreebuffModelSelector } from '../freebuff-model-selector'
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_FREEBUFF_MODEL_ID,
|
|
9
|
+
FALLBACK_FREEBUFF_MODEL_ID,
|
|
10
|
+
FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
|
|
11
|
+
FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
|
|
12
|
+
FREEBUFF_FABLE_5_MODEL_ID,
|
|
13
|
+
FREEBUFF_GLM_V52_MODEL_ID,
|
|
14
|
+
FREEBUFF_GPT_5_6_LUNA_MODEL_ID,
|
|
15
|
+
FREEBUFF_MINIMAX_M3_MODEL_ID,
|
|
16
|
+
FREEBUFF_MODELS,
|
|
17
|
+
getFreebuffModelSupersededBy,
|
|
18
|
+
isFreebuffModelId,
|
|
19
|
+
} from '@anvil/common/constants/freebuff-models'
|
|
20
|
+
|
|
21
|
+
import { initializeThemeStore } from '../../hooks/use-theme'
|
|
22
|
+
import {
|
|
23
|
+
getSelectedFreebuffModel,
|
|
24
|
+
useFreebuffModelStore,
|
|
25
|
+
} from '../../state/freebuff-model-store'
|
|
26
|
+
import { useFreebuffSessionStore } from '../../state/freebuff-session-store'
|
|
27
|
+
|
|
28
|
+
let cleanupRenderer: (() => void) | undefined
|
|
29
|
+
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
initializeThemeStore()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanupRenderer?.()
|
|
36
|
+
cleanupRenderer = undefined
|
|
37
|
+
useFreebuffSessionStore.getState().setSession(null)
|
|
38
|
+
useFreebuffSessionStore.getState().setFailure(null)
|
|
39
|
+
useFreebuffModelStore.getState().setSelectedModel(FALLBACK_FREEBUFF_MODEL_ID)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const renderSelector = async (maxHeight = 40) => {
|
|
43
|
+
// Tear down any selector this test already rendered. Only the LAST one was
|
|
44
|
+
// reachable from afterEach, so a test that renders twice used to leave the
|
|
45
|
+
// earlier root mounted — and a mounted selector keeps running its landing
|
|
46
|
+
// repair effect, rewriting the shared model store out from under whichever
|
|
47
|
+
// test ran next.
|
|
48
|
+
cleanupRenderer?.()
|
|
49
|
+
cleanupRenderer = undefined
|
|
50
|
+
const setup = await createTestRenderer({ width: 100, height: 40 })
|
|
51
|
+
const root = createRoot(setup.renderer)
|
|
52
|
+
cleanupRenderer = () => {
|
|
53
|
+
flushSync(() => root.unmount())
|
|
54
|
+
setup.renderer.destroy()
|
|
55
|
+
}
|
|
56
|
+
flushSync(() => root.render(<FreebuffModelSelector maxHeight={maxHeight} />))
|
|
57
|
+
await setup.renderOnce()
|
|
58
|
+
return setup
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const renderSelectorWithGlmRemaining = async (remaining?: number) => {
|
|
62
|
+
useFreebuffSessionStore.getState().setSession({
|
|
63
|
+
status: 'none',
|
|
64
|
+
accessTier: 'full',
|
|
65
|
+
referral: {
|
|
66
|
+
code: 'test-referral',
|
|
67
|
+
referrerName: null,
|
|
68
|
+
qualifiedCount: 1,
|
|
69
|
+
...(remaining === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: { weeklySessionsRemaining: remaining }),
|
|
72
|
+
resetAt: new Date(Date.now() + 60_000).toISOString(),
|
|
73
|
+
githubLinked: true,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
useFreebuffModelStore.getState().setSelectedModel(FREEBUFF_GLM_V52_MODEL_ID)
|
|
77
|
+
|
|
78
|
+
const nextSetup = await renderSelector(30)
|
|
79
|
+
await nextSetup.renderOnce()
|
|
80
|
+
await Promise.resolve()
|
|
81
|
+
await nextSetup.renderOnce()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('FreebuffModelSelector referral selection', () => {
|
|
85
|
+
test('keeps a fractional unlocked GLM session selected while its request is pending', async () => {
|
|
86
|
+
await renderSelectorWithGlmRemaining(0.25)
|
|
87
|
+
expect(getSelectedFreebuffModel()).toBe(FREEBUFF_GLM_V52_MODEL_ID)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('still repairs a locked GLM selection to a visible grid model', async () => {
|
|
91
|
+
await renderSelectorWithGlmRemaining(0)
|
|
92
|
+
expect(isFreebuffModelId(getSelectedFreebuffModel())).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('treats an omitted GLM balance as locked', async () => {
|
|
96
|
+
await renderSelectorWithGlmRemaining()
|
|
97
|
+
expect(isFreebuffModelId(getSelectedFreebuffModel())).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('FreebuffModelSelector tier layout', () => {
|
|
102
|
+
test('keeps the referral actions on one condensed row', async () => {
|
|
103
|
+
useFreebuffSessionStore.getState().setSession({
|
|
104
|
+
status: 'none',
|
|
105
|
+
accessTier: 'full',
|
|
106
|
+
referral: {
|
|
107
|
+
code: 'test-referral',
|
|
108
|
+
referrerName: null,
|
|
109
|
+
qualifiedCount: 0,
|
|
110
|
+
weeklySessionsRemaining: 0,
|
|
111
|
+
resetAt: new Date(Date.now() + 60_000).toISOString(),
|
|
112
|
+
githubLinked: true,
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
useFreebuffModelStore
|
|
116
|
+
.getState()
|
|
117
|
+
.setSelectedModel(FREEBUFF_MINIMAX_M3_MODEL_ID)
|
|
118
|
+
|
|
119
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
120
|
+
const actionRow =
|
|
121
|
+
frame.split('\n').find((line) => line.includes('Copy invite link')) ?? ''
|
|
122
|
+
|
|
123
|
+
expect(actionRow).toContain('Open referral status')
|
|
124
|
+
expect(frame).not.toContain('Or earn')
|
|
125
|
+
expect(frame).not.toContain('for small tasks')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('orders Luna above MiniMax while keeping the saved premium model focused', async () => {
|
|
129
|
+
useFreebuffSessionStore.getState().setSession({
|
|
130
|
+
status: 'none',
|
|
131
|
+
accessTier: 'full',
|
|
132
|
+
})
|
|
133
|
+
// The saved pick has to be something OTHER than the recommended hero, or
|
|
134
|
+
// the landing picker opens collapsed and there are no tier headers to order.
|
|
135
|
+
// Since 2026-08-12 the hero is itself premium (DeepSeek V4 Pro), so Luna is
|
|
136
|
+
// the premium row that exercises "saved model stays focused" here.
|
|
137
|
+
useFreebuffModelStore
|
|
138
|
+
.getState()
|
|
139
|
+
.setSelectedModel(FREEBUFF_GPT_5_6_LUNA_MODEL_ID)
|
|
140
|
+
|
|
141
|
+
const setup = await renderSelector()
|
|
142
|
+
const frame = setup.captureCharFrame()
|
|
143
|
+
const premiumHeaderIndex = frame.indexOf('PREMIUM')
|
|
144
|
+
const recommendedModelIndex = frame.indexOf('DeepSeek V4 Pro')
|
|
145
|
+
const selectedModelIndex = frame.indexOf('GPT-5.6 Luna')
|
|
146
|
+
const minimaxModelIndex = frame.indexOf('MiniMax M3')
|
|
147
|
+
const unlimitedHeaderIndex = frame.indexOf('UNLIMITED')
|
|
148
|
+
|
|
149
|
+
expect(premiumHeaderIndex).toBeGreaterThanOrEqual(0)
|
|
150
|
+
expect(recommendedModelIndex).toBeGreaterThan(premiumHeaderIndex)
|
|
151
|
+
expect(selectedModelIndex).toBeGreaterThan(recommendedModelIndex)
|
|
152
|
+
expect(minimaxModelIndex).toBeGreaterThan(selectedModelIndex)
|
|
153
|
+
expect(unlimitedHeaderIndex).toBeGreaterThan(minimaxModelIndex)
|
|
154
|
+
expect(frame).toContain('› GPT-5.6 Luna')
|
|
155
|
+
expect(frame).not.toContain('› MiniMax M3')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('shows the switch-to-Flash nudge only on the row the user is on', async () => {
|
|
159
|
+
useFreebuffSessionStore.getState().setSession({
|
|
160
|
+
status: 'none',
|
|
161
|
+
accessTier: 'full',
|
|
162
|
+
})
|
|
163
|
+
// Assert against the real copy rather than a hardcoded fragment, so
|
|
164
|
+
// rewording the notice doesn't fail this test for the wrong reason. It must
|
|
165
|
+
// still render on ONE line — the width math reserves exactly its length.
|
|
166
|
+
const notice = getFreebuffModelSupersededBy(
|
|
167
|
+
FREEBUFF_MINIMAX_M3_MODEL_ID,
|
|
168
|
+
FREEBUFF_MODELS.map((m) => m.id),
|
|
169
|
+
)!.notice
|
|
170
|
+
const occurrences = (frame: string) => frame.split(notice).length - 1
|
|
171
|
+
|
|
172
|
+
// On a superseded model: the nudge appears, once, on that model's card.
|
|
173
|
+
useFreebuffModelStore
|
|
174
|
+
.getState()
|
|
175
|
+
.setSelectedModel(FREEBUFF_MINIMAX_M3_MODEL_ID)
|
|
176
|
+
const onSuperseded = (await renderSelector()).captureCharFrame()
|
|
177
|
+
expect(occurrences(onSuperseded)).toBe(1)
|
|
178
|
+
// It names the dated build, which is what the row it steers to is labelled.
|
|
179
|
+
expect(notice).toContain('DeepSeek V4 Flash 07/31')
|
|
180
|
+
// The new builds are badged so a returning user notices they changed.
|
|
181
|
+
expect(onSuperseded).toContain('NEW')
|
|
182
|
+
|
|
183
|
+
// MiMo is superseded too and is on screen here, but only the selected row
|
|
184
|
+
// nags — otherwise the list would repeat the same notice on every row it
|
|
185
|
+
// applies to. V4 Pro is on screen and is NOT superseded at all since its
|
|
186
|
+
// 08/13 GA build, so selecting it shows no nudge anywhere.
|
|
187
|
+
useFreebuffModelStore
|
|
188
|
+
.getState()
|
|
189
|
+
.setSelectedModel(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID)
|
|
190
|
+
const onUnsuperseded = (await renderSelector()).captureCharFrame()
|
|
191
|
+
expect(onUnsuperseded).toContain('DeepSeek V4 Pro 08/13')
|
|
192
|
+
expect(occurrences(onUnsuperseded)).toBe(0)
|
|
193
|
+
|
|
194
|
+
// On the replacement itself: no nudge at all. (Picking the recommended
|
|
195
|
+
// model also collapses the picker to its hero card.)
|
|
196
|
+
useFreebuffModelStore
|
|
197
|
+
.getState()
|
|
198
|
+
.setSelectedModel(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)
|
|
199
|
+
const onCurrent = (await renderSelector()).captureCharFrame()
|
|
200
|
+
expect(occurrences(onCurrent)).toBe(0)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('places the exhausted-quota recommendation beneath UNLIMITED', async () => {
|
|
204
|
+
const resetAt = new Date(Date.now() + 60_000).toISOString()
|
|
205
|
+
useFreebuffSessionStore.getState().setSession({
|
|
206
|
+
status: 'none',
|
|
207
|
+
accessTier: 'full',
|
|
208
|
+
rateLimitsByModel: {
|
|
209
|
+
[FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: {
|
|
210
|
+
model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
|
|
211
|
+
limit: 6,
|
|
212
|
+
period: 'pacific_day',
|
|
213
|
+
resetTimeZone: 'America/Los_Angeles',
|
|
214
|
+
resetAt,
|
|
215
|
+
windowHours: 24,
|
|
216
|
+
recentCount: 6,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
useFreebuffModelStore
|
|
221
|
+
.getState()
|
|
222
|
+
.setSelectedModel(FREEBUFF_MINIMAX_M3_MODEL_ID)
|
|
223
|
+
|
|
224
|
+
const setup = await renderSelector()
|
|
225
|
+
const frame = setup.captureCharFrame()
|
|
226
|
+
const premiumHeaderIndex = frame.indexOf('PREMIUM')
|
|
227
|
+
const unlimitedHeaderIndex = frame.indexOf('UNLIMITED')
|
|
228
|
+
const recommendedLabelIndex = frame.indexOf('RECOMMENDED')
|
|
229
|
+
const recommendedModelIndex = frame.indexOf(
|
|
230
|
+
'DeepSeek V4 Flash',
|
|
231
|
+
recommendedLabelIndex,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect(unlimitedHeaderIndex).toBeGreaterThan(premiumHeaderIndex)
|
|
235
|
+
expect(recommendedLabelIndex).toBeGreaterThan(unlimitedHeaderIndex)
|
|
236
|
+
expect(recommendedModelIndex).toBeGreaterThan(recommendedLabelIndex)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('collapses to the unlimited hero when the premium default is spent', async () => {
|
|
240
|
+
// The default selection has been premium since 2026-08-12, so a returning
|
|
241
|
+
// user who has spent their pool opens the picker already sitting on a row
|
|
242
|
+
// `pick` silently refuses. Both the selection AND the cursor have to leave
|
|
243
|
+
// it, or Enter does nothing with no explanation — and the picker has to
|
|
244
|
+
// collapse onto the replacement, or it opens on three greyed, unusable
|
|
245
|
+
// premium rows with the recommendation fourth.
|
|
246
|
+
const resetAt = new Date(Date.now() + 60_000).toISOString()
|
|
247
|
+
useFreebuffSessionStore.getState().setSession({
|
|
248
|
+
status: 'none',
|
|
249
|
+
accessTier: 'full',
|
|
250
|
+
rateLimitsByModel: {
|
|
251
|
+
[FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: {
|
|
252
|
+
model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
|
|
253
|
+
limit: 6,
|
|
254
|
+
period: 'pacific_day',
|
|
255
|
+
resetTimeZone: 'America/Los_Angeles',
|
|
256
|
+
resetAt,
|
|
257
|
+
windowHours: 24,
|
|
258
|
+
recentCount: 6,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
useFreebuffModelStore.getState().setSelectedModel(DEFAULT_FREEBUFF_MODEL_ID)
|
|
263
|
+
|
|
264
|
+
const setup = await renderSelector()
|
|
265
|
+
await Promise.resolve()
|
|
266
|
+
await setup.renderOnce()
|
|
267
|
+
await setup.renderOnce()
|
|
268
|
+
|
|
269
|
+
expect(getSelectedFreebuffModel()).toBe(FALLBACK_FREEBUFF_MODEL_ID)
|
|
270
|
+
const frame = setup.captureCharFrame()
|
|
271
|
+
// `›` is the cursor: it has to be on the row Enter now commits.
|
|
272
|
+
expect(frame).toContain('› DeepSeek V4 Flash')
|
|
273
|
+
// …and that row is the whole screen, exactly as for a user who is already
|
|
274
|
+
// on the recommendation. The spent rows live behind the toggle.
|
|
275
|
+
expect(frame).toContain('See all')
|
|
276
|
+
expect(frame).not.toContain('PREMIUM')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('repairs an invalid selection to the unlimited recommendation when premium is exhausted', async () => {
|
|
280
|
+
const resetAt = new Date(Date.now() + 60_000).toISOString()
|
|
281
|
+
useFreebuffSessionStore.getState().setSession({
|
|
282
|
+
status: 'none',
|
|
283
|
+
accessTier: 'full',
|
|
284
|
+
rateLimitsByModel: {
|
|
285
|
+
[FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: {
|
|
286
|
+
model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
|
|
287
|
+
limit: 6,
|
|
288
|
+
period: 'pacific_day',
|
|
289
|
+
resetTimeZone: 'America/Los_Angeles',
|
|
290
|
+
resetAt,
|
|
291
|
+
windowHours: 24,
|
|
292
|
+
recentCount: 6,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
useFreebuffModelStore.getState().setSelectedModel(FREEBUFF_GLM_V52_MODEL_ID)
|
|
297
|
+
|
|
298
|
+
const setup = await renderSelector()
|
|
299
|
+
await Promise.resolve()
|
|
300
|
+
await setup.renderOnce()
|
|
301
|
+
await setup.renderOnce()
|
|
302
|
+
|
|
303
|
+
expect(getSelectedFreebuffModel()).toBe(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)
|
|
304
|
+
expect(setup.captureCharFrame()).toContain('› DeepSeek V4 Flash')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('shows every limited-tier model when the access tier arrives after mount', async () => {
|
|
308
|
+
useFreebuffSessionStore.getState().setSession({
|
|
309
|
+
status: 'none',
|
|
310
|
+
accessTier: 'full',
|
|
311
|
+
})
|
|
312
|
+
useFreebuffModelStore
|
|
313
|
+
.getState()
|
|
314
|
+
.setSelectedModel(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID)
|
|
315
|
+
const setup = await renderSelector()
|
|
316
|
+
|
|
317
|
+
flushSync(() => {
|
|
318
|
+
useFreebuffSessionStore.getState().setSession({
|
|
319
|
+
status: 'none',
|
|
320
|
+
accessTier: 'limited',
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
await Promise.resolve()
|
|
324
|
+
await setup.renderOnce()
|
|
325
|
+
await setup.renderOnce()
|
|
326
|
+
|
|
327
|
+
const frame = setup.captureCharFrame()
|
|
328
|
+
expect(frame).toContain('DeepSeek V4 Flash')
|
|
329
|
+
expect(frame).toContain('MiMo 2.5')
|
|
330
|
+
expect(frame).not.toContain('PREMIUM')
|
|
331
|
+
expect(frame).not.toContain('UNLIMITED')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('badges only natively multimodal rows with Images', async () => {
|
|
335
|
+
useFreebuffSessionStore.getState().setSession({
|
|
336
|
+
status: 'none',
|
|
337
|
+
accessTier: 'full',
|
|
338
|
+
})
|
|
339
|
+
// Expanded (a saved non-recommended pick) so every row is on screen.
|
|
340
|
+
useFreebuffModelStore
|
|
341
|
+
.getState()
|
|
342
|
+
.setSelectedModel(FREEBUFF_MINIMAX_M3_MODEL_ID)
|
|
343
|
+
|
|
344
|
+
const rowOf = (frame: string, name: string) =>
|
|
345
|
+
frame.split('\n').find((line) => line.includes(name)) ?? ''
|
|
346
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
347
|
+
|
|
348
|
+
// Natively multimodal: the badge is a real capability claim.
|
|
349
|
+
expect(rowOf(frame, 'MiniMax M3')).toContain('Images')
|
|
350
|
+
expect(rowOf(frame, 'GPT-5.6 Luna')).toContain('Images')
|
|
351
|
+
expect(rowOf(frame, 'MiMo 2.5')).toContain('Images')
|
|
352
|
+
// Text-only. They still accept a pasted image (read server-side as a
|
|
353
|
+
// description), but badging them made the label mean nothing — and the
|
|
354
|
+
// badge is what widened the hero card.
|
|
355
|
+
expect(rowOf(frame, 'DeepSeek V4 Flash')).not.toContain('Images')
|
|
356
|
+
expect(rowOf(frame, 'DeepSeek V4 Pro')).not.toContain('Images')
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('says the reasoning effort on rows whose catalog entry carries one', async () => {
|
|
360
|
+
useFreebuffSessionStore.getState().setSession({
|
|
361
|
+
status: 'none',
|
|
362
|
+
accessTier: 'full',
|
|
363
|
+
})
|
|
364
|
+
useFreebuffModelStore
|
|
365
|
+
.getState()
|
|
366
|
+
.setSelectedModel(FREEBUFF_MINIMAX_M3_MODEL_ID)
|
|
367
|
+
|
|
368
|
+
// Anchored on taglines: model names also appear in superseded-notice lines
|
|
369
|
+
const rowOf = (frame: string, tagline: string) =>
|
|
370
|
+
frame.split('\n').find((line) => line.includes(tagline)) ?? ''
|
|
371
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
372
|
+
|
|
373
|
+
expect(rowOf(frame, 'Smart & Fast')).toContain('Reasoning: high')
|
|
374
|
+
expect(rowOf(frame, 'Smartest')).toContain('Reasoning: high')
|
|
375
|
+
const lunaRow = rowOf(frame, 'GPT-5.6 Luna')
|
|
376
|
+
expect(lunaRow).toContain('Strong all-around')
|
|
377
|
+
expect(lunaRow).toContain('Reasoning: high')
|
|
378
|
+
expect(rowOf(frame, 'MiniMax M3')).not.toContain('Reasoning')
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('sizes the hero card to its content, with no Press-Enter gutter', async () => {
|
|
382
|
+
useFreebuffSessionStore.getState().setSession({
|
|
383
|
+
status: 'none',
|
|
384
|
+
accessTier: 'full',
|
|
385
|
+
})
|
|
386
|
+
useFreebuffModelStore
|
|
387
|
+
.getState()
|
|
388
|
+
.setSelectedModel(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID)
|
|
389
|
+
|
|
390
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
391
|
+
// trimEnd drops the terminal's blank columns to the right of the card, so
|
|
392
|
+
// what's left ends at the card's own right border.
|
|
393
|
+
const heroRow = (
|
|
394
|
+
frame.split('\n').find((line) => line.includes('› DeepSeek V4 Flash')) ??
|
|
395
|
+
''
|
|
396
|
+
).trimEnd()
|
|
397
|
+
|
|
398
|
+
expect(frame).not.toContain('Press Enter')
|
|
399
|
+
// The reserved cue gutter used to sit between the last badge and the right
|
|
400
|
+
// border, padding the card out by ~17 columns of empty space. What remains
|
|
401
|
+
// is ordinary slack from the widest row in the set.
|
|
402
|
+
const gapToBorder =
|
|
403
|
+
heroRow.length - 1 - (heroRow.indexOf('NEW') + 'NEW'.length)
|
|
404
|
+
expect(heroRow.endsWith('│')).toBe(true)
|
|
405
|
+
expect(gapToBorder).toBeLessThan(10)
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
describe('FreebuffModelSelector limited-model offer', () => {
|
|
410
|
+
const offerSession = (
|
|
411
|
+
offer: Partial<{
|
|
412
|
+
remaining: number
|
|
413
|
+
total: number
|
|
414
|
+
userRemaining: number
|
|
415
|
+
userResetAt: string
|
|
416
|
+
}> = {},
|
|
417
|
+
) => ({
|
|
418
|
+
status: 'none' as const,
|
|
419
|
+
accessTier: 'full' as const,
|
|
420
|
+
limitedModelOffers: [
|
|
421
|
+
{
|
|
422
|
+
model: FREEBUFF_FABLE_5_MODEL_ID,
|
|
423
|
+
remaining: 38,
|
|
424
|
+
total: 50,
|
|
425
|
+
userRemaining: 1,
|
|
426
|
+
userResetAt: new Date(Date.now() + 5 * 60 * 60_000).toISOString(),
|
|
427
|
+
...offer,
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test('renders nothing when the server sends no offer', async () => {
|
|
433
|
+
// The regression that matters most: a user who is not in the wave must see
|
|
434
|
+
// the picker exactly as it was before the offer existed.
|
|
435
|
+
useFreebuffSessionStore.getState().setSession({
|
|
436
|
+
status: 'none',
|
|
437
|
+
accessTier: 'full',
|
|
438
|
+
})
|
|
439
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
440
|
+
expect(frame).not.toContain('LIMITED TRIAL')
|
|
441
|
+
expect(frame).not.toContain('Fable')
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('renders the offered model with its scarcity and data-use label', async () => {
|
|
445
|
+
useFreebuffSessionStore.getState().setSession(offerSession())
|
|
446
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
447
|
+
expect(frame).toContain('LIMITED TRIAL')
|
|
448
|
+
expect(frame).toContain('38 of 50 sessions left')
|
|
449
|
+
expect(frame).toContain('Claude Fable 5')
|
|
450
|
+
// The disclosure that makes collecting the traces legitimate travels on the
|
|
451
|
+
// row itself, not in a footnote somewhere else.
|
|
452
|
+
expect(frame).toContain('May use data for AI training')
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
test('stays visible while collapsed, unlike the ordinary tiers', async () => {
|
|
456
|
+
// The picker opens collapsed for a user already on the recommended model.
|
|
457
|
+
// A wave nobody sees is a wave nobody joins. Read off the constant so the
|
|
458
|
+
// collapsed state survives the next flip of the recommended default.
|
|
459
|
+
useFreebuffModelStore
|
|
460
|
+
.getState()
|
|
461
|
+
.setSelectedModel(DEFAULT_FREEBUFF_MODEL_ID)
|
|
462
|
+
useFreebuffSessionStore.getState().setSession(offerSession())
|
|
463
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
464
|
+
expect(frame).toContain('See all')
|
|
465
|
+
expect(frame).toContain('Claude Fable 5')
|
|
466
|
+
expect(frame).not.toContain('PREMIUM')
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
test('explains a spent personal allowance instead of hiding the row', async () => {
|
|
470
|
+
useFreebuffSessionStore
|
|
471
|
+
.getState()
|
|
472
|
+
.setSession(offerSession({ userRemaining: 0 }))
|
|
473
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
474
|
+
expect(frame).toContain('Claude Fable 5')
|
|
475
|
+
expect(frame).toContain("you've used yours")
|
|
476
|
+
expect(frame).toContain('resets in')
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
test('drops an offer this build has no catalog entry for', async () => {
|
|
480
|
+
// A server rolling out a model older clients don't know must be a no-op,
|
|
481
|
+
// not a row with a blank name and no data-use warning.
|
|
482
|
+
useFreebuffSessionStore.getState().setSession({
|
|
483
|
+
status: 'none',
|
|
484
|
+
accessTier: 'full',
|
|
485
|
+
limitedModelOffers: [
|
|
486
|
+
{
|
|
487
|
+
model: 'someone/unreleased-model-9',
|
|
488
|
+
remaining: 5,
|
|
489
|
+
total: 50,
|
|
490
|
+
userRemaining: 1,
|
|
491
|
+
userResetAt: new Date(Date.now() + 60_000).toISOString(),
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
})
|
|
495
|
+
const frame = (await renderSelector()).captureCharFrame()
|
|
496
|
+
expect(frame).not.toContain('LIMITED TRIAL')
|
|
497
|
+
expect(frame).not.toContain('unreleased-model-9')
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('keeps an offered selection instead of repairing it away', async () => {
|
|
501
|
+
// The offer model is not in FREEBUFF_MODELS, so the picker's
|
|
502
|
+
// invalid-selection repair would otherwise bounce the user off the row they
|
|
503
|
+
// just picked.
|
|
504
|
+
useFreebuffSessionStore.getState().setSession(offerSession())
|
|
505
|
+
useFreebuffModelStore.getState().setSelectedModel(FREEBUFF_FABLE_5_MODEL_ID)
|
|
506
|
+
await renderSelector()
|
|
507
|
+
expect(getSelectedFreebuffModel()).toBe(FREEBUFF_FABLE_5_MODEL_ID)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('repairs the selection once the wave ends', async () => {
|
|
511
|
+
useFreebuffSessionStore.getState().setSession({
|
|
512
|
+
status: 'none',
|
|
513
|
+
accessTier: 'full',
|
|
514
|
+
})
|
|
515
|
+
useFreebuffModelStore.getState().setSelectedModel(FREEBUFF_FABLE_5_MODEL_ID)
|
|
516
|
+
await renderSelector()
|
|
517
|
+
expect(isFreebuffModelId(getSelectedFreebuffModel())).toBe(true)
|
|
518
|
+
})
|
|
519
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The CLI can offer two kinds of freebuff row: the picker grid, and the referral banner's earned
|
|
2
|
+
// GLM 5.2 action. Both end up as a POST the server gates, and as a free-mode root agent that has
|
|
3
|
+
// to allow the model — so a row this surface can show must survive all of it. Desktop shipped the
|
|
4
|
+
// mirror-image of this bug (an offered GLM row its own route answered 400 for), which is what
|
|
5
|
+
// these lock down here.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test'
|
|
8
|
+
|
|
9
|
+
import { getFreebuffRootAgentIdForModel } from '@anvil/common/constants/free-agents'
|
|
10
|
+
import {
|
|
11
|
+
FREEBUFF_GLM_V52_MODEL_ID,
|
|
12
|
+
resolveFreebuffModelForAccessTier,
|
|
13
|
+
} from '@anvil/common/constants/freebuff-models'
|
|
14
|
+
import { freebuffOfferViolations } from '@anvil/common/testing/freebuff-offer-invariants'
|
|
15
|
+
|
|
16
|
+
import { freebuffCliOfferedModelIds } from '../freebuff-model-selector'
|
|
17
|
+
|
|
18
|
+
describe('freebuff rows the CLI offers', () => {
|
|
19
|
+
for (const accessTier of ['full', 'limited'] as const) {
|
|
20
|
+
test(`are all usable on the ${accessTier} tier`, () => {
|
|
21
|
+
expect(
|
|
22
|
+
freebuffOfferViolations({
|
|
23
|
+
surface: `cli picker + referral banner (${accessTier})`,
|
|
24
|
+
accessTier,
|
|
25
|
+
offered: freebuffCliOfferedModelIds(accessTier),
|
|
26
|
+
// the CLI's own resolver, which every session start runs the selection through: a model
|
|
27
|
+
// it coerces away is one the user picked and never got
|
|
28
|
+
accepts: (model) =>
|
|
29
|
+
resolveFreebuffModelForAccessTier(model, accessTier) === model,
|
|
30
|
+
rootAgentIdFor: getFreebuffRootAgentIdForModel,
|
|
31
|
+
catalog: 'supported',
|
|
32
|
+
}),
|
|
33
|
+
).toEqual([])
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('the earned reward is offered on BOTH tiers, and the grid never shows it', () => {
|
|
38
|
+
// Limited access included: a bounty grant is redeemable there, so the row has to be
|
|
39
|
+
// reachable there. The banner still only renders it against a live balance.
|
|
40
|
+
expect(freebuffCliOfferedModelIds('full')).toContain(FREEBUFF_GLM_V52_MODEL_ID)
|
|
41
|
+
expect(freebuffCliOfferedModelIds('limited')).toContain(
|
|
42
|
+
FREEBUFF_GLM_V52_MODEL_ID,
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// 'base2-free' is the fallback root, and its allowlist has never included the referral reward.
|
|
47
|
+
// A GLM row that fell through to it would 403 with free_mode_invalid_agent_model on the first
|
|
48
|
+
// turn instead of failing at selection, so the mapping is what keeps the reward runnable.
|
|
49
|
+
test('the reward maps to its own root agent rather than the fallback', () => {
|
|
50
|
+
expect(getFreebuffRootAgentIdForModel(FREEBUFF_GLM_V52_MODEL_ID)).toBe(
|
|
51
|
+
'base2-free-glm',
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
})
|