@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,806 @@
|
|
|
1
|
+
import { isDirectDeepSeekMode } from '@anvil/common/constants/deepseek-direct'
|
|
2
|
+
import {
|
|
3
|
+
FALLBACK_FREEBUFF_MODEL_ID,
|
|
4
|
+
getFreebuffModel,
|
|
5
|
+
isFreebuffLimitedOfferModelId,
|
|
6
|
+
LIMITED_FREEBUFF_MODEL_ID,
|
|
7
|
+
resolveFreebuffModelForAccessTier,
|
|
8
|
+
} from '@anvil/common/constants/freebuff-models'
|
|
9
|
+
import {
|
|
10
|
+
getLimitedModelOffers,
|
|
11
|
+
getRateLimitsByModel,
|
|
12
|
+
getReferralInfo,
|
|
13
|
+
} from '@anvil/common/types/freebuff-session'
|
|
14
|
+
import { useEffect } from 'react'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
getSelectedFreebuffModel,
|
|
18
|
+
useFreebuffModelStore,
|
|
19
|
+
} from '../state/freebuff-model-store'
|
|
20
|
+
import { useChatStore } from '../state/chat-store'
|
|
21
|
+
import { useFreebuffSessionStore } from '../state/freebuff-session-store'
|
|
22
|
+
import { getAuthTokenDetails } from '../utils/auth'
|
|
23
|
+
import { stopActiveRun } from '../utils/active-run'
|
|
24
|
+
import { IS_FREEBUFF } from '../utils/constants'
|
|
25
|
+
import {
|
|
26
|
+
isFreebuffInstanceOwnedByDeadLocalProcess,
|
|
27
|
+
recordFreebuffInstanceOwner,
|
|
28
|
+
} from '../utils/freebuff-instance-owner'
|
|
29
|
+
import { logger } from '../utils/logger'
|
|
30
|
+
import { getSystemMessage } from '../utils/message-history'
|
|
31
|
+
import {
|
|
32
|
+
clearReferralCache,
|
|
33
|
+
getCachedReferral,
|
|
34
|
+
rememberReferral,
|
|
35
|
+
} from '../utils/freebuff-referral-cache'
|
|
36
|
+
import {
|
|
37
|
+
callFreebuffSession,
|
|
38
|
+
classifyFreebuffSessionRequestFailure,
|
|
39
|
+
FreebuffSessionRequestError,
|
|
40
|
+
holdsLiveFreebuffSlot,
|
|
41
|
+
isFreebuffSessionTimeoutError,
|
|
42
|
+
mergeCompactActiveSession,
|
|
43
|
+
releaseFreebuffSlot,
|
|
44
|
+
} from '../utils/freebuff-session-api'
|
|
45
|
+
import {
|
|
46
|
+
failedPollDelayMs,
|
|
47
|
+
jitterPollIntervalMs,
|
|
48
|
+
} from '../utils/polling-backoff'
|
|
49
|
+
import { saveFreebuffModelPreference } from '../utils/settings'
|
|
50
|
+
|
|
51
|
+
import type { FreebuffSessionResponse } from '../types/freebuff-session'
|
|
52
|
+
import type {
|
|
53
|
+
FreebuffCountryBlockReason,
|
|
54
|
+
FreebuffIpPrivacySignal,
|
|
55
|
+
} from '@anvil/common/types/freebuff-session'
|
|
56
|
+
|
|
57
|
+
const POLL_INTERVAL_ACTIVE_MS = 30_000
|
|
58
|
+
|
|
59
|
+
/** Play the terminal bell so users get an audible notification on admission. */
|
|
60
|
+
const playAdmissionSound = () => {
|
|
61
|
+
try {
|
|
62
|
+
process.stdout.write('\x07')
|
|
63
|
+
} catch {
|
|
64
|
+
// Silent fallback — some terminals/pipes disallow writing to stdout.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Picks the poll delay after a successful tick. Returns null when the state
|
|
69
|
+
* is terminal (no further polling). */
|
|
70
|
+
function nextDelayMs(next: FreebuffSessionResponse): number | null {
|
|
71
|
+
const activeCadenceMs = jitterPollIntervalMs({
|
|
72
|
+
intervalMs: POLL_INTERVAL_ACTIVE_MS,
|
|
73
|
+
})
|
|
74
|
+
switch (next.status) {
|
|
75
|
+
case 'active':
|
|
76
|
+
// Poll at the normal cadence, but ensure we land just after
|
|
77
|
+
// `expires_at` so the transition shows up promptly instead of leaving
|
|
78
|
+
// the countdown stuck at 0 for up to a full interval.
|
|
79
|
+
return Math.max(
|
|
80
|
+
1_000,
|
|
81
|
+
Math.min(activeCadenceMs, next.remainingMs + 1_000),
|
|
82
|
+
)
|
|
83
|
+
case 'ended':
|
|
84
|
+
// Inside the grace window we keep checking so the post-grace transition
|
|
85
|
+
// (server returns `none`, we synthesize ended-no-instanceId) is prompt.
|
|
86
|
+
return next.instanceId ? activeCadenceMs : null
|
|
87
|
+
case 'none':
|
|
88
|
+
case 'superseded':
|
|
89
|
+
case 'takeover_prompt':
|
|
90
|
+
case 'country_blocked':
|
|
91
|
+
case 'banned':
|
|
92
|
+
case 'model_locked':
|
|
93
|
+
case 'rate_limited':
|
|
94
|
+
case 'spend_limited':
|
|
95
|
+
case 'ip_capped':
|
|
96
|
+
case 'model_unavailable':
|
|
97
|
+
case 'premium_slot_taken':
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Poll-loop control surface ---------------------------------------------
|
|
103
|
+
//
|
|
104
|
+
// The hook below registers a controller object here on mount; module-level
|
|
105
|
+
// imperative functions (restart / mark superseded / mark ended / etc.) talk
|
|
106
|
+
// to it without going through React. Non-React callers (chat-completions
|
|
107
|
+
// gate, exit paths) hit those functions directly.
|
|
108
|
+
|
|
109
|
+
/** How the next tick should behave after a forced restart.
|
|
110
|
+
* - 'rejoin' → POST: claim/rotate a seat (used after explicit end-and-rejoin
|
|
111
|
+
* or when the chat gate kicks us back to the queue).
|
|
112
|
+
* - 'landing' → GET: drop to the model-picker (status 'none') so the user
|
|
113
|
+
* reconfirms a model before rejoining. */
|
|
114
|
+
type RestartMode = 'rejoin' | 'landing'
|
|
115
|
+
|
|
116
|
+
interface PollController {
|
|
117
|
+
/** Cancel the in-flight tick + timer and start a fresh one in `mode`. */
|
|
118
|
+
restart: (mode: RestartMode) => Promise<void>
|
|
119
|
+
apply: (next: FreebuffSessionResponse) => void
|
|
120
|
+
abort: () => void
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let controller: PollController | null = null
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The model of the most recent EXPLICIT user pick (startFreebuffSession),
|
|
127
|
+
* consumed by the first server response that follows it. Lets the
|
|
128
|
+
* `model_locked` branch tell a deliberate pick apart from a background
|
|
129
|
+
* rejoin/race: only the former deserves a visible explanation. Cleared on
|
|
130
|
+
* every response so a stale pick can never annotate a later, unrelated lock.
|
|
131
|
+
*/
|
|
132
|
+
let pendingExplicitPickModel: string | null = null
|
|
133
|
+
|
|
134
|
+
/** Read the current instance id for outgoing chat requests. Defined via
|
|
135
|
+
* `holdsLiveFreebuffSlot` so the two can't drift: an id exists exactly while
|
|
136
|
+
* we hold a live slot (active, or `ended` inside the server-side grace
|
|
137
|
+
* window where the row stays alive until `expires_at + grace`). */
|
|
138
|
+
export function getFreebuffInstanceId(): string | undefined {
|
|
139
|
+
// Direct-to-DeepSeek mode has no server-side session; hand back a stable
|
|
140
|
+
// local id so the chat gate (which requires a live slot) lets messages
|
|
141
|
+
// through instead of holding them for a rejoin that can never happen.
|
|
142
|
+
if (isDirectDeepSeekMode()) return 'local-deepseek'
|
|
143
|
+
const current = useFreebuffSessionStore.getState().session
|
|
144
|
+
if (!current || !holdsLiveFreebuffSlot(current)) return undefined
|
|
145
|
+
return 'instanceId' in current ? current.instanceId : undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** True when the session represents a server-side slot the caller is
|
|
149
|
+
* holding (active, or in the post-expiry grace window with a live
|
|
150
|
+
* instance id). Chat requests are only admissible in these states — once
|
|
151
|
+
* the slot is gone, `getFreebuffInstanceId` returns undefined and the
|
|
152
|
+
* server rejects the request — so the message queue gates on this before
|
|
153
|
+
* firing queued work. Same predicate gates DELETE on exit: outside these
|
|
154
|
+
* states there is no server row to release. */
|
|
155
|
+
function toLandingSession(
|
|
156
|
+
current: FreebuffSessionResponse | null,
|
|
157
|
+
): Extract<FreebuffSessionResponse, { status: 'none' }> {
|
|
158
|
+
const accessTier =
|
|
159
|
+
current && 'accessTier' in current ? current.accessTier : undefined
|
|
160
|
+
const rateLimitsByModel = getRateLimitsByModel(current)
|
|
161
|
+
const referral = accessTier
|
|
162
|
+
? (getReferralInfo(current) ?? getCachedReferral(accessTier))
|
|
163
|
+
: undefined
|
|
164
|
+
const countryCode =
|
|
165
|
+
current && 'countryCode' in current ? current.countryCode : undefined
|
|
166
|
+
const countryBlockReason =
|
|
167
|
+
current && 'countryBlockReason' in current
|
|
168
|
+
? current.countryBlockReason
|
|
169
|
+
: undefined
|
|
170
|
+
const ipPrivacySignals =
|
|
171
|
+
current && 'ipPrivacySignals' in current
|
|
172
|
+
? current.ipPrivacySignals
|
|
173
|
+
: undefined
|
|
174
|
+
// Carried over so the picker doesn't lose the limited-offer row for the one
|
|
175
|
+
// frame between synthesizing this state and the GET that refreshes it. The
|
|
176
|
+
// GET is authoritative: if the wave has since been spent, the next response
|
|
177
|
+
// simply omits the offer and the row disappears.
|
|
178
|
+
const limitedModelOffers = getLimitedModelOffers(current)
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
status: 'none',
|
|
182
|
+
...(accessTier ? { accessTier } : {}),
|
|
183
|
+
...(rateLimitsByModel ? { rateLimitsByModel } : {}),
|
|
184
|
+
...(referral ? { referral } : {}),
|
|
185
|
+
...(limitedModelOffers.length > 0 ? { limitedModelOffers } : {}),
|
|
186
|
+
...(countryCode ? { countryCode } : {}),
|
|
187
|
+
...(countryBlockReason ? { countryBlockReason } : {}),
|
|
188
|
+
...(ipPrivacySignals ? { ipPrivacySignals } : {}),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface RestartOpts {
|
|
193
|
+
resetChat?: boolean
|
|
194
|
+
/** DELETE the held slot before restarting so the next POST starts clean. */
|
|
195
|
+
releaseSlot?: boolean
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function restartFreebuffSession(
|
|
199
|
+
mode: RestartMode,
|
|
200
|
+
opts: RestartOpts = {},
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
if (!IS_FREEBUFF) return
|
|
203
|
+
// A reset changes chat ownership. Stop and checkpoint the old run before
|
|
204
|
+
// resetting its store so late deltas cannot land in the next session.
|
|
205
|
+
if (opts.resetChat) {
|
|
206
|
+
stopActiveRun('session-transition')
|
|
207
|
+
useChatStore.getState().reset()
|
|
208
|
+
}
|
|
209
|
+
// Halt the running poll loop before we touch local stores or DELETE the
|
|
210
|
+
// slot. Otherwise an in-flight GET could land mid-reset and overwrite
|
|
211
|
+
// state, or the next scheduled tick could fire between DELETE and
|
|
212
|
+
// restart() with stale assumptions. restart() re-aborts and re-arms
|
|
213
|
+
// below; the extra abort here is cheap.
|
|
214
|
+
controller?.abort()
|
|
215
|
+
if (opts.releaseSlot) await releaseFreebuffSlot()
|
|
216
|
+
await controller?.restart(mode)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Re-POST to the server (rejoining the queue / rotating the instance id).
|
|
221
|
+
* Pass `resetChat: true` to also wipe local chat history — used when
|
|
222
|
+
* rejoining after a session ended so the next admitted session starts fresh.
|
|
223
|
+
*/
|
|
224
|
+
export function refreshFreebuffSession(
|
|
225
|
+
opts: { resetChat?: boolean } = {},
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
return restartFreebuffSession('rejoin', { resetChat: opts.resetChat })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Drop back to the pre-join landing state (model picker) instead of auto
|
|
232
|
+
* re-queuing. Used after a session ends: the user lands on the picker so
|
|
233
|
+
* they consciously choose a model and hit Enter to join, rather than being
|
|
234
|
+
* silently re-queued for whatever model they last used.
|
|
235
|
+
*/
|
|
236
|
+
export function returnToFreebuffLanding(
|
|
237
|
+
opts: { resetChat?: boolean } = {},
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
return restartFreebuffSession('landing', {
|
|
240
|
+
resetChat: opts.resetChat,
|
|
241
|
+
releaseSlot: true,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Refresh picker-only metadata (quota and queue depths) while staying on the
|
|
246
|
+
* model selection screen. Used when a midnight-Pacific session quota reset
|
|
247
|
+
* passes while the landing screen is open. */
|
|
248
|
+
export function refreshFreebuffLandingMetadata(): Promise<void> {
|
|
249
|
+
return restartFreebuffSession('landing')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Start a session on `model` (admitted immediately server-side). Dual-purpose:
|
|
254
|
+
* - First start: called from the pre-chat landing picker. The session starts
|
|
255
|
+
* at `none` (GET-only); this is the user's explicit commitment to enter.
|
|
256
|
+
* - Switch: called when the user picks a different model from the landing
|
|
257
|
+
* screen. The server admits them on the new model right away.
|
|
258
|
+
*
|
|
259
|
+
* If the server has already admitted them on a different model, it responds
|
|
260
|
+
* with `model_locked`; because this is a deliberate pick, the tick loop ends
|
|
261
|
+
* that session and re-claims on the requested model (see the model_locked
|
|
262
|
+
* branch). Background rejoins hitting the same lock revert silently instead.
|
|
263
|
+
*/
|
|
264
|
+
export function startFreebuffSession(model: string): Promise<void> {
|
|
265
|
+
if (!IS_FREEBUFF) return Promise.resolve()
|
|
266
|
+
// This is the only explicit user-pick path (called from the picker on
|
|
267
|
+
// click / Enter), so persistence belongs here — and ONLY here. Server-
|
|
268
|
+
// driven flips (`model_locked`, `model_unavailable`, takeover) go
|
|
269
|
+
// through `setSelectedModel` directly, which never writes to disk.
|
|
270
|
+
const current = useFreebuffSessionStore.getState().session
|
|
271
|
+
const accessTier =
|
|
272
|
+
current && 'accessTier' in current ? current.accessTier : 'full'
|
|
273
|
+
const resolved = resolveFreebuffModelForAccessTier(model, accessTier)
|
|
274
|
+
// Remember that the next POST is a deliberate pick, so a `model_locked`
|
|
275
|
+
// rejection explains itself in chat instead of reverting silently.
|
|
276
|
+
pendingExplicitPickModel = resolved
|
|
277
|
+
useFreebuffModelStore.getState().setSelectedModel(resolved)
|
|
278
|
+
saveFreebuffModelPreference(resolved)
|
|
279
|
+
return restartFreebuffSession('rejoin')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let takeoverInFlight: Promise<void> | null = null
|
|
283
|
+
|
|
284
|
+
export function takeOverFreebuffSession(): Promise<void> {
|
|
285
|
+
if (!IS_FREEBUFF) return Promise.resolve()
|
|
286
|
+
if (takeoverInFlight) return takeoverInFlight
|
|
287
|
+
|
|
288
|
+
const { session } = useFreebuffSessionStore.getState()
|
|
289
|
+
if (session?.status !== 'takeover_prompt') {
|
|
290
|
+
return Promise.resolve()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
useFreebuffModelStore.getState().setSelectedModel(session.model)
|
|
294
|
+
takeoverInFlight = restartFreebuffSession('rejoin').finally(() => {
|
|
295
|
+
takeoverInFlight = null
|
|
296
|
+
})
|
|
297
|
+
return takeoverInFlight
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function markFreebuffSessionSuperseded(): void {
|
|
301
|
+
if (!IS_FREEBUFF) return
|
|
302
|
+
controller?.abort()
|
|
303
|
+
controller?.apply({ status: 'superseded' })
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Flip into the terminal `country_blocked` state from outside the poll loop.
|
|
307
|
+
* Used when the chat-completions gate rejects on country even though the
|
|
308
|
+
* session-level country check did not catch the request first.
|
|
309
|
+
* Transitioning the session state here unmounts the Chat surface in favor of
|
|
310
|
+
* the landing screen's country_blocked message, so the user can't keep typing
|
|
311
|
+
* and sending doomed requests. */
|
|
312
|
+
export function markFreebuffSessionCountryBlocked(params: {
|
|
313
|
+
countryCode: string
|
|
314
|
+
countryBlockReason?: FreebuffCountryBlockReason
|
|
315
|
+
ipPrivacySignals?: FreebuffIpPrivacySignal[]
|
|
316
|
+
}): void {
|
|
317
|
+
if (!IS_FREEBUFF) return
|
|
318
|
+
controller?.abort()
|
|
319
|
+
controller?.apply({ status: 'country_blocked', ...params })
|
|
320
|
+
// Best-effort DELETE so we don't hold a session row the server is already
|
|
321
|
+
// refusing to serve at chat time.
|
|
322
|
+
releaseFreebuffSlot().catch(() => {})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Flip into the local `ended` state without an instanceId (server has lost
|
|
326
|
+
* our row). The chat surface stays mounted with the rejoin banner.
|
|
327
|
+
* Preserves any `rateLimitsByModel` snapshot from the prior session so the
|
|
328
|
+
* banner can show today's session count without an extra fetch. */
|
|
329
|
+
export function markFreebuffSessionEnded(): void {
|
|
330
|
+
if (!IS_FREEBUFF) return
|
|
331
|
+
controller?.abort()
|
|
332
|
+
const current = useFreebuffSessionStore.getState().session
|
|
333
|
+
const rateLimitsByModel = getRateLimitsByModel(current)
|
|
334
|
+
controller?.apply({
|
|
335
|
+
status: 'ended',
|
|
336
|
+
accessTier:
|
|
337
|
+
current && 'accessTier' in current ? current.accessTier : undefined,
|
|
338
|
+
rateLimitsByModel,
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
interface UseFreebuffSessionResult {
|
|
343
|
+
session: FreebuffSessionResponse | null
|
|
344
|
+
failure: ReturnType<typeof useFreebuffSessionStore.getState>['failure']
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Manages the freebuff session lifecycle:
|
|
349
|
+
* - GET on mount to probe state (no auto-join; the user picks a model in
|
|
350
|
+
* the landing screen, which calls startFreebuffSession)
|
|
351
|
+
* - if the probe sees an existing seat, auto-takes-over when the prior
|
|
352
|
+
* local owner process is gone; otherwise asks before POSTing to rotate
|
|
353
|
+
* the instance id so any other CLI on the same account is superseded
|
|
354
|
+
* - polls GET while active to keep state fresh
|
|
355
|
+
* - re-POSTs on explicit refresh (chat gate rejected us, user switched
|
|
356
|
+
* models, user rejoined after ending)
|
|
357
|
+
* - DELETE on unmount so the slot frees up for the next user
|
|
358
|
+
* - plays a bell on admission to an active session
|
|
359
|
+
*/
|
|
360
|
+
export function useFreebuffSession(): UseFreebuffSessionResult {
|
|
361
|
+
const session = useFreebuffSessionStore((s) => s.session)
|
|
362
|
+
const failure = useFreebuffSessionStore((s) => s.failure)
|
|
363
|
+
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
const { setSession, setFailure } = useFreebuffSessionStore.getState()
|
|
366
|
+
|
|
367
|
+
if (!IS_FREEBUFF) {
|
|
368
|
+
// Non-freebuff (Anvil) builds never gate on a free session; leave the
|
|
369
|
+
// store empty (app.tsx's session routing is all behind IS_FREEBUFF).
|
|
370
|
+
setSession(null)
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (isDirectDeepSeekMode()) {
|
|
375
|
+
// No server-side session to poll in direct-to-DeepSeek mode. Set a
|
|
376
|
+
// synthetic active session so the chat surface mounts normally and
|
|
377
|
+
// getFreebuffInstanceId() satisfies the message-send gate.
|
|
378
|
+
setSession({
|
|
379
|
+
status: 'active',
|
|
380
|
+
accessTier: 'full',
|
|
381
|
+
instanceId: 'local-deepseek',
|
|
382
|
+
model: getSelectedFreebuffModel() || FALLBACK_FREEBUFF_MODEL_ID,
|
|
383
|
+
admittedAt: new Date().toISOString(),
|
|
384
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
385
|
+
remainingMs: 24 * 60 * 60 * 1000,
|
|
386
|
+
})
|
|
387
|
+
setFailure(null)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { token } = getAuthTokenDetails()
|
|
392
|
+
if (!token) {
|
|
393
|
+
logger.warn(
|
|
394
|
+
{},
|
|
395
|
+
'[freebuff-session] No auth token; skipping free-session admission',
|
|
396
|
+
)
|
|
397
|
+
setFailure({
|
|
398
|
+
type: 'other',
|
|
399
|
+
message: 'Not authenticated',
|
|
400
|
+
retry: null,
|
|
401
|
+
outcomeUnknown: false,
|
|
402
|
+
})
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let cancelled = false
|
|
407
|
+
let abortController = new AbortController()
|
|
408
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
409
|
+
let previousStatus: FreebuffSessionResponse['status'] | null = null
|
|
410
|
+
// A compact response for an unexpected session identity has no safe quota
|
|
411
|
+
// snapshot to retain, so force exactly one rich poll to restore it.
|
|
412
|
+
let needsFullActivePoll = false
|
|
413
|
+
let restartGeneration = 0
|
|
414
|
+
let consecutiveFailures = 0
|
|
415
|
+
// Method for the NEXT tick. GET is read-only; POST claims/rotates a seat.
|
|
416
|
+
// Startup is GET (probe before committing). After any POST completes we
|
|
417
|
+
// flip back to GET. refresh() sets it to 'POST' for explicit join/rejoin;
|
|
418
|
+
// the startup takeover branch does the same when the probe finds a seat.
|
|
419
|
+
let nextMethod: 'GET' | 'POST' = 'GET'
|
|
420
|
+
|
|
421
|
+
const apply = (next: FreebuffSessionResponse) => {
|
|
422
|
+
rememberReferral(next)
|
|
423
|
+
if (next.status === 'active') {
|
|
424
|
+
useFreebuffModelStore.getState().setSelectedModel(next.model)
|
|
425
|
+
recordFreebuffInstanceOwner(next.instanceId)
|
|
426
|
+
} else if (next.status === 'none' && next.accessTier === 'limited') {
|
|
427
|
+
useFreebuffModelStore
|
|
428
|
+
.getState()
|
|
429
|
+
.setSelectedModel(LIMITED_FREEBUFF_MODEL_ID)
|
|
430
|
+
}
|
|
431
|
+
setSession(next)
|
|
432
|
+
setFailure(null)
|
|
433
|
+
previousStatus = next.status
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const clearTimer = () => {
|
|
437
|
+
if (timer) {
|
|
438
|
+
clearTimeout(timer)
|
|
439
|
+
timer = null
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const schedule = (ms: number) => {
|
|
444
|
+
if (cancelled) return
|
|
445
|
+
clearTimer()
|
|
446
|
+
timer = setTimeout(tick, ms)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const tick = async () => {
|
|
450
|
+
if (cancelled) return
|
|
451
|
+
const method = nextMethod
|
|
452
|
+
const instanceId = getFreebuffInstanceId()
|
|
453
|
+
const model = getSelectedFreebuffModel()
|
|
454
|
+
const compact =
|
|
455
|
+
method === 'GET' && previousStatus === 'active' && !needsFullActivePoll
|
|
456
|
+
const fetchController = abortController
|
|
457
|
+
const generation = restartGeneration
|
|
458
|
+
try {
|
|
459
|
+
const next = await callFreebuffSession(method, token, {
|
|
460
|
+
signal: fetchController.signal,
|
|
461
|
+
instanceId,
|
|
462
|
+
model,
|
|
463
|
+
compact,
|
|
464
|
+
})
|
|
465
|
+
if (
|
|
466
|
+
cancelled ||
|
|
467
|
+
fetchController.signal.aborted ||
|
|
468
|
+
generation !== restartGeneration
|
|
469
|
+
) {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
consecutiveFailures = 0
|
|
473
|
+
// After any successful call, default back to GET polling. The
|
|
474
|
+
// takeover and model_locked branches below override this when they
|
|
475
|
+
// need another POST.
|
|
476
|
+
nextMethod = 'GET'
|
|
477
|
+
|
|
478
|
+
// Consume the explicit-pick marker: it annotates exactly the first
|
|
479
|
+
// response after a user pick, whatever that response turns out to be.
|
|
480
|
+
const explicitPickModel = pendingExplicitPickModel
|
|
481
|
+
pendingExplicitPickModel = null
|
|
482
|
+
|
|
483
|
+
// The session is model-locked server-side: an active session on
|
|
484
|
+
// another model rejects the switch. Two cases:
|
|
485
|
+
// - DELIBERATE pick (the explicit-pick marker was set): honor the
|
|
486
|
+
// click — end the locked session (usually a stale row from a
|
|
487
|
+
// crashed CLI; DELETE is keyed on user, not instance) and
|
|
488
|
+
// re-claim on the requested model. The marker is consume-once,
|
|
489
|
+
// so if the retried POST races another instance back into
|
|
490
|
+
// model_locked we take the revert branch instead of looping.
|
|
491
|
+
// - Background rejoin racing an admission: revert the local
|
|
492
|
+
// selection so the active session stays intact. Reverting a
|
|
493
|
+
// deliberate pick silently made "I clicked GLM 5.2 and it
|
|
494
|
+
// switched to DeepSeek V4 Flash" a recurring bug report
|
|
495
|
+
// (2026-07-30): sessions live 1h even when idle, so users
|
|
496
|
+
// constantly pick a model while a row is still active.
|
|
497
|
+
if (next.status === 'model_locked') {
|
|
498
|
+
if (explicitPickModel && explicitPickModel !== next.currentModel) {
|
|
499
|
+
const current = getFreebuffModel(next.currentModel).displayName
|
|
500
|
+
const requested = getFreebuffModel(explicitPickModel).displayName
|
|
501
|
+
let released = false
|
|
502
|
+
try {
|
|
503
|
+
await callFreebuffSession('DELETE', token, {
|
|
504
|
+
signal: fetchController.signal,
|
|
505
|
+
})
|
|
506
|
+
released = true
|
|
507
|
+
} catch {
|
|
508
|
+
// DELETE failed — fall through to the revert-with-explanation
|
|
509
|
+
// path below rather than stranding the user mid-switch.
|
|
510
|
+
}
|
|
511
|
+
if (
|
|
512
|
+
cancelled ||
|
|
513
|
+
fetchController.signal.aborted ||
|
|
514
|
+
generation !== restartGeneration
|
|
515
|
+
) {
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
if (released) {
|
|
519
|
+
useChatStore
|
|
520
|
+
.getState()
|
|
521
|
+
.setMessages((prev) => [
|
|
522
|
+
...prev,
|
|
523
|
+
getSystemMessage(
|
|
524
|
+
`Ended your previous session on ${current} and switched to ${requested}.`,
|
|
525
|
+
),
|
|
526
|
+
])
|
|
527
|
+
nextMethod = 'POST'
|
|
528
|
+
schedule(0)
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
useChatStore
|
|
532
|
+
.getState()
|
|
533
|
+
.setMessages((prev) => [
|
|
534
|
+
...prev,
|
|
535
|
+
getSystemMessage(
|
|
536
|
+
`You're already in an active session on ${current}, and ending it failed, so the switch to ${requested} was not applied. Run /end-session, then pick ${requested}. (Sessions end on their own after 1 hour.)`,
|
|
537
|
+
),
|
|
538
|
+
])
|
|
539
|
+
}
|
|
540
|
+
useFreebuffModelStore.getState().setSelectedModel(next.currentModel)
|
|
541
|
+
schedule(0)
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
if (next.status === 'model_unavailable') {
|
|
545
|
+
// Server says the requested model isn't available right now. Flip
|
|
546
|
+
// to the always-available fallback for this run. In-memory only —
|
|
547
|
+
// `setSelectedModel` doesn't persist, so the user's saved preference
|
|
548
|
+
// is preserved for their next launch.
|
|
549
|
+
//
|
|
550
|
+
// A limited-offer model gets a sentence about it. Silence is fine for
|
|
551
|
+
// deployment hours (the picker row says when they open), but here the
|
|
552
|
+
// user pressed Enter on a row that was on screen a second ago and
|
|
553
|
+
// would otherwise land on a different model with no explanation —
|
|
554
|
+
// they lost a race for the wave's last slot.
|
|
555
|
+
if (isFreebuffLimitedOfferModelId(next.requestedModel)) {
|
|
556
|
+
const requested = getFreebuffModel(next.requestedModel).displayName
|
|
557
|
+
const fallback = getFreebuffModel(
|
|
558
|
+
FALLBACK_FREEBUFF_MODEL_ID,
|
|
559
|
+
).displayName
|
|
560
|
+
useChatStore
|
|
561
|
+
.getState()
|
|
562
|
+
.setMessages((prev) => [
|
|
563
|
+
...prev,
|
|
564
|
+
getSystemMessage(
|
|
565
|
+
`${requested}'s trial sessions just ran out, so this session started on ${fallback} instead. Check back later — we release more in batches.`,
|
|
566
|
+
),
|
|
567
|
+
])
|
|
568
|
+
}
|
|
569
|
+
useFreebuffModelStore
|
|
570
|
+
.getState()
|
|
571
|
+
.setSelectedModel(FALLBACK_FREEBUFF_MODEL_ID)
|
|
572
|
+
// The unavailable response came from a POST attempt. Re-POST with
|
|
573
|
+
// the fallback model; a GET would only redisplay the old ended row
|
|
574
|
+
// and leave the restart banner stuck in its pending state.
|
|
575
|
+
nextMethod = 'POST'
|
|
576
|
+
schedule(0)
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Startup takeover: the initial probe GET saw we already hold a seat
|
|
581
|
+
// (from a prior CLI instance). Stop here and ask before POSTing to
|
|
582
|
+
// rotate our instance id; otherwise opening a second freebuff would
|
|
583
|
+
// immediately supersede the first one.
|
|
584
|
+
// `previousStatus === null` fences this to the very first tick only.
|
|
585
|
+
// Pin the selected model to whatever the server thinks we're on so
|
|
586
|
+
// an explicit takeover preserves our queue position instead of
|
|
587
|
+
// switching queues.
|
|
588
|
+
if (
|
|
589
|
+
method === 'GET' &&
|
|
590
|
+
previousStatus === null &&
|
|
591
|
+
next.status === 'active'
|
|
592
|
+
) {
|
|
593
|
+
useFreebuffModelStore.getState().setSelectedModel(next.model)
|
|
594
|
+
// A fast restart after Ctrl+C can observe the old server row before
|
|
595
|
+
// best-effort DELETE lands. If the row belongs to a dead local
|
|
596
|
+
// process, silently do the same POST as the Take over button.
|
|
597
|
+
if (isFreebuffInstanceOwnedByDeadLocalProcess(next.instanceId)) {
|
|
598
|
+
nextMethod = 'POST'
|
|
599
|
+
schedule(0)
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
apply({ status: 'takeover_prompt', model: next.model })
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Bell on admission: the user committed to a model on the landing
|
|
607
|
+
// screen (status 'none'), which POSTs and lands them straight on an
|
|
608
|
+
// active session (admission is immediate).
|
|
609
|
+
if (previousStatus === 'none' && next.status === 'active') {
|
|
610
|
+
playAdmissionSound()
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// active|ended → none means we've passed the server's hard cutoff.
|
|
614
|
+
// Synthesize a no-instanceId ended state so the chat surface stays
|
|
615
|
+
// mounted with the Enter-to-rejoin banner instead of looping back
|
|
616
|
+
// through the landing screen. Carry forward whichever rate-limit
|
|
617
|
+
// snapshot we have — preferring the fresh `none` snapshot, falling
|
|
618
|
+
// back to whatever was on the prior active/ended row — so the
|
|
619
|
+
// banner's "N of M used today" line stays populated.
|
|
620
|
+
if (
|
|
621
|
+
(previousStatus === 'active' || previousStatus === 'ended') &&
|
|
622
|
+
next.status === 'none'
|
|
623
|
+
) {
|
|
624
|
+
const current = useFreebuffSessionStore.getState().session
|
|
625
|
+
const rateLimitsByModel =
|
|
626
|
+
next.rateLimitsByModel ?? getRateLimitsByModel(current)
|
|
627
|
+
apply({
|
|
628
|
+
status: 'ended',
|
|
629
|
+
accessTier:
|
|
630
|
+
next.accessTier ??
|
|
631
|
+
(current && 'accessTier' in current
|
|
632
|
+
? current.accessTier
|
|
633
|
+
: undefined),
|
|
634
|
+
rateLimitsByModel,
|
|
635
|
+
})
|
|
636
|
+
return
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (compact && next.status === 'active') {
|
|
640
|
+
const merged = mergeCompactActiveSession(
|
|
641
|
+
useFreebuffSessionStore.getState().session,
|
|
642
|
+
next,
|
|
643
|
+
)
|
|
644
|
+
needsFullActivePoll = merged === null
|
|
645
|
+
apply(merged ?? next)
|
|
646
|
+
} else {
|
|
647
|
+
needsFullActivePoll = false
|
|
648
|
+
apply(next)
|
|
649
|
+
}
|
|
650
|
+
if (needsFullActivePoll) {
|
|
651
|
+
schedule(0)
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
const delay = nextDelayMs(next)
|
|
655
|
+
if (delay !== null) schedule(delay)
|
|
656
|
+
} catch (err) {
|
|
657
|
+
if (
|
|
658
|
+
cancelled ||
|
|
659
|
+
fetchController.signal.aborted ||
|
|
660
|
+
generation !== restartGeneration
|
|
661
|
+
) {
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
665
|
+
consecutiveFailures++
|
|
666
|
+
const disposition = classifyFreebuffSessionRequestFailure(method, err)
|
|
667
|
+
const shouldRetry = disposition === 'retry'
|
|
668
|
+
const retryAfterMs =
|
|
669
|
+
err instanceof FreebuffSessionRequestError
|
|
670
|
+
? err.retryAfterMs
|
|
671
|
+
: undefined
|
|
672
|
+
const delayMs = shouldRetry
|
|
673
|
+
? failedPollDelayMs({
|
|
674
|
+
consecutiveFailures,
|
|
675
|
+
retryAfterMs,
|
|
676
|
+
})
|
|
677
|
+
: null
|
|
678
|
+
logger.warn(
|
|
679
|
+
{ error: msg, method, consecutiveFailures, delayMs, shouldRetry },
|
|
680
|
+
shouldRetry
|
|
681
|
+
? '[freebuff-session] fetch failed; backing off'
|
|
682
|
+
: '[freebuff-session] fetch failed; automatic retry stopped',
|
|
683
|
+
)
|
|
684
|
+
const retry =
|
|
685
|
+
delayMs === null
|
|
686
|
+
? null
|
|
687
|
+
: {
|
|
688
|
+
attempt: consecutiveFailures + 1,
|
|
689
|
+
retryAtMs: Date.now() + delayMs,
|
|
690
|
+
}
|
|
691
|
+
const failure = {
|
|
692
|
+
message: msg,
|
|
693
|
+
retry,
|
|
694
|
+
outcomeUnknown: disposition === 'unknown',
|
|
695
|
+
}
|
|
696
|
+
if (err instanceof FreebuffSessionRequestError) {
|
|
697
|
+
setFailure({
|
|
698
|
+
...failure,
|
|
699
|
+
type: 'http',
|
|
700
|
+
statusCode: err.statusCode,
|
|
701
|
+
})
|
|
702
|
+
} else if (isFreebuffSessionTimeoutError(err)) {
|
|
703
|
+
setFailure({ ...failure, type: 'timeout' })
|
|
704
|
+
} else {
|
|
705
|
+
setFailure({ ...failure, type: 'other' })
|
|
706
|
+
}
|
|
707
|
+
if (delayMs !== null) schedule(delayMs)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
controller = {
|
|
712
|
+
restart: async (mode) => {
|
|
713
|
+
const generation = ++restartGeneration
|
|
714
|
+
clearTimer()
|
|
715
|
+
// Abort any in-flight fetch so it can't race us and overwrite state.
|
|
716
|
+
abortController.abort()
|
|
717
|
+
abortController = new AbortController()
|
|
718
|
+
// Reset previousStatus so the admission bell still fires after
|
|
719
|
+
// a forced restart, and so the active|ended → none synthesis below
|
|
720
|
+
// doesn't bounce a 'landing' restart straight back to 'ended'.
|
|
721
|
+
previousStatus = null
|
|
722
|
+
needsFullActivePoll = false
|
|
723
|
+
consecutiveFailures = 0
|
|
724
|
+
setFailure(null)
|
|
725
|
+
if (mode === 'landing') {
|
|
726
|
+
nextMethod = 'GET'
|
|
727
|
+
// Land on the picker immediately. We can't go through the normal
|
|
728
|
+
// tick/apply path because a server-side row that hasn't been
|
|
729
|
+
// swept yet would trip the startup-takeover branch into an
|
|
730
|
+
// auto-POST — the exact silent-rejoin this mode exists to
|
|
731
|
+
// prevent. But the picker still needs live quota snapshots, so kick
|
|
732
|
+
// off a fire-and-forget GET and extract only picker metadata from
|
|
733
|
+
// the response, ignoring whatever status it claims. Polling resumes
|
|
734
|
+
// when the user commits to a model via startFreebuffSession.
|
|
735
|
+
const landingSession = toLandingSession(
|
|
736
|
+
useFreebuffSessionStore.getState().session,
|
|
737
|
+
)
|
|
738
|
+
apply(landingSession)
|
|
739
|
+
const fetchController = abortController
|
|
740
|
+
callFreebuffSession('GET', token, {
|
|
741
|
+
signal: fetchController.signal,
|
|
742
|
+
})
|
|
743
|
+
.then((response) => {
|
|
744
|
+
if (
|
|
745
|
+
cancelled ||
|
|
746
|
+
fetchController.signal.aborted ||
|
|
747
|
+
generation !== restartGeneration
|
|
748
|
+
) {
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
if (response.status === 'none') {
|
|
752
|
+
// Preserve cached quota/location fields only when the tier is
|
|
753
|
+
// unchanged (or an older response omitted it). Fresh response
|
|
754
|
+
// fields win through the following object spread.
|
|
755
|
+
const canReuseLandingMetadata =
|
|
756
|
+
response.accessTier === undefined ||
|
|
757
|
+
response.accessTier === landingSession.accessTier
|
|
758
|
+
apply({
|
|
759
|
+
...(canReuseLandingMetadata ? landingSession : {}),
|
|
760
|
+
...response,
|
|
761
|
+
status: 'none',
|
|
762
|
+
accessTier: response.accessTier ?? landingSession.accessTier,
|
|
763
|
+
// A clean `none` response is authoritative for referral
|
|
764
|
+
// state. Do not retain the cached landing value when the
|
|
765
|
+
// server omits it (program disabled / identity removed).
|
|
766
|
+
referral: response.referral,
|
|
767
|
+
})
|
|
768
|
+
}
|
|
769
|
+
})
|
|
770
|
+
.catch(() => {
|
|
771
|
+
// Silent — blank hints are acceptable if the fetch fails.
|
|
772
|
+
})
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
nextMethod = 'POST'
|
|
776
|
+
await tick()
|
|
777
|
+
},
|
|
778
|
+
apply,
|
|
779
|
+
abort: () => {
|
|
780
|
+
clearTimer()
|
|
781
|
+
abortController.abort()
|
|
782
|
+
},
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
tick()
|
|
786
|
+
|
|
787
|
+
return () => {
|
|
788
|
+
cancelled = true
|
|
789
|
+
abortController.abort()
|
|
790
|
+
clearTimer()
|
|
791
|
+
const current = useFreebuffSessionStore.getState().session
|
|
792
|
+
controller = null
|
|
793
|
+
clearReferralCache()
|
|
794
|
+
|
|
795
|
+
// Fire-and-forget DELETE. Only release if we actually held a slot so
|
|
796
|
+
// we don't generate spurious DELETEs (e.g. HMR before POST completes).
|
|
797
|
+
if (holdsLiveFreebuffSlot(current)) {
|
|
798
|
+
callFreebuffSession('DELETE', token).catch(() => {})
|
|
799
|
+
}
|
|
800
|
+
setSession(null)
|
|
801
|
+
setFailure(null)
|
|
802
|
+
}
|
|
803
|
+
}, [])
|
|
804
|
+
|
|
805
|
+
return { session, failure }
|
|
806
|
+
}
|