@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,865 @@
|
|
|
1
|
+
import { TextAttributes } from '@opentui/core'
|
|
2
|
+
import { useKeyboard, useRenderer } from '@opentui/react'
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Button } from './button'
|
|
6
|
+
import { ChoiceAdBanner, AD_CARD_HEIGHT } from './ad-banner'
|
|
7
|
+
import { FreebuffModelSelector } from './freebuff-model-selector'
|
|
8
|
+
import { ShimmerText } from './shimmer-text'
|
|
9
|
+
import {
|
|
10
|
+
refreshFreebuffLandingMetadata,
|
|
11
|
+
takeOverFreebuffSession,
|
|
12
|
+
} from '../hooks/use-freebuff-session'
|
|
13
|
+
import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
|
|
14
|
+
import { useFreebuffStreakQuery } from '../hooks/use-freebuff-streak-query'
|
|
15
|
+
import { useGravityAd } from '../hooks/use-gravity-ad'
|
|
16
|
+
import { useLogo } from '../hooks/use-logo'
|
|
17
|
+
import { useNow } from '../hooks/use-now'
|
|
18
|
+
import { useSheenAnimation } from '../hooks/use-sheen-animation'
|
|
19
|
+
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
|
|
20
|
+
import { useTheme } from '../hooks/use-theme'
|
|
21
|
+
import { exitCliCleanly } from '../utils/exit-cleanly'
|
|
22
|
+
import {
|
|
23
|
+
formatFreebuffPremiumResetCountdown,
|
|
24
|
+
getFreebuffPremiumResetAt,
|
|
25
|
+
} from '../utils/freebuff-premium-reset'
|
|
26
|
+
import {
|
|
27
|
+
FREEBUFF_STREAK_INLINE_GAP,
|
|
28
|
+
FREEBUFF_STREAK_LABEL_GAP,
|
|
29
|
+
fitsFreebuffStreakOnHeadingRow,
|
|
30
|
+
getFreebuffStreakBonusNoteForLayout,
|
|
31
|
+
getFreebuffStreakLine,
|
|
32
|
+
} from '../utils/freebuff-streak-line'
|
|
33
|
+
import { formatSessionUnits } from '../utils/format-session-units'
|
|
34
|
+
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
|
|
35
|
+
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
|
|
36
|
+
import { INVERTED_CTA_FG } from '../utils/ui-constants'
|
|
37
|
+
import {
|
|
38
|
+
FREEBUFF_ENABLE_STREAK_IN_UI,
|
|
39
|
+
FREEBUFF_LIMITED_SESSION_LIMIT,
|
|
40
|
+
FREEBUFF_PREMIUM_SESSION_LIMIT,
|
|
41
|
+
} from '@anvil/common/constants/freebuff-models'
|
|
42
|
+
import {
|
|
43
|
+
getRateLimitsByModel,
|
|
44
|
+
getReferralInfo,
|
|
45
|
+
} from '@anvil/common/types/freebuff-session'
|
|
46
|
+
import { getFreebuffModelAvailabilityNotice } from '@anvil/common/util/freebuff-model-availability'
|
|
47
|
+
import { formatFreebuffHardBlockedPrivacySignals } from '@anvil/common/util/freebuff-privacy'
|
|
48
|
+
|
|
49
|
+
import type { FreebuffStreakLine } from '../utils/freebuff-streak-line'
|
|
50
|
+
import type { FreebuffSessionFailure } from '../state/freebuff-session-store'
|
|
51
|
+
import type { FreebuffSessionResponse } from '../types/freebuff-session'
|
|
52
|
+
import type { KeyEvent } from '@opentui/core'
|
|
53
|
+
|
|
54
|
+
interface FreebuffLandingScreenProps {
|
|
55
|
+
session: FreebuffSessionResponse | null
|
|
56
|
+
failure: FreebuffSessionFailure | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Landing-screen heading. Referenced both as rendered text and by the
|
|
60
|
+
* picker's height-budget math (wrappedRows), so it lives in one place to keep
|
|
61
|
+
* the two from drifting. */
|
|
62
|
+
const LANDING_HEADING = 'Start coding for free'
|
|
63
|
+
const COLLAPSED_LOGO_MIN_HEIGHT = 26
|
|
64
|
+
|
|
65
|
+
/** "in ~3h 20m" / "in ~45 min" / "in under a minute". Used on the
|
|
66
|
+
* rate-limited screen so users know when they can try again. */
|
|
67
|
+
const formatRetryAfter = (ms: number): string => {
|
|
68
|
+
if (!Number.isFinite(ms) || ms <= 0) return 'any moment now'
|
|
69
|
+
const minutes = Math.round(ms / 60_000)
|
|
70
|
+
if (minutes < 1) return 'under a minute'
|
|
71
|
+
if (minutes < 60) return `${minutes} min`
|
|
72
|
+
const hours = Math.floor(minutes / 60)
|
|
73
|
+
const rem = minutes % 60
|
|
74
|
+
return rem === 0 ? `${hours}h` : `${hours}h ${rem}m`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Rendered directly under the model list — that's where "why these models?"
|
|
78
|
+
// gets asked. The copy itself is shared with Freebuff Desktop's model menu; see
|
|
79
|
+
// `getFreebuffModelAvailabilityNotice` for the tone rules it follows.
|
|
80
|
+
const getLimitedModeNotice = (
|
|
81
|
+
session: FreebuffSessionResponse | null,
|
|
82
|
+
): string | null =>
|
|
83
|
+
getFreebuffModelAvailabilityNotice(
|
|
84
|
+
session && 'countryBlockReason' in session ? session : null,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
function getTakeoverErrorMessage(failure: FreebuffSessionFailure): string {
|
|
88
|
+
if (failure.type === 'http' && failure.statusCode === 503) {
|
|
89
|
+
return "Freebuff is busy and couldn't complete the takeover yet."
|
|
90
|
+
}
|
|
91
|
+
if (failure.type === 'timeout') {
|
|
92
|
+
return failure.outcomeUnknown
|
|
93
|
+
? 'The takeover request timed out and may have succeeded. Check the warning, then retry if you still want to take over.'
|
|
94
|
+
: failure.retry
|
|
95
|
+
? 'The takeover request timed out while Freebuff was busy.'
|
|
96
|
+
: 'The takeover request timed out.'
|
|
97
|
+
}
|
|
98
|
+
if (failure.outcomeUnknown) {
|
|
99
|
+
return "Freebuff couldn't confirm whether the takeover succeeded. Check the warning, then retry if you still want to take over."
|
|
100
|
+
}
|
|
101
|
+
return failure.message.trim()
|
|
102
|
+
? `Takeover failed: ${failure.message}`
|
|
103
|
+
: 'The takeover failed unexpectedly.'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const TakeoverPrompt: React.FC<{
|
|
107
|
+
failure: FreebuffSessionFailure | null
|
|
108
|
+
onTakeOver?: () => Promise<void>
|
|
109
|
+
}> = ({ failure, onTakeOver = takeOverFreebuffSession }) => {
|
|
110
|
+
const theme = useTheme()
|
|
111
|
+
const [pending, setPending] = useState(false)
|
|
112
|
+
const [focusedIndex, setFocusedIndex] = useState(0) // 0 = Take over, 1 = Exit
|
|
113
|
+
const takeoverInFlightRef = useRef(false)
|
|
114
|
+
const retry = failure?.retry ?? null
|
|
115
|
+
const retryTick = useNow(1_000, retry !== null)
|
|
116
|
+
// `useNow` freezes while disabled. Use the current time when a retry first
|
|
117
|
+
// appears so a long-open prompt cannot render a stale countdown for a frame.
|
|
118
|
+
const retryNow = retry ? Math.max(retryTick, Date.now()) : retryTick
|
|
119
|
+
const retrySeconds = retry
|
|
120
|
+
? Math.max(0, Math.ceil((retry.retryAtMs - retryNow) / 1_000))
|
|
121
|
+
: 0
|
|
122
|
+
const outcomeUnknown = failure?.outcomeUnknown ?? false
|
|
123
|
+
const blocked = pending
|
|
124
|
+
const displayError = failure ? getTakeoverErrorMessage(failure) : null
|
|
125
|
+
|
|
126
|
+
const handleTakeover = useCallback(async () => {
|
|
127
|
+
// `pending` updates on the next render. The ref closes the gap where two
|
|
128
|
+
// keyboard/mouse events arrive in the same frame and would both POST.
|
|
129
|
+
if (takeoverInFlightRef.current) return
|
|
130
|
+
takeoverInFlightRef.current = true
|
|
131
|
+
setPending(true)
|
|
132
|
+
try {
|
|
133
|
+
await onTakeOver()
|
|
134
|
+
} finally {
|
|
135
|
+
takeoverInFlightRef.current = false
|
|
136
|
+
setPending(false)
|
|
137
|
+
}
|
|
138
|
+
}, [onTakeOver])
|
|
139
|
+
|
|
140
|
+
useKeyboard(
|
|
141
|
+
useCallback(
|
|
142
|
+
(key: KeyEvent) => {
|
|
143
|
+
const name = key.name ?? ''
|
|
144
|
+
const isConfirm = isPlainEnterKey(key)
|
|
145
|
+
const isExit = name === 'escape' || name === 'esc'
|
|
146
|
+
const isTab = name === 'tab'
|
|
147
|
+
const isShiftTab = key.shift === true && isTab
|
|
148
|
+
const isRight = name === 'right'
|
|
149
|
+
const isLeft = name === 'left'
|
|
150
|
+
|
|
151
|
+
if (isExit) {
|
|
152
|
+
key.preventDefault?.()
|
|
153
|
+
void exitCliCleanly()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isConfirm) {
|
|
158
|
+
key.preventDefault?.()
|
|
159
|
+
if (focusedIndex === 0) {
|
|
160
|
+
void handleTakeover()
|
|
161
|
+
} else {
|
|
162
|
+
void exitCliCleanly()
|
|
163
|
+
}
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (isRight || isTab) {
|
|
168
|
+
key.preventDefault?.()
|
|
169
|
+
setFocusedIndex((prev) => (prev + 1) % 2)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (isLeft || isShiftTab) {
|
|
174
|
+
key.preventDefault?.()
|
|
175
|
+
setFocusedIndex((prev) => (prev - 1 + 2) % 2)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[focusedIndex, handleTakeover],
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const isTakeoverFocused = focusedIndex === 0
|
|
184
|
+
const isExitFocused = focusedIndex === 1
|
|
185
|
+
const takeoverLabel = pending
|
|
186
|
+
? 'Taking over...'
|
|
187
|
+
: outcomeUnknown
|
|
188
|
+
? 'Try takeover again'
|
|
189
|
+
: retry
|
|
190
|
+
? 'Retry now'
|
|
191
|
+
: 'Take over'
|
|
192
|
+
const takeoverForeground = blocked
|
|
193
|
+
? theme.muted
|
|
194
|
+
: isTakeoverFocused
|
|
195
|
+
? INVERTED_CTA_FG
|
|
196
|
+
: theme.foreground
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<box
|
|
200
|
+
style={{
|
|
201
|
+
flexDirection: 'column',
|
|
202
|
+
alignItems: 'center',
|
|
203
|
+
gap: 1,
|
|
204
|
+
width: '100%',
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<text style={{ fg: theme.foreground }} attributes={TextAttributes.BOLD}>
|
|
208
|
+
Freebuff is already running
|
|
209
|
+
</text>
|
|
210
|
+
|
|
211
|
+
<text style={{ fg: theme.muted }}>
|
|
212
|
+
Only one freebuff instance is allowed at a time.
|
|
213
|
+
</text>
|
|
214
|
+
|
|
215
|
+
{displayError && (
|
|
216
|
+
<text style={{ fg: theme.secondary, wrapMode: 'word' }}>
|
|
217
|
+
⚠ {displayError}
|
|
218
|
+
</text>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{retry && (
|
|
222
|
+
<text style={{ fg: theme.muted }}>
|
|
223
|
+
{retrySeconds > 0
|
|
224
|
+
? `Retrying automatically in ${retrySeconds}s (attempt ${retry.attempt}).`
|
|
225
|
+
: `Retrying automatically now (attempt ${retry.attempt}).`}
|
|
226
|
+
</text>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
<box style={{ flexDirection: 'row', gap: 2, marginTop: 1 }}>
|
|
230
|
+
<Button
|
|
231
|
+
onClick={blocked ? undefined : handleTakeover}
|
|
232
|
+
onMouseOver={() => setFocusedIndex(0)}
|
|
233
|
+
style={{ paddingLeft: 1, paddingRight: 1 }}
|
|
234
|
+
border={['top', 'bottom', 'left', 'right']}
|
|
235
|
+
borderStyle="single"
|
|
236
|
+
borderColor={blocked ? theme.muted : theme.primary}
|
|
237
|
+
>
|
|
238
|
+
<text
|
|
239
|
+
style={{
|
|
240
|
+
// theme.background is 'transparent' and can't serve as inverted
|
|
241
|
+
// text — on the green fill it renders the label invisible.
|
|
242
|
+
fg: takeoverForeground,
|
|
243
|
+
bg: isTakeoverFocused && !blocked ? theme.primary : undefined,
|
|
244
|
+
}}
|
|
245
|
+
attributes={TextAttributes.BOLD}
|
|
246
|
+
>
|
|
247
|
+
{takeoverLabel}
|
|
248
|
+
</text>
|
|
249
|
+
</Button>
|
|
250
|
+
<Button
|
|
251
|
+
onClick={() => exitCliCleanly()}
|
|
252
|
+
onMouseOver={() => setFocusedIndex(1)}
|
|
253
|
+
style={{ paddingLeft: 1, paddingRight: 1 }}
|
|
254
|
+
border={['top', 'bottom', 'left', 'right']}
|
|
255
|
+
borderStyle="single"
|
|
256
|
+
borderColor={isExitFocused ? theme.foreground : theme.muted}
|
|
257
|
+
>
|
|
258
|
+
<text
|
|
259
|
+
style={{ fg: isExitFocused ? theme.foreground : theme.muted }}
|
|
260
|
+
attributes={
|
|
261
|
+
isExitFocused ? TextAttributes.BOLD : TextAttributes.NONE
|
|
262
|
+
}
|
|
263
|
+
>
|
|
264
|
+
Exit
|
|
265
|
+
</text>
|
|
266
|
+
</Button>
|
|
267
|
+
</box>
|
|
268
|
+
</box>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** "N day streak" then its progress dots, as spans so both placements (beside
|
|
273
|
+
* the heading, or its own line under it) draw the streak identically. */
|
|
274
|
+
const streakSpans = (
|
|
275
|
+
line: FreebuffStreakLine,
|
|
276
|
+
theme: ReturnType<typeof useTheme>,
|
|
277
|
+
) => [
|
|
278
|
+
<span key="label" fg={theme.foreground}>
|
|
279
|
+
{line.label}
|
|
280
|
+
</span>,
|
|
281
|
+
<span key="dots" fg={theme.primary}>
|
|
282
|
+
{`${' '.repeat(FREEBUFF_STREAK_LABEL_GAP)}${line.dots}`}
|
|
283
|
+
</span>,
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
/** Streak on its own line under the heading, for terminals too narrow to
|
|
287
|
+
* share the row. For streak === 0 the line is rendered blank so new / lapsed
|
|
288
|
+
* users are nudged to start using the product rather than shown an empty
|
|
289
|
+
* streak (and so the picker doesn't jump once they earn their first day). */
|
|
290
|
+
const StreakInlineLine: React.FC<{
|
|
291
|
+
line: FreebuffStreakLine | null
|
|
292
|
+
}> = ({ line }) => {
|
|
293
|
+
const theme = useTheme()
|
|
294
|
+
|
|
295
|
+
if (!line) {
|
|
296
|
+
return <text style={{ flexShrink: 0 }}> </text>
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<text style={{ flexShrink: 0, wrapMode: 'none' }}>
|
|
301
|
+
{streakSpans(line, theme)}
|
|
302
|
+
</text>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Heading row: the title, with the streak beside it when the caller passes
|
|
307
|
+
* one (only when it fits — see fitsFreebuffStreakOnHeadingRow).
|
|
308
|
+
*
|
|
309
|
+
* Deliberately ONE text rather than a space-between flex row. The row this
|
|
310
|
+
* replaces put the two in a stretched row inside a shrink-to-fit column,
|
|
311
|
+
* which had two problems: with nothing wider on screen there was no free
|
|
312
|
+
* space to distribute, so the two rendered flush ("Start coding for
|
|
313
|
+
* free18 day streak"), and a space-between row whose content overflows its
|
|
314
|
+
* container segfaults the renderer — reachable by dragging the terminal
|
|
315
|
+
* narrower, since the native layout runs against the old tree before React
|
|
316
|
+
* re-renders. A single text just wraps, and the gap is explicit.
|
|
317
|
+
*
|
|
318
|
+
* Exported for the layout test. */
|
|
319
|
+
export const LandingHeadingRow: React.FC<{
|
|
320
|
+
streakLine: FreebuffStreakLine | null
|
|
321
|
+
marginBottom: number
|
|
322
|
+
}> = ({ streakLine, marginBottom }) => {
|
|
323
|
+
const theme = useTheme()
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<text style={{ marginBottom, wrapMode: 'word' }}>
|
|
327
|
+
<span fg={theme.foreground} attributes={TextAttributes.BOLD}>
|
|
328
|
+
{LANDING_HEADING}
|
|
329
|
+
</span>
|
|
330
|
+
{streakLine && [
|
|
331
|
+
<span key="gap">{' '.repeat(FREEBUFF_STREAK_INLINE_GAP)}</span>,
|
|
332
|
+
...streakSpans(streakLine, theme),
|
|
333
|
+
]}
|
|
334
|
+
</text>
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export const FreebuffLandingScreen: React.FC<FreebuffLandingScreenProps> = ({
|
|
339
|
+
session,
|
|
340
|
+
failure,
|
|
341
|
+
}) => {
|
|
342
|
+
const theme = useTheme()
|
|
343
|
+
const renderer = useRenderer()
|
|
344
|
+
const { terminalWidth, terminalHeight, contentMaxWidth } =
|
|
345
|
+
useTerminalDimensions()
|
|
346
|
+
|
|
347
|
+
// Progressive disclosure as the terminal gets shorter. The picker is the
|
|
348
|
+
// only thing the user must be able to reach, so chrome is shed first:
|
|
349
|
+
// tall (>=40): full 6-line ASCII logo + roomy spacing, content anchored low
|
|
350
|
+
// shorter : no logo — the heading already identifies the experience
|
|
351
|
+
// tiny (<18) : also drop the ad banner
|
|
352
|
+
// The big logo is reserved for genuinely tall windows; at the common ~30-row
|
|
353
|
+
// height we hide the branding so more models fit without scrolling.
|
|
354
|
+
// Section headers always show — the picker scrolls within whatever rows
|
|
355
|
+
// remain (see selectorMaxHeight below), so there's no need to hide them.
|
|
356
|
+
//
|
|
357
|
+
// Exception: when the picker is collapsed it shrinks to ~5 rows, freeing the
|
|
358
|
+
// ~6 rows the big logo needs. So on a mid-height window with a collapsed,
|
|
359
|
+
// referral-free picker we still show the full ASCII logo — it fills what
|
|
360
|
+
// would otherwise be dead space above the card. A referral card or expanded
|
|
361
|
+
// list gives those rows back to the scrollable menu. 26 is the smallest
|
|
362
|
+
// window where the logo block,
|
|
363
|
+
// heading, collapsed picker, streak, and ad all coexist without scrolling.
|
|
364
|
+
//
|
|
365
|
+
// The picker (rendered below) owns this and reports it via onExpandedChange;
|
|
366
|
+
// we default to collapsed so the first paint reserves logo space correctly.
|
|
367
|
+
const [selectorExpanded, setSelectorExpanded] = useState(false)
|
|
368
|
+
const hasReferralMenu =
|
|
369
|
+
session?.status === 'none' && Boolean(getReferralInfo(session))
|
|
370
|
+
const logoHeightFits =
|
|
371
|
+
terminalHeight >= 40 ||
|
|
372
|
+
(!selectorExpanded &&
|
|
373
|
+
!hasReferralMenu &&
|
|
374
|
+
terminalHeight >= COLLAPSED_LOGO_MIN_HEIGHT)
|
|
375
|
+
const compact = terminalHeight < 22
|
|
376
|
+
const showAds = terminalHeight >= 18
|
|
377
|
+
const textMarginBottom = 1
|
|
378
|
+
|
|
379
|
+
const [sheenPosition, setSheenPosition] = useState(0)
|
|
380
|
+
const blockColor = getLogoBlockColor(theme.name)
|
|
381
|
+
const accentColor = getLogoAccentColor(theme.name)
|
|
382
|
+
const { applySheenToChar } = useSheenAnimation({
|
|
383
|
+
logoColor: theme.foreground,
|
|
384
|
+
accentColor,
|
|
385
|
+
blockColor,
|
|
386
|
+
terminalWidth: renderer?.width ?? terminalWidth,
|
|
387
|
+
sheenPosition,
|
|
388
|
+
setSheenPosition,
|
|
389
|
+
})
|
|
390
|
+
const { component: logoComponent, textBlock: logoTextBlock } = useLogo({
|
|
391
|
+
availableWidth: contentMaxWidth,
|
|
392
|
+
accentColor,
|
|
393
|
+
blockColor,
|
|
394
|
+
applySheenToChar,
|
|
395
|
+
})
|
|
396
|
+
// useLogo falls back to a one-line brand label when the ASCII variants do
|
|
397
|
+
// not fit horizontally. This landing screen intentionally hides that label.
|
|
398
|
+
const showFullLogo = logoHeightFits && logoTextBlock.length > 0
|
|
399
|
+
|
|
400
|
+
// Always enable ads on the landing screen — this is where monetization lives.
|
|
401
|
+
// forceStart bypasses the "wait for first user message" gate inside the hook,
|
|
402
|
+
// which would otherwise block ads here since no conversation exists yet.
|
|
403
|
+
// The server tries Gravity first, then falls back to ZeroClick and Carbon.
|
|
404
|
+
const { ads, recordClick, recordImpression } = useGravityAd({
|
|
405
|
+
enabled: true,
|
|
406
|
+
forceStart: true,
|
|
407
|
+
provider: 'gravity',
|
|
408
|
+
// Legacy wire name for this surface — the ads API maps it to placements,
|
|
409
|
+
// so it must not change with the component rename.
|
|
410
|
+
surface: 'waiting_room',
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
useFreebuffCtrlCExit()
|
|
414
|
+
|
|
415
|
+
const [exitHover, setExitHover] = useState(false)
|
|
416
|
+
|
|
417
|
+
const accessTier =
|
|
418
|
+
session && 'accessTier' in session ? session.accessTier : 'full'
|
|
419
|
+
// Hidden in compact terminals: the notice is nice-to-have context, and
|
|
420
|
+
// below 22 rows every line competes with the picker itself.
|
|
421
|
+
const limitedModeNotice =
|
|
422
|
+
accessTier === 'limited' && !compact ? getLimitedModeNotice(session) : null
|
|
423
|
+
// 'none' = user hasn't started a session yet. We're in the pre-chat landing
|
|
424
|
+
// state: show the picker with a prompt. Picking a model triggers
|
|
425
|
+
// startFreebuffSession, which POSTs and transitions straight to 'active' (chat).
|
|
426
|
+
const isLanding = session?.status === 'none'
|
|
427
|
+
const streakQuery = useFreebuffStreakQuery({
|
|
428
|
+
enabled: FREEBUFF_ENABLE_STREAK_IN_UI && isLanding,
|
|
429
|
+
})
|
|
430
|
+
const streak = streakQuery.data?.streak ?? 0
|
|
431
|
+
// The indicator normally shares the heading row, so keep its slot even in
|
|
432
|
+
// compact-height terminals. It renders blank space until a streak is active.
|
|
433
|
+
const showStreakIndicator = FREEBUFF_ENABLE_STREAK_IN_UI && isLanding
|
|
434
|
+
// Once a full week is earned, explain the recurring perk under the picker so
|
|
435
|
+
// the streak reads as worth keeping. Pre-milestone countdown copy stays
|
|
436
|
+
// hidden; this dedicated row is only for a bonus the user has already earned.
|
|
437
|
+
const streakBonusNote = showStreakIndicator
|
|
438
|
+
? getFreebuffStreakBonusNoteForLayout({
|
|
439
|
+
streak,
|
|
440
|
+
accessTier: accessTier === 'limited' ? 'limited' : 'full',
|
|
441
|
+
terminalHeight,
|
|
442
|
+
availableWidth: contentMaxWidth,
|
|
443
|
+
})
|
|
444
|
+
: null
|
|
445
|
+
// On the landing screen the streak rides on the heading row, a fixed gap
|
|
446
|
+
// after the title. Measured against the real strings (the label grows with
|
|
447
|
+
// the day count), so
|
|
448
|
+
// a streak that can't clear the inline gap drops to its own line under the
|
|
449
|
+
// heading instead of colliding with it. With no streak yet, the day-one line
|
|
450
|
+
// stands in, reserving the slot where the streak will land.
|
|
451
|
+
const streakLine = showStreakIndicator ? getFreebuffStreakLine(streak) : null
|
|
452
|
+
const streakOnHeadingRow =
|
|
453
|
+
showStreakIndicator &&
|
|
454
|
+
fitsFreebuffStreakOnHeadingRow({
|
|
455
|
+
line: streakLine,
|
|
456
|
+
headingWidth: LANDING_HEADING.length,
|
|
457
|
+
availableWidth: contentMaxWidth,
|
|
458
|
+
})
|
|
459
|
+
// On the landing picker we tick once a minute so the session reset countdown
|
|
460
|
+
// stays fresh.
|
|
461
|
+
const now = useNow(60_000, isLanding)
|
|
462
|
+
|
|
463
|
+
// Free-session quota counter for the title line. All free models share one
|
|
464
|
+
// pool; the server replicates the same snapshot under each free model
|
|
465
|
+
// id, so any entry has the right count. Renders amber when exhausted so
|
|
466
|
+
// the limit reads as "you've hit it" rather than just another count.
|
|
467
|
+
const rateLimitsByModel = getRateLimitsByModel(session)
|
|
468
|
+
const sessionRateLimit = rateLimitsByModel
|
|
469
|
+
? Object.values(rateLimitsByModel)[0]
|
|
470
|
+
: undefined
|
|
471
|
+
const sharedSessionUsed = sessionRateLimit?.recentCount ?? 0
|
|
472
|
+
// Hide the "0 of N … used" line entirely for a fresh user — a zeroed counter
|
|
473
|
+
// is noise on the landing screen. It appears once any session is consumed.
|
|
474
|
+
//
|
|
475
|
+
// For the regular tiers the PREMIUM section header inside the expanded
|
|
476
|
+
// picker carries this quota inline, so the below-picker line survives for
|
|
477
|
+
// the limited tier (which has no premium section to host it) and for the
|
|
478
|
+
// collapsed picker. When the collapsed recommended hero is a premium model
|
|
479
|
+
// (getRecommendedFreebuffModelId, while the pool has sessions left) the count
|
|
480
|
+
// is exactly what Enter is about to spend.
|
|
481
|
+
const showSessionCounter = sharedSessionUsed > 0
|
|
482
|
+
const showBelowPickerCounter =
|
|
483
|
+
showSessionCounter && (accessTier === 'limited' || !selectorExpanded)
|
|
484
|
+
// Prefer the server-sent limit (base + streak/referral bonuses) so the
|
|
485
|
+
// counter and its amber exhausted cue match what admission will enforce;
|
|
486
|
+
// the static constants only cover the pre-snapshot fallback.
|
|
487
|
+
const sessionLimit =
|
|
488
|
+
sessionRateLimit?.limit ??
|
|
489
|
+
(accessTier === 'limited'
|
|
490
|
+
? FREEBUFF_LIMITED_SESSION_LIMIT
|
|
491
|
+
: FREEBUFF_PREMIUM_SESSION_LIMIT)
|
|
492
|
+
const isSessionExhausted = sharedSessionUsed >= sessionLimit
|
|
493
|
+
const sessionUsedColor = isSessionExhausted ? theme.secondary : theme.muted
|
|
494
|
+
const sessionLabel =
|
|
495
|
+
accessTier === 'limited' ? 'sessions' : 'premium sessions'
|
|
496
|
+
const formattedSharedSessionUsed = formatSessionUnits(sharedSessionUsed)
|
|
497
|
+
const sessionResetAt = getFreebuffPremiumResetAt({
|
|
498
|
+
rateLimitsByModel,
|
|
499
|
+
nowMs: now,
|
|
500
|
+
})
|
|
501
|
+
const sessionResetAtMs = sessionResetAt.getTime()
|
|
502
|
+
const sessionResetCountdown = formatFreebuffPremiumResetCountdown(
|
|
503
|
+
sessionResetAt,
|
|
504
|
+
now,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
// Rows the picker may occupy = terminal height minus the fixed chrome
|
|
508
|
+
// around it. Each term mirrors the real layout exactly (no padded
|
|
509
|
+
// estimate, no blanket safety row) so the scrollbox fills the available
|
|
510
|
+
// space with no dead band below it:
|
|
511
|
+
// - top bar: paddingTop 1 + the ✕ row = 2
|
|
512
|
+
// - ad banner: AD_CARD_HEIGHT, only when shown
|
|
513
|
+
// - main box: paddingBottom 1
|
|
514
|
+
// - logo block: lines + marginBottom 1 (always, when shown) + gap (full)
|
|
515
|
+
// - the prompt/counter (landing)
|
|
516
|
+
// Line wrapping is derived from the actual strings vs contentMaxWidth, so
|
|
517
|
+
// a wrapped counter is accounted for precisely instead of guessed at.
|
|
518
|
+
const wrappedRows = (text: string) =>
|
|
519
|
+
Math.max(1, Math.ceil(text.length / contentMaxWidth))
|
|
520
|
+
const logoBlockRows = showFullLogo
|
|
521
|
+
? 8 /* 6 logo lines + marginBottom + gap */
|
|
522
|
+
: 0
|
|
523
|
+
const adRows = showAds ? AD_CARD_HEIGHT : 0
|
|
524
|
+
// Status lines render below the picker, each with marginTop 1: the
|
|
525
|
+
// limited-mode notice, then the streak. They still eat into the picker's
|
|
526
|
+
// height budget regardless of being above or below it. (The session counter
|
|
527
|
+
// used to sit here too; it now rides inside the selector — see below.)
|
|
528
|
+
// Placement varies: on a wide landing screen the streak shares
|
|
529
|
+
// the heading row (0 extra rows, already counted in landingTextRows); on a
|
|
530
|
+
// narrow landing screen it drops to its own line under the heading (1 row,
|
|
531
|
+
// no top margin).
|
|
532
|
+
const streakRows = !showStreakIndicator ? 0 : streakOnHeadingRow ? 0 : 1
|
|
533
|
+
const noticeRows = limitedModeNotice
|
|
534
|
+
? 1 /* marginTop */ + wrappedRows(limitedModeNotice)
|
|
535
|
+
: 0
|
|
536
|
+
// Earned streak perk note: one marginTop row + wrap.
|
|
537
|
+
const streakBonusRows = streakBonusNote
|
|
538
|
+
? 1 /* marginTop */ + wrappedRows(streakBonusNote)
|
|
539
|
+
: 0
|
|
540
|
+
// The referral/GLM card and the session counter both live inside the model
|
|
541
|
+
// selector's scrollbox now (the counter rides `belowToggle`, between the
|
|
542
|
+
// expand toggle and the referral pitch), so only genuinely fixed status
|
|
543
|
+
// lines below the selector reduce its viewport. Anything inside the
|
|
544
|
+
// scrollbox is measured by the selector itself and must NOT be reserved
|
|
545
|
+
// here as well, or the viewport shrinks while the content grows.
|
|
546
|
+
const belowPickerRows = streakRows + noticeRows + streakBonusRows
|
|
547
|
+
const reservedChrome = 2 + adRows + 1 /* main paddingBottom */ + logoBlockRows
|
|
548
|
+
const landingTextRows =
|
|
549
|
+
wrappedRows(LANDING_HEADING) + textMarginBottom + belowPickerRows
|
|
550
|
+
// Floor = one whole recommended card: 2 border rows + its 2 text lines (name
|
|
551
|
+
// + tagline, then the AI-training warning on its own line). Rows grew from
|
|
552
|
+
// one text line to two when the warning stopped inlining, so the old floor of
|
|
553
|
+
// 3 left the card's bottom border clipped on a very short terminal. (The
|
|
554
|
+
// warning itself stays visible either way — scrollTop starts at 0, so it is
|
|
555
|
+
// the last row that gets cut, not the first.)
|
|
556
|
+
const MIN_SELECTOR_ROWS = 4
|
|
557
|
+
const selectorMaxHeight = Math.max(
|
|
558
|
+
MIN_SELECTOR_ROWS,
|
|
559
|
+
terminalHeight - reservedChrome - landingTextRows,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
if (!isLanding || !sessionRateLimit) return
|
|
564
|
+
|
|
565
|
+
const delayMs = Math.max(0, sessionResetAtMs - Date.now() + 1_000)
|
|
566
|
+
const timer = setTimeout(() => {
|
|
567
|
+
refreshFreebuffLandingMetadata().catch(() => {})
|
|
568
|
+
}, delayMs)
|
|
569
|
+
|
|
570
|
+
return () => clearTimeout(timer)
|
|
571
|
+
}, [isLanding, sessionRateLimit, sessionResetAtMs])
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
<box
|
|
575
|
+
style={{
|
|
576
|
+
width: '100%',
|
|
577
|
+
height: '100%',
|
|
578
|
+
flexDirection: 'column',
|
|
579
|
+
backgroundColor: theme.background,
|
|
580
|
+
}}
|
|
581
|
+
>
|
|
582
|
+
{/* Top-right exit affordance so mouse users have a clear way out even
|
|
583
|
+
when they don't know Ctrl+C works. width: '100%' is required for
|
|
584
|
+
justifyContent to actually push the X to the right. */}
|
|
585
|
+
<box
|
|
586
|
+
style={{
|
|
587
|
+
width: '100%',
|
|
588
|
+
flexDirection: 'row',
|
|
589
|
+
justifyContent: 'space-between',
|
|
590
|
+
paddingTop: 1,
|
|
591
|
+
paddingLeft: 2,
|
|
592
|
+
paddingRight: 2,
|
|
593
|
+
flexShrink: 0,
|
|
594
|
+
}}
|
|
595
|
+
>
|
|
596
|
+
{/* Empty spacer: justifyContent space-between needs a left sibling to
|
|
597
|
+
keep the ✕ pushed to the right. */}
|
|
598
|
+
<box />
|
|
599
|
+
<Button
|
|
600
|
+
onClick={() => exitCliCleanly()}
|
|
601
|
+
onMouseOver={() => setExitHover(true)}
|
|
602
|
+
onMouseOut={() => setExitHover(false)}
|
|
603
|
+
style={{ paddingLeft: 1, paddingRight: 1 }}
|
|
604
|
+
>
|
|
605
|
+
<text
|
|
606
|
+
style={{ fg: exitHover ? theme.foreground : theme.muted }}
|
|
607
|
+
attributes={TextAttributes.BOLD}
|
|
608
|
+
>
|
|
609
|
+
✕
|
|
610
|
+
</text>
|
|
611
|
+
</Button>
|
|
612
|
+
</box>
|
|
613
|
+
|
|
614
|
+
<box
|
|
615
|
+
style={{
|
|
616
|
+
flexGrow: 1,
|
|
617
|
+
flexDirection: 'column',
|
|
618
|
+
alignItems: 'center',
|
|
619
|
+
// Full logo: anchor the clump low (flex-end), matching how chat pins
|
|
620
|
+
// its header/messages to the input bar. Without a logo, hug the top
|
|
621
|
+
// so the freed rows remain available to the picker.
|
|
622
|
+
justifyContent: showFullLogo ? 'flex-end' : 'flex-start',
|
|
623
|
+
paddingLeft: 2,
|
|
624
|
+
paddingRight: 2,
|
|
625
|
+
paddingBottom: 1,
|
|
626
|
+
gap: showFullLogo ? 1 : 0,
|
|
627
|
+
}}
|
|
628
|
+
>
|
|
629
|
+
{showFullLogo && (
|
|
630
|
+
<box style={{ marginBottom: 1, flexShrink: 0 }}>{logoComponent}</box>
|
|
631
|
+
)}
|
|
632
|
+
|
|
633
|
+
<box
|
|
634
|
+
style={{
|
|
635
|
+
flexDirection: 'column',
|
|
636
|
+
alignItems: 'center',
|
|
637
|
+
gap: 0,
|
|
638
|
+
maxWidth: contentMaxWidth,
|
|
639
|
+
}}
|
|
640
|
+
>
|
|
641
|
+
{failure && (!session || session.status === 'none') && (
|
|
642
|
+
<text style={{ fg: theme.secondary, wrapMode: 'word' }}>
|
|
643
|
+
⚠ {failure.message}
|
|
644
|
+
</text>
|
|
645
|
+
)}
|
|
646
|
+
|
|
647
|
+
{!session && !failure && (
|
|
648
|
+
<text style={{ fg: theme.muted }}>
|
|
649
|
+
<ShimmerText text="Connecting…" />
|
|
650
|
+
</text>
|
|
651
|
+
)}
|
|
652
|
+
|
|
653
|
+
{isLanding && (
|
|
654
|
+
<box
|
|
655
|
+
style={{
|
|
656
|
+
flexDirection: 'column',
|
|
657
|
+
alignItems: 'flex-start',
|
|
658
|
+
gap: 0,
|
|
659
|
+
}}
|
|
660
|
+
>
|
|
661
|
+
<LandingHeadingRow
|
|
662
|
+
streakLine={streakOnHeadingRow ? streakLine : null}
|
|
663
|
+
marginBottom={textMarginBottom}
|
|
664
|
+
/>
|
|
665
|
+
{showStreakIndicator && !streakOnHeadingRow && (
|
|
666
|
+
<StreakInlineLine line={streakLine} />
|
|
667
|
+
)}
|
|
668
|
+
<FreebuffModelSelector
|
|
669
|
+
maxHeight={selectorMaxHeight}
|
|
670
|
+
onExpandedChange={setSelectorExpanded}
|
|
671
|
+
belowToggle={
|
|
672
|
+
showBelowPickerCounter ? (
|
|
673
|
+
<text
|
|
674
|
+
style={{
|
|
675
|
+
fg: theme.muted,
|
|
676
|
+
marginTop: 1,
|
|
677
|
+
wrapMode: 'word',
|
|
678
|
+
}}
|
|
679
|
+
>
|
|
680
|
+
<span fg={sessionUsedColor}>
|
|
681
|
+
{formattedSharedSessionUsed} of {sessionLimit}{' '}
|
|
682
|
+
{sessionLabel} used
|
|
683
|
+
</span>
|
|
684
|
+
<span fg={theme.muted}>
|
|
685
|
+
{', '}
|
|
686
|
+
resets in {sessionResetCountdown}
|
|
687
|
+
</span>
|
|
688
|
+
</text>
|
|
689
|
+
) : null
|
|
690
|
+
}
|
|
691
|
+
/>
|
|
692
|
+
{limitedModeNotice && (
|
|
693
|
+
<text
|
|
694
|
+
style={{ fg: theme.muted, wrapMode: 'word', marginTop: 1 }}
|
|
695
|
+
>
|
|
696
|
+
{limitedModeNotice}
|
|
697
|
+
</text>
|
|
698
|
+
)}
|
|
699
|
+
{streakBonusNote && (
|
|
700
|
+
<text
|
|
701
|
+
style={{ fg: theme.primary, wrapMode: 'word', marginTop: 1 }}
|
|
702
|
+
>
|
|
703
|
+
{streakBonusNote}
|
|
704
|
+
</text>
|
|
705
|
+
)}
|
|
706
|
+
</box>
|
|
707
|
+
)}
|
|
708
|
+
|
|
709
|
+
{session?.status === 'takeover_prompt' && (
|
|
710
|
+
<TakeoverPrompt failure={failure} />
|
|
711
|
+
)}
|
|
712
|
+
|
|
713
|
+
{/* Country outside the free-mode allowlist. Terminal — polling has
|
|
714
|
+
stopped. Tell the user up front rather than letting them send a
|
|
715
|
+
request that the chat/completions gate would reject. */}
|
|
716
|
+
{session?.status === 'country_blocked' && (
|
|
717
|
+
<>
|
|
718
|
+
<text style={{ fg: theme.secondary, marginBottom: 1 }}>
|
|
719
|
+
⚠ Free mode isn't available in your region
|
|
720
|
+
</text>
|
|
721
|
+
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
|
|
722
|
+
{session.countryBlockReason === 'anonymous_network' ? (
|
|
723
|
+
<>
|
|
724
|
+
We detected{' '}
|
|
725
|
+
{formatFreebuffHardBlockedPrivacySignals(
|
|
726
|
+
session.ipPrivacySignals,
|
|
727
|
+
)}{' '}
|
|
728
|
+
traffic
|
|
729
|
+
{session.countryCode === 'UNKNOWN' ? (
|
|
730
|
+
''
|
|
731
|
+
) : (
|
|
732
|
+
<>
|
|
733
|
+
{' '}
|
|
734
|
+
from{' '}
|
|
735
|
+
<span fg={theme.foreground}>{session.countryCode}</span>
|
|
736
|
+
</>
|
|
737
|
+
)}
|
|
738
|
+
. Freebuff can't be used from VPN, proxy, or Tor traffic.
|
|
739
|
+
Disable it and restart Freebuff to try again.
|
|
740
|
+
</>
|
|
741
|
+
) : session.countryCode === 'UNKNOWN' ? (
|
|
742
|
+
<>
|
|
743
|
+
We couldn't verify an eligible location for this request.
|
|
744
|
+
VPN, Tor, proxy, or unknown-location traffic can't use
|
|
745
|
+
freebuff. Press Ctrl+C to exit.
|
|
746
|
+
</>
|
|
747
|
+
) : (
|
|
748
|
+
<>
|
|
749
|
+
We detected your location as{' '}
|
|
750
|
+
<span fg={theme.foreground}>{session.countryCode}</span>,
|
|
751
|
+
which is outside the countries where freebuff is currently
|
|
752
|
+
offered. Press Ctrl+C to exit.
|
|
753
|
+
</>
|
|
754
|
+
)}
|
|
755
|
+
</text>
|
|
756
|
+
</>
|
|
757
|
+
)}
|
|
758
|
+
|
|
759
|
+
{/* Account banned. Terminal — polling has stopped. Blocking here
|
|
760
|
+
stops banned bots from re-entering free mode. */}
|
|
761
|
+
{session?.status === 'banned' && (
|
|
762
|
+
<>
|
|
763
|
+
<text style={{ fg: theme.secondary, marginBottom: 1 }}>
|
|
764
|
+
⚠ Account unavailable
|
|
765
|
+
</text>
|
|
766
|
+
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
|
|
767
|
+
This account has been suspended and can't use freebuff. If you
|
|
768
|
+
think this is a mistake, contact support@codebuff.com. Press
|
|
769
|
+
Ctrl+C to exit.
|
|
770
|
+
</text>
|
|
771
|
+
</>
|
|
772
|
+
)}
|
|
773
|
+
|
|
774
|
+
{/* Shared free-session quota exhausted. Terminal for this run —
|
|
775
|
+
the user can exit and return after its daily or weekly reset. */}
|
|
776
|
+
{session?.status === 'rate_limited' && (
|
|
777
|
+
<>
|
|
778
|
+
<text style={{ fg: theme.secondary, marginBottom: 1 }}>
|
|
779
|
+
⚠ Session limit reached
|
|
780
|
+
</text>
|
|
781
|
+
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
|
|
782
|
+
You've used{' '}
|
|
783
|
+
<span fg={theme.foreground}>
|
|
784
|
+
{formatSessionUnits(session.recentCount)} of {session.limit}
|
|
785
|
+
</span>{' '}
|
|
786
|
+
sessions{' '}
|
|
787
|
+
{session.period === 'pacific_week' ? 'this week' : 'today'}.
|
|
788
|
+
Try again in{' '}
|
|
789
|
+
<span fg={theme.foreground}>
|
|
790
|
+
{formatRetryAfter(session.retryAfterMs)}
|
|
791
|
+
</span>
|
|
792
|
+
. Press Ctrl+C to exit.
|
|
793
|
+
</text>
|
|
794
|
+
</>
|
|
795
|
+
)}
|
|
796
|
+
|
|
797
|
+
{/* Daily provider-spend admission budget reached. Existing sessions
|
|
798
|
+
are never interrupted; this screen only follows a rejected fresh
|
|
799
|
+
admission and gives the user a friendly, concrete return time. */}
|
|
800
|
+
{session?.status === 'spend_limited' && (
|
|
801
|
+
<>
|
|
802
|
+
<text style={{ fg: theme.secondary, marginBottom: 1 }}>
|
|
803
|
+
☕ Daily Freebuff limit reached
|
|
804
|
+
</text>
|
|
805
|
+
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
|
|
806
|
+
{session.message} Come back in{' '}
|
|
807
|
+
<span fg={theme.foreground}>
|
|
808
|
+
{formatRetryAfter(session.retryAfterMs)}
|
|
809
|
+
</span>
|
|
810
|
+
{' — '}your free usage resets automatically at midnight Pacific.
|
|
811
|
+
Press Ctrl+C to exit.
|
|
812
|
+
</text>
|
|
813
|
+
</>
|
|
814
|
+
)}
|
|
815
|
+
|
|
816
|
+
{/* Too many distinct users hold a free session on this egress IP.
|
|
817
|
+
Unlike the quota screens above there is no reset time — a slot
|
|
818
|
+
frees as soon as any session on the IP ends — so this offers a
|
|
819
|
+
short retry rather than a "come back tomorrow". */}
|
|
820
|
+
{session?.status === 'ip_capped' && (
|
|
821
|
+
<>
|
|
822
|
+
<text style={{ fg: theme.secondary, marginBottom: 1 }}>
|
|
823
|
+
🚦 Too many Freebuff sessions on this network
|
|
824
|
+
</text>
|
|
825
|
+
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
|
|
826
|
+
{session.activeUsersForIp} other people are already using
|
|
827
|
+
Freebuff from your network, which is the most we allow at once.
|
|
828
|
+
Try again in{' '}
|
|
829
|
+
<span fg={theme.foreground}>
|
|
830
|
+
{formatRetryAfter(session.retryAfterMs)}
|
|
831
|
+
</span>
|
|
832
|
+
{' — '}a slot opens as soon as one of them finishes. Press
|
|
833
|
+
Ctrl+C to exit.
|
|
834
|
+
</text>
|
|
835
|
+
</>
|
|
836
|
+
)}
|
|
837
|
+
</box>
|
|
838
|
+
</box>
|
|
839
|
+
|
|
840
|
+
{/* Reserve the ad banner slot before the async ad fetch resolves so the
|
|
841
|
+
landing content does not jump when the banner fills. On very
|
|
842
|
+
short terminals the banner is dropped entirely to give the picker
|
|
843
|
+
back its 5 rows. */}
|
|
844
|
+
{showAds && (
|
|
845
|
+
<box
|
|
846
|
+
style={{
|
|
847
|
+
width: '100%',
|
|
848
|
+
flexShrink: 0,
|
|
849
|
+
height: AD_CARD_HEIGHT,
|
|
850
|
+
}}
|
|
851
|
+
>
|
|
852
|
+
{ads ? (
|
|
853
|
+
<ChoiceAdBanner
|
|
854
|
+
ads={ads}
|
|
855
|
+
onClick={recordClick}
|
|
856
|
+
onImpression={recordImpression}
|
|
857
|
+
/>
|
|
858
|
+
) : (
|
|
859
|
+
<text style={{ fg: theme.muted }}>{'─'.repeat(terminalWidth)}</text>
|
|
860
|
+
)}
|
|
861
|
+
</box>
|
|
862
|
+
)}
|
|
863
|
+
</box>
|
|
864
|
+
)
|
|
865
|
+
}
|