@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,604 @@
|
|
|
1
|
+
import { TextAttributes } from '@opentui/core'
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button } from './button'
|
|
5
|
+
import { useCopyToClipboard } from './copy-button'
|
|
6
|
+
import { FREEBUFF_GLM_V52_MODEL_ID } from '@anvil/common/constants/freebuff-models'
|
|
7
|
+
import { REFERRAL_CLI_DAILY_SESSION_BONUS_CAP } from '@anvil/common/constants/freebuff-referral-tiers'
|
|
8
|
+
import { pluralize } from '@anvil/common/util/string'
|
|
9
|
+
|
|
10
|
+
import { startFreebuffSession } from '../hooks/use-freebuff-session'
|
|
11
|
+
import { useNow } from '../hooks/use-now'
|
|
12
|
+
import { useTheme } from '../hooks/use-theme'
|
|
13
|
+
import { LOGIN_WEBSITE_URL } from '../login/constants'
|
|
14
|
+
import { formatFreebuffPremiumResetCountdown } from '../utils/freebuff-premium-reset'
|
|
15
|
+
import { safeOpen } from '../utils/open-url'
|
|
16
|
+
import { BORDER_CHARS } from '../utils/ui-constants'
|
|
17
|
+
|
|
18
|
+
import type { FreebuffAccessTier } from '@anvil/common/constants/freebuff-models'
|
|
19
|
+
import type {
|
|
20
|
+
FreebuffGlmPromo,
|
|
21
|
+
FreebuffReferralInfo,
|
|
22
|
+
} from '@anvil/common/types/freebuff-session'
|
|
23
|
+
|
|
24
|
+
/** Build a friend's share link from the referral code. Points at the
|
|
25
|
+
* /get-started page (CLI install walkthrough + hero + FAQs) rather than the
|
|
26
|
+
* bare landing page; the `?ref=` code is still captured into the attribution
|
|
27
|
+
* cookie there via the root layout's ReferralCodeCapture. When we know the
|
|
28
|
+
* inviter's name we pass `?referrer=` too so the page greets the friend with
|
|
29
|
+
* "X invited you to try Freebuff!". */
|
|
30
|
+
function referralLink(code: string, referrerName: string | null): string {
|
|
31
|
+
const params = new URLSearchParams({ ref: code })
|
|
32
|
+
if (referrerName) params.set('referrer', referrerName)
|
|
33
|
+
return `${LOGIN_WEBSITE_URL}/get-started?${params.toString()}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The referral tab and status section, not the broader Earn landing state. */
|
|
37
|
+
const EARN_URL = `${LOGIN_WEBSITE_URL}/earn?tab=referrals#referral-status`
|
|
38
|
+
const DASHBOARD_LABEL = 'Open referral status ↵'
|
|
39
|
+
// Two columns of leading space separate the label from the copy control beside
|
|
40
|
+
// it (the row itself has no gap), and they stay put in every state so keyboard
|
|
41
|
+
// navigation never shifts the rest of the action row.
|
|
42
|
+
const DASHBOARD_GUTTER = ' '
|
|
43
|
+
const DASHBOARD_BUTTON_WIDTH = DASHBOARD_LABEL.length + DASHBOARD_GUTTER.length
|
|
44
|
+
|
|
45
|
+
/** The temporary promo still needs its deadline even though the standing
|
|
46
|
+
* bounty pitch is already covered elsewhere in the picker and Earn page. */
|
|
47
|
+
function BountyPromoLine({
|
|
48
|
+
theme,
|
|
49
|
+
promo,
|
|
50
|
+
}: {
|
|
51
|
+
theme: ReturnType<typeof useTheme>
|
|
52
|
+
promo?: FreebuffGlmPromo
|
|
53
|
+
}) {
|
|
54
|
+
if (!promo) return null
|
|
55
|
+
return (
|
|
56
|
+
<text style={{ wrapMode: 'word' }}>
|
|
57
|
+
<span fg={theme.success ?? theme.foreground}>
|
|
58
|
+
✦ Promo: earn a bounty, spend up to {promo.dailySessions} a day
|
|
59
|
+
</span>
|
|
60
|
+
<span fg={theme.muted}> (ends {formatPromoEnd(promo.endsAt)})</span>
|
|
61
|
+
</text>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function DashboardButton({
|
|
66
|
+
theme,
|
|
67
|
+
focused,
|
|
68
|
+
onOpen,
|
|
69
|
+
}: {
|
|
70
|
+
theme: ReturnType<typeof useTheme>
|
|
71
|
+
focused: boolean
|
|
72
|
+
onOpen: () => void
|
|
73
|
+
}) {
|
|
74
|
+
// Focus is shown by the accent color alone, like the inline copy control it
|
|
75
|
+
// sits next to. A leading marker would have to eat the gutter that keeps the
|
|
76
|
+
// two labels apart, so a focused dashboard link ran flush into "Copy invite
|
|
77
|
+
// link" and read as overlapping it.
|
|
78
|
+
return (
|
|
79
|
+
<Button onClick={onOpen}>
|
|
80
|
+
<text style={{ wrapMode: 'word' }}>
|
|
81
|
+
<span
|
|
82
|
+
fg={focused ? theme.primary : theme.secondary}
|
|
83
|
+
attributes={focused ? TextAttributes.BOLD : TextAttributes.NONE}
|
|
84
|
+
>
|
|
85
|
+
{DASHBOARD_GUTTER}
|
|
86
|
+
{DASHBOARD_LABEL}
|
|
87
|
+
</span>
|
|
88
|
+
</text>
|
|
89
|
+
</Button>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Short, local date for the promo's end. The server sends the instant; every
|
|
94
|
+
* surface formats it, so none of them can drift from the real window. */
|
|
95
|
+
function formatPromoEnd(endsAt: string): string {
|
|
96
|
+
const at = new Date(endsAt)
|
|
97
|
+
return Number.isNaN(at.getTime())
|
|
98
|
+
? 'soon'
|
|
99
|
+
: at.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Navigation ids for the banner's keyboard-focusable buttons. The model
|
|
103
|
+
// selector owns the landing keyboard handler and appends these after its rows.
|
|
104
|
+
const COPY_FOCUS_ID = '__freebuff_referral_copy__'
|
|
105
|
+
const GLM_FOCUS_ID = '__freebuff_referral_glm__'
|
|
106
|
+
/** Opens the Earn page — bounties to claim, referrals to track, balance to
|
|
107
|
+
* read. The CLI can open a browser (`safeOpen`), so this is a real button
|
|
108
|
+
* rather than a URL the user has to select and paste. */
|
|
109
|
+
const DASHBOARD_FOCUS_ID = '__freebuff_referral_dashboard__'
|
|
110
|
+
const BUTTON_HORIZONTAL_CHROME = 6 // two border + four padding columns
|
|
111
|
+
|
|
112
|
+
export interface FreebuffReferralFocusTarget {
|
|
113
|
+
id: string
|
|
114
|
+
activate: () => void
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Below this menu width, the model action and referral/dashboard action group
|
|
118
|
+
* no longer fit beside each other. */
|
|
119
|
+
const shouldStackFreebuffReferralActions = (width: number): boolean =>
|
|
120
|
+
width < 70
|
|
121
|
+
|
|
122
|
+
const firstLabelThatFits = (
|
|
123
|
+
availableWidth: number,
|
|
124
|
+
labels: readonly string[],
|
|
125
|
+
chrome: number = BUTTON_HORIZONTAL_CHROME,
|
|
126
|
+
): string =>
|
|
127
|
+
labels.find((label) => label.length + chrome <= availableWidth) ??
|
|
128
|
+
labels.at(-1)!
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A "copy invite link" control, in one of two weights. Flips to an accent
|
|
132
|
+
* "✔ Copied!" confirmation for a couple seconds after a successful copy in
|
|
133
|
+
* both. Presentational: the copy action and copied flag are owned by the
|
|
134
|
+
* banner so the same action can be fired by keyboard navigation from the
|
|
135
|
+
* model picker.
|
|
136
|
+
*
|
|
137
|
+
* - 'bordered' — a rounded box that reads as a button. Used inside the
|
|
138
|
+
* unlocked GLM card, where it sits beside the "Use GLM 5.2 ↵" button and
|
|
139
|
+
* has to match it.
|
|
140
|
+
* - 'inline' — no box; focus/hover shown by color alone, exactly like the
|
|
141
|
+
* picker's "See all N models" toggle. Used in the two LOCKED states,
|
|
142
|
+
* which render on the landing screen directly under the recommended card:
|
|
143
|
+
* a second bordered box there competes with the hero for the eye, and the
|
|
144
|
+
* hero is what Enter actually does. Sits one row down (like the toggle) so
|
|
145
|
+
* whitespace, not a border, separates it from the pitch above — and rests
|
|
146
|
+
* at `foreground` rather than `muted`, since unlike the toggle it is
|
|
147
|
+
* adjacent to muted body copy it must not blend into.
|
|
148
|
+
*/
|
|
149
|
+
const CopyInviteLinkButton: React.FC<{
|
|
150
|
+
isCopied: boolean
|
|
151
|
+
focused: boolean
|
|
152
|
+
onCopy: () => void
|
|
153
|
+
availableWidth: number
|
|
154
|
+
labels?: readonly string[]
|
|
155
|
+
variant?: 'bordered' | 'inline'
|
|
156
|
+
}> = ({
|
|
157
|
+
isCopied,
|
|
158
|
+
focused,
|
|
159
|
+
onCopy,
|
|
160
|
+
availableWidth,
|
|
161
|
+
labels = ['⎘ Copy invite link', '⎘ Copy link', '⎘ Copy'],
|
|
162
|
+
variant = 'bordered',
|
|
163
|
+
}) => {
|
|
164
|
+
const theme = useTheme()
|
|
165
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
166
|
+
const inline = variant === 'inline'
|
|
167
|
+
// Borderless labels spend no columns on chrome, so they fit at widths the
|
|
168
|
+
// boxed variant would have had to abbreviate at.
|
|
169
|
+
const chrome = inline ? 0 : BUTTON_HORIZONTAL_CHROME
|
|
170
|
+
const label = firstLabelThatFits(availableWidth, labels, chrome)
|
|
171
|
+
const copiedLabel = firstLabelThatFits(
|
|
172
|
+
availableWidth,
|
|
173
|
+
['✔ Copied!', '✔'],
|
|
174
|
+
chrome,
|
|
175
|
+
)
|
|
176
|
+
const highlighted = isCopied || focused || isHovered
|
|
177
|
+
|
|
178
|
+
if (inline) {
|
|
179
|
+
return (
|
|
180
|
+
<Button
|
|
181
|
+
id={COPY_FOCUS_ID}
|
|
182
|
+
onClick={onCopy}
|
|
183
|
+
onMouseOver={() => setIsHovered(true)}
|
|
184
|
+
onMouseOut={() => setIsHovered(false)}
|
|
185
|
+
style={{ flexShrink: 0 }}
|
|
186
|
+
>
|
|
187
|
+
<text style={{ wrapMode: 'none' }}>
|
|
188
|
+
<span
|
|
189
|
+
fg={highlighted ? theme.primary : theme.foreground}
|
|
190
|
+
attributes={
|
|
191
|
+
isCopied || focused ? TextAttributes.BOLD : TextAttributes.NONE
|
|
192
|
+
}
|
|
193
|
+
>
|
|
194
|
+
{isCopied ? copiedLabel : label}
|
|
195
|
+
</span>
|
|
196
|
+
</text>
|
|
197
|
+
</Button>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Keyboard focus and mouse hover share the highlighted look; a keyboard-
|
|
202
|
+
// focused row gets the brighter accent border so it matches the picker's
|
|
203
|
+
// focused-row treatment above it.
|
|
204
|
+
const borderColor = isCopied
|
|
205
|
+
? theme.primary
|
|
206
|
+
: focused
|
|
207
|
+
? theme.primary
|
|
208
|
+
: isHovered
|
|
209
|
+
? theme.foreground
|
|
210
|
+
: theme.border
|
|
211
|
+
const fg = isCopied
|
|
212
|
+
? theme.primary
|
|
213
|
+
: focused || isHovered
|
|
214
|
+
? theme.foreground
|
|
215
|
+
: theme.muted
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Button
|
|
219
|
+
id={COPY_FOCUS_ID}
|
|
220
|
+
onClick={onCopy}
|
|
221
|
+
onMouseOver={() => setIsHovered(true)}
|
|
222
|
+
onMouseOut={() => setIsHovered(false)}
|
|
223
|
+
border
|
|
224
|
+
borderStyle="rounded"
|
|
225
|
+
borderColor={borderColor}
|
|
226
|
+
customBorderChars={BORDER_CHARS}
|
|
227
|
+
style={{
|
|
228
|
+
paddingLeft: 2,
|
|
229
|
+
paddingRight: 2,
|
|
230
|
+
backgroundColor: 'transparent',
|
|
231
|
+
// Hug the label and never let a width-constrained row squash the
|
|
232
|
+
// bordered box (which would clip the label and mangle the border).
|
|
233
|
+
flexShrink: 0,
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
<text style={{ wrapMode: 'none' }}>
|
|
237
|
+
<span fg={fg}>{isCopied ? copiedLabel : label}</span>
|
|
238
|
+
</text>
|
|
239
|
+
</Button>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Advertises the "invite friends" reward on the landing model screen. The
|
|
245
|
+
* reward — and the presentation — depends on the session's access tier:
|
|
246
|
+
*
|
|
247
|
+
* - LIMITED tier: referrals earn a daily free-session bonus (not GLM). One
|
|
248
|
+
* quiet muted line ("refer friends → more sessions per day") + the inline
|
|
249
|
+
* copy control, so it advertises the perk without crowding the picker.
|
|
250
|
+
* - FULL tier, UNLOCKED (you have GLM sessions today): a flashy accent-
|
|
251
|
+
* bordered card with your remaining sessions and a prominent "Use GLM 5.2 ↵"
|
|
252
|
+
* launch button, so the reward feels earned and inviting.
|
|
253
|
+
* - FULL tier, LOCKED (no GLM sessions yet): a single quiet muted line
|
|
254
|
+
* inviting referrals.
|
|
255
|
+
*
|
|
256
|
+
* Both LOCKED states render on the landing screen under the recommended model
|
|
257
|
+
* card, so their copy control is borderless — the hero card stays the only
|
|
258
|
+
* bordered element there. The UNLOCKED card is a screen of its own making: the
|
|
259
|
+
* reward is earned, so it gets a box and boxed buttons.
|
|
260
|
+
*
|
|
261
|
+
* Every locked-state pitch is sized to fit its tier's card width on ONE line
|
|
262
|
+
* (the banner wraps at `buttonOuterWidth`, not the terminal width) and ends
|
|
263
|
+
* in a colon that hands off to the inline copy control below it. Now that the
|
|
264
|
+
* control is borderless it no longer announces itself as a button, so the
|
|
265
|
+
* colon is what carries the eye down to it. The one exception is the
|
|
266
|
+
* limited-tier at-cap line: referring more earns nothing there, so it ends
|
|
267
|
+
* flat rather than pointing at an action that would not pay off.
|
|
268
|
+
*
|
|
269
|
+
* Renders nothing unless the server attached a `referral` block, so
|
|
270
|
+
* pre-referral-code users never see it.
|
|
271
|
+
*/
|
|
272
|
+
interface FreebuffReferralBannerProps {
|
|
273
|
+
width: number
|
|
274
|
+
referral: FreebuffReferralInfo
|
|
275
|
+
/** A live GLM promo, or undefined. Undefined is the ordinary state. */
|
|
276
|
+
glmPromo?: FreebuffGlmPromo
|
|
277
|
+
accessTier: FreebuffAccessTier
|
|
278
|
+
focusedId: string
|
|
279
|
+
onFocusTargetsChange: (targets: FreebuffReferralFocusTarget[]) => void
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const FreebuffReferralBanner: React.FC<FreebuffReferralBannerProps> = ({
|
|
283
|
+
width,
|
|
284
|
+
referral,
|
|
285
|
+
glmPromo,
|
|
286
|
+
accessTier,
|
|
287
|
+
focusedId,
|
|
288
|
+
onFocusTargetsChange,
|
|
289
|
+
}) => {
|
|
290
|
+
const theme = useTheme()
|
|
291
|
+
const now = useNow(60_000)
|
|
292
|
+
const [joining, setJoining] = useState(false)
|
|
293
|
+
const joiningRef = useRef(false)
|
|
294
|
+
const [glmHovered, setGlmHovered] = useState(false)
|
|
295
|
+
const copyFocused = focusedId === COPY_FOCUS_ID
|
|
296
|
+
const glmFocused = focusedId === GLM_FOCUS_ID
|
|
297
|
+
const dashboardFocused = focusedId === DASHBOARD_FOCUS_ID
|
|
298
|
+
|
|
299
|
+
const useGlm = useCallback(() => {
|
|
300
|
+
if (joiningRef.current) return
|
|
301
|
+
joiningRef.current = true
|
|
302
|
+
setJoining(true)
|
|
303
|
+
startFreebuffSession(FREEBUFF_GLM_V52_MODEL_ID).finally(() => {
|
|
304
|
+
joiningRef.current = false
|
|
305
|
+
setJoining(false)
|
|
306
|
+
})
|
|
307
|
+
}, [])
|
|
308
|
+
|
|
309
|
+
const link = referralLink(referral.code, referral.referrerName)
|
|
310
|
+
const { isCopied, copy } = useCopyToClipboard(link)
|
|
311
|
+
|
|
312
|
+
// Register this banner's buttons as keyboard focus targets so the model
|
|
313
|
+
// selector's arrow navigation flows from "see all models" into them (and
|
|
314
|
+
// wraps back up). Locked states show copy then dashboard; the unlocked card
|
|
315
|
+
// leads with "Use GLM 5.2", then copy and dashboard.
|
|
316
|
+
// A limited-tier user can now hold GLM sessions too — bounty grants are
|
|
317
|
+
// redeemable in every region — so the unlocked card is keyed on the balance
|
|
318
|
+
// alone rather than on the tier.
|
|
319
|
+
const isLocked = (referral.weeklySessionsRemaining ?? 0) <= 0
|
|
320
|
+
const openDashboard = useCallback(() => {
|
|
321
|
+
void safeOpen(EARN_URL)
|
|
322
|
+
}, [])
|
|
323
|
+
const lockedReferralActions = (
|
|
324
|
+
<box
|
|
325
|
+
style={{
|
|
326
|
+
flexDirection: 'row',
|
|
327
|
+
alignItems: 'center',
|
|
328
|
+
gap: 0,
|
|
329
|
+
marginTop: 1,
|
|
330
|
+
flexShrink: 0,
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
<CopyInviteLinkButton
|
|
334
|
+
isCopied={isCopied}
|
|
335
|
+
focused={copyFocused}
|
|
336
|
+
onCopy={copy}
|
|
337
|
+
availableWidth={Math.max(0, width - DASHBOARD_BUTTON_WIDTH)}
|
|
338
|
+
variant="inline"
|
|
339
|
+
/>
|
|
340
|
+
<DashboardButton
|
|
341
|
+
theme={theme}
|
|
342
|
+
focused={dashboardFocused}
|
|
343
|
+
onOpen={openDashboard}
|
|
344
|
+
/>
|
|
345
|
+
</box>
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
onFocusTargetsChange(
|
|
350
|
+
isLocked
|
|
351
|
+
? [
|
|
352
|
+
{ id: COPY_FOCUS_ID, activate: copy },
|
|
353
|
+
{ id: DASHBOARD_FOCUS_ID, activate: openDashboard },
|
|
354
|
+
]
|
|
355
|
+
: [
|
|
356
|
+
{ id: GLM_FOCUS_ID, activate: useGlm },
|
|
357
|
+
{ id: COPY_FOCUS_ID, activate: copy },
|
|
358
|
+
{ id: DASHBOARD_FOCUS_ID, activate: openDashboard },
|
|
359
|
+
],
|
|
360
|
+
)
|
|
361
|
+
return () => onFocusTargetsChange([])
|
|
362
|
+
}, [isLocked, copy, useGlm, openDashboard, onFocusTargetsChange])
|
|
363
|
+
|
|
364
|
+
const { qualifiedCount, githubLinked } = referral
|
|
365
|
+
|
|
366
|
+
// LIMITED tier: referrals earn a daily free-session bonus, not GLM. Keep it
|
|
367
|
+
// quiet — one line advertising the perk + the share button below it, with the
|
|
368
|
+
// earned bonus (capped) shown as progress. `qualifiedCount` is the capped
|
|
369
|
+
// bonus sessions/day already earned.
|
|
370
|
+
// ...unless they hold bounty-earned GLM sessions, which fall through to the
|
|
371
|
+
// shared unlocked card below.
|
|
372
|
+
if (accessTier === 'limited' && isLocked) {
|
|
373
|
+
const atCap = qualifiedCount >= REFERRAL_CLI_DAILY_SESSION_BONUS_CAP
|
|
374
|
+
return (
|
|
375
|
+
<box
|
|
376
|
+
style={{
|
|
377
|
+
flexDirection: 'column',
|
|
378
|
+
alignItems: 'flex-start',
|
|
379
|
+
gap: 0,
|
|
380
|
+
marginTop: 1,
|
|
381
|
+
// Never let a height-starved landing column squash the banner — that
|
|
382
|
+
// would draw the copy control on top of the pitch line above it.
|
|
383
|
+
flexShrink: 0,
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
<text style={{ wrapMode: 'word' }}>
|
|
387
|
+
<span fg={theme.muted}>✦ </span>
|
|
388
|
+
{qualifiedCount > 0 ? (
|
|
389
|
+
<>
|
|
390
|
+
<span fg={theme.foreground}>
|
|
391
|
+
+{pluralize(qualifiedCount, 'session')}/day
|
|
392
|
+
</span>
|
|
393
|
+
<span fg={theme.muted}>
|
|
394
|
+
{' '}
|
|
395
|
+
from referrals
|
|
396
|
+
{atCap
|
|
397
|
+
? ''
|
|
398
|
+
: ` · refer more (${qualifiedCount}/${REFERRAL_CLI_DAILY_SESSION_BONUS_CAP}):`}
|
|
399
|
+
</span>
|
|
400
|
+
</>
|
|
401
|
+
) : (
|
|
402
|
+
<span fg={theme.muted}>
|
|
403
|
+
Refer friends to unlock more free sessions per day:
|
|
404
|
+
</span>
|
|
405
|
+
)}
|
|
406
|
+
</text>
|
|
407
|
+
{lockedReferralActions}
|
|
408
|
+
<BountyPromoLine theme={theme} promo={glmPromo} />
|
|
409
|
+
</box>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// FULL tier: GLM 5.2 reward. The GLM-only fields are always present on a
|
|
414
|
+
// full-tier block from the server; default defensively for the wire type.
|
|
415
|
+
const weeklySessionsRemaining = referral.weeklySessionsRemaining ?? 0
|
|
416
|
+
const resetsIn = formatFreebuffPremiumResetCountdown(
|
|
417
|
+
referral.resetAt ? new Date(referral.resetAt) : new Date(now),
|
|
418
|
+
now,
|
|
419
|
+
{
|
|
420
|
+
withDays: true,
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
// NOT USABLE: keep it quiet — one line that advertises the reward, with the
|
|
425
|
+
// share link as a clearly-clickable button below it. Message adapts to *why*
|
|
426
|
+
// it's locked — no referrals yet vs. today's sessions already spent.
|
|
427
|
+
if (weeklySessionsRemaining <= 0) {
|
|
428
|
+
return (
|
|
429
|
+
<box
|
|
430
|
+
style={{
|
|
431
|
+
flexDirection: 'column',
|
|
432
|
+
alignItems: 'flex-start',
|
|
433
|
+
gap: 0,
|
|
434
|
+
marginTop: 1,
|
|
435
|
+
// Never let a height-starved landing column squash the banner — that
|
|
436
|
+
// would draw the copy control on top of the pitch line above it.
|
|
437
|
+
flexShrink: 0,
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
<text style={{ wrapMode: 'word' }}>
|
|
441
|
+
<span fg={theme.muted}>✦ </span>
|
|
442
|
+
{qualifiedCount > 0 ? (
|
|
443
|
+
<>
|
|
444
|
+
<span fg={theme.foreground}>GLM 5.2</span>
|
|
445
|
+
<span fg={theme.muted}>
|
|
446
|
+
{' '}
|
|
447
|
+
refills in {resetsIn} · refer more ({qualifiedCount} earned):
|
|
448
|
+
</span>
|
|
449
|
+
</>
|
|
450
|
+
) : (
|
|
451
|
+
<>
|
|
452
|
+
<span fg={theme.muted}>Refer friends → </span>
|
|
453
|
+
<span fg={theme.foreground}>GLM 5.2</span>
|
|
454
|
+
<span fg={theme.muted}>, top open-source model:</span>
|
|
455
|
+
</>
|
|
456
|
+
)}
|
|
457
|
+
</text>
|
|
458
|
+
{lockedReferralActions}
|
|
459
|
+
<BountyPromoLine theme={theme} promo={glmPromo} />
|
|
460
|
+
</box>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// USABLE: flashy accent card. Round the (possibly fractional) remaining up to
|
|
465
|
+
// whole sessions for a clean count — an early-ended session leaves a fraction
|
|
466
|
+
// that the user can still spend, so never show 0 here.
|
|
467
|
+
const sessionsLeft = Math.max(1, Math.ceil(weeklySessionsRemaining))
|
|
468
|
+
const stackActions = shouldStackFreebuffReferralActions(width)
|
|
469
|
+
const actionRowWidth = width - 4 // card border + horizontal padding
|
|
470
|
+
const glmLabel = firstLabelThatFits(actionRowWidth, [
|
|
471
|
+
'▶ Use GLM 5.2 ↵',
|
|
472
|
+
'▶ GLM 5.2',
|
|
473
|
+
'▶ GLM',
|
|
474
|
+
])
|
|
475
|
+
const inviteAvailableWidth = stackActions
|
|
476
|
+
? actionRowWidth - DASHBOARD_BUTTON_WIDTH
|
|
477
|
+
: actionRowWidth -
|
|
478
|
+
(glmLabel.length + BUTTON_HORIZONTAL_CHROME) -
|
|
479
|
+
2 -
|
|
480
|
+
DASHBOARD_BUTTON_WIDTH
|
|
481
|
+
// No "max earned" state: the reward is uncapped, so inviting always adds
|
|
482
|
+
// another daily session.
|
|
483
|
+
const inviteLabels = [
|
|
484
|
+
`⎘ Invite for +1/day (${qualifiedCount} earned)`,
|
|
485
|
+
'⎘ Invite +1/day',
|
|
486
|
+
'⎘ Invite',
|
|
487
|
+
]
|
|
488
|
+
const githubLabel =
|
|
489
|
+
actionRowWidth >=
|
|
490
|
+
'Signed up with Google? Connect GitHub to qualify ↗'.length
|
|
491
|
+
? 'Signed up with Google? Connect GitHub to qualify ↗'
|
|
492
|
+
: 'Connect GitHub to qualify ↗'
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<box
|
|
496
|
+
style={{
|
|
497
|
+
flexDirection: 'column',
|
|
498
|
+
alignItems: 'flex-start',
|
|
499
|
+
gap: 0,
|
|
500
|
+
paddingLeft: 1,
|
|
501
|
+
paddingRight: 1,
|
|
502
|
+
borderStyle: 'rounded',
|
|
503
|
+
borderColor: theme.muted,
|
|
504
|
+
marginTop: 1,
|
|
505
|
+
width,
|
|
506
|
+
// Never let a height-starved landing column squash the card — that
|
|
507
|
+
// would draw the bordered action buttons on top of the status line.
|
|
508
|
+
flexShrink: 0,
|
|
509
|
+
}}
|
|
510
|
+
border={['top', 'bottom', 'left', 'right']}
|
|
511
|
+
title=" ✦ GLM 5.2 unlocked "
|
|
512
|
+
titleAlignment="left"
|
|
513
|
+
>
|
|
514
|
+
<text style={{ wrapMode: 'word' }}>
|
|
515
|
+
<span fg={theme.foreground} attributes={TextAttributes.BOLD}>
|
|
516
|
+
{pluralize(sessionsLeft, 'session')}
|
|
517
|
+
</span>
|
|
518
|
+
<span fg={theme.foreground}> available today</span>
|
|
519
|
+
<span fg={theme.muted}> · resets in {resetsIn}</span>
|
|
520
|
+
</text>
|
|
521
|
+
|
|
522
|
+
<box
|
|
523
|
+
style={{
|
|
524
|
+
flexDirection: stackActions ? 'column' : 'row',
|
|
525
|
+
alignItems: stackActions ? 'flex-start' : 'center',
|
|
526
|
+
gap: stackActions ? 0 : 2,
|
|
527
|
+
}}
|
|
528
|
+
>
|
|
529
|
+
<Button
|
|
530
|
+
id={GLM_FOCUS_ID}
|
|
531
|
+
onClick={useGlm}
|
|
532
|
+
onMouseOver={() => setGlmHovered(true)}
|
|
533
|
+
onMouseOut={() => setGlmHovered(false)}
|
|
534
|
+
border
|
|
535
|
+
borderStyle="rounded"
|
|
536
|
+
// Standard button treatment: muted border at rest, green when
|
|
537
|
+
// keyboard-focused, brighter on hover — same scheme as the
|
|
538
|
+
// "Copy invite link" button below it.
|
|
539
|
+
borderColor={
|
|
540
|
+
glmFocused
|
|
541
|
+
? theme.primary
|
|
542
|
+
: glmHovered
|
|
543
|
+
? theme.foreground
|
|
544
|
+
: theme.border
|
|
545
|
+
}
|
|
546
|
+
customBorderChars={BORDER_CHARS}
|
|
547
|
+
style={{
|
|
548
|
+
paddingLeft: 2,
|
|
549
|
+
paddingRight: 2,
|
|
550
|
+
backgroundColor: 'transparent',
|
|
551
|
+
}}
|
|
552
|
+
>
|
|
553
|
+
<text style={{ wrapMode: 'none' }}>
|
|
554
|
+
<span
|
|
555
|
+
fg={
|
|
556
|
+
joining
|
|
557
|
+
? theme.muted
|
|
558
|
+
: glmFocused || glmHovered
|
|
559
|
+
? theme.foreground
|
|
560
|
+
: theme.muted
|
|
561
|
+
}
|
|
562
|
+
attributes={TextAttributes.BOLD}
|
|
563
|
+
>
|
|
564
|
+
{joining ? 'Starting…' : glmLabel}
|
|
565
|
+
</span>
|
|
566
|
+
</text>
|
|
567
|
+
</Button>
|
|
568
|
+
<box
|
|
569
|
+
style={{
|
|
570
|
+
flexDirection: 'row',
|
|
571
|
+
alignItems: 'center',
|
|
572
|
+
gap: 0,
|
|
573
|
+
flexShrink: 0,
|
|
574
|
+
}}
|
|
575
|
+
>
|
|
576
|
+
<CopyInviteLinkButton
|
|
577
|
+
isCopied={isCopied}
|
|
578
|
+
focused={copyFocused}
|
|
579
|
+
onCopy={copy}
|
|
580
|
+
availableWidth={inviteAvailableWidth}
|
|
581
|
+
labels={inviteLabels}
|
|
582
|
+
/>
|
|
583
|
+
<DashboardButton
|
|
584
|
+
theme={theme}
|
|
585
|
+
focused={dashboardFocused}
|
|
586
|
+
onOpen={openDashboard}
|
|
587
|
+
/>
|
|
588
|
+
</box>
|
|
589
|
+
</box>
|
|
590
|
+
|
|
591
|
+
<BountyPromoLine theme={theme} promo={glmPromo} />
|
|
592
|
+
|
|
593
|
+
{!githubLinked && (
|
|
594
|
+
<Button
|
|
595
|
+
onClick={() => void safeOpen(`${LOGIN_WEBSITE_URL}/web/settings`)}
|
|
596
|
+
>
|
|
597
|
+
<text style={{ wrapMode: 'word' }}>
|
|
598
|
+
<span fg={theme.secondary}>{githubLabel}</span>
|
|
599
|
+
</text>
|
|
600
|
+
</Button>
|
|
601
|
+
)}
|
|
602
|
+
</box>
|
|
603
|
+
)
|
|
604
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TextAttributes } from '@opentui/core'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
|
|
5
|
+
import { useLogo } from '../hooks/use-logo'
|
|
6
|
+
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
|
|
7
|
+
import { useTheme } from '../hooks/use-theme'
|
|
8
|
+
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Terminal state shown after a 409 session_superseded response. Another CLI on
|
|
12
|
+
* the same account rotated our instance id and we've stopped polling — the
|
|
13
|
+
* user needs to close the other instance and restart.
|
|
14
|
+
*/
|
|
15
|
+
export const FreebuffSupersededScreen: React.FC = () => {
|
|
16
|
+
const theme = useTheme()
|
|
17
|
+
const { contentMaxWidth } = useTerminalDimensions()
|
|
18
|
+
const blockColor = getLogoBlockColor(theme.name)
|
|
19
|
+
const accentColor = getLogoAccentColor(theme.name)
|
|
20
|
+
const { component: logoComponent } = useLogo({
|
|
21
|
+
availableWidth: contentMaxWidth,
|
|
22
|
+
accentColor,
|
|
23
|
+
blockColor,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
useFreebuffCtrlCExit()
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<box
|
|
30
|
+
style={{
|
|
31
|
+
width: '100%',
|
|
32
|
+
height: '100%',
|
|
33
|
+
flexDirection: 'column',
|
|
34
|
+
backgroundColor: theme.background,
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
paddingLeft: 2,
|
|
38
|
+
paddingRight: 2,
|
|
39
|
+
gap: 1,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<box style={{ marginBottom: 1 }}>{logoComponent}</box>
|
|
43
|
+
<text
|
|
44
|
+
style={{ fg: theme.foreground, marginBottom: 1 }}
|
|
45
|
+
attributes={TextAttributes.BOLD}
|
|
46
|
+
>
|
|
47
|
+
Another freebuff instance took over this account.
|
|
48
|
+
</text>
|
|
49
|
+
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
|
|
50
|
+
Only one CLI per account can be active at a time.
|
|
51
|
+
</text>
|
|
52
|
+
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
|
|
53
|
+
Close the other instance, then restart freebuff here.
|
|
54
|
+
</text>
|
|
55
|
+
<box style={{ marginTop: 1 }}>
|
|
56
|
+
<text style={{ fg: theme.muted }}>
|
|
57
|
+
Press <span fg={theme.primary}>Ctrl+C</span> to exit.
|
|
58
|
+
</text>
|
|
59
|
+
</box>
|
|
60
|
+
</box>
|
|
61
|
+
)
|
|
62
|
+
}
|