@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,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getFreebuffBase3RootAgentIdForModel,
|
|
3
|
+
getFreebuffRootAgentIdForModel,
|
|
4
|
+
} from '@anvil/common/constants/free-agents'
|
|
5
|
+
|
|
6
|
+
import { getSelectedFreebuffModel } from '../state/freebuff-model-store'
|
|
7
|
+
import {
|
|
8
|
+
AGENT_MODE_TO_ID,
|
|
9
|
+
CLI_HARNESS,
|
|
10
|
+
IS_FREEBUFF,
|
|
11
|
+
type AgentMode,
|
|
12
|
+
} from './constants'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Freebuff is locked to LITE (chat-store's setAgentMode is a no-op when
|
|
16
|
+
* IS_FREEBUFF), so this is effectively "which root does the selected model
|
|
17
|
+
* run". Both harnesses have a root per picker model; CLI_HARNESS picks the
|
|
18
|
+
* family. It is currently base3; keeping both branches live preserves the
|
|
19
|
+
* release-based rollback path for the CLI.
|
|
20
|
+
*/
|
|
21
|
+
export function getFreebuffCliAgentIdForModel(model: string): string {
|
|
22
|
+
return CLI_HARNESS === 'base3'
|
|
23
|
+
? getFreebuffBase3RootAgentIdForModel(model)
|
|
24
|
+
: getFreebuffRootAgentIdForModel(model)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAgentIdForMode(agentMode: AgentMode): string {
|
|
28
|
+
if (IS_FREEBUFF && agentMode === 'LITE') {
|
|
29
|
+
return getFreebuffCliAgentIdForModel(getSelectedFreebuffModel())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return AGENT_MODE_TO_ID[agentMode]
|
|
33
|
+
}
|
|
@@ -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 FreebuffInstanceOwner {
|
|
8
|
+
instanceId: string
|
|
9
|
+
pid: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const OWNER_FILE = 'freebuff-instance-owner.json'
|
|
13
|
+
|
|
14
|
+
const getOwnerPath = (): string => path.join(getConfigDir(), OWNER_FILE)
|
|
15
|
+
|
|
16
|
+
function readOwner(): FreebuffInstanceOwner | null {
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(getOwnerPath(), 'utf8')
|
|
19
|
+
const parsed = JSON.parse(raw) as Partial<FreebuffInstanceOwner>
|
|
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 recordFreebuffInstanceOwner(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
|
+
'[freebuff-session] Failed to record local owner',
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isFreebuffInstanceOwnedByDeadLocalProcess(
|
|
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 FreebuffModelNavigationDirection = '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 nextFreebuffModelId(params: {
|
|
13
|
+
modelIds: readonly string[]
|
|
14
|
+
focusedId: string
|
|
15
|
+
direction: FreebuffModelNavigationDirection
|
|
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 freebuffModelNavigationDirectionForKey(key: {
|
|
28
|
+
name?: string
|
|
29
|
+
shift?: boolean
|
|
30
|
+
sequence?: string
|
|
31
|
+
raw?: string
|
|
32
|
+
}): FreebuffModelNavigationDirection | 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 { FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE } from '@anvil/common/constants/freebuff-models'
|
|
2
|
+
import { getZonedDayBounds } from '@anvil/common/util/zoned-time'
|
|
3
|
+
|
|
4
|
+
import type { FreebuffSessionRateLimitByModel } from '@anvil/common/types/freebuff-session'
|
|
5
|
+
|
|
6
|
+
export function getFreebuffPremiumResetAt(params: {
|
|
7
|
+
rateLimitsByModel?: FreebuffSessionRateLimitByModel
|
|
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
|
+
FREEBUFF_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 formatFreebuffPremiumResetCountdown(
|
|
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/freebuff-session'
|
|
2
|
+
|
|
3
|
+
import type { FreebuffAccessTier } from '@anvil/common/constants/freebuff-models'
|
|
4
|
+
import type { FreebuffReferralInfo } from '@anvil/common/types/freebuff-session'
|
|
5
|
+
import type { FreebuffSessionResponse } from '../types/freebuff-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<FreebuffAccessTier, FreebuffReferralInfo>
|
|
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: FreebuffSessionResponse | 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: FreebuffAccessTier | undefined,
|
|
44
|
+
): FreebuffReferralInfo | 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
|
+
FREEBUFF_COMPACT_SESSION_HEADER,
|
|
4
|
+
FREEBUFF_INSTANCE_HEADER,
|
|
5
|
+
FREEBUFF_MODEL_HEADER,
|
|
6
|
+
} from '@anvil/common/constants/freebuff-models'
|
|
7
|
+
|
|
8
|
+
import { useFreebuffSessionStore } from '../state/freebuff-session-store'
|
|
9
|
+
import { getAuthTokenDetails } from './auth'
|
|
10
|
+
import { IS_FREEBUFF } from './constants'
|
|
11
|
+
|
|
12
|
+
import type { FreebuffSessionResponse } from '../types/freebuff-session'
|
|
13
|
+
import type { FreebuffSessionServerResponse } from '@anvil/common/types/freebuff-session'
|
|
14
|
+
|
|
15
|
+
const SESSION_FETCH_TIMEOUT_MS = 20_000
|
|
16
|
+
export type FreebuffSessionMethod = 'POST' | 'GET' | 'DELETE'
|
|
17
|
+
|
|
18
|
+
export class FreebuffSessionRequestError 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 = 'FreebuffSessionRequestError'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isFreebuffSessionTimeoutError(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 FreebuffSessionFailureDisposition = '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 classifyFreebuffSessionRequestFailure(
|
|
47
|
+
method: Extract<FreebuffSessionMethod, 'POST' | 'GET'>,
|
|
48
|
+
error: unknown,
|
|
49
|
+
): FreebuffSessionFailureDisposition {
|
|
50
|
+
if (method === 'POST') {
|
|
51
|
+
if (!(error instanceof FreebuffSessionRequestError)) 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 FreebuffSessionRequestError)) 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/freebuff/session`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function callFreebuffSession(
|
|
104
|
+
method: FreebuffSessionMethod,
|
|
105
|
+
token: string,
|
|
106
|
+
opts: {
|
|
107
|
+
instanceId?: string
|
|
108
|
+
model?: string
|
|
109
|
+
signal?: AbortSignal
|
|
110
|
+
compact?: boolean
|
|
111
|
+
} = {},
|
|
112
|
+
): Promise<FreebuffSessionServerResponse> {
|
|
113
|
+
const headers: Record<string, string> = { Authorization: `Bearer ${token}` }
|
|
114
|
+
if (method === 'GET' && opts.instanceId) {
|
|
115
|
+
headers[FREEBUFF_INSTANCE_HEADER] = opts.instanceId
|
|
116
|
+
}
|
|
117
|
+
if (method === 'GET' && opts.compact) {
|
|
118
|
+
headers[FREEBUFF_COMPACT_SESSION_HEADER] = '1'
|
|
119
|
+
}
|
|
120
|
+
if (method === 'POST' && opts.model) {
|
|
121
|
+
headers[FREEBUFF_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 FreebuffSessionServerResponse | 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 FreebuffSessionServerResponse | 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 FreebuffSessionServerResponse | 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 FreebuffSessionRequestError(
|
|
182
|
+
`freebuff 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 FreebuffSessionServerResponse
|
|
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: FreebuffSessionResponse | null,
|
|
197
|
+
next: FreebuffSessionServerResponse,
|
|
198
|
+
): FreebuffSessionServerResponse | 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 holdsLiveFreebuffSlot(
|
|
215
|
+
current: FreebuffSessionResponse | 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 releaseFreebuffSlot(): Promise<void> {
|
|
226
|
+
const current = useFreebuffSessionStore.getState().session
|
|
227
|
+
if (!holdsLiveFreebuffSlot(current)) return
|
|
228
|
+
|
|
229
|
+
const { token } = getAuthTokenDetails()
|
|
230
|
+
if (!token) return
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await callFreebuffSession('DELETE', token)
|
|
234
|
+
} catch {
|
|
235
|
+
// The server-side sweep is the backstop.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Release the Freebuff slot on exit paths that skip React unmount. */
|
|
240
|
+
export async function endFreebuffSessionBestEffort(): Promise<void> {
|
|
241
|
+
if (!IS_FREEBUFF) return
|
|
242
|
+
await releaseFreebuffSlot()
|
|
243
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const FREEBUFF_COUNTDOWN_VISIBLE_MS = 5 * 60_000
|
|
2
|
+
|
|
3
|
+
export const formatFreebuffSessionCountdown = (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 formatFreebuffSessionRemaining = (ms: number): string => {
|
|
12
|
+
if (ms <= 0) return 'expiring…'
|
|
13
|
+
if (ms < FREEBUFF_COUNTDOWN_VISIBLE_MS) {
|
|
14
|
+
return `${formatFreebuffSessionCountdown(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
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// The label/dots/perk-note logic is shared with Freebuff Desktop and lives in
|
|
2
|
+
// common; this module re-exports it and adds the terminal rendering and layout
|
|
3
|
+
// gating only the CLI needs.
|
|
4
|
+
export {
|
|
5
|
+
FREEBUFF_STREAK_WEEK,
|
|
6
|
+
getFreebuffStreakBonusNote,
|
|
7
|
+
} from '@anvil/common/util/freebuff-streak-line'
|
|
8
|
+
export type { FreebuffStreakLine } from '@anvil/common/util/freebuff-streak-line'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
FREEBUFF_STREAK_WEEK,
|
|
12
|
+
getFreebuffStreakBonusNote,
|
|
13
|
+
getFreebuffStreakLine as getSharedFreebuffStreakLine,
|
|
14
|
+
} from '@anvil/common/util/freebuff-streak-line'
|
|
15
|
+
|
|
16
|
+
import type { FreebuffStreakLine } from '@anvil/common/util/freebuff-streak-line'
|
|
17
|
+
|
|
18
|
+
const FREEBUFF_STREAK_BONUS_MIN_HEIGHT = 30
|
|
19
|
+
|
|
20
|
+
/** Columns between the count label and its progress dots. */
|
|
21
|
+
export const FREEBUFF_STREAK_LABEL_GAP = 2
|
|
22
|
+
|
|
23
|
+
/** Columns kept clear between the heading and the streak when they share a
|
|
24
|
+
* row. The heading row is laid out space-between inside a shrink-to-fit
|
|
25
|
+
* column, so when the row is the widest child there is no free space to
|
|
26
|
+
* distribute and the two would otherwise render flush against each other
|
|
27
|
+
* ("Start coding for free18 day streak"). This is the floor, and the same
|
|
28
|
+
* number decides whether they may share a row at all. */
|
|
29
|
+
export const FREEBUFF_STREAK_INLINE_GAP = 3
|
|
30
|
+
|
|
31
|
+
/** Progress glyphs for a terminal — the same ●/○ pair the shared module and
|
|
32
|
+
* the desktop app use. Bullet and middle dot were tried here because U+25CF
|
|
33
|
+
* is missing from a few terminal fonts and lands as a tofu box, but • and ·
|
|
34
|
+
* differ only in size: at a glance a partial week and a full one look alike,
|
|
35
|
+
* which is the whole point of the row. Filled-vs-hollow reads instantly, and
|
|
36
|
+
* the CLI already bets on ●/○ for its agent status indicators, so a font that
|
|
37
|
+
* can't draw them is already visibly broken elsewhere.
|
|
38
|
+
*
|
|
39
|
+
* If a font ever fails these, █/░ (Block Elements) is the fallback pair: the
|
|
40
|
+
* ASCII logo and the progress bar are built from them, so anything that
|
|
41
|
+
* renders the CLI at all renders those. */
|
|
42
|
+
const TERMINAL_DOT_CHARS = { filled: '●', empty: '○' }
|
|
43
|
+
|
|
44
|
+
/** The streak line as the CLI draws it. */
|
|
45
|
+
export function getFreebuffStreakLine(
|
|
46
|
+
streak: number,
|
|
47
|
+
): FreebuffStreakLine | null {
|
|
48
|
+
return getSharedFreebuffStreakLine(streak, TERMINAL_DOT_CHARS)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Rendered width of the streak, e.g. "18 day streak ●●●●●●●+". */
|
|
52
|
+
export function getFreebuffStreakInlineWidth(line: FreebuffStreakLine): number {
|
|
53
|
+
return line.label.length + FREEBUFF_STREAK_LABEL_GAP + line.dots.length
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What a user with no streak yet is about to earn. The empty slot is measured
|
|
57
|
+
* against it so the row doesn't move on day one. */
|
|
58
|
+
const DAY_ONE_LINE = getFreebuffStreakLine(1)!
|
|
59
|
+
|
|
60
|
+
/** Whether the heading and the streak can share a row with the inline gap left
|
|
61
|
+
* clear between them. A streak long enough to widen its own label (or a
|
|
62
|
+
* narrow terminal) pushes the streak onto its own line instead of letting the
|
|
63
|
+
* two collide. */
|
|
64
|
+
export function fitsFreebuffStreakOnHeadingRow(params: {
|
|
65
|
+
/** null when the user has no streak yet — measured as day one. */
|
|
66
|
+
line: FreebuffStreakLine | null
|
|
67
|
+
headingWidth: number
|
|
68
|
+
availableWidth: number
|
|
69
|
+
}): boolean {
|
|
70
|
+
return (
|
|
71
|
+
params.headingWidth +
|
|
72
|
+
FREEBUFF_STREAK_INLINE_GAP +
|
|
73
|
+
getFreebuffStreakInlineWidth(params.line ?? DAY_ONE_LINE) <=
|
|
74
|
+
params.availableWidth
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Returns the earned perk note only when the landing layout can show it
|
|
79
|
+
* without crowding the picker or wrapping onto additional rows. */
|
|
80
|
+
export function getFreebuffStreakBonusNoteForLayout(params: {
|
|
81
|
+
streak: number
|
|
82
|
+
accessTier: 'full' | 'limited'
|
|
83
|
+
terminalHeight: number
|
|
84
|
+
availableWidth: number
|
|
85
|
+
}): string | null {
|
|
86
|
+
if (params.streak < FREEBUFF_STREAK_WEEK) return null
|
|
87
|
+
if (params.terminalHeight < FREEBUFF_STREAK_BONUS_MIN_HEIGHT) return null
|
|
88
|
+
|
|
89
|
+
const note = getFreebuffStreakBonusNote(params)
|
|
90
|
+
if (!note || note.length > params.availableWidth) return null
|
|
91
|
+
|
|
92
|
+
return note
|
|
93
|
+
}
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { dirname, join } from 'path'
|
|
3
|
+
|
|
4
|
+
export function findGitRoot(params: { cwd: string }): string | null {
|
|
5
|
+
const { cwd } = params
|
|
6
|
+
|
|
7
|
+
let currentDir = cwd
|
|
8
|
+
|
|
9
|
+
while (currentDir !== dirname(currentDir)) {
|
|
10
|
+
if (existsSync(join(currentDir, '.git'))) {
|
|
11
|
+
return currentDir
|
|
12
|
+
}
|
|
13
|
+
currentDir = dirname(currentDir)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
let timestampFormatter: Intl.DateTimeFormat | null = null
|
|
2
|
+
export function initTimestampFormatter() {
|
|
3
|
+
if (timestampFormatter) return
|
|
4
|
+
try {
|
|
5
|
+
timestampFormatter = new Intl.DateTimeFormat(undefined, {
|
|
6
|
+
hour: '2-digit',
|
|
7
|
+
minute: '2-digit',
|
|
8
|
+
})
|
|
9
|
+
} catch {
|
|
10
|
+
timestampFormatter = null
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatTimestamp(date = new Date()): string {
|
|
15
|
+
if (timestampFormatter) {
|
|
16
|
+
return timestampFormatter.format(date)
|
|
17
|
+
}
|
|
18
|
+
return date.toLocaleTimeString([], {
|
|
19
|
+
hour: '2-digit',
|
|
20
|
+
minute: '2-digit',
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatQueuedPreview(
|
|
25
|
+
messages: Array<{ content: string }>,
|
|
26
|
+
maxChars: number = 60,
|
|
27
|
+
): string {
|
|
28
|
+
if (messages.length === 0) return ''
|
|
29
|
+
|
|
30
|
+
const latestMessage = messages[messages.length - 1].content
|
|
31
|
+
const singleLine = latestMessage.replace(/\s+/g, ' ').trim()
|
|
32
|
+
if (!singleLine) return ''
|
|
33
|
+
|
|
34
|
+
const countSuffix = messages.length > 1 ? ` (+ ${messages.length - 1})` : ''
|
|
35
|
+
const prefix = '↑ '
|
|
36
|
+
const suffix = ' ↑'
|
|
37
|
+
const availableChars =
|
|
38
|
+
maxChars - prefix.length - suffix.length - countSuffix.length
|
|
39
|
+
|
|
40
|
+
let messagePreview = singleLine
|
|
41
|
+
if (singleLine.length > availableChars) {
|
|
42
|
+
messagePreview =
|
|
43
|
+
singleLine.slice(0, Math.max(0, availableChars - 3)) + '...'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `${prefix}${messagePreview}${countSuffix}${suffix}`
|
|
47
|
+
}
|