@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,656 @@
|
|
|
1
|
+
import { WEBSITE_URL } from '@anvil/sdk'
|
|
2
|
+
import { AnalyticsEvent } from '@anvil/common/constants/analytics-events'
|
|
3
|
+
import { getAdUserAgent } from '@anvil/common/util/ad-user-agent'
|
|
4
|
+
import { useEffect, useRef, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import { useTerminalLayout } from './use-terminal-layout'
|
|
7
|
+
import { getAdsEnabled } from '../commands/ads'
|
|
8
|
+
import { useChatStore } from '../state/chat-store'
|
|
9
|
+
import { isUserActive, subscribeToActivity } from '../utils/activity-tracker'
|
|
10
|
+
import { getAuthToken } from '../utils/auth'
|
|
11
|
+
import { IS_FREEBUFF } from '../utils/constants'
|
|
12
|
+
import { getCliEnv } from '../utils/env'
|
|
13
|
+
import { logger } from '../utils/logger'
|
|
14
|
+
import { AI_MESSAGE_ID_PREFIX } from '../utils/ai-message-id'
|
|
15
|
+
import { trackEvent } from '../utils/analytics'
|
|
16
|
+
import {
|
|
17
|
+
createLazyResponseAdQueue,
|
|
18
|
+
MAX_RESPONSE_AD_POOL_SIZE,
|
|
19
|
+
requestLazyResponseAds,
|
|
20
|
+
} from '../utils/lazy-response-ads'
|
|
21
|
+
|
|
22
|
+
import type { Message } from '@anvil/sdk'
|
|
23
|
+
import type { ChatMessage } from '../types/chat'
|
|
24
|
+
|
|
25
|
+
const AD_ROTATION_INTERVAL_MS = 60 * 1000 // 60 seconds per ad
|
|
26
|
+
const MAX_ADS_AFTER_ACTIVITY = 3 // Show up to 3 ads after last activity, then pause fetching new ads
|
|
27
|
+
const ACTIVITY_THRESHOLD_MS = 30_000 // 30 seconds idle threshold for fetching new ads
|
|
28
|
+
const MAX_AD_CACHE_SIZE = 50 // Maximum number of ads to keep in cache
|
|
29
|
+
const ZEROCLICK_IMPRESSIONS_URL = 'https://zeroclick.dev/api/v2/impressions'
|
|
30
|
+
|
|
31
|
+
// Ad response type (normalized shape across providers; credits added after impression)
|
|
32
|
+
export type AdResponse = {
|
|
33
|
+
adText: string
|
|
34
|
+
title: string
|
|
35
|
+
cta: string
|
|
36
|
+
url: string
|
|
37
|
+
favicon: string
|
|
38
|
+
clickUrl: string
|
|
39
|
+
impUrl: string
|
|
40
|
+
provider?: AdProvider
|
|
41
|
+
impressionIds?: string[]
|
|
42
|
+
credits?: number // Set after impression is recorded (in cents)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Which upstream ad network to query. The server maps each provider onto the
|
|
47
|
+
* same normalized response shape, so the rest of the hook is provider-agnostic.
|
|
48
|
+
*/
|
|
49
|
+
export type AdProvider = 'gravity' | 'carbon' | 'zeroclick'
|
|
50
|
+
// Product surfaces the ads API maps to Gravity placements. 'waiting_room' is the
|
|
51
|
+
// legacy wire name for the freebuff landing screen; 'cli_chat' is the inline
|
|
52
|
+
// transcript ad in the coding-agent chat. Values must match the server's
|
|
53
|
+
// AD_SURFACES enum, so don't rename them.
|
|
54
|
+
export type AdSurface = 'waiting_room' | 'cli_chat'
|
|
55
|
+
|
|
56
|
+
export type GravityAdState = {
|
|
57
|
+
ads: AdResponse[] | null
|
|
58
|
+
/**
|
|
59
|
+
* On-demand ad pools keyed by assistant message id. The renderer repeats a
|
|
60
|
+
* full pool when the response has more eligible slots than distinct ads.
|
|
61
|
+
*/
|
|
62
|
+
responseAds: Record<string, AdResponse[]>
|
|
63
|
+
/** Lazily fill the response's bounded ad pool as slots become eligible. */
|
|
64
|
+
requestResponseAds: (messageId: string, count: number) => void
|
|
65
|
+
isLoading: boolean
|
|
66
|
+
recordClick: (ad: AdResponse) => void
|
|
67
|
+
recordImpression: (ad: AdResponse) => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Consolidated controller state for the ad rotation logic
|
|
71
|
+
type GravityController = {
|
|
72
|
+
choiceCache: AdResponse[][] // Cache of ad sets (choice or single-ad units)
|
|
73
|
+
choiceCacheIndex: number
|
|
74
|
+
impressionsFired: Set<string>
|
|
75
|
+
adsShownSinceActivity: number
|
|
76
|
+
tickInFlight: boolean
|
|
77
|
+
inlineQueue: ReturnType<typeof createLazyResponseAdQueue<AdResponse>>
|
|
78
|
+
eligibleSlotCounts: Map<string, number>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Pure helper: add an ad set to the cache
|
|
82
|
+
function addToChoiceCache(ctrl: GravityController, ads: AdResponse[]): void {
|
|
83
|
+
// ZeroClick offer responses must not be stored for later display. Keep them
|
|
84
|
+
// out of the rotation cache and only render them for the live request.
|
|
85
|
+
if (ads.some((ad) => ad.provider === 'zeroclick')) return
|
|
86
|
+
|
|
87
|
+
// Deduplicate by checking if any set has the same first impUrl
|
|
88
|
+
const key = ads[0]?.impUrl
|
|
89
|
+
if (key && ctrl.choiceCache.some((set) => set[0]?.impUrl === key)) return
|
|
90
|
+
if (ctrl.choiceCache.length >= MAX_AD_CACHE_SIZE) ctrl.choiceCache.shift()
|
|
91
|
+
ctrl.choiceCache.push(ads)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pure helper: get the next cached ad set
|
|
95
|
+
function nextFromChoiceCache(ctrl: GravityController): AdResponse[] | null {
|
|
96
|
+
if (ctrl.choiceCache.length === 0) return null
|
|
97
|
+
const set = ctrl.choiceCache[ctrl.choiceCacheIndex % ctrl.choiceCache.length]!
|
|
98
|
+
ctrl.choiceCacheIndex = (ctrl.choiceCacheIndex + 1) % ctrl.choiceCache.length
|
|
99
|
+
return set
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A streamed LLM answer (possibly still in flight). Other top-level
|
|
104
|
+
* 'ai'-variant messages (bash echoes, system notices, mode dividers) are
|
|
105
|
+
* excluded via the `ai-` id prefix.
|
|
106
|
+
*/
|
|
107
|
+
export function isAnswerMessage(m: ChatMessage): boolean {
|
|
108
|
+
return (
|
|
109
|
+
!m.parentId && m.variant === 'ai' && m.id.startsWith(AI_MESSAGE_ID_PREFIX)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isInlineAdEligibleAnswer(m: ChatMessage): boolean {
|
|
114
|
+
return isAnswerMessage(m) && m.metadata?.allowInlineAds === true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function claimAdImpression(
|
|
118
|
+
impressionsFired: Set<string>,
|
|
119
|
+
impUrl: string,
|
|
120
|
+
): boolean {
|
|
121
|
+
if (impressionsFired.has(impUrl)) return false
|
|
122
|
+
impressionsFired.add(impUrl)
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function trackInlineAdEvent(
|
|
127
|
+
event: AnalyticsEvent,
|
|
128
|
+
properties: Record<string, unknown>,
|
|
129
|
+
): void {
|
|
130
|
+
try {
|
|
131
|
+
trackEvent(event, properties)
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// Telemetry must never interfere with fetching or rendering an ad.
|
|
134
|
+
logger.debug({ error, event }, '[ads] Failed to track inline ad event')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type GravityAdOptionsBase = {
|
|
139
|
+
enabled?: boolean
|
|
140
|
+
/** Skip the "wait for first user message" gate. Used by the freebuff
|
|
141
|
+
* landing screen, which has no conversation but still needs ads. */
|
|
142
|
+
forceStart?: boolean
|
|
143
|
+
/** Ad network to request first. The server owns fallback ordering. */
|
|
144
|
+
provider?: AdProvider
|
|
145
|
+
/** Product surface requesting the ad. The server maps this to placements. */
|
|
146
|
+
surface?: AdSurface
|
|
147
|
+
/** Explicit provider placement id for the rotating `ads[0]` slot. */
|
|
148
|
+
slotPlacementId?: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type GravityAdOptions = GravityAdOptionsBase &
|
|
152
|
+
(
|
|
153
|
+
| {
|
|
154
|
+
/** Lazily fetch interspersed ads as the assistant response grows. */
|
|
155
|
+
inline: true
|
|
156
|
+
/** Reusable provider placement id for every lazy inline auction. */
|
|
157
|
+
inlinePlacementId: string
|
|
158
|
+
}
|
|
159
|
+
| {
|
|
160
|
+
inline?: false
|
|
161
|
+
inlinePlacementId?: never
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Fetches the rotating ad slot and, with `inline`, one reusable placement each
|
|
167
|
+
* time another interspersed response slot becomes eligible. Short answers make
|
|
168
|
+
* no unnecessary inline requests; long answers repeat a pool of four ads.
|
|
169
|
+
*/
|
|
170
|
+
export const useGravityAd = (options?: GravityAdOptions): GravityAdState => {
|
|
171
|
+
const enabled = options?.enabled ?? true
|
|
172
|
+
const forceStart = options?.forceStart ?? false
|
|
173
|
+
const provider: AdProvider = options?.provider ?? 'gravity'
|
|
174
|
+
const surface = options?.surface
|
|
175
|
+
const inline = options?.inline ?? false
|
|
176
|
+
const inlinePlacementId = options?.inlinePlacementId
|
|
177
|
+
const slotPlacementId = options?.slotPlacementId
|
|
178
|
+
const [ads, setAds] = useState<AdResponse[] | null>(null)
|
|
179
|
+
const [responseAds, setResponseAds] = useState<Record<string, AdResponse[]>>(
|
|
180
|
+
{},
|
|
181
|
+
)
|
|
182
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
183
|
+
|
|
184
|
+
// Check if terminal height is too small to show ads
|
|
185
|
+
const { terminalHeight } = useTerminalLayout()
|
|
186
|
+
const isVeryCompactHeight = terminalHeight <= 17
|
|
187
|
+
|
|
188
|
+
// Freebuff always shows ads even on compact screens (ads are mandatory there).
|
|
189
|
+
const isFreeMode = IS_FREEBUFF
|
|
190
|
+
|
|
191
|
+
// Skip ads on very compact screens unless we're in Freebuff (where ads are mandatory)
|
|
192
|
+
// Also skip if explicitly disabled (e.g. user has a subscription)
|
|
193
|
+
const shouldHideAds = !enabled || (isVeryCompactHeight && !isFreeMode)
|
|
194
|
+
|
|
195
|
+
// Use Zustand selector instead of manual subscription - only rerenders when value changes
|
|
196
|
+
const hasUserMessagedStore = useChatStore((s) =>
|
|
197
|
+
s.messages.some((m) => m.variant === 'user'),
|
|
198
|
+
)
|
|
199
|
+
// forceStart lets callers (e.g. the landing screen) opt out of the
|
|
200
|
+
// "wait for the first user message" gate.
|
|
201
|
+
const shouldStart = forceStart || hasUserMessagedStore
|
|
202
|
+
|
|
203
|
+
// Single consolidated controller ref
|
|
204
|
+
const ctrlRef = useRef<GravityController>({
|
|
205
|
+
choiceCache: [],
|
|
206
|
+
choiceCacheIndex: 0,
|
|
207
|
+
impressionsFired: new Set(),
|
|
208
|
+
adsShownSinceActivity: 0,
|
|
209
|
+
tickInFlight: false,
|
|
210
|
+
inlineQueue: createLazyResponseAdQueue<AdResponse>(),
|
|
211
|
+
eligibleSlotCounts: new Map(),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Ref for the tick function (avoids useCallback dependency issues)
|
|
215
|
+
const tickRef = useRef<() => void>(() => {})
|
|
216
|
+
|
|
217
|
+
// Ref to track whether ads should be hidden for use in async code
|
|
218
|
+
const shouldHideAdsRef = useRef(shouldHideAds)
|
|
219
|
+
shouldHideAdsRef.current = shouldHideAds
|
|
220
|
+
|
|
221
|
+
// Fire impression and update credits (called when showing an ad)
|
|
222
|
+
const recordImpressionOnce = (ad: AdResponse): void => {
|
|
223
|
+
// Don't record impressions when ads should be hidden
|
|
224
|
+
if (shouldHideAdsRef.current) return
|
|
225
|
+
|
|
226
|
+
const ctrl = ctrlRef.current
|
|
227
|
+
const { impUrl } = ad
|
|
228
|
+
if (!claimAdImpression(ctrl.impressionsFired, impUrl)) return
|
|
229
|
+
|
|
230
|
+
const recordLocalImpression = async (): Promise<void> => {
|
|
231
|
+
const authToken = getAuthToken()
|
|
232
|
+
if (!authToken) {
|
|
233
|
+
logger.warn('[ads] No auth token, skipping local impression recording')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Include mode in request - Freebuff should not grant credits (no balance concept).
|
|
238
|
+
const agentMode = useChatStore.getState().agentMode
|
|
239
|
+
|
|
240
|
+
const res = await fetch(`${WEBSITE_URL}/api/v1/ads/impression`, {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: {
|
|
243
|
+
'Content-Type': 'application/json',
|
|
244
|
+
Authorization: `Bearer ${authToken}`,
|
|
245
|
+
'User-Agent': getCliAdRequestUserAgent(),
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
impUrl,
|
|
249
|
+
mode: agentMode,
|
|
250
|
+
}),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (!res.ok) {
|
|
254
|
+
logger.debug(
|
|
255
|
+
{ status: res.status },
|
|
256
|
+
'[ads] Failed to record local ad impression',
|
|
257
|
+
)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const data = await res.json()
|
|
262
|
+
if (data.creditsGranted > 0) {
|
|
263
|
+
logger.info(
|
|
264
|
+
{ creditsGranted: data.creditsGranted },
|
|
265
|
+
'[ads] Ad impression credits granted',
|
|
266
|
+
)
|
|
267
|
+
// Also update credits in visible ads
|
|
268
|
+
setAds((cur) => {
|
|
269
|
+
if (!cur) return cur
|
|
270
|
+
return cur.map((a) =>
|
|
271
|
+
a.impUrl === impUrl ? { ...a, credits: data.creditsGranted } : a,
|
|
272
|
+
)
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (ad.provider === 'zeroclick' && ad.impressionIds?.length) {
|
|
278
|
+
void (async () => {
|
|
279
|
+
try {
|
|
280
|
+
const res = await fetch(ZEROCLICK_IMPRESSIONS_URL, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
body: JSON.stringify({ ids: ad.impressionIds }),
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
if (!res.ok) {
|
|
287
|
+
logger.debug(
|
|
288
|
+
{ status: res.status },
|
|
289
|
+
'[ads] Failed to record ZeroClick impression',
|
|
290
|
+
)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
logger.debug({ err }, '[ads] Failed to record ZeroClick impression')
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
recordLocalImpression().catch((err) => {
|
|
299
|
+
logger.debug({ err }, '[ads] Failed to record local ad impression')
|
|
300
|
+
})
|
|
301
|
+
})()
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
recordLocalImpression().catch((err) => {
|
|
306
|
+
logger.debug({ err }, '[ads] Failed to record ad impression')
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const recordClick = (ad: AdResponse): void => {
|
|
311
|
+
const authToken = getAuthToken()
|
|
312
|
+
if (!authToken) {
|
|
313
|
+
logger.warn('[ads] No auth token, skipping ad click recording')
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
void fetch(`${WEBSITE_URL}/api/v1/ads/click`, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: {
|
|
320
|
+
'Content-Type': 'application/json',
|
|
321
|
+
Authorization: `Bearer ${authToken}`,
|
|
322
|
+
'User-Agent': getCliAdRequestUserAgent(),
|
|
323
|
+
},
|
|
324
|
+
body: JSON.stringify({
|
|
325
|
+
impUrl: ad.impUrl,
|
|
326
|
+
...(surface ? { surface } : {}),
|
|
327
|
+
}),
|
|
328
|
+
})
|
|
329
|
+
.then((res) => {
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
logger.debug(
|
|
332
|
+
{ status: res.status },
|
|
333
|
+
'[ads] Failed to record ad click',
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
.catch((err) => {
|
|
338
|
+
logger.debug({ err }, '[ads] Failed to record ad click')
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
type FetchAdResult = { ads: AdResponse[] } | null
|
|
343
|
+
|
|
344
|
+
// Fetch an ad via web API
|
|
345
|
+
const fetchAd = async (params?: {
|
|
346
|
+
placementId?: string
|
|
347
|
+
}): Promise<FetchAdResult> => {
|
|
348
|
+
// Don't fetch ads when they should be hidden
|
|
349
|
+
if (shouldHideAdsRef.current) return null
|
|
350
|
+
if (!getAdsEnabled()) return null
|
|
351
|
+
|
|
352
|
+
const authToken = getAuthToken()
|
|
353
|
+
if (!authToken) {
|
|
354
|
+
logger.warn('[ads] No auth token available')
|
|
355
|
+
return null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Get message history from runState (populated after LLM responds)
|
|
359
|
+
const currentRunState = useChatStore.getState().runState
|
|
360
|
+
const messageHistory =
|
|
361
|
+
currentRunState?.sessionState?.mainAgentState?.messageHistory ?? []
|
|
362
|
+
const adMessages = convertToAdMessages(messageHistory)
|
|
363
|
+
|
|
364
|
+
// Also check UI messages for the latest user message
|
|
365
|
+
// (UI messages update immediately, runState.messageHistory updates after LLM responds)
|
|
366
|
+
const uiMessages = useChatStore.getState().messages
|
|
367
|
+
const lastUIMessage = [...uiMessages]
|
|
368
|
+
.reverse()
|
|
369
|
+
.find((msg) => msg.variant === 'user')
|
|
370
|
+
|
|
371
|
+
// If the latest UI user message isn't in our converted history, append it
|
|
372
|
+
// This ensures we always include the most recent user message even before LLM responds
|
|
373
|
+
if (lastUIMessage?.content) {
|
|
374
|
+
const lastAdUserMessage = [...adMessages]
|
|
375
|
+
.reverse()
|
|
376
|
+
.find((m) => m.role === 'user')
|
|
377
|
+
if (
|
|
378
|
+
!lastAdUserMessage ||
|
|
379
|
+
!lastAdUserMessage.content.includes(lastUIMessage.content)
|
|
380
|
+
) {
|
|
381
|
+
adMessages.push({
|
|
382
|
+
role: 'user',
|
|
383
|
+
content: `<user_message>${lastUIMessage.content}</user_message>`,
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const response = await fetch(`${WEBSITE_URL}/api/v1/ads`, {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
headers: {
|
|
392
|
+
'Content-Type': 'application/json',
|
|
393
|
+
Authorization: `Bearer ${authToken}`,
|
|
394
|
+
'User-Agent': getCliAdRequestUserAgent(),
|
|
395
|
+
},
|
|
396
|
+
body: JSON.stringify({
|
|
397
|
+
provider,
|
|
398
|
+
messages: adMessages,
|
|
399
|
+
sessionId: useChatStore.getState().chatSessionId,
|
|
400
|
+
device: getDeviceInfo(),
|
|
401
|
+
...(surface ? { surface } : {}),
|
|
402
|
+
...(params?.placementId ? { placementId: params.placementId } : {}),
|
|
403
|
+
// Native runtime UAs look bot-like to ad networks. Send the shared
|
|
404
|
+
// browser-like UA so every provider sees a usable targeting signal.
|
|
405
|
+
userAgent: getAdUserAgent(),
|
|
406
|
+
}),
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
let responseBody: unknown
|
|
411
|
+
try {
|
|
412
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
413
|
+
responseBody = contentType.includes('application/json')
|
|
414
|
+
? await response.json()
|
|
415
|
+
: await response.text()
|
|
416
|
+
} catch {
|
|
417
|
+
responseBody = 'Unable to parse error response'
|
|
418
|
+
}
|
|
419
|
+
logger.warn(
|
|
420
|
+
{ provider, status: response.status, response: responseBody },
|
|
421
|
+
'[ads] Web API returned error',
|
|
422
|
+
)
|
|
423
|
+
return null
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const data = await response.json()
|
|
427
|
+
|
|
428
|
+
if (Array.isArray(data.ads) && data.ads.length > 0) {
|
|
429
|
+
return {
|
|
430
|
+
ads: (data.ads as AdResponse[]).map((ad) => ({
|
|
431
|
+
...ad,
|
|
432
|
+
provider: data.provider ?? provider,
|
|
433
|
+
})),
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
logger.error({ err, provider }, '[ads] Failed to fetch ad')
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Update tick function (uses ref to avoid useCallback dependency issues)
|
|
444
|
+
tickRef.current = () => {
|
|
445
|
+
void (async () => {
|
|
446
|
+
const ctrl = ctrlRef.current
|
|
447
|
+
if (ctrl.tickInFlight) return
|
|
448
|
+
ctrl.tickInFlight = true
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
if (!getAdsEnabled()) return
|
|
452
|
+
|
|
453
|
+
// Derive "can fetch new ads" from counter and activity (no separate paused ref needed)
|
|
454
|
+
const canFetchNew =
|
|
455
|
+
ctrl.adsShownSinceActivity < MAX_ADS_AFTER_ACTIVITY &&
|
|
456
|
+
isUserActive(ACTIVITY_THRESHOLD_MS)
|
|
457
|
+
|
|
458
|
+
const result = canFetchNew
|
|
459
|
+
? await fetchAd({ placementId: slotPlacementId })
|
|
460
|
+
: null
|
|
461
|
+
|
|
462
|
+
if (result) {
|
|
463
|
+
addToChoiceCache(ctrl, result.ads)
|
|
464
|
+
ctrl.adsShownSinceActivity += 1
|
|
465
|
+
setAds(result.ads)
|
|
466
|
+
} else {
|
|
467
|
+
// Fall back to cached ads
|
|
468
|
+
const cachedSet = nextFromChoiceCache(ctrl)
|
|
469
|
+
if (cachedSet) {
|
|
470
|
+
ctrl.adsShownSinceActivity += 1
|
|
471
|
+
setAds(cachedSet)
|
|
472
|
+
} else {
|
|
473
|
+
setAds((cur) => (cur?.[0]?.provider === 'zeroclick' ? null : cur))
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} finally {
|
|
477
|
+
ctrl.tickInFlight = false
|
|
478
|
+
}
|
|
479
|
+
})()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Reset ads shown counter on user activity
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
if (!getAdsEnabled()) return
|
|
485
|
+
return subscribeToActivity(() => {
|
|
486
|
+
ctrlRef.current.adsShownSinceActivity = 0
|
|
487
|
+
})
|
|
488
|
+
}, [])
|
|
489
|
+
|
|
490
|
+
// Start rotation when user sends first message (or immediately if forced).
|
|
491
|
+
useEffect(() => {
|
|
492
|
+
if (!shouldStart || !getAdsEnabled() || shouldHideAds) return
|
|
493
|
+
|
|
494
|
+
setIsLoading(true)
|
|
495
|
+
|
|
496
|
+
// Fetch first ad immediately
|
|
497
|
+
void (async () => {
|
|
498
|
+
const result = await fetchAd({ placementId: slotPlacementId })
|
|
499
|
+
if (result) {
|
|
500
|
+
const ctrl = ctrlRef.current
|
|
501
|
+
addToChoiceCache(ctrl, result.ads)
|
|
502
|
+
setAds(result.ads)
|
|
503
|
+
ctrl.adsShownSinceActivity = 1
|
|
504
|
+
}
|
|
505
|
+
setIsLoading(false)
|
|
506
|
+
})()
|
|
507
|
+
|
|
508
|
+
// Start interval for rotation (consistent 60s intervals)
|
|
509
|
+
const id = setInterval(() => tickRef.current(), AD_ROTATION_INTERVAL_MS)
|
|
510
|
+
|
|
511
|
+
return () => {
|
|
512
|
+
clearInterval(id)
|
|
513
|
+
}
|
|
514
|
+
}, [shouldStart, shouldHideAds, provider, surface])
|
|
515
|
+
|
|
516
|
+
// Called by BlocksRenderer only when its streamed node count makes another
|
|
517
|
+
// between-node slot eligible, until the four-ad pool is full. Requests use
|
|
518
|
+
// the same placement id and are serialized per answer so higher-value early
|
|
519
|
+
// results retain their order. The renderer cycles that exact pool for later
|
|
520
|
+
// slots without additional auctions or impression events.
|
|
521
|
+
const requestResponseAds = (messageId: string, count: number): void => {
|
|
522
|
+
if (
|
|
523
|
+
!inline ||
|
|
524
|
+
!inlinePlacementId ||
|
|
525
|
+
count <= 0 ||
|
|
526
|
+
shouldHideAdsRef.current ||
|
|
527
|
+
!getAdsEnabled()
|
|
528
|
+
) {
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const messages = useChatStore.getState().messages
|
|
533
|
+
const answer = messages.find((m) => m.id === messageId)
|
|
534
|
+
if (!answer || !isInlineAdEligibleAnswer(answer)) {
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const ctrl = ctrlRef.current
|
|
539
|
+
const previousEligibleCount = ctrl.eligibleSlotCounts.get(messageId) ?? 0
|
|
540
|
+
if (count > previousEligibleCount) {
|
|
541
|
+
ctrl.eligibleSlotCounts.set(messageId, count)
|
|
542
|
+
const telemetryProperties = {
|
|
543
|
+
response_id: messageId,
|
|
544
|
+
chat_session_id: useChatStore.getState().chatSessionId,
|
|
545
|
+
eligible_slot_count: count,
|
|
546
|
+
pool_size: MAX_RESPONSE_AD_POOL_SIZE,
|
|
547
|
+
provider,
|
|
548
|
+
surface,
|
|
549
|
+
placement_id: inlinePlacementId,
|
|
550
|
+
is_freebuff: IS_FREEBUFF,
|
|
551
|
+
}
|
|
552
|
+
trackInlineAdEvent(
|
|
553
|
+
AnalyticsEvent.CLI_INLINE_AD_SLOT_ELIGIBLE,
|
|
554
|
+
telemetryProperties,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
if (
|
|
558
|
+
count > MAX_RESPONSE_AD_POOL_SIZE &&
|
|
559
|
+
previousEligibleCount <= MAX_RESPONSE_AD_POOL_SIZE
|
|
560
|
+
) {
|
|
561
|
+
trackInlineAdEvent(
|
|
562
|
+
AnalyticsEvent.CLI_INLINE_AD_POOL_REUSED,
|
|
563
|
+
telemetryProperties,
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
void requestLazyResponseAds({
|
|
569
|
+
queue: ctrl.inlineQueue,
|
|
570
|
+
messageId,
|
|
571
|
+
count,
|
|
572
|
+
fetchOne: async () => {
|
|
573
|
+
const result = await fetchAd({ placementId: inlinePlacementId })
|
|
574
|
+
return result?.ads[0] ?? null
|
|
575
|
+
},
|
|
576
|
+
onAd: (ad) => {
|
|
577
|
+
setResponseAds((prev) => ({
|
|
578
|
+
...prev,
|
|
579
|
+
[messageId]: [...(prev[messageId] ?? []), ad],
|
|
580
|
+
}))
|
|
581
|
+
},
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Don't return ads when ads should be hidden
|
|
586
|
+
const visible = shouldStart && !shouldHideAds
|
|
587
|
+
return {
|
|
588
|
+
ads: visible ? ads : null,
|
|
589
|
+
responseAds: visible ? responseAds : {},
|
|
590
|
+
requestResponseAds,
|
|
591
|
+
isLoading,
|
|
592
|
+
recordClick,
|
|
593
|
+
recordImpression: recordImpressionOnce,
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
type AdMessage = { role: 'user' | 'assistant'; content: string }
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Convert LLM message history to ad API format.
|
|
601
|
+
* Includes only user and assistant messages.
|
|
602
|
+
*/
|
|
603
|
+
const convertToAdMessages = (messages: Message[]): AdMessage[] => {
|
|
604
|
+
const adMessages: AdMessage[] = messages
|
|
605
|
+
.filter(
|
|
606
|
+
(message) => message.role === 'assistant' || message.role === 'user',
|
|
607
|
+
)
|
|
608
|
+
.filter(
|
|
609
|
+
(message) =>
|
|
610
|
+
!message.tags || !message.tags.includes('INSTRUCTIONS_PROMPT'),
|
|
611
|
+
)
|
|
612
|
+
.map((message) => ({
|
|
613
|
+
role: message.role,
|
|
614
|
+
content: message.content
|
|
615
|
+
.filter((c) => c.type === 'text')
|
|
616
|
+
.map((c) => c.text.trim())
|
|
617
|
+
.filter((c) => c !== '')
|
|
618
|
+
.join('\n\n')
|
|
619
|
+
.trim(),
|
|
620
|
+
}))
|
|
621
|
+
.filter((message) => message.content !== '')
|
|
622
|
+
|
|
623
|
+
return adMessages
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Device info sent to the ads API for targeting */
|
|
627
|
+
type DeviceInfo = {
|
|
628
|
+
os: 'macos' | 'windows' | 'linux'
|
|
629
|
+
timezone: string
|
|
630
|
+
locale: string
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Get device info for ads API */
|
|
634
|
+
function getDeviceInfo(): DeviceInfo {
|
|
635
|
+
// Map Node.js platform to Gravity API os values
|
|
636
|
+
const platformToOs: Record<string, 'macos' | 'windows' | 'linux'> = {
|
|
637
|
+
darwin: 'macos',
|
|
638
|
+
win32: 'windows',
|
|
639
|
+
linux: 'linux',
|
|
640
|
+
}
|
|
641
|
+
const os = platformToOs[process.platform] ?? 'linux'
|
|
642
|
+
|
|
643
|
+
// Get IANA timezone (e.g., "America/New_York")
|
|
644
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
645
|
+
|
|
646
|
+
// Get locale (e.g., "en-US")
|
|
647
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale
|
|
648
|
+
|
|
649
|
+
return { os, timezone, locale }
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function getCliAdRequestUserAgent(): string {
|
|
653
|
+
const product = IS_FREEBUFF ? 'Freebuff-CLI' : 'Anvil-CLI'
|
|
654
|
+
const version = getCliEnv().ANVIL_CLI_VERSION ?? 'dev'
|
|
655
|
+
return `${product}/${version}`
|
|
656
|
+
}
|