@alumbwe/anvil 1.0.19 → 1.0.20
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 +28 -30
- package/bin/anvil +2 -0
- package/package.json +71 -75
- package/src/agents/bundled-agents.generated.d.ts +14 -0
- package/src/agents/bundled-agents.generated.ts +3760 -0
- package/src/app.tsx +399 -0
- package/src/chat.tsx +1698 -0
- package/src/cli-args.ts +134 -0
- package/src/commands/__tests__/anvil-command-aliases.test.ts +56 -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__/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__/anvil-landing-heading-row.test.tsx +91 -0
- package/src/components/__tests__/anvil-model-selector.test.tsx +519 -0
- package/src/components/__tests__/anvil-offer-invariants.test.ts +54 -0
- package/src/components/__tests__/anvil-takeover-prompt.test.tsx +212 -0
- package/src/components/__tests__/chat-input-bar.test.tsx +130 -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-active-session-summary.tsx +63 -0
- package/src/components/anvil-landing-screen.tsx +865 -0
- package/src/components/anvil-mark.tsx +52 -0
- package/src/components/anvil-model-selector.tsx +1087 -0
- package/src/components/anvil-referral-banner.tsx +604 -0
- package/src/components/anvil-superseded-screen.tsx +62 -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/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-anvil-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-anvil-ctrl-c-exit.ts +23 -0
- package/src/hooks/use-anvil-session-progress.ts +34 -0
- package/src/hooks/use-anvil-session.ts +806 -0
- package/src/hooks/use-anvil-streak-query.ts +61 -0
- package/src/hooks/use-ask-user-bridge.ts +92 -0
- package/src/hooks/use-auth-query.ts +262 -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-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 +473 -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/setup/key-setup.ts +295 -0
- package/src/setup/load-env.ts +106 -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/anvil-model-store.ts +42 -0
- package/src/state/anvil-session-store.ts +53 -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/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/types/anvil-session.ts +17 -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/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__/anvil-instance-owner.test.ts +69 -0
- package/src/utils/__tests__/anvil-model-navigation.test.ts +102 -0
- package/src/utils/__tests__/anvil-premium-reset.test.ts +79 -0
- package/src/utils/__tests__/anvil-referral-cache.test.ts +116 -0
- package/src/utils/__tests__/anvil-session-api.test.ts +215 -0
- package/src/utils/__tests__/anvil-session-display.test.ts +23 -0
- package/src/utils/__tests__/anvil-streak-line.test.ts +217 -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__/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-agent-selection.ts +33 -0
- package/src/utils/anvil-api.ts +612 -0
- package/src/utils/anvil-client.ts +217 -0
- package/src/utils/anvil-instance-owner.ts +66 -0
- package/src/utils/anvil-model-navigation.ts +50 -0
- package/src/utils/anvil-premium-reset.ts +55 -0
- package/src/utils/anvil-referral-cache.ts +51 -0
- package/src/utils/anvil-session-api.ts +243 -0
- package/src/utils/anvil-session-display.ts +21 -0
- package/src/utils/anvil-streak-line.ts +93 -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/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
- package/bin/anvil.cjs +0 -40
- package/dist/bundled.js +0 -123508
- package/dist/bundled.js.map +0 -828
- package/dist/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/vendor/ripgrep/x64-win32/rg.exe +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { API_KEY_ENV_VAR } from '@anvil/common/old-constants'
|
|
2
|
+
import { AskUserBridge } from '@anvil/common/utils/ask-user-bridge'
|
|
3
|
+
import { AnvilClient } from '@anvil/sdk'
|
|
4
|
+
|
|
5
|
+
import { getAuthTokenDetails } from './auth'
|
|
6
|
+
import { getCliEnv, getSystemProcessEnv } from './env'
|
|
7
|
+
import { loadAgentDefinitions } from './local-agent-registry'
|
|
8
|
+
import { logger } from './logger'
|
|
9
|
+
import { createTraceWriter } from './trace-writer'
|
|
10
|
+
import { terminalCommandBroker } from './terminal-command-broker'
|
|
11
|
+
import { getRgPath } from '../native/ripgrep'
|
|
12
|
+
import { getProjectRoot } from '../project-files'
|
|
13
|
+
|
|
14
|
+
import type { ClientToolCall } from '@anvil/common/tools/list'
|
|
15
|
+
|
|
16
|
+
let clientInstance: AnvilClient | null = null
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Recursively removes undefined values from an object to ensure clean JSON serialization.
|
|
20
|
+
* This prevents issues with APIs that don't accept explicit undefined values.
|
|
21
|
+
*/
|
|
22
|
+
function removeUndefinedValues<T>(obj: T): T {
|
|
23
|
+
if (obj === null || obj === undefined) {
|
|
24
|
+
return obj
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(obj)) {
|
|
27
|
+
return obj.map(removeUndefinedValues) as T
|
|
28
|
+
}
|
|
29
|
+
if (typeof obj === 'object') {
|
|
30
|
+
const result: Record<string, unknown> = {}
|
|
31
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
32
|
+
if (value !== undefined) {
|
|
33
|
+
result[key] = removeUndefinedValues(value)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result as T
|
|
37
|
+
}
|
|
38
|
+
return obj
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reset the cached AnvilClient instance.
|
|
43
|
+
* This should be called after login to ensure the client is re-initialized with new credentials.
|
|
44
|
+
*/
|
|
45
|
+
export function resetAnvilClient(): void {
|
|
46
|
+
clientInstance = null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function getAnvilClient(): Promise<AnvilClient | null> {
|
|
50
|
+
if (!clientInstance) {
|
|
51
|
+
const { token: apiKey } = getAuthTokenDetails()
|
|
52
|
+
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
logger.warn(
|
|
55
|
+
{},
|
|
56
|
+
`No authentication token found. Please run the login flow or set ${API_KEY_ENV_VAR}.`,
|
|
57
|
+
)
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const projectRoot = getProjectRoot()
|
|
62
|
+
|
|
63
|
+
// Set up ripgrep path for SDK to use
|
|
64
|
+
const env = getCliEnv()
|
|
65
|
+
if (env.ANVIL_IS_BINARY) {
|
|
66
|
+
try {
|
|
67
|
+
const rgPath = await getRgPath()
|
|
68
|
+
// Note: We still set process.env here because SDK reads from it
|
|
69
|
+
getSystemProcessEnv().ANVIL_RG_PATH = rgPath
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error(error, 'Failed to set up ripgrep binary for SDK')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const agentDefinitions = loadAgentDefinitions()
|
|
77
|
+
clientInstance = new AnvilClient({
|
|
78
|
+
apiKey,
|
|
79
|
+
cwd: projectRoot,
|
|
80
|
+
agentDefinitions,
|
|
81
|
+
logger,
|
|
82
|
+
traceWriter: createTraceWriter(),
|
|
83
|
+
terminalCommandBroker,
|
|
84
|
+
overrideTools: {
|
|
85
|
+
ask_user: async (input: ClientToolCall<'ask_user'>['input']) => {
|
|
86
|
+
const askUserResponse = await AskUserBridge.request(
|
|
87
|
+
'cli-override',
|
|
88
|
+
input.questions,
|
|
89
|
+
)
|
|
90
|
+
const response = askUserResponse as {
|
|
91
|
+
answers?: Array<{ questionIndex: number; selectedOption: string }>
|
|
92
|
+
skipped?: boolean
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
type: 'json',
|
|
97
|
+
value: removeUndefinedValues(response),
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.error(error, 'Failed to initialize AnvilClient')
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return clientInstance
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getToolDisplayInfo(toolName: string): {
|
|
113
|
+
name: string
|
|
114
|
+
type: string
|
|
115
|
+
} {
|
|
116
|
+
const TOOL_NAME_OVERRIDES: Record<string, string> = {
|
|
117
|
+
list_directory: 'List Directories',
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const capitalizeWords = (str: string) => {
|
|
121
|
+
return str.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: TOOL_NAME_OVERRIDES[toolName] ?? capitalizeWords(toolName),
|
|
126
|
+
type: 'tool',
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toYaml(obj: any, indent = 0): string {
|
|
131
|
+
const spaces = ' '.repeat(indent)
|
|
132
|
+
|
|
133
|
+
if (obj === null || obj === undefined) {
|
|
134
|
+
return 'null'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof obj === 'string') {
|
|
138
|
+
if (obj.includes('\n')) {
|
|
139
|
+
const lines = obj.split('\n')
|
|
140
|
+
return (
|
|
141
|
+
'|\n' + lines.map((line) => ' '.repeat(indent + 1) + line).join('\n')
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
return obj.includes(':') || obj.includes('#') ? `"${obj}"` : obj
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
|
148
|
+
return String(obj)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (Array.isArray(obj)) {
|
|
152
|
+
if (obj.length === 0) return '[]'
|
|
153
|
+
return (
|
|
154
|
+
'\n' +
|
|
155
|
+
obj
|
|
156
|
+
.map((item) => spaces + '- ' + toYaml(item, indent + 1).trimStart())
|
|
157
|
+
.join('\n')
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof obj === 'object') {
|
|
162
|
+
const entries = Object.entries(obj)
|
|
163
|
+
if (entries.length === 0) return '{}'
|
|
164
|
+
|
|
165
|
+
return entries
|
|
166
|
+
.map(([key, value]) => {
|
|
167
|
+
const yamlValue = toYaml(value, indent + 1)
|
|
168
|
+
if (
|
|
169
|
+
typeof value === 'object' &&
|
|
170
|
+
value !== null &&
|
|
171
|
+
!Array.isArray(value) &&
|
|
172
|
+
Object.keys(value).length > 0
|
|
173
|
+
) {
|
|
174
|
+
return `${spaces}${key}:\n${yamlValue}`
|
|
175
|
+
}
|
|
176
|
+
if (typeof value === 'string' && value.includes('\n')) {
|
|
177
|
+
return `${spaces}${key}: ${yamlValue}`
|
|
178
|
+
}
|
|
179
|
+
return `${spaces}${key}: ${yamlValue}`
|
|
180
|
+
})
|
|
181
|
+
.join('\n')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return String(obj)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function formatToolOutput(output: unknown): string {
|
|
188
|
+
if (!output) return ''
|
|
189
|
+
|
|
190
|
+
if (Array.isArray(output)) {
|
|
191
|
+
return output
|
|
192
|
+
.map((item) => {
|
|
193
|
+
if (item.type === 'json') {
|
|
194
|
+
// Handle errorMessage in the value object
|
|
195
|
+
if (
|
|
196
|
+
item.value &&
|
|
197
|
+
typeof item.value === 'object' &&
|
|
198
|
+
'errorMessage' in item.value
|
|
199
|
+
) {
|
|
200
|
+
return String(item.value.errorMessage)
|
|
201
|
+
}
|
|
202
|
+
return toYaml(item.value)
|
|
203
|
+
}
|
|
204
|
+
if (item.type === 'text') {
|
|
205
|
+
return item.text || ''
|
|
206
|
+
}
|
|
207
|
+
return String(item)
|
|
208
|
+
})
|
|
209
|
+
.join('\n')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (typeof output === 'string') {
|
|
213
|
+
return output
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return toYaml(output)
|
|
217
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import { getConfigDir } from './auth'
|
|
5
|
+
import { logger } from './logger'
|
|
6
|
+
|
|
7
|
+
interface AnvilInstanceOwner {
|
|
8
|
+
instanceId: string
|
|
9
|
+
pid: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const OWNER_FILE = 'anvil-instance-owner.json'
|
|
13
|
+
|
|
14
|
+
const getOwnerPath = (): string => path.join(getConfigDir(), OWNER_FILE)
|
|
15
|
+
|
|
16
|
+
function readOwner(): AnvilInstanceOwner | null {
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(getOwnerPath(), 'utf8')
|
|
19
|
+
const parsed = JSON.parse(raw) as Partial<AnvilInstanceOwner>
|
|
20
|
+
if (
|
|
21
|
+
typeof parsed.instanceId !== 'string' ||
|
|
22
|
+
typeof parsed.pid !== 'number'
|
|
23
|
+
) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
instanceId: parsed.instanceId,
|
|
28
|
+
pid: parsed.pid,
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isProcessRunning(pid: number): boolean {
|
|
36
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
37
|
+
try {
|
|
38
|
+
process.kill(pid, 0)
|
|
39
|
+
return true
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function recordAnvilInstanceOwner(instanceId: string): void {
|
|
46
|
+
try {
|
|
47
|
+
fs.mkdirSync(getConfigDir(), { recursive: true })
|
|
48
|
+
fs.writeFileSync(
|
|
49
|
+
getOwnerPath(),
|
|
50
|
+
JSON.stringify({ instanceId, pid: process.pid }, null, 2),
|
|
51
|
+
)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.debug(
|
|
54
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
55
|
+
'[anvil-session] Failed to record local owner',
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isAnvilInstanceOwnedByDeadLocalProcess(
|
|
61
|
+
instanceId: string,
|
|
62
|
+
): boolean {
|
|
63
|
+
const owner = readOwner()
|
|
64
|
+
if (!owner || owner.instanceId !== instanceId) return false
|
|
65
|
+
return !isProcessRunning(owner.pid)
|
|
66
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type AnvilModelNavigationDirection = 'forward' | 'backward'
|
|
2
|
+
|
|
3
|
+
const FORWARD_KEY_NAMES = new Set(['right', 'down'])
|
|
4
|
+
const BACKWARD_KEY_NAMES = new Set(['left', 'up'])
|
|
5
|
+
const FORWARD_TAB_SEQUENCES = new Set(['\t', '\x1b[9u'])
|
|
6
|
+
const BACKWARD_TAB_SEQUENCES = new Set([
|
|
7
|
+
'\x1b[Z',
|
|
8
|
+
'\x1b[9;2u',
|
|
9
|
+
'\x1b[27;2;9~',
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
export function nextAnvilModelId(params: {
|
|
13
|
+
modelIds: readonly string[]
|
|
14
|
+
focusedId: string
|
|
15
|
+
direction: AnvilModelNavigationDirection
|
|
16
|
+
}): string | null {
|
|
17
|
+
const { modelIds, focusedId, direction } = params
|
|
18
|
+
if (modelIds.length === 0) return null
|
|
19
|
+
|
|
20
|
+
const currentIdx = modelIds.indexOf(focusedId)
|
|
21
|
+
if (currentIdx === -1) return modelIds[0] ?? null
|
|
22
|
+
|
|
23
|
+
const step = direction === 'forward' ? 1 : -1
|
|
24
|
+
return modelIds[(currentIdx + step + modelIds.length) % modelIds.length]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function anvilModelNavigationDirectionForKey(key: {
|
|
28
|
+
name?: string
|
|
29
|
+
shift?: boolean
|
|
30
|
+
sequence?: string
|
|
31
|
+
raw?: string
|
|
32
|
+
}): AnvilModelNavigationDirection | null {
|
|
33
|
+
const name = (key.name ?? '').toLowerCase()
|
|
34
|
+
const sequence = key.sequence ?? key.raw ?? ''
|
|
35
|
+
|
|
36
|
+
if (FORWARD_KEY_NAMES.has(name)) return 'forward'
|
|
37
|
+
if (BACKWARD_KEY_NAMES.has(name)) return 'backward'
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
(name === 'tab' && Boolean(key.shift)) ||
|
|
41
|
+
BACKWARD_TAB_SEQUENCES.has(sequence)
|
|
42
|
+
) {
|
|
43
|
+
return 'backward'
|
|
44
|
+
}
|
|
45
|
+
if (name === 'tab' || FORWARD_TAB_SEQUENCES.has(sequence)) {
|
|
46
|
+
return 'forward'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ANVIL_PREMIUM_SESSION_RESET_TIMEZONE } from '@anvil/common/constants/anvil-models'
|
|
2
|
+
import { getZonedDayBounds } from '@anvil/common/util/zoned-time'
|
|
3
|
+
|
|
4
|
+
import type { AnvilSessionRateLimitByModel } from '@anvil/common/types/anvil-session'
|
|
5
|
+
|
|
6
|
+
export function getAnvilPremiumResetAt(params: {
|
|
7
|
+
rateLimitsByModel?: AnvilSessionRateLimitByModel
|
|
8
|
+
nowMs: number
|
|
9
|
+
}): Date {
|
|
10
|
+
const { rateLimitsByModel, nowMs } = params
|
|
11
|
+
const serverResetAt = rateLimitsByModel
|
|
12
|
+
? Object.values(rateLimitsByModel)[0]?.resetAt
|
|
13
|
+
: undefined
|
|
14
|
+
const parsedServerResetAt = serverResetAt ? new Date(serverResetAt) : null
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
parsedServerResetAt &&
|
|
18
|
+
Number.isFinite(parsedServerResetAt.getTime())
|
|
19
|
+
) {
|
|
20
|
+
return parsedServerResetAt
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return getZonedDayBounds(
|
|
24
|
+
new Date(nowMs),
|
|
25
|
+
ANVIL_PREMIUM_SESSION_RESET_TIMEZONE,
|
|
26
|
+
).resetsAt
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Human "resets in …" countdown. Daily pools stop at hours (`withDays` off);
|
|
31
|
+
* a multi-day window (e.g. a lapsed reset) sets `withDays` so it reads as "2d 5h"
|
|
32
|
+
* instead of a 100-hour figure.
|
|
33
|
+
*/
|
|
34
|
+
export function formatAnvilPremiumResetCountdown(
|
|
35
|
+
resetAt: Date,
|
|
36
|
+
nowMs: number,
|
|
37
|
+
{ withDays = false }: { withDays?: boolean } = {},
|
|
38
|
+
): string {
|
|
39
|
+
const diffMs = resetAt.getTime() - nowMs
|
|
40
|
+
if (!Number.isFinite(diffMs) || diffMs <= 0) return 'now'
|
|
41
|
+
|
|
42
|
+
const totalMinutes = Math.max(1, Math.floor(diffMs / 60_000))
|
|
43
|
+
if (withDays) {
|
|
44
|
+
const days = Math.floor(totalMinutes / (60 * 24))
|
|
45
|
+
if (days > 0) {
|
|
46
|
+
const hours = Math.floor((totalMinutes % (60 * 24)) / 60)
|
|
47
|
+
return hours === 0 ? `${days}d` : `${days}d ${hours}h`
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const hours = Math.floor(totalMinutes / 60)
|
|
51
|
+
const minutes = totalMinutes % 60
|
|
52
|
+
|
|
53
|
+
if (hours === 0) return `${minutes}m`
|
|
54
|
+
return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`
|
|
55
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getReferralInfo } from '@anvil/common/types/anvil-session'
|
|
2
|
+
|
|
3
|
+
import type { AnvilAccessTier } from '@anvil/common/constants/anvil-models'
|
|
4
|
+
import type { AnvilReferralInfo } from '@anvil/common/types/anvil-session'
|
|
5
|
+
import type { AnvilSessionResponse } from '../types/anvil-session'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Process-wide cache of the most recent referral block the server sent for
|
|
9
|
+
* each access tier.
|
|
10
|
+
*
|
|
11
|
+
* The server only attaches `referral` to `none` (landing) responses — once the
|
|
12
|
+
* user joins (queued/active) or ends a session (ended) it's dropped from the
|
|
13
|
+
* payload. That breaks the return-to-landing flow: after a session ends the
|
|
14
|
+
* slot DELETE leaves an `ended` row, so the landing GET sees `ended` (no
|
|
15
|
+
* referral) until that row is swept, which would otherwise blank the GLM
|
|
16
|
+
* referral banner for the whole visit. Caching the last-known block lets the
|
|
17
|
+
* picker re-render it immediately; a later clean `none` GET refreshes it.
|
|
18
|
+
*/
|
|
19
|
+
let referralByAccessTier: Partial<
|
|
20
|
+
Record<AnvilAccessTier, AnvilReferralInfo>
|
|
21
|
+
> = {}
|
|
22
|
+
|
|
23
|
+
/** Remember the referral block whenever a response includes one, so it can be
|
|
24
|
+
* carried across the join → end → return-to-landing round-trip. Active/ended
|
|
25
|
+
* responses intentionally omit it and keep the prior value; an authoritative
|
|
26
|
+
* `none` response without it clears stale metadata for that tier. */
|
|
27
|
+
export function rememberReferral(session: AnvilSessionResponse | null): void {
|
|
28
|
+
const referral = getReferralInfo(session)
|
|
29
|
+
const accessTier =
|
|
30
|
+
session && 'accessTier' in session ? session.accessTier : undefined
|
|
31
|
+
// Full- and limited-tier referral blocks have different meanings. Never
|
|
32
|
+
// cache one without the tier needed to interpret it safely.
|
|
33
|
+
if (!accessTier) return
|
|
34
|
+
if (referral) {
|
|
35
|
+
referralByAccessTier[accessTier] = referral
|
|
36
|
+
} else if (session?.status === 'none') {
|
|
37
|
+
delete referralByAccessTier[accessTier]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The last referral block seen for this tier, if any. */
|
|
42
|
+
export function getCachedReferral(
|
|
43
|
+
accessTier: AnvilAccessTier | undefined,
|
|
44
|
+
): AnvilReferralInfo | undefined {
|
|
45
|
+
return accessTier ? referralByAccessTier[accessTier] : undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Clear account-scoped referral metadata when its session owner unmounts. */
|
|
49
|
+
export function clearReferralCache(): void {
|
|
50
|
+
referralByAccessTier = {}
|
|
51
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { env } from '@anvil/common/env'
|
|
2
|
+
import {
|
|
3
|
+
ANVIL_COMPACT_SESSION_HEADER,
|
|
4
|
+
ANVIL_INSTANCE_HEADER,
|
|
5
|
+
ANVIL_MODEL_HEADER,
|
|
6
|
+
} from '@anvil/common/constants/anvil-models'
|
|
7
|
+
|
|
8
|
+
import { useAnvilSessionStore } from '../state/anvil-session-store'
|
|
9
|
+
import { getAuthTokenDetails } from './auth'
|
|
10
|
+
import { IS_ANVIL } from './constants'
|
|
11
|
+
|
|
12
|
+
import type { AnvilSessionResponse } from '../types/anvil-session'
|
|
13
|
+
import type { AnvilSessionServerResponse } from '@anvil/common/types/anvil-session'
|
|
14
|
+
|
|
15
|
+
const SESSION_FETCH_TIMEOUT_MS = 20_000
|
|
16
|
+
export type AnvilSessionMethod = 'POST' | 'GET' | 'DELETE'
|
|
17
|
+
|
|
18
|
+
export class AnvilSessionRequestError extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
message: string,
|
|
21
|
+
readonly statusCode: number,
|
|
22
|
+
readonly retryAfterMs?: number,
|
|
23
|
+
readonly errorCode?: string,
|
|
24
|
+
) {
|
|
25
|
+
super(message)
|
|
26
|
+
this.name = 'AnvilSessionRequestError'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isAnvilSessionTimeoutError(error: unknown): boolean {
|
|
31
|
+
return (
|
|
32
|
+
error instanceof Error &&
|
|
33
|
+
(error.name === 'TimeoutError' || /timeout|timed out/i.test(error.message))
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type AnvilSessionFailureDisposition = 'retry' | 'stop' | 'unknown'
|
|
38
|
+
|
|
39
|
+
/** How the poll loop should handle a failed request.
|
|
40
|
+
*
|
|
41
|
+
* A POST without a response may already have rotated the active instance, so
|
|
42
|
+
* repeating it is unsafe without protocol-level idempotency. HTTP 408, 429,
|
|
43
|
+
* and 503 responses are the exception: edge rejection or admission shedding
|
|
44
|
+
* produces them before the session mutation can commit. GET is read-only and
|
|
45
|
+
* can retry transient failures normally. */
|
|
46
|
+
export function classifyAnvilSessionRequestFailure(
|
|
47
|
+
method: Extract<AnvilSessionMethod, 'POST' | 'GET'>,
|
|
48
|
+
error: unknown,
|
|
49
|
+
): AnvilSessionFailureDisposition {
|
|
50
|
+
if (method === 'POST') {
|
|
51
|
+
if (!(error instanceof AnvilSessionRequestError)) return 'unknown'
|
|
52
|
+
// These responses are produced before the session mutation can commit:
|
|
53
|
+
// 408/429 come from an edge or unparsed response (the endpoint's typed
|
|
54
|
+
// rate-limit responses are returned above), and a 503 means no handler was
|
|
55
|
+
// available or admission shed the request. Retrying them cannot repeat a
|
|
56
|
+
// successful takeover.
|
|
57
|
+
if ([408, 429, 503].includes(error.statusCode)) {
|
|
58
|
+
return 'retry'
|
|
59
|
+
}
|
|
60
|
+
return error.statusCode >= 400 && error.statusCode < 500
|
|
61
|
+
? 'stop'
|
|
62
|
+
: 'unknown'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!(error instanceof AnvilSessionRequestError)) return 'retry'
|
|
66
|
+
return error.statusCode === 408 ||
|
|
67
|
+
error.statusCode === 429 ||
|
|
68
|
+
error.statusCode >= 500
|
|
69
|
+
? 'retry'
|
|
70
|
+
: 'stop'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseRetryAfterMs(
|
|
74
|
+
value: string | null,
|
|
75
|
+
nowMs = Date.now(),
|
|
76
|
+
): number | undefined {
|
|
77
|
+
if (!value) return undefined
|
|
78
|
+
const seconds = Number(value)
|
|
79
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
80
|
+
const milliseconds = seconds * 1_000
|
|
81
|
+
return Number.isFinite(milliseconds) ? Math.ceil(milliseconds) : undefined
|
|
82
|
+
}
|
|
83
|
+
const dateMs = Date.parse(value)
|
|
84
|
+
return Number.isFinite(dateMs) ? Math.max(0, dateMs - nowMs) : undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Combine the caller's abort signal with a per-request timeout. */
|
|
88
|
+
export function sessionFetchSignal(
|
|
89
|
+
signal: AbortSignal | undefined,
|
|
90
|
+
timeoutMs: number = SESSION_FETCH_TIMEOUT_MS,
|
|
91
|
+
): AbortSignal {
|
|
92
|
+
const timeout = AbortSignal.timeout(timeoutMs)
|
|
93
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sessionEndpoint(): string {
|
|
97
|
+
const base = (
|
|
98
|
+
env.NEXT_PUBLIC_ANVIL_APP_URL || 'https://codebuff.com'
|
|
99
|
+
).replace(/\/$/, '')
|
|
100
|
+
return `${base}/api/v1/anvil/session`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function callAnvilSession(
|
|
104
|
+
method: AnvilSessionMethod,
|
|
105
|
+
token: string,
|
|
106
|
+
opts: {
|
|
107
|
+
instanceId?: string
|
|
108
|
+
model?: string
|
|
109
|
+
signal?: AbortSignal
|
|
110
|
+
compact?: boolean
|
|
111
|
+
} = {},
|
|
112
|
+
): Promise<AnvilSessionServerResponse> {
|
|
113
|
+
const headers: Record<string, string> = { Authorization: `Bearer ${token}` }
|
|
114
|
+
if (method === 'GET' && opts.instanceId) {
|
|
115
|
+
headers[ANVIL_INSTANCE_HEADER] = opts.instanceId
|
|
116
|
+
}
|
|
117
|
+
if (method === 'GET' && opts.compact) {
|
|
118
|
+
headers[ANVIL_COMPACT_SESSION_HEADER] = '1'
|
|
119
|
+
}
|
|
120
|
+
if (method === 'POST' && opts.model) {
|
|
121
|
+
headers[ANVIL_MODEL_HEADER] = opts.model
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const response = await fetch(sessionEndpoint(), {
|
|
125
|
+
method,
|
|
126
|
+
headers,
|
|
127
|
+
signal: sessionFetchSignal(opts.signal),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (response.status === 404) {
|
|
131
|
+
return { status: 'none' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (response.status === 403) {
|
|
135
|
+
const body = (await response
|
|
136
|
+
.json()
|
|
137
|
+
.catch(() => null)) as AnvilSessionServerResponse | null
|
|
138
|
+
if (
|
|
139
|
+
body &&
|
|
140
|
+
(body.status === 'country_blocked' || body.status === 'banned')
|
|
141
|
+
) {
|
|
142
|
+
return body
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (response.status === 409 && method === 'POST') {
|
|
147
|
+
const body = (await response
|
|
148
|
+
.json()
|
|
149
|
+
.catch(() => null)) as AnvilSessionServerResponse | null
|
|
150
|
+
if (
|
|
151
|
+
body &&
|
|
152
|
+
(body.status === 'model_locked' || body.status === 'model_unavailable')
|
|
153
|
+
) {
|
|
154
|
+
return body
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (response.status === 429 && method === 'POST') {
|
|
159
|
+
const body = (await response
|
|
160
|
+
.json()
|
|
161
|
+
.catch(() => null)) as AnvilSessionServerResponse | null
|
|
162
|
+
if (
|
|
163
|
+
body &&
|
|
164
|
+
(body.status === 'rate_limited' ||
|
|
165
|
+
body.status === 'spend_limited' ||
|
|
166
|
+
body.status === 'ip_capped')
|
|
167
|
+
) {
|
|
168
|
+
return body
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const text = await response.text().catch(() => '')
|
|
174
|
+
let errorCode: string | undefined
|
|
175
|
+
try {
|
|
176
|
+
const body = JSON.parse(text) as { error?: unknown }
|
|
177
|
+
if (typeof body.error === 'string') errorCode = body.error
|
|
178
|
+
} catch {
|
|
179
|
+
// Non-JSON errors have no machine-readable code.
|
|
180
|
+
}
|
|
181
|
+
throw new AnvilSessionRequestError(
|
|
182
|
+
`anvil session ${method} failed: ${response.status} ${text.slice(0, 200)}`,
|
|
183
|
+
response.status,
|
|
184
|
+
parseRetryAfterMs(response.headers.get('retry-after')),
|
|
185
|
+
errorCode,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (await response.json()) as AnvilSessionServerResponse
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A compact poll omits quota fields that were already returned by admission.
|
|
193
|
+
* Keep that snapshot only for the same active session; null tells the poller
|
|
194
|
+
* to fetch one full response before compacting again. */
|
|
195
|
+
export function mergeCompactActiveSession(
|
|
196
|
+
current: AnvilSessionResponse | null,
|
|
197
|
+
next: AnvilSessionServerResponse,
|
|
198
|
+
): AnvilSessionServerResponse | null {
|
|
199
|
+
if (
|
|
200
|
+
current?.status !== 'active' ||
|
|
201
|
+
next.status !== 'active' ||
|
|
202
|
+
current.instanceId !== next.instanceId ||
|
|
203
|
+
current.model !== next.model
|
|
204
|
+
) {
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
...next,
|
|
209
|
+
rateLimit: next.rateLimit ?? current.rateLimit,
|
|
210
|
+
rateLimitsByModel: next.rateLimitsByModel ?? current.rateLimitsByModel,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function holdsLiveAnvilSlot(
|
|
215
|
+
current: AnvilSessionResponse | null,
|
|
216
|
+
): boolean {
|
|
217
|
+
if (!current) return false
|
|
218
|
+
return (
|
|
219
|
+
current.status === 'active' ||
|
|
220
|
+
(current.status === 'ended' && Boolean(current.instanceId))
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Best-effort DELETE of the caller's session row when it holds a live slot. */
|
|
225
|
+
export async function releaseAnvilSlot(): Promise<void> {
|
|
226
|
+
const current = useAnvilSessionStore.getState().session
|
|
227
|
+
if (!holdsLiveAnvilSlot(current)) return
|
|
228
|
+
|
|
229
|
+
const { token } = getAuthTokenDetails()
|
|
230
|
+
if (!token) return
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await callAnvilSession('DELETE', token)
|
|
234
|
+
} catch {
|
|
235
|
+
// The server-side sweep is the backstop.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Release the Anvil slot on exit paths that skip React unmount. */
|
|
240
|
+
export async function endAnvilSessionBestEffort(): Promise<void> {
|
|
241
|
+
if (!IS_ANVIL) return
|
|
242
|
+
await releaseAnvilSlot()
|
|
243
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const ANVIL_COUNTDOWN_VISIBLE_MS = 5 * 60_000
|
|
2
|
+
|
|
3
|
+
export const formatAnvilSessionCountdown = (ms: number): string => {
|
|
4
|
+
if (ms <= 0) return 'expiring…'
|
|
5
|
+
const totalSeconds = Math.ceil(ms / 1000)
|
|
6
|
+
const m = Math.floor(totalSeconds / 60)
|
|
7
|
+
const s = totalSeconds % 60
|
|
8
|
+
return `${m}:${s.toString().padStart(2, '0')}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const formatAnvilSessionRemaining = (ms: number): string => {
|
|
12
|
+
if (ms <= 0) return 'expiring…'
|
|
13
|
+
if (ms < ANVIL_COUNTDOWN_VISIBLE_MS) {
|
|
14
|
+
return `${formatAnvilSessionCountdown(ms)} left`
|
|
15
|
+
}
|
|
16
|
+
const totalMinutes = Math.ceil(ms / 60_000)
|
|
17
|
+
if (totalMinutes < 60) return `${totalMinutes}m left`
|
|
18
|
+
const hours = Math.floor(totalMinutes / 60)
|
|
19
|
+
const minutes = totalMinutes % 60
|
|
20
|
+
return minutes === 0 ? `${hours}h left` : `${hours}h ${minutes}m left`
|
|
21
|
+
}
|