@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,668 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'
|
|
2
|
+
|
|
3
|
+
import { isUserActive, subscribeToActivity } from '../utils/activity-tracker'
|
|
4
|
+
|
|
5
|
+
// Global query cache
|
|
6
|
+
type CacheEntry<T> = {
|
|
7
|
+
// allow error-only entries (first fetch failure) without pretending data exists
|
|
8
|
+
data?: T
|
|
9
|
+
dataUpdatedAt: number // 0 means "no successful data yet" (also stale)
|
|
10
|
+
error: Error | null
|
|
11
|
+
errorUpdatedAt: number | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type KeySnapshot<T> = {
|
|
15
|
+
entry: CacheEntry<T> | undefined
|
|
16
|
+
isFetching: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CacheState = {
|
|
20
|
+
entries: Map<string, CacheEntry<unknown>>
|
|
21
|
+
// Per-key listeners
|
|
22
|
+
keyListeners: Map<string, Set<() => void>>
|
|
23
|
+
// Reference counts
|
|
24
|
+
refCounts: Map<string, number>
|
|
25
|
+
// Global fetch status per key
|
|
26
|
+
fetchingKeys: Set<string>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cache: CacheState = {
|
|
30
|
+
entries: new Map(),
|
|
31
|
+
keyListeners: new Map(),
|
|
32
|
+
refCounts: new Map(),
|
|
33
|
+
fetchingKeys: new Set(),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// In-flight promises for request deduplication
|
|
37
|
+
const inFlight = new Map<string, Promise<unknown>>()
|
|
38
|
+
|
|
39
|
+
// Per-key snapshot memoization so fetching-status changes trigger rerenders
|
|
40
|
+
// even if the cache entry object didn’t change.
|
|
41
|
+
const snapshotMemo = new Map<
|
|
42
|
+
string,
|
|
43
|
+
{
|
|
44
|
+
entryRef: CacheEntry<unknown> | undefined
|
|
45
|
+
fetching: boolean
|
|
46
|
+
snap: KeySnapshot<unknown>
|
|
47
|
+
}
|
|
48
|
+
>()
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Notify listeners for a specific cache key.
|
|
52
|
+
*/
|
|
53
|
+
function notifyKeyListeners(key: string) {
|
|
54
|
+
const listeners = cache.keyListeners.get(key)
|
|
55
|
+
if (!listeners) return
|
|
56
|
+
for (const listener of listeners) listener()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Subscribe to cache changes for a specific key. Used by useSyncExternalStore.
|
|
61
|
+
*/
|
|
62
|
+
function subscribeToKey(key: string, callback: () => void): () => void {
|
|
63
|
+
let listeners = cache.keyListeners.get(key)
|
|
64
|
+
if (!listeners) {
|
|
65
|
+
listeners = new Set()
|
|
66
|
+
cache.keyListeners.set(key, listeners)
|
|
67
|
+
}
|
|
68
|
+
listeners.add(callback)
|
|
69
|
+
return () => {
|
|
70
|
+
listeners!.delete(callback)
|
|
71
|
+
if (listeners!.size === 0) {
|
|
72
|
+
cache.keyListeners.delete(key)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Snapshot includes BOTH entry + isFetching, and is memoized so Object.is only changes
|
|
79
|
+
* when either changes. This fixes "notify but no rerender" when only fetch-status changes.
|
|
80
|
+
*/
|
|
81
|
+
function getKeySnapshot<T>(key: string): KeySnapshot<T> {
|
|
82
|
+
const entry = cache.entries.get(key) as CacheEntry<T> | undefined
|
|
83
|
+
const fetching = cache.fetchingKeys.has(key)
|
|
84
|
+
|
|
85
|
+
const memo = snapshotMemo.get(key)
|
|
86
|
+
if (memo && memo.entryRef === (entry as any) && memo.fetching === fetching) {
|
|
87
|
+
return memo.snap as KeySnapshot<T>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const snap: KeySnapshot<T> = { entry, isFetching: fetching }
|
|
91
|
+
snapshotMemo.set(key, {
|
|
92
|
+
entryRef: entry as any,
|
|
93
|
+
fetching,
|
|
94
|
+
snap: snap as any,
|
|
95
|
+
})
|
|
96
|
+
return snap
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function setCacheEntry<T>(key: string, entry: CacheEntry<T>): void {
|
|
100
|
+
cache.entries.set(key, entry as CacheEntry<unknown>)
|
|
101
|
+
// bust memo for this key
|
|
102
|
+
snapshotMemo.delete(key)
|
|
103
|
+
notifyKeyListeners(key)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getCacheEntry<T>(key: string): CacheEntry<T> | undefined {
|
|
107
|
+
return cache.entries.get(key) as CacheEntry<T> | undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a cache entry is stale based on staleTime.
|
|
112
|
+
* Exported for testing purposes.
|
|
113
|
+
*/
|
|
114
|
+
export function isEntryStale(key: string, staleTime: number): boolean {
|
|
115
|
+
const entry = getCacheEntry(key)
|
|
116
|
+
if (!entry) return true
|
|
117
|
+
|
|
118
|
+
// If we have successful data, use its timestamp for staleness
|
|
119
|
+
if (entry.dataUpdatedAt !== 0) {
|
|
120
|
+
return staleTime === 0 || Date.now() - entry.dataUpdatedAt > staleTime
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// No successful data - check if we have a recent error
|
|
124
|
+
// Use errorUpdatedAt to prevent rapid retries on persistent errors
|
|
125
|
+
if (entry.errorUpdatedAt !== null) {
|
|
126
|
+
return staleTime === 0 || Date.now() - entry.errorUpdatedAt > staleTime
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// No data and no error timestamp - entry is stale
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setQueryFetching(key: string, fetching: boolean): void {
|
|
134
|
+
const wasFetching = cache.fetchingKeys.has(key)
|
|
135
|
+
if (fetching) cache.fetchingKeys.add(key)
|
|
136
|
+
else cache.fetchingKeys.delete(key)
|
|
137
|
+
|
|
138
|
+
if (wasFetching !== fetching) {
|
|
139
|
+
// bust memo so snapshot changes even if entry didn’t
|
|
140
|
+
snapshotMemo.delete(key)
|
|
141
|
+
notifyKeyListeners(key)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function incrementRefCount(key: string): void {
|
|
146
|
+
const current = cache.refCounts.get(key) ?? 0
|
|
147
|
+
cache.refCounts.set(key, current + 1)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function decrementRefCount(key: string): number {
|
|
151
|
+
const current = cache.refCounts.get(key) ?? 0
|
|
152
|
+
const next = Math.max(0, current - 1)
|
|
153
|
+
if (next === 0) cache.refCounts.delete(key)
|
|
154
|
+
else cache.refCounts.set(key, next)
|
|
155
|
+
return next
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getRefCount(key: string): number {
|
|
159
|
+
return cache.refCounts.get(key) ?? 0
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Serialize a query key to a string for cache lookup.
|
|
164
|
+
*/
|
|
165
|
+
function serializeQueryKey(queryKey: readonly unknown[]): string {
|
|
166
|
+
return JSON.stringify(queryKey)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Module-level map to track GC timeouts (survives component unmount)
|
|
170
|
+
const gcTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
171
|
+
|
|
172
|
+
// Per-key retry state (so unmounting one observer doesn’t cancel retries for others)
|
|
173
|
+
const retryCounts = new Map<string, number>()
|
|
174
|
+
const retryTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
175
|
+
|
|
176
|
+
// Per-key generation to prevent "resurrecting" deleted entries from late in-flight responses
|
|
177
|
+
const generations = new Map<string, number>()
|
|
178
|
+
function bumpGeneration(key: string) {
|
|
179
|
+
generations.set(key, (generations.get(key) ?? 0) + 1)
|
|
180
|
+
}
|
|
181
|
+
function getGeneration(key: string) {
|
|
182
|
+
return generations.get(key) ?? 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function clearRetryTimeout(key: string) {
|
|
186
|
+
const t = retryTimeouts.get(key)
|
|
187
|
+
if (t) clearTimeout(t)
|
|
188
|
+
retryTimeouts.delete(key)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function clearRetryState(key: string) {
|
|
192
|
+
clearRetryTimeout(key)
|
|
193
|
+
retryCounts.delete(key)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function deleteCacheEntry(key: string): void {
|
|
197
|
+
bumpGeneration(key)
|
|
198
|
+
clearRetryState(key)
|
|
199
|
+
inFlight.delete(key)
|
|
200
|
+
cache.fetchingKeys.delete(key)
|
|
201
|
+
cache.entries.delete(key)
|
|
202
|
+
cache.refCounts.delete(key)
|
|
203
|
+
snapshotMemo.delete(key)
|
|
204
|
+
notifyKeyListeners(key)
|
|
205
|
+
}
|
|
206
|
+
export type UseActivityQueryOptions<T> = {
|
|
207
|
+
/** Unique key for caching the query */
|
|
208
|
+
queryKey: readonly unknown[]
|
|
209
|
+
/** Function that fetches the data */
|
|
210
|
+
queryFn: () => Promise<T>
|
|
211
|
+
/** Whether the query is enabled (default: true) */
|
|
212
|
+
enabled?: boolean
|
|
213
|
+
/** Time in ms before data is considered stale (default: 0) */
|
|
214
|
+
staleTime?: number
|
|
215
|
+
/** Time in ms to keep unused cache entries (default: 5 minutes) */
|
|
216
|
+
gcTime?: number
|
|
217
|
+
/** Number of retry attempts on failure (default: 0) */
|
|
218
|
+
retry?: number | false
|
|
219
|
+
/** Interval in ms to refetch data (default: false/disabled) */
|
|
220
|
+
refetchInterval?: number | false
|
|
221
|
+
|
|
222
|
+
/** Refetch when component mounts (default: false) */
|
|
223
|
+
refetchOnMount?: boolean | 'always'
|
|
224
|
+
/** Refetch stale data when user becomes active after being idle (default: false) */
|
|
225
|
+
refetchOnActivity?: boolean
|
|
226
|
+
/** Pause polling when user is idle (default: false) */
|
|
227
|
+
pauseWhenIdle?: boolean
|
|
228
|
+
/** Time in ms to consider user idle (default: 30 seconds) */
|
|
229
|
+
idleThreshold?: number
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type UseActivityQueryResult<T> = {
|
|
233
|
+
/** The query data, undefined if not yet fetched */
|
|
234
|
+
data: T | undefined
|
|
235
|
+
/** Whether the initial fetch is in progress */
|
|
236
|
+
isLoading: boolean
|
|
237
|
+
/** Whether any fetch (initial or refetch) is in progress */
|
|
238
|
+
isFetching: boolean
|
|
239
|
+
/** Whether the query has successfully fetched data */
|
|
240
|
+
isSuccess: boolean
|
|
241
|
+
/** Error from the last fetch attempt */
|
|
242
|
+
error: Error | null
|
|
243
|
+
/** Manually trigger a refetch */
|
|
244
|
+
refetch: () => Promise<void>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Activity-aware query hook that provides caching and refetching based on user activity.
|
|
249
|
+
*
|
|
250
|
+
* This hook replaces TanStack Query with terminal-specific activity awareness:
|
|
251
|
+
* - Detects when user is active (typing, mouse movement, keyboard shortcuts)
|
|
252
|
+
* - Can pause polling when user is idle to save resources
|
|
253
|
+
* - Can refetch stale data when user becomes active again
|
|
254
|
+
*/
|
|
255
|
+
export function useActivityQuery<T>(
|
|
256
|
+
options: UseActivityQueryOptions<T>,
|
|
257
|
+
): UseActivityQueryResult<T> {
|
|
258
|
+
const {
|
|
259
|
+
queryKey,
|
|
260
|
+
queryFn,
|
|
261
|
+
enabled = true,
|
|
262
|
+
staleTime = 0,
|
|
263
|
+
gcTime = 5 * 60 * 1000,
|
|
264
|
+
retry = 0,
|
|
265
|
+
refetchInterval = false,
|
|
266
|
+
refetchOnMount = false,
|
|
267
|
+
refetchOnActivity = false,
|
|
268
|
+
pauseWhenIdle = false,
|
|
269
|
+
idleThreshold = 30_000,
|
|
270
|
+
} = options
|
|
271
|
+
|
|
272
|
+
const serializedKey = serializeQueryKey(queryKey)
|
|
273
|
+
const mountedRef = useRef(true)
|
|
274
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
275
|
+
const wasIdleRef = useRef(false)
|
|
276
|
+
|
|
277
|
+
// Store queryFn in a ref to avoid recreating doFetch when queryFn changes.
|
|
278
|
+
// This is critical because inline arrow functions create new references on every render,
|
|
279
|
+
// which would cause the polling interval to reset constantly.
|
|
280
|
+
const queryFnRef = useRef(queryFn)
|
|
281
|
+
queryFnRef.current = queryFn
|
|
282
|
+
|
|
283
|
+
// Snapshot includes entry + isFetching (so fetch-status updates rerender correctly)
|
|
284
|
+
const snap = useSyncExternalStore(
|
|
285
|
+
(cb) => subscribeToKey(serializedKey, cb),
|
|
286
|
+
() => getKeySnapshot<T>(serializedKey),
|
|
287
|
+
() => getKeySnapshot<T>(serializedKey),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const cachedEntry = snap.entry
|
|
291
|
+
const isFetching = snap.isFetching
|
|
292
|
+
|
|
293
|
+
const data = cachedEntry?.data
|
|
294
|
+
const error = cachedEntry?.error ?? null
|
|
295
|
+
const dataUpdatedAt = cachedEntry?.dataUpdatedAt ?? 0
|
|
296
|
+
|
|
297
|
+
// Initial load = fetching with no successful data yet
|
|
298
|
+
const isLoading = isFetching && (cachedEntry == null || dataUpdatedAt === 0)
|
|
299
|
+
|
|
300
|
+
const doFetch = useCallback(async (): Promise<void> => {
|
|
301
|
+
if (!enabled) return
|
|
302
|
+
|
|
303
|
+
// global dedupe
|
|
304
|
+
const existing = inFlight.get(serializedKey)
|
|
305
|
+
if (existing) {
|
|
306
|
+
await existing
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const myGen = getGeneration(serializedKey)
|
|
311
|
+
setQueryFetching(serializedKey, true)
|
|
312
|
+
|
|
313
|
+
const fetchPromise = (async () => {
|
|
314
|
+
try {
|
|
315
|
+
// Use ref to get latest queryFn without including it in dependencies
|
|
316
|
+
const result = await queryFnRef.current()
|
|
317
|
+
|
|
318
|
+
// If someone removed/GC'd this key while we were in-flight, don’t resurrect it.
|
|
319
|
+
if (getGeneration(serializedKey) !== myGen) return
|
|
320
|
+
|
|
321
|
+
setCacheEntry(serializedKey, {
|
|
322
|
+
data: result,
|
|
323
|
+
dataUpdatedAt: Date.now(),
|
|
324
|
+
error: null,
|
|
325
|
+
errorUpdatedAt: null,
|
|
326
|
+
})
|
|
327
|
+
retryCounts.set(serializedKey, 0)
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
330
|
+
const maxRetries = retry === false ? 0 : retry
|
|
331
|
+
const currentRetries = retryCounts.get(serializedKey) ?? 0
|
|
332
|
+
|
|
333
|
+
if (currentRetries < maxRetries && getRefCount(serializedKey) > 0) {
|
|
334
|
+
const next = currentRetries + 1
|
|
335
|
+
retryCounts.set(serializedKey, next)
|
|
336
|
+
|
|
337
|
+
// allow a new in-flight request for the retry attempt
|
|
338
|
+
inFlight.delete(serializedKey)
|
|
339
|
+
setQueryFetching(serializedKey, false)
|
|
340
|
+
|
|
341
|
+
// Only clear the previous timeout, NOT the retry count.
|
|
342
|
+
// Using clearRetryState here would reset retryCounts, causing infinite retries.
|
|
343
|
+
// (see: _retryTestHelpers.simulateFailedFetch mirrors this logic)
|
|
344
|
+
clearRetryTimeout(serializedKey)
|
|
345
|
+
const t = setTimeout(() => {
|
|
346
|
+
retryTimeouts.delete(serializedKey)
|
|
347
|
+
// only retry if still mounted somewhere and key not deleted
|
|
348
|
+
if (getRefCount(serializedKey) > 0 && getGeneration(serializedKey) === myGen) {
|
|
349
|
+
void doFetch()
|
|
350
|
+
}
|
|
351
|
+
}, 1000 * next)
|
|
352
|
+
retryTimeouts.set(serializedKey, t)
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
retryCounts.set(serializedKey, 0)
|
|
357
|
+
|
|
358
|
+
// Store error even if we have no existing data (error-only entry).
|
|
359
|
+
if (getGeneration(serializedKey) !== myGen) return
|
|
360
|
+
|
|
361
|
+
const existingEntry = getCacheEntry<T>(serializedKey)
|
|
362
|
+
setCacheEntry(serializedKey, {
|
|
363
|
+
data: existingEntry?.data,
|
|
364
|
+
dataUpdatedAt: existingEntry?.dataUpdatedAt ?? 0,
|
|
365
|
+
error: e,
|
|
366
|
+
errorUpdatedAt: Date.now(),
|
|
367
|
+
})
|
|
368
|
+
} finally {
|
|
369
|
+
inFlight.delete(serializedKey)
|
|
370
|
+
setQueryFetching(serializedKey, false)
|
|
371
|
+
|
|
372
|
+
// If nobody is watching and the entry was deleted, keep things tidy.
|
|
373
|
+
if (getRefCount(serializedKey) === 0) {
|
|
374
|
+
clearRetryState(serializedKey)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
})()
|
|
378
|
+
|
|
379
|
+
inFlight.set(serializedKey, fetchPromise)
|
|
380
|
+
await fetchPromise
|
|
381
|
+
}, [enabled, serializedKey, retry])
|
|
382
|
+
|
|
383
|
+
const refetch = useCallback(async (): Promise<void> => {
|
|
384
|
+
clearRetryState(serializedKey)
|
|
385
|
+
await doFetch()
|
|
386
|
+
}, [doFetch, serializedKey])
|
|
387
|
+
|
|
388
|
+
// Refcount + cancel pending GC when (re)subscribing
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
const existingTimeout = gcTimeouts.get(serializedKey)
|
|
391
|
+
if (existingTimeout) {
|
|
392
|
+
clearTimeout(existingTimeout)
|
|
393
|
+
gcTimeouts.delete(serializedKey)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
wasIdleRef.current = false
|
|
397
|
+
incrementRefCount(serializedKey)
|
|
398
|
+
|
|
399
|
+
return () => {
|
|
400
|
+
const next = decrementRefCount(serializedKey)
|
|
401
|
+
|
|
402
|
+
// If last observer is gone, don’t keep retry timers around.
|
|
403
|
+
if (next === 0) {
|
|
404
|
+
clearRetryState(serializedKey)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}, [serializedKey])
|
|
408
|
+
|
|
409
|
+
// Initial fetch on mount/key change/enabled toggle (intentionally minimal deps)
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
mountedRef.current = true
|
|
412
|
+
if (!enabled) return
|
|
413
|
+
|
|
414
|
+
const currentEntry = getCacheEntry<T>(serializedKey)
|
|
415
|
+
// Use isEntryStale for consistent staleness calculation that considers
|
|
416
|
+
// both dataUpdatedAt and errorUpdatedAt (prevents rapid refetch loops
|
|
417
|
+
// when endpoint returns persistent errors)
|
|
418
|
+
const currentlyStale = isEntryStale(serializedKey, staleTime)
|
|
419
|
+
|
|
420
|
+
const shouldFetchOnMount =
|
|
421
|
+
refetchOnMount === 'always' ||
|
|
422
|
+
(refetchOnMount && currentlyStale) ||
|
|
423
|
+
(!currentEntry)
|
|
424
|
+
|
|
425
|
+
if (shouldFetchOnMount) void doFetch()
|
|
426
|
+
|
|
427
|
+
return () => {
|
|
428
|
+
mountedRef.current = false
|
|
429
|
+
}
|
|
430
|
+
}, [enabled, serializedKey])
|
|
431
|
+
|
|
432
|
+
// Polling
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
if (!enabled || !refetchInterval) {
|
|
435
|
+
if (intervalRef.current) {
|
|
436
|
+
clearInterval(intervalRef.current)
|
|
437
|
+
intervalRef.current = null
|
|
438
|
+
}
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const tick = () => {
|
|
443
|
+
if (pauseWhenIdle && !isUserActive(idleThreshold)) {
|
|
444
|
+
wasIdleRef.current = true
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
if (isEntryStale(serializedKey, staleTime)) {
|
|
448
|
+
void doFetch()
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
intervalRef.current = setInterval(tick, refetchInterval)
|
|
453
|
+
return () => {
|
|
454
|
+
if (intervalRef.current) {
|
|
455
|
+
clearInterval(intervalRef.current)
|
|
456
|
+
intervalRef.current = null
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}, [enabled, refetchInterval, pauseWhenIdle, idleThreshold, staleTime, serializedKey, doFetch])
|
|
460
|
+
|
|
461
|
+
// Refetch on activity after idle
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
if (!enabled || !refetchOnActivity) return
|
|
464
|
+
|
|
465
|
+
const unsubscribe = subscribeToActivity(() => {
|
|
466
|
+
if (wasIdleRef.current) {
|
|
467
|
+
wasIdleRef.current = false
|
|
468
|
+
if (isEntryStale(serializedKey, staleTime)) {
|
|
469
|
+
void doFetch()
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
const checkIdle = setInterval(() => {
|
|
475
|
+
if (!isUserActive(idleThreshold)) {
|
|
476
|
+
wasIdleRef.current = true
|
|
477
|
+
}
|
|
478
|
+
}, 5000)
|
|
479
|
+
|
|
480
|
+
return () => {
|
|
481
|
+
unsubscribe()
|
|
482
|
+
clearInterval(checkIdle)
|
|
483
|
+
}
|
|
484
|
+
}, [enabled, refetchOnActivity, idleThreshold, staleTime, serializedKey, doFetch])
|
|
485
|
+
|
|
486
|
+
// Garbage collection
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
return () => {
|
|
489
|
+
const timeoutId = setTimeout(() => {
|
|
490
|
+
if (getRefCount(serializedKey) === 0) {
|
|
491
|
+
deleteCacheEntry(serializedKey)
|
|
492
|
+
gcTimeouts.delete(serializedKey)
|
|
493
|
+
}
|
|
494
|
+
}, gcTime)
|
|
495
|
+
|
|
496
|
+
gcTimeouts.set(serializedKey, timeoutId)
|
|
497
|
+
}
|
|
498
|
+
}, [serializedKey, gcTime])
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
data,
|
|
502
|
+
isLoading,
|
|
503
|
+
isFetching,
|
|
504
|
+
isSuccess: cachedEntry != null && cachedEntry.error == null && cachedEntry.dataUpdatedAt !== 0,
|
|
505
|
+
error,
|
|
506
|
+
refetch,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Invalidate a query, causing it to refetch on next access.
|
|
512
|
+
*/
|
|
513
|
+
export function invalidateActivityQuery(queryKey: readonly unknown[]): void {
|
|
514
|
+
const key = serializeQueryKey(queryKey)
|
|
515
|
+
const entry = getCacheEntry(key)
|
|
516
|
+
if (!entry) return
|
|
517
|
+
setCacheEntry(key, { ...entry, dataUpdatedAt: 0 })
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Remove a query from the cache entirely.
|
|
522
|
+
*/
|
|
523
|
+
export function removeActivityQuery(queryKey: readonly unknown[]): void {
|
|
524
|
+
const key = serializeQueryKey(queryKey)
|
|
525
|
+
|
|
526
|
+
const existingTimeout = gcTimeouts.get(key)
|
|
527
|
+
if (existingTimeout) {
|
|
528
|
+
clearTimeout(existingTimeout)
|
|
529
|
+
gcTimeouts.delete(key)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
deleteCacheEntry(key)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Read cached data.
|
|
537
|
+
*/
|
|
538
|
+
export function getActivityQueryData<T>(queryKey: readonly unknown[]): T | undefined {
|
|
539
|
+
const key = serializeQueryKey(queryKey)
|
|
540
|
+
return getCacheEntry<T>(key)?.data
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Write cached data (optimistic updates).
|
|
545
|
+
*/
|
|
546
|
+
export function setActivityQueryData<T>(queryKey: readonly unknown[], data: T): void {
|
|
547
|
+
const key = serializeQueryKey(queryKey)
|
|
548
|
+
setCacheEntry(key, {
|
|
549
|
+
data,
|
|
550
|
+
dataUpdatedAt: Date.now(),
|
|
551
|
+
error: null,
|
|
552
|
+
errorUpdatedAt: null,
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function useInvalidateActivityQuery() {
|
|
557
|
+
return useCallback((queryKey: readonly unknown[]) => {
|
|
558
|
+
invalidateActivityQuery(queryKey)
|
|
559
|
+
}, [])
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Reset the activity query cache (mainly for testing).
|
|
564
|
+
*/
|
|
565
|
+
export function resetActivityQueryCache(): void {
|
|
566
|
+
for (const timeoutId of gcTimeouts.values()) clearTimeout(timeoutId)
|
|
567
|
+
gcTimeouts.clear()
|
|
568
|
+
|
|
569
|
+
for (const t of retryTimeouts.values()) clearTimeout(t)
|
|
570
|
+
retryTimeouts.clear()
|
|
571
|
+
retryCounts.clear()
|
|
572
|
+
|
|
573
|
+
cache.entries.clear()
|
|
574
|
+
cache.keyListeners.clear()
|
|
575
|
+
cache.refCounts.clear()
|
|
576
|
+
cache.fetchingKeys.clear()
|
|
577
|
+
|
|
578
|
+
inFlight.clear()
|
|
579
|
+
snapshotMemo.clear()
|
|
580
|
+
generations.clear()
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Set an error-only cache entry (for testing).
|
|
585
|
+
* This simulates what happens when a fetch fails with no prior successful data.
|
|
586
|
+
*/
|
|
587
|
+
export function setErrorOnlyCacheEntry(
|
|
588
|
+
queryKey: readonly unknown[],
|
|
589
|
+
error: Error,
|
|
590
|
+
errorUpdatedAt?: number,
|
|
591
|
+
): void {
|
|
592
|
+
const key = serializeQueryKey(queryKey)
|
|
593
|
+
setCacheEntry(key, {
|
|
594
|
+
data: undefined,
|
|
595
|
+
dataUpdatedAt: 0,
|
|
596
|
+
error,
|
|
597
|
+
errorUpdatedAt: errorUpdatedAt ?? Date.now(),
|
|
598
|
+
})
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Test helpers for verifying retry behavior.
|
|
603
|
+
* These expose internal retry state to allow unit testing the retry logic
|
|
604
|
+
* without needing a React renderer.
|
|
605
|
+
*/
|
|
606
|
+
export const _retryTestHelpers = {
|
|
607
|
+
getRetryCount(queryKey: readonly unknown[]): number {
|
|
608
|
+
return retryCounts.get(serializeQueryKey(queryKey)) ?? 0
|
|
609
|
+
},
|
|
610
|
+
setRetryCount(queryKey: readonly unknown[], count: number): void {
|
|
611
|
+
retryCounts.set(serializeQueryKey(queryKey), count)
|
|
612
|
+
},
|
|
613
|
+
getRetryTimeout(queryKey: readonly unknown[]): ReturnType<typeof setTimeout> | undefined {
|
|
614
|
+
return retryTimeouts.get(serializeQueryKey(queryKey))
|
|
615
|
+
},
|
|
616
|
+
setRefCount(queryKey: readonly unknown[], count: number): void {
|
|
617
|
+
const key = serializeQueryKey(queryKey)
|
|
618
|
+
if (count === 0) cache.refCounts.delete(key)
|
|
619
|
+
else cache.refCounts.set(key, count)
|
|
620
|
+
},
|
|
621
|
+
setFetching(queryKey: readonly unknown[], fetching: boolean): void {
|
|
622
|
+
setQueryFetching(serializeQueryKey(queryKey), fetching)
|
|
623
|
+
},
|
|
624
|
+
getInFlight(queryKey: readonly unknown[]): boolean {
|
|
625
|
+
return inFlight.has(serializeQueryKey(queryKey))
|
|
626
|
+
},
|
|
627
|
+
/**
|
|
628
|
+
* Simulate the exact retry scheduling logic from doFetch's catch block.
|
|
629
|
+
* This reproduces the code path that caused the infinite retry loop bug.
|
|
630
|
+
* Returns whether a retry was scheduled (true) or retries were exhausted (false).
|
|
631
|
+
*/
|
|
632
|
+
simulateFailedFetch(
|
|
633
|
+
queryKey: readonly unknown[],
|
|
634
|
+
maxRetries: number,
|
|
635
|
+
): { retryScheduled: boolean; retryCount: number } {
|
|
636
|
+
const key = serializeQueryKey(queryKey)
|
|
637
|
+
const currentRetries = retryCounts.get(key) ?? 0
|
|
638
|
+
|
|
639
|
+
if (currentRetries < maxRetries && (cache.refCounts.get(key) ?? 0) > 0) {
|
|
640
|
+
const next = currentRetries + 1
|
|
641
|
+
retryCounts.set(key, next)
|
|
642
|
+
|
|
643
|
+
inFlight.delete(key)
|
|
644
|
+
setQueryFetching(key, false)
|
|
645
|
+
|
|
646
|
+
// This is the fixed line — uses clearRetryTimeout instead of clearRetryState
|
|
647
|
+
clearRetryTimeout(key)
|
|
648
|
+
|
|
649
|
+
// Don't actually schedule a setTimeout in tests, just record the intent
|
|
650
|
+
return { retryScheduled: true, retryCount: next }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
retryCounts.set(key, 0)
|
|
654
|
+
|
|
655
|
+
const existingEntry = getCacheEntry(key)
|
|
656
|
+
setCacheEntry(key, {
|
|
657
|
+
data: existingEntry?.data,
|
|
658
|
+
dataUpdatedAt: existingEntry?.dataUpdatedAt ?? 0,
|
|
659
|
+
error: new Error('Simulated fetch error'),
|
|
660
|
+
errorUpdatedAt: Date.now(),
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
inFlight.delete(key)
|
|
664
|
+
setQueryFetching(key, false)
|
|
665
|
+
|
|
666
|
+
return { retryScheduled: false, retryCount: 0 }
|
|
667
|
+
},
|
|
668
|
+
}
|