@stigmer/react 0.3.4 → 0.4.1
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/billing/AutoRechargeCard.d.ts +38 -0
- package/billing/AutoRechargeCard.d.ts.map +1 -0
- package/billing/AutoRechargeCard.js +90 -0
- package/billing/AutoRechargeCard.js.map +1 -0
- package/billing/BillingSection.d.ts +32 -0
- package/billing/BillingSection.d.ts.map +1 -0
- package/billing/BillingSection.js +81 -0
- package/billing/BillingSection.js.map +1 -0
- package/billing/CreditBalanceCard.d.ts +25 -0
- package/billing/CreditBalanceCard.d.ts.map +1 -0
- package/billing/CreditBalanceCard.js +28 -0
- package/billing/CreditBalanceCard.js.map +1 -0
- package/billing/CreditLedgerTable.d.ts +22 -0
- package/billing/CreditLedgerTable.d.ts.map +1 -0
- package/billing/CreditLedgerTable.js +75 -0
- package/billing/CreditLedgerTable.js.map +1 -0
- package/billing/CreditPackGrid.d.ts +31 -0
- package/billing/CreditPackGrid.d.ts.map +1 -0
- package/billing/CreditPackGrid.js +35 -0
- package/billing/CreditPackGrid.js.map +1 -0
- package/billing/LowBalanceBanner.d.ts +26 -0
- package/billing/LowBalanceBanner.d.ts.map +1 -0
- package/billing/LowBalanceBanner.js +33 -0
- package/billing/LowBalanceBanner.js.map +1 -0
- package/billing/PaymentMethodCard.d.ts +35 -0
- package/billing/PaymentMethodCard.d.ts.map +1 -0
- package/billing/PaymentMethodCard.js +48 -0
- package/billing/PaymentMethodCard.js.map +1 -0
- package/billing/credit-packs.d.ts +25 -0
- package/billing/credit-packs.d.ts.map +1 -0
- package/billing/credit-packs.js +39 -0
- package/billing/credit-packs.js.map +1 -0
- package/billing/format.d.ts +39 -0
- package/billing/format.d.ts.map +1 -0
- package/billing/format.js +90 -0
- package/billing/format.js.map +1 -0
- package/billing/index.d.ts +32 -0
- package/billing/index.d.ts.map +1 -0
- package/billing/index.js +21 -0
- package/billing/index.js.map +1 -0
- package/billing/useBillingAccount.d.ts +40 -0
- package/billing/useBillingAccount.d.ts.map +1 -0
- package/billing/useBillingAccount.js +35 -0
- package/billing/useBillingAccount.js.map +1 -0
- package/billing/useBillingUsageReport.d.ts +42 -0
- package/billing/useBillingUsageReport.d.ts.map +1 -0
- package/billing/useBillingUsageReport.js +43 -0
- package/billing/useBillingUsageReport.js.map +1 -0
- package/billing/useCreateBillingPortalSession.d.ts +35 -0
- package/billing/useCreateBillingPortalSession.d.ts.map +1 -0
- package/billing/useCreateBillingPortalSession.js +50 -0
- package/billing/useCreateBillingPortalSession.js.map +1 -0
- package/billing/useCreateCheckoutSession.d.ts +54 -0
- package/billing/useCreateCheckoutSession.d.ts.map +1 -0
- package/billing/useCreateCheckoutSession.js +58 -0
- package/billing/useCreateCheckoutSession.js.map +1 -0
- package/billing/useCreditLedger.d.ts +48 -0
- package/billing/useCreditLedger.d.ts.map +1 -0
- package/billing/useCreditLedger.js +39 -0
- package/billing/useCreditLedger.js.map +1 -0
- package/billing/useCustomerModelPricing.d.ts +41 -0
- package/billing/useCustomerModelPricing.d.ts.map +1 -0
- package/billing/useCustomerModelPricing.js +37 -0
- package/billing/useCustomerModelPricing.js.map +1 -0
- package/billing/useSetAutoRechargeConfig.d.ts +50 -0
- package/billing/useSetAutoRechargeConfig.d.ts.map +1 -0
- package/billing/useSetAutoRechargeConfig.js +53 -0
- package/billing/useSetAutoRechargeConfig.js.map +1 -0
- package/composer/ComposerToolbar.js +1 -1
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +1 -1
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +19 -4
- package/composer/SessionComposer.js.map +1 -1
- package/composer/__tests__/SessionComposer-memo.test.d.ts +2 -0
- package/composer/__tests__/SessionComposer-memo.test.d.ts.map +1 -0
- package/composer/__tests__/SessionComposer-memo.test.js +23 -0
- package/composer/__tests__/SessionComposer-memo.test.js.map +1 -0
- package/execution/ApprovalCard.d.ts +5 -1
- package/execution/ApprovalCard.d.ts.map +1 -1
- package/execution/ApprovalCard.js +7 -3
- package/execution/ApprovalCard.js.map +1 -1
- package/execution/ExecutionPhaseBadge.d.ts +1 -1
- package/execution/ExecutionPhaseBadge.d.ts.map +1 -1
- package/execution/ExecutionPhaseBadge.js +3 -2
- package/execution/ExecutionPhaseBadge.js.map +1 -1
- package/execution/MessageEntry.d.ts +7 -3
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +19 -8
- package/execution/MessageEntry.js.map +1 -1
- package/execution/MessageThread.d.ts +84 -3
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +113 -65
- package/execution/MessageThread.js.map +1 -1
- package/execution/SetupProgress.d.ts +1 -1
- package/execution/SetupProgress.d.ts.map +1 -1
- package/execution/SetupProgress.js +3 -3
- package/execution/SetupProgress.js.map +1 -1
- package/execution/SubAgentSection.d.ts +5 -1
- package/execution/SubAgentSection.d.ts.map +1 -1
- package/execution/SubAgentSection.js +13 -7
- package/execution/SubAgentSection.js.map +1 -1
- package/execution/ThreadSkeleton.d.ts +22 -0
- package/execution/ThreadSkeleton.d.ts.map +1 -0
- package/execution/ThreadSkeleton.js +26 -0
- package/execution/ThreadSkeleton.js.map +1 -0
- package/execution/ToolCallGroup.d.ts +16 -1
- package/execution/ToolCallGroup.d.ts.map +1 -1
- package/execution/ToolCallGroup.js +31 -3
- package/execution/ToolCallGroup.js.map +1 -1
- package/execution/UsageWidget.d.ts +1 -1
- package/execution/__tests__/message-entry.test.d.ts +2 -0
- package/execution/__tests__/message-entry.test.d.ts.map +1 -0
- package/execution/__tests__/message-entry.test.js +178 -0
- package/execution/__tests__/message-entry.test.js.map +1 -0
- package/execution/__tests__/thread-keys.test.d.ts +2 -0
- package/execution/__tests__/thread-keys.test.d.ts.map +1 -0
- package/execution/__tests__/thread-keys.test.js +289 -0
- package/execution/__tests__/thread-keys.test.js.map +1 -0
- package/execution/__tests__/thread-memoization.test.d.ts +2 -0
- package/execution/__tests__/thread-memoization.test.d.ts.map +1 -0
- package/execution/__tests__/thread-memoization.test.js +262 -0
- package/execution/__tests__/thread-memoization.test.js.map +1 -0
- package/execution/__tests__/thread-skeleton.test.d.ts +2 -0
- package/execution/__tests__/thread-skeleton.test.d.ts.map +1 -0
- package/execution/__tests__/thread-skeleton.test.js +35 -0
- package/execution/__tests__/thread-skeleton.test.js.map +1 -0
- package/execution/__tests__/useExecutionStream.test.js +73 -10
- package/execution/__tests__/useExecutionStream.test.js.map +1 -1
- package/execution/__tests__/useSessionVariables-stability.test.d.ts +2 -0
- package/execution/__tests__/useSessionVariables-stability.test.d.ts.map +1 -0
- package/execution/__tests__/useSessionVariables-stability.test.js +69 -0
- package/execution/__tests__/useSessionVariables-stability.test.js.map +1 -0
- package/execution/__tests__/virtualized-thread.test.d.ts +2 -0
- package/execution/__tests__/virtualized-thread.test.d.ts.map +1 -0
- package/execution/__tests__/virtualized-thread.test.js +274 -0
- package/execution/__tests__/virtualized-thread.test.js.map +1 -0
- package/execution/index.d.ts +2 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -0
- package/execution/index.js.map +1 -1
- package/execution/useExecutionStream.d.ts +35 -10
- package/execution/useExecutionStream.d.ts.map +1 -1
- package/execution/useExecutionStream.js +79 -40
- package/execution/useExecutionStream.js.map +1 -1
- package/execution/useSessionVariables.d.ts.map +1 -1
- package/execution/useSessionVariables.js +4 -3
- package/execution/useSessionVariables.js.map +1 -1
- package/github/useGitHubConnection.d.ts.map +1 -1
- package/github/useGitHubConnection.js +5 -4
- package/github/useGitHubConnection.js.map +1 -1
- package/identity-account/index.d.ts +2 -0
- package/identity-account/index.d.ts.map +1 -0
- package/identity-account/index.js +2 -0
- package/identity-account/index.js.map +1 -0
- package/identity-account/useIdentityAccountGate.d.ts +81 -0
- package/identity-account/useIdentityAccountGate.d.ts.map +1 -0
- package/identity-account/useIdentityAccountGate.js +100 -0
- package/identity-account/useIdentityAccountGate.js.map +1 -0
- package/index.d.ts +10 -4
- package/index.d.ts.map +1 -1
- package/index.js +8 -2
- package/index.js.map +1 -1
- package/internal/FetchCacheProvider.d.ts +44 -0
- package/internal/FetchCacheProvider.d.ts.map +1 -0
- package/internal/FetchCacheProvider.js +61 -0
- package/internal/FetchCacheProvider.js.map +1 -0
- package/internal/JumpToLatestButton.d.ts +14 -0
- package/internal/JumpToLatestButton.d.ts.map +1 -0
- package/internal/JumpToLatestButton.js +19 -0
- package/internal/JumpToLatestButton.js.map +1 -0
- package/internal/ThreadItemWrapper.d.ts +20 -0
- package/internal/ThreadItemWrapper.d.ts.map +1 -0
- package/internal/ThreadItemWrapper.js +44 -0
- package/internal/ThreadItemWrapper.js.map +1 -0
- package/internal/VirtualizedThread.d.ts +25 -0
- package/internal/VirtualizedThread.d.ts.map +1 -0
- package/internal/VirtualizedThread.js +58 -0
- package/internal/VirtualizedThread.js.map +1 -0
- package/internal/__tests__/fetch-cache.test.d.ts +2 -0
- package/internal/__tests__/fetch-cache.test.d.ts.map +1 -0
- package/internal/__tests__/fetch-cache.test.js +182 -0
- package/internal/__tests__/fetch-cache.test.js.map +1 -0
- package/internal/__tests__/stream-controller.test.d.ts +2 -0
- package/internal/__tests__/stream-controller.test.d.ts.map +1 -0
- package/internal/__tests__/stream-controller.test.js +294 -0
- package/internal/__tests__/stream-controller.test.js.map +1 -0
- package/internal/__tests__/thread-animation.test.d.ts +2 -0
- package/internal/__tests__/thread-animation.test.d.ts.map +1 -0
- package/internal/__tests__/thread-animation.test.js +79 -0
- package/internal/__tests__/thread-animation.test.js.map +1 -0
- package/internal/__tests__/useAutoScroll.test.d.ts +2 -0
- package/internal/__tests__/useAutoScroll.test.d.ts.map +1 -0
- package/internal/__tests__/useAutoScroll.test.js +188 -0
- package/internal/__tests__/useAutoScroll.test.js.map +1 -0
- package/internal/__tests__/useFetch-cache.test.d.ts +2 -0
- package/internal/__tests__/useFetch-cache.test.d.ts.map +1 -0
- package/internal/__tests__/useFetch-cache.test.js +137 -0
- package/internal/__tests__/useFetch-cache.test.js.map +1 -0
- package/internal/dev/__tests__/use-key-stability.test.d.ts +2 -0
- package/internal/dev/__tests__/use-key-stability.test.d.ts.map +1 -0
- package/internal/dev/__tests__/use-key-stability.test.js +72 -0
- package/internal/dev/__tests__/use-key-stability.test.js.map +1 -0
- package/internal/dev/__tests__/use-render-tracer.test.d.ts +2 -0
- package/internal/dev/__tests__/use-render-tracer.test.d.ts.map +1 -0
- package/internal/dev/__tests__/use-render-tracer.test.js +55 -0
- package/internal/dev/__tests__/use-render-tracer.test.js.map +1 -0
- package/internal/dev/dom-counter.d.ts +14 -0
- package/internal/dev/dom-counter.d.ts.map +1 -0
- package/internal/dev/dom-counter.js +39 -0
- package/internal/dev/dom-counter.js.map +1 -0
- package/internal/dev/index.d.ts +6 -0
- package/internal/dev/index.d.ts.map +1 -0
- package/internal/dev/index.js +6 -0
- package/internal/dev/index.js.map +1 -0
- package/internal/dev/profiler-wrapper.d.ts +16 -0
- package/internal/dev/profiler-wrapper.d.ts.map +1 -0
- package/internal/dev/profiler-wrapper.js +31 -0
- package/internal/dev/profiler-wrapper.js.map +1 -0
- package/internal/dev/use-key-stability.d.ts +22 -0
- package/internal/dev/use-key-stability.d.ts.map +1 -0
- package/internal/dev/use-key-stability.js +67 -0
- package/internal/dev/use-key-stability.js.map +1 -0
- package/internal/dev/use-render-tracer.d.ts +13 -0
- package/internal/dev/use-render-tracer.d.ts.map +1 -0
- package/internal/dev/use-render-tracer.js +57 -0
- package/internal/dev/use-render-tracer.js.map +1 -0
- package/internal/dev/use-stream-rate.d.ts +23 -0
- package/internal/dev/use-stream-rate.d.ts.map +1 -0
- package/internal/dev/use-stream-rate.js +94 -0
- package/internal/dev/use-stream-rate.js.map +1 -0
- package/internal/fetch-cache.d.ts +72 -0
- package/internal/fetch-cache.d.ts.map +1 -0
- package/internal/fetch-cache.js +118 -0
- package/internal/fetch-cache.js.map +1 -0
- package/internal/store/__tests__/conversation-store.test.d.ts +2 -0
- package/internal/store/__tests__/conversation-store.test.d.ts.map +1 -0
- package/internal/store/__tests__/conversation-store.test.js +200 -0
- package/internal/store/__tests__/conversation-store.test.js.map +1 -0
- package/internal/store/__tests__/structural-share.test.d.ts +2 -0
- package/internal/store/__tests__/structural-share.test.d.ts.map +1 -0
- package/internal/store/__tests__/structural-share.test.js +368 -0
- package/internal/store/__tests__/structural-share.test.js.map +1 -0
- package/internal/store/conversation-store.d.ts +62 -0
- package/internal/store/conversation-store.d.ts.map +1 -0
- package/internal/store/conversation-store.js +95 -0
- package/internal/store/conversation-store.js.map +1 -0
- package/internal/store/index.d.ts +31 -0
- package/internal/store/index.d.ts.map +1 -0
- package/internal/store/index.js +54 -0
- package/internal/store/index.js.map +1 -0
- package/internal/store/structural-share.d.ts +13 -0
- package/internal/store/structural-share.d.ts.map +1 -0
- package/internal/store/structural-share.js +240 -0
- package/internal/store/structural-share.js.map +1 -0
- package/internal/stream-controller.d.ts +85 -0
- package/internal/stream-controller.d.ts.map +1 -0
- package/internal/stream-controller.js +146 -0
- package/internal/stream-controller.js.map +1 -0
- package/internal/useAutoScroll.d.ts +32 -0
- package/internal/useAutoScroll.d.ts.map +1 -0
- package/internal/useAutoScroll.js +97 -0
- package/internal/useAutoScroll.js.map +1 -0
- package/internal/useFetch.d.ts +14 -0
- package/internal/useFetch.d.ts.map +1 -1
- package/internal/useFetch.js +32 -2
- package/internal/useFetch.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +3 -3
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.js +37 -9
- package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
- package/package.json +7 -5
- package/session/__tests__/useNewSessionFlow.test.js +16 -0
- package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
- package/session/__tests__/usePersistedModel.test.d.ts +2 -0
- package/session/__tests__/usePersistedModel.test.d.ts.map +1 -0
- package/session/__tests__/usePersistedModel.test.js +82 -0
- package/session/__tests__/usePersistedModel.test.js.map +1 -0
- package/session/__tests__/useSession.test.d.ts +2 -0
- package/session/__tests__/useSession.test.d.ts.map +1 -0
- package/session/__tests__/useSession.test.js +130 -0
- package/session/__tests__/useSession.test.js.map +1 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +12 -6
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/usePersistedModel.d.ts +3 -0
- package/session/usePersistedModel.d.ts.map +1 -1
- package/session/usePersistedModel.js +27 -2
- package/session/usePersistedModel.js.map +1 -1
- package/session/useSession.d.ts.map +1 -1
- package/session/useSession.js +1 -1
- package/session/useSession.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +9 -1
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +1 -1
- package/session/useSessionExecutions.js.map +1 -1
- package/session/useSessionPageFlow.js +1 -1
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useSessionUsage.d.ts +24 -40
- package/session/useSessionUsage.d.ts.map +1 -1
- package/session/useSessionUsage.js +64 -97
- package/session/useSessionUsage.js.map +1 -1
- package/settings/BillingSection.d.ts +3 -0
- package/settings/BillingSection.d.ts.map +1 -0
- package/settings/BillingSection.js +3 -0
- package/settings/BillingSection.js.map +1 -0
- package/settings/index.d.ts +2 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +1 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.js +1 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/billing/AutoRechargeCard.tsx +274 -0
- package/src/billing/BillingSection.tsx +255 -0
- package/src/billing/CreditBalanceCard.tsx +81 -0
- package/src/billing/CreditLedgerTable.tsx +281 -0
- package/src/billing/CreditPackGrid.tsx +132 -0
- package/src/billing/LowBalanceBanner.tsx +67 -0
- package/src/billing/PaymentMethodCard.tsx +133 -0
- package/src/billing/credit-packs.ts +54 -0
- package/src/billing/format.ts +97 -0
- package/src/billing/index.ts +51 -0
- package/src/billing/useBillingAccount.ts +64 -0
- package/src/billing/useBillingUsageReport.ts +73 -0
- package/src/billing/useCreateBillingPortalSession.ts +76 -0
- package/src/billing/useCreateCheckoutSession.ts +101 -0
- package/src/billing/useCreditLedger.ts +79 -0
- package/src/billing/useCustomerModelPricing.ts +67 -0
- package/src/billing/useSetAutoRechargeConfig.ts +90 -0
- package/src/composer/ComposerToolbar.tsx +1 -1
- package/src/composer/SessionComposer.tsx +22 -4
- package/src/composer/__tests__/SessionComposer-memo.test.ts +26 -0
- package/src/execution/ApprovalCard.tsx +7 -3
- package/src/execution/ExecutionPhaseBadge.tsx +3 -2
- package/src/execution/MessageEntry.tsx +27 -16
- package/src/execution/MessageThread.tsx +308 -131
- package/src/execution/SetupProgress.tsx +3 -3
- package/src/execution/SubAgentSection.tsx +14 -6
- package/src/execution/ThreadSkeleton.tsx +73 -0
- package/src/execution/ToolCallGroup.tsx +36 -3
- package/src/execution/UsageWidget.tsx +1 -1
- package/src/execution/__tests__/message-entry.test.tsx +236 -0
- package/src/execution/__tests__/thread-keys.test.ts +409 -0
- package/src/execution/__tests__/thread-memoization.test.ts +320 -0
- package/src/execution/__tests__/thread-skeleton.test.tsx +44 -0
- package/src/execution/__tests__/useExecutionStream.test.tsx +109 -12
- package/src/execution/__tests__/useSessionVariables-stability.test.ts +95 -0
- package/src/execution/__tests__/virtualized-thread.test.tsx +401 -0
- package/src/execution/index.ts +3 -0
- package/src/execution/useExecutionStream.ts +123 -48
- package/src/execution/useSessionVariables.ts +17 -12
- package/src/github/useGitHubConnection.ts +18 -13
- package/src/identity-account/index.ts +5 -0
- package/src/identity-account/useIdentityAccountGate.ts +163 -0
- package/src/index.ts +73 -0
- package/src/internal/FetchCacheProvider.tsx +74 -0
- package/src/internal/JumpToLatestButton.tsx +61 -0
- package/src/internal/ThreadItemWrapper.tsx +65 -0
- package/src/internal/VirtualizedThread.tsx +162 -0
- package/src/internal/__tests__/fetch-cache.test.ts +230 -0
- package/src/internal/__tests__/stream-controller.test.ts +395 -0
- package/src/internal/__tests__/thread-animation.test.tsx +121 -0
- package/src/internal/__tests__/useAutoScroll.test.tsx +261 -0
- package/src/internal/__tests__/useFetch-cache.test.ts +214 -0
- package/src/internal/dev/__tests__/use-key-stability.test.ts +124 -0
- package/src/internal/dev/__tests__/use-render-tracer.test.ts +78 -0
- package/src/internal/dev/dom-counter.ts +47 -0
- package/src/internal/dev/index.ts +5 -0
- package/src/internal/dev/profiler-wrapper.tsx +52 -0
- package/src/internal/dev/use-key-stability.ts +86 -0
- package/src/internal/dev/use-render-tracer.ts +70 -0
- package/src/internal/dev/use-stream-rate.ts +138 -0
- package/src/internal/fetch-cache.ts +155 -0
- package/src/internal/store/__tests__/conversation-store.test.ts +257 -0
- package/src/internal/store/__tests__/structural-share.test.ts +454 -0
- package/src/internal/store/conversation-store.ts +128 -0
- package/src/internal/store/index.ts +68 -0
- package/src/internal/store/structural-share.ts +318 -0
- package/src/internal/stream-controller.ts +201 -0
- package/src/internal/useAutoScroll.ts +121 -0
- package/src/internal/useFetch.ts +51 -2
- package/src/mcp-server/McpServerDetailView.tsx +15 -0
- package/src/mcp-server/useMcpServerOAuthConnect.ts +37 -9
- package/src/session/__tests__/useNewSessionFlow.test.tsx +22 -0
- package/src/session/__tests__/usePersistedModel.test.tsx +117 -0
- package/src/session/__tests__/useSession.test.tsx +187 -0
- package/src/session/useNewSessionFlow.ts +12 -6
- package/src/session/usePersistedModel.ts +28 -2
- package/src/session/useSession.ts +1 -0
- package/src/session/useSessionConversation.ts +11 -2
- package/src/session/useSessionExecutions.ts +1 -0
- package/src/session/useSessionPageFlow.ts +1 -1
- package/src/session/useSessionUsage.ts +102 -123
- package/src/settings/BillingSection.tsx +4 -0
- package/src/settings/index.ts +2 -0
- package/src/settings/settings-nav.ts +1 -1
- package/src/styles.css +31 -0
- package/src/usage/AgentBreakdownList.tsx +147 -0
- package/src/usage/CreditRunwayIndicator.tsx +71 -0
- package/src/usage/ExportButton.tsx +115 -0
- package/src/usage/HarnessSplitCard.tsx +103 -0
- package/src/usage/OrgUsagePanel.tsx +109 -45
- package/src/usage/index.ts +15 -0
- package/src/usage/useExportCSV.ts +115 -0
- package/src/usage/useOrgUsageReport.ts +2 -1
- package/src/workspace/__tests__/useWorkspaceEntries-stability.test.ts +76 -0
- package/src/workspace/useWorkspaceEntries.ts +16 -11
- package/styles.css +1 -1
- package/usage/AgentBreakdownList.d.ts +21 -0
- package/usage/AgentBreakdownList.d.ts.map +1 -0
- package/usage/AgentBreakdownList.js +44 -0
- package/usage/AgentBreakdownList.js.map +1 -0
- package/usage/CreditRunwayIndicator.d.ts +21 -0
- package/usage/CreditRunwayIndicator.d.ts.map +1 -0
- package/usage/CreditRunwayIndicator.js +38 -0
- package/usage/CreditRunwayIndicator.js.map +1 -0
- package/usage/ExportButton.d.ts +20 -0
- package/usage/ExportButton.d.ts.map +1 -0
- package/usage/ExportButton.js +36 -0
- package/usage/ExportButton.js.map +1 -0
- package/usage/HarnessSplitCard.d.ts +17 -0
- package/usage/HarnessSplitCard.d.ts.map +1 -0
- package/usage/HarnessSplitCard.js +38 -0
- package/usage/HarnessSplitCard.js.map +1 -0
- package/usage/OrgUsagePanel.d.ts.map +1 -1
- package/usage/OrgUsagePanel.js +30 -22
- package/usage/OrgUsagePanel.js.map +1 -1
- package/usage/index.d.ts +10 -0
- package/usage/index.d.ts.map +1 -1
- package/usage/index.js +5 -0
- package/usage/index.js.map +1 -1
- package/usage/useExportCSV.d.ts +23 -0
- package/usage/useExportCSV.d.ts.map +1 -0
- package/usage/useExportCSV.js +81 -0
- package/usage/useExportCSV.js.map +1 -0
- package/usage/useOrgUsageReport.d.ts +2 -1
- package/usage/useOrgUsageReport.d.ts.map +1 -1
- package/usage/useOrgUsageReport.js +2 -1
- package/usage/useOrgUsageReport.js.map +1 -1
- package/workspace/__tests__/useWorkspaceEntries-stability.test.d.ts +2 -0
- package/workspace/__tests__/useWorkspaceEntries-stability.test.d.ts.map +1 -0
- package/workspace/__tests__/useWorkspaceEntries-stability.test.js +57 -0
- package/workspace/__tests__/useWorkspaceEntries-stability.test.js.map +1 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js +5 -4
- package/workspace/useWorkspaceEntries.js.map +1 -1
package/src/internal/useFetch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { type DependencyList, useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useFetchCache } from "./FetchCacheProvider";
|
|
4
5
|
import { toError } from "./toError";
|
|
5
6
|
|
|
6
7
|
/** Options for {@link useFetch}. */
|
|
@@ -13,6 +14,21 @@ export interface UseFetchOptions {
|
|
|
13
14
|
* request piling on slow connections.
|
|
14
15
|
*/
|
|
15
16
|
readonly refetchInterval?: number | false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Stable string key for cross-mount caching.
|
|
20
|
+
*
|
|
21
|
+
* When provided (and a {@link FetchCacheProvider} is mounted above
|
|
22
|
+
* this component), the hook reads cached data on mount to avoid a
|
|
23
|
+
* loading skeleton, and writes fresh data to the cache on every
|
|
24
|
+
* successful fetch.
|
|
25
|
+
*
|
|
26
|
+
* Pass `undefined` to opt out of caching for a given call.
|
|
27
|
+
*
|
|
28
|
+
* @example `session:${id}`
|
|
29
|
+
* @example `session-executions:${sessionId}`
|
|
30
|
+
*/
|
|
31
|
+
readonly cacheKey?: string;
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
/** Return value of {@link useFetch}. */
|
|
@@ -59,14 +75,33 @@ export function useFetch<T>(
|
|
|
59
75
|
initialData: T,
|
|
60
76
|
options?: UseFetchOptions,
|
|
61
77
|
): UseFetchReturn<T> {
|
|
62
|
-
const
|
|
78
|
+
const cache = useFetchCache();
|
|
79
|
+
const cacheKey = options?.cacheKey;
|
|
80
|
+
|
|
81
|
+
// Resolve initial state from cache when available. The initializer
|
|
82
|
+
// function runs once on mount — exactly the right time to seed state
|
|
83
|
+
// from a previous mount's result and skip the loading skeleton.
|
|
84
|
+
const [data, setData] = useState<T>(() => {
|
|
85
|
+
if (cacheKey && cache) {
|
|
86
|
+
const cached = cache.get<T>(cacheKey);
|
|
87
|
+
if (cached !== undefined) return cached;
|
|
88
|
+
}
|
|
89
|
+
return initialData;
|
|
90
|
+
});
|
|
63
91
|
const [error, setError] = useState<Error | null>(null);
|
|
64
92
|
const [fetchKey, setFetchKey] = useState(0);
|
|
65
93
|
|
|
66
|
-
const hasDataRef = useRef(
|
|
94
|
+
const hasDataRef = useRef(
|
|
95
|
+
cacheKey && cache ? cache.has(cacheKey) : false,
|
|
96
|
+
);
|
|
67
97
|
const isFetchingRef = useRef(false);
|
|
68
98
|
const [isFetching, setIsFetching] = useState(false);
|
|
69
99
|
|
|
100
|
+
// Stable ref for cache — avoids adding cache to effect deps while
|
|
101
|
+
// still letting the effect body access the current instance.
|
|
102
|
+
const cacheRef = useRef(cache);
|
|
103
|
+
cacheRef.current = cache;
|
|
104
|
+
|
|
70
105
|
const refetch = useCallback(() => setFetchKey((k) => k + 1), []);
|
|
71
106
|
|
|
72
107
|
useEffect(() => {
|
|
@@ -79,6 +114,17 @@ export function useFetch<T>(
|
|
|
79
114
|
return;
|
|
80
115
|
}
|
|
81
116
|
|
|
117
|
+
// On dep change (without remount), check cache for the new key so
|
|
118
|
+
// we can show cached data immediately rather than stale data from
|
|
119
|
+
// a different identity (e.g. session A's data while session B loads).
|
|
120
|
+
if (cacheKey && cacheRef.current) {
|
|
121
|
+
const cached = cacheRef.current.get<T>(cacheKey);
|
|
122
|
+
if (cached !== undefined) {
|
|
123
|
+
setData(cached);
|
|
124
|
+
hasDataRef.current = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
82
128
|
const cancelled = { current: false };
|
|
83
129
|
setIsFetching(true);
|
|
84
130
|
isFetchingRef.current = true;
|
|
@@ -91,6 +137,9 @@ export function useFetch<T>(
|
|
|
91
137
|
hasDataRef.current = true;
|
|
92
138
|
setIsFetching(false);
|
|
93
139
|
isFetchingRef.current = false;
|
|
140
|
+
if (cacheKey && cacheRef.current) {
|
|
141
|
+
cacheRef.current.set(cacheKey, result);
|
|
142
|
+
}
|
|
94
143
|
},
|
|
95
144
|
(err) => {
|
|
96
145
|
if (cancelled.current) return;
|
|
@@ -383,6 +383,7 @@ export function McpServerDetailView({
|
|
|
383
383
|
credentials.setManualOverride(false);
|
|
384
384
|
setShowCredentialForm(false);
|
|
385
385
|
}}
|
|
386
|
+
onCancelOAuth={oauth.clearError}
|
|
386
387
|
/>
|
|
387
388
|
|
|
388
389
|
{showCredentialForm && credentials.missingVariables.length > 0 && (
|
|
@@ -566,6 +567,7 @@ function ConnectBar({
|
|
|
566
567
|
manualOverride,
|
|
567
568
|
onManualOverride,
|
|
568
569
|
onBackToOAuth,
|
|
570
|
+
onCancelOAuth,
|
|
569
571
|
}: {
|
|
570
572
|
readonly isConnecting: boolean;
|
|
571
573
|
readonly connectionError: Error | null;
|
|
@@ -600,6 +602,7 @@ function ConnectBar({
|
|
|
600
602
|
readonly manualOverride: boolean;
|
|
601
603
|
readonly onManualOverride: () => void;
|
|
602
604
|
readonly onBackToOAuth: () => void;
|
|
605
|
+
readonly onCancelOAuth: () => void;
|
|
603
606
|
}) {
|
|
604
607
|
const [disconnectPhase, setDisconnectPhase] = useState<DisconnectPhase>("idle");
|
|
605
608
|
const [removeOrgAppPhase, setRemoveOrgAppPhase] = useState<RemoveOrgAppPhase>("idle");
|
|
@@ -853,6 +856,18 @@ function ConnectBar({
|
|
|
853
856
|
</button>
|
|
854
857
|
</div>
|
|
855
858
|
|
|
859
|
+
{oauthPhase === "awaiting-callback" && (
|
|
860
|
+
<div className="flex items-center gap-3 border-t border-border px-3 py-1.5">
|
|
861
|
+
<button
|
|
862
|
+
type="button"
|
|
863
|
+
onClick={onCancelOAuth}
|
|
864
|
+
className="text-[11px] text-muted-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:text-foreground hover:decoration-foreground"
|
|
865
|
+
>
|
|
866
|
+
Cancel sign-in
|
|
867
|
+
</button>
|
|
868
|
+
</div>
|
|
869
|
+
)}
|
|
870
|
+
|
|
856
871
|
{/* Vendor approval blocked banner with BYOA CTA */}
|
|
857
872
|
{oauthSignInDisabled && (
|
|
858
873
|
<div className="flex items-start gap-2 border-t border-amber-500/20 bg-amber-500/5 px-3 py-2">
|
|
@@ -122,6 +122,7 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
|
|
|
122
122
|
|
|
123
123
|
const popupRef = useRef<Window | null>(null);
|
|
124
124
|
const cleanupRef = useRef<(() => void) | null>(null);
|
|
125
|
+
const cancelledRef = useRef(false);
|
|
125
126
|
|
|
126
127
|
useEffect(() => {
|
|
127
128
|
return () => {
|
|
@@ -130,6 +131,13 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
|
|
|
130
131
|
}, []);
|
|
131
132
|
|
|
132
133
|
const clearError = useCallback(() => {
|
|
134
|
+
if (cleanupRef.current || popupRef.current) {
|
|
135
|
+
cancelledRef.current = true;
|
|
136
|
+
cleanupRef.current?.();
|
|
137
|
+
closePopup(popupRef.current);
|
|
138
|
+
popupRef.current = null;
|
|
139
|
+
cleanupRef.current = null;
|
|
140
|
+
}
|
|
133
141
|
setPhase("idle");
|
|
134
142
|
setError(null);
|
|
135
143
|
}, []);
|
|
@@ -138,6 +146,7 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
|
|
|
138
146
|
async (mcpServerId: string, org: string, declaredEnvKeys?: readonly string[]): Promise<McpServer> => {
|
|
139
147
|
setPhase("initiating");
|
|
140
148
|
setError(null);
|
|
149
|
+
cancelledRef.current = false;
|
|
141
150
|
|
|
142
151
|
cleanupRef.current?.();
|
|
143
152
|
|
|
@@ -214,9 +223,12 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
|
|
|
214
223
|
return server;
|
|
215
224
|
} catch (err) {
|
|
216
225
|
const wrapped = toError(err);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
if (!cancelledRef.current) {
|
|
227
|
+
setError(wrapped);
|
|
228
|
+
setPhase("idle");
|
|
229
|
+
closePopup(popup);
|
|
230
|
+
}
|
|
231
|
+
cancelledRef.current = false;
|
|
220
232
|
throw wrapped;
|
|
221
233
|
} finally {
|
|
222
234
|
popupRef.current = null;
|
|
@@ -241,9 +253,14 @@ export function useMcpServerOAuthConnect(): UseMcpServerOAuthConnectReturn {
|
|
|
241
253
|
|
|
242
254
|
/**
|
|
243
255
|
* Grace period (ms) after `popup.closed` is first detected before treating
|
|
244
|
-
* it as a user-initiated close.
|
|
245
|
-
*
|
|
246
|
-
*
|
|
256
|
+
* it as a user-initiated close.
|
|
257
|
+
*
|
|
258
|
+
* Only used when BroadcastChannel is unavailable. When BC is available,
|
|
259
|
+
* `popup.closed` polling is skipped entirely because COOP providers
|
|
260
|
+
* (e.g. Sentry, GitHub) sever the opener reference on cross-origin
|
|
261
|
+
* navigation, making `popup.closed` permanently `true` while the popup
|
|
262
|
+
* is still active. The overall {@link POPUP_CALLBACK_TIMEOUT_MS} serves
|
|
263
|
+
* as the safety net for abandoned flows instead.
|
|
247
264
|
*/
|
|
248
265
|
const POPUP_CLOSED_GRACE_MS = 5_000;
|
|
249
266
|
|
|
@@ -257,6 +274,7 @@ function waitForOAuthCallback(
|
|
|
257
274
|
let timeoutId: ReturnType<typeof setTimeout>;
|
|
258
275
|
let pollId: ReturnType<typeof setInterval>;
|
|
259
276
|
let bc: BroadcastChannel | null = null;
|
|
277
|
+
let hasBroadcastChannel = false;
|
|
260
278
|
|
|
261
279
|
function cleanup() {
|
|
262
280
|
if (timeoutId) clearTimeout(timeoutId);
|
|
@@ -314,6 +332,7 @@ function waitForOAuthCallback(
|
|
|
314
332
|
// BroadcastChannel — works even when COOP severs window.opener.
|
|
315
333
|
try {
|
|
316
334
|
bc = new BroadcastChannel(OAUTH_BROADCAST_CHANNEL);
|
|
335
|
+
hasBroadcastChannel = true;
|
|
317
336
|
bc.onmessage = (event: MessageEvent) => {
|
|
318
337
|
validateAndSettle(event.data as OAuthCallbackMessage | undefined);
|
|
319
338
|
};
|
|
@@ -332,12 +351,21 @@ function waitForOAuthCallback(
|
|
|
332
351
|
closePopup(popup);
|
|
333
352
|
}, POPUP_CALLBACK_TIMEOUT_MS);
|
|
334
353
|
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
354
|
+
// When BroadcastChannel is available, skip popup.closed polling.
|
|
355
|
+
// COOP providers (Sentry, GitHub, etc.) sever the opener reference
|
|
356
|
+
// on cross-origin navigation, making popup.closed permanently true
|
|
357
|
+
// while the popup is still active. BroadcastChannel reliably
|
|
358
|
+
// delivers the callback regardless of COOP; the overall timeout
|
|
359
|
+
// above catches abandoned flows.
|
|
360
|
+
//
|
|
361
|
+
// When BroadcastChannel is NOT available (legacy browsers), fall
|
|
362
|
+
// back to popup.closed polling with a short grace period — it is
|
|
363
|
+
// the only signal we have in that degraded path.
|
|
338
364
|
let popupClosedAt: number | null = null;
|
|
339
365
|
|
|
340
366
|
pollId = setInterval(() => {
|
|
367
|
+
if (hasBroadcastChannel) return;
|
|
368
|
+
|
|
341
369
|
if (popup.closed) {
|
|
342
370
|
if (popupClosedAt === null) {
|
|
343
371
|
popupClosedAt = Date.now();
|
|
@@ -190,6 +190,28 @@ describe("useNewSessionFlow", () => {
|
|
|
190
190
|
// DEFAULT_MODEL_ID (anthropic) is not in cursor registry
|
|
191
191
|
expect(result.current.modelId).not.toBe(DEFAULT_MODEL_ID);
|
|
192
192
|
});
|
|
193
|
+
|
|
194
|
+
it("strips compound keys before persisting to localStorage", () => {
|
|
195
|
+
localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
|
|
196
|
+
const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
|
|
197
|
+
|
|
198
|
+
// Simulate compound key from unified mode ModelSelector
|
|
199
|
+
act(() => result.current.setModelId("cursor/default"));
|
|
200
|
+
|
|
201
|
+
// Should store plain modelId, not compound key
|
|
202
|
+
expect(localStorage.getItem(STORAGE_KEY_MODEL_CURSOR)).toBe("default");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("restores compound keys from localStorage as plain modelId", () => {
|
|
206
|
+
localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
|
|
207
|
+
// Legacy: compound key was stored before fix
|
|
208
|
+
localStorage.setItem(STORAGE_KEY_MODEL_CURSOR, "cursor/default");
|
|
209
|
+
|
|
210
|
+
const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
|
|
211
|
+
|
|
212
|
+
// Should extract plain modelId and validate against registry
|
|
213
|
+
expect(result.current.modelId).toBe(DEFAULT_CURSOR_MODEL_ID);
|
|
214
|
+
});
|
|
193
215
|
});
|
|
194
216
|
|
|
195
217
|
describe("submit with harness", () => {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import { usePersistedModel } from "../usePersistedModel";
|
|
4
|
+
import { DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID } from "../../models/registry";
|
|
5
|
+
|
|
6
|
+
const STORAGE_KEY_NATIVE = "stigmer:session:model";
|
|
7
|
+
const STORAGE_KEY_CURSOR = "stigmer:session:model:cursor";
|
|
8
|
+
|
|
9
|
+
describe("usePersistedModel", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
localStorage.clear();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
localStorage.clear();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("basic persistence", () => {
|
|
19
|
+
it("returns undefined when localStorage is empty", () => {
|
|
20
|
+
const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
|
|
21
|
+
expect(result.current[0]).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("restores a valid model from localStorage", () => {
|
|
25
|
+
localStorage.setItem(STORAGE_KEY_NATIVE, DEFAULT_MODEL_ID);
|
|
26
|
+
const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
|
|
27
|
+
expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns undefined for an invalid model in localStorage", () => {
|
|
31
|
+
localStorage.setItem(STORAGE_KEY_NATIVE, "nonexistent-model-xyz");
|
|
32
|
+
const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
|
|
33
|
+
expect(result.current[0]).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("persists model on change", () => {
|
|
37
|
+
const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
|
|
38
|
+
|
|
39
|
+
act(() => result.current[1](DEFAULT_MODEL_ID));
|
|
40
|
+
|
|
41
|
+
expect(localStorage.getItem(STORAGE_KEY_NATIVE)).toBe(DEFAULT_MODEL_ID);
|
|
42
|
+
expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("uses cursor-specific key for cursor harness", () => {
|
|
46
|
+
localStorage.setItem(STORAGE_KEY_CURSOR, DEFAULT_CURSOR_MODEL_ID);
|
|
47
|
+
const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }));
|
|
48
|
+
expect(result.current[0]).toBe(DEFAULT_CURSOR_MODEL_ID);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("compound key handling", () => {
|
|
53
|
+
it("extracts plain modelId from compound key in localStorage", () => {
|
|
54
|
+
localStorage.setItem(STORAGE_KEY_CURSOR, "cursor/default");
|
|
55
|
+
const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }));
|
|
56
|
+
expect(result.current[0]).toBe(DEFAULT_CURSOR_MODEL_ID);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("extracts plain modelId from native compound key", () => {
|
|
60
|
+
localStorage.setItem(STORAGE_KEY_NATIVE, `native/${DEFAULT_MODEL_ID}`);
|
|
61
|
+
const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
|
|
62
|
+
expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("handles non-compound values unchanged", () => {
|
|
66
|
+
localStorage.setItem(STORAGE_KEY_CURSOR, DEFAULT_CURSOR_MODEL_ID);
|
|
67
|
+
const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }));
|
|
68
|
+
expect(result.current[0]).toBe(DEFAULT_CURSOR_MODEL_ID);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("harness transition (key change re-sync)", () => {
|
|
73
|
+
it("re-reads from new localStorage key when harness changes", () => {
|
|
74
|
+
localStorage.setItem(STORAGE_KEY_NATIVE, DEFAULT_MODEL_ID);
|
|
75
|
+
localStorage.setItem(STORAGE_KEY_CURSOR, DEFAULT_CURSOR_MODEL_ID);
|
|
76
|
+
|
|
77
|
+
const { result, rerender } = renderHook(
|
|
78
|
+
({ harness }: { harness: "native" | "cursor" }) => usePersistedModel({ harness }),
|
|
79
|
+
{ initialProps: { harness: "native" } },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
|
|
83
|
+
|
|
84
|
+
rerender({ harness: "cursor" });
|
|
85
|
+
|
|
86
|
+
expect(result.current[0]).toBe(DEFAULT_CURSOR_MODEL_ID);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns undefined after harness change when new key has no stored value", () => {
|
|
90
|
+
localStorage.setItem(STORAGE_KEY_NATIVE, DEFAULT_MODEL_ID);
|
|
91
|
+
|
|
92
|
+
const { result, rerender } = renderHook(
|
|
93
|
+
({ harness }: { harness: "native" | "cursor" }) => usePersistedModel({ harness }),
|
|
94
|
+
{ initialProps: { harness: "native" } },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
|
|
98
|
+
|
|
99
|
+
rerender({ harness: "cursor" });
|
|
100
|
+
|
|
101
|
+
expect(result.current[0]).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("handles compound key in new storage key after harness transition", () => {
|
|
105
|
+
localStorage.setItem(STORAGE_KEY_CURSOR, "cursor/default");
|
|
106
|
+
|
|
107
|
+
const { result, rerender } = renderHook(
|
|
108
|
+
({ harness }: { harness: "native" | "cursor" }) => usePersistedModel({ harness }),
|
|
109
|
+
{ initialProps: { harness: "native" } },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
rerender({ harness: "cursor" });
|
|
113
|
+
|
|
114
|
+
expect(result.current[0]).toBe(DEFAULT_CURSOR_MODEL_ID);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import {
|
|
6
|
+
SessionSchema,
|
|
7
|
+
type Session,
|
|
8
|
+
} from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
9
|
+
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
10
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
11
|
+
import { StigmerContext } from "../../context";
|
|
12
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
13
|
+
import { FetchCache } from "../../internal/fetch-cache";
|
|
14
|
+
import { useSession } from "../useSession";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
function makeSession(id: string): Session {
|
|
21
|
+
const session = create(SessionSchema);
|
|
22
|
+
const metadata = create(ApiResourceMetadataSchema);
|
|
23
|
+
metadata.id = id;
|
|
24
|
+
session.metadata = metadata;
|
|
25
|
+
return session;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createMockStigmer(sessionGet: ReturnType<typeof vi.fn>): Stigmer {
|
|
29
|
+
return {
|
|
30
|
+
session: { get: sessionGet },
|
|
31
|
+
} as unknown as Stigmer;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function flush(): Promise<void> {
|
|
35
|
+
await act(async () => {
|
|
36
|
+
await Promise.resolve();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wrapper that provides both the Stigmer client and a shared FetchCache
|
|
42
|
+
* instance via context. Sharing the cache instance across renderHook
|
|
43
|
+
* calls mirrors the production layout where FetchCacheProvider sits
|
|
44
|
+
* above the key-based remount boundary.
|
|
45
|
+
*/
|
|
46
|
+
function createWrapper(client: Stigmer, cache: FetchCache) {
|
|
47
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
48
|
+
return (
|
|
49
|
+
<FetchCacheContext.Provider value={cache}>
|
|
50
|
+
<StigmerContext.Provider value={client}>
|
|
51
|
+
{children}
|
|
52
|
+
</StigmerContext.Provider>
|
|
53
|
+
</FetchCacheContext.Provider>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Tests
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
describe("useSession — cache behavior", () => {
|
|
63
|
+
it("first visit shows loading then resolves data", async () => {
|
|
64
|
+
const session = makeSession("ses_1");
|
|
65
|
+
const sessionGet = vi.fn().mockResolvedValue(session);
|
|
66
|
+
const cache = new FetchCache();
|
|
67
|
+
const wrapper = createWrapper(createMockStigmer(sessionGet), cache);
|
|
68
|
+
|
|
69
|
+
const { result } = renderHook(() => useSession("ses_1"), { wrapper });
|
|
70
|
+
|
|
71
|
+
expect(result.current.isLoading).toBe(true);
|
|
72
|
+
expect(result.current.session).toBeNull();
|
|
73
|
+
|
|
74
|
+
await flush();
|
|
75
|
+
|
|
76
|
+
expect(result.current.isLoading).toBe(false);
|
|
77
|
+
expect(result.current.session).toBe(session);
|
|
78
|
+
expect(sessionGet).toHaveBeenCalledOnce();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("remount serves cached data instantly (no isLoading)", async () => {
|
|
82
|
+
const session1 = makeSession("ses_1");
|
|
83
|
+
const sessionGet = vi.fn().mockResolvedValue(session1);
|
|
84
|
+
const client = createMockStigmer(sessionGet);
|
|
85
|
+
const cache = new FetchCache();
|
|
86
|
+
const wrapper = createWrapper(client, cache);
|
|
87
|
+
|
|
88
|
+
// First mount — populates the cache.
|
|
89
|
+
const { result: r1, unmount } = renderHook(
|
|
90
|
+
() => useSession("ses_1"),
|
|
91
|
+
{ wrapper },
|
|
92
|
+
);
|
|
93
|
+
await flush();
|
|
94
|
+
expect(r1.current.session).toBe(session1);
|
|
95
|
+
unmount();
|
|
96
|
+
|
|
97
|
+
// Second mount (simulates remount after key={activeSessionId} change).
|
|
98
|
+
const freshSession = makeSession("ses_1");
|
|
99
|
+
sessionGet.mockResolvedValue(freshSession);
|
|
100
|
+
|
|
101
|
+
const { result: r2 } = renderHook(
|
|
102
|
+
() => useSession("ses_1"),
|
|
103
|
+
{ wrapper },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Cached data is served synchronously — no loading skeleton.
|
|
107
|
+
expect(r2.current.isLoading).toBe(false);
|
|
108
|
+
expect(r2.current.session).toBe(session1);
|
|
109
|
+
expect(r2.current.isRefetching).toBe(true);
|
|
110
|
+
|
|
111
|
+
// Background fetch completes with fresh data.
|
|
112
|
+
await flush();
|
|
113
|
+
expect(r2.current.session).toBe(freshSession);
|
|
114
|
+
expect(r2.current.isRefetching).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("different session IDs have independent cache entries", async () => {
|
|
118
|
+
const sessionA = makeSession("ses_A");
|
|
119
|
+
const sessionB = makeSession("ses_B");
|
|
120
|
+
const sessionGet = vi.fn()
|
|
121
|
+
.mockResolvedValueOnce(sessionA)
|
|
122
|
+
.mockResolvedValueOnce(sessionB)
|
|
123
|
+
.mockResolvedValue(sessionA);
|
|
124
|
+
const cache = new FetchCache();
|
|
125
|
+
const wrapper = createWrapper(createMockStigmer(sessionGet), cache);
|
|
126
|
+
|
|
127
|
+
// Mount session A.
|
|
128
|
+
const { unmount: unmountA } = renderHook(
|
|
129
|
+
() => useSession("ses_A"),
|
|
130
|
+
{ wrapper },
|
|
131
|
+
);
|
|
132
|
+
await flush();
|
|
133
|
+
unmountA();
|
|
134
|
+
|
|
135
|
+
// Mount session B.
|
|
136
|
+
const { unmount: unmountB } = renderHook(
|
|
137
|
+
() => useSession("ses_B"),
|
|
138
|
+
{ wrapper },
|
|
139
|
+
);
|
|
140
|
+
await flush();
|
|
141
|
+
unmountB();
|
|
142
|
+
|
|
143
|
+
// Remount session A — should get A's cached data, not B's.
|
|
144
|
+
const { result } = renderHook(
|
|
145
|
+
() => useSession("ses_A"),
|
|
146
|
+
{ wrapper },
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(result.current.isLoading).toBe(false);
|
|
150
|
+
expect(result.current.session?.metadata?.id).toBe("ses_A");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("works without FetchCacheProvider (standard loading flow)", async () => {
|
|
154
|
+
const session = makeSession("ses_1");
|
|
155
|
+
const sessionGet = vi.fn().mockResolvedValue(session);
|
|
156
|
+
const client = createMockStigmer(sessionGet);
|
|
157
|
+
|
|
158
|
+
function NoCacheWrapper({ children }: { children: ReactNode }) {
|
|
159
|
+
return (
|
|
160
|
+
<StigmerContext.Provider value={client}>
|
|
161
|
+
{children}
|
|
162
|
+
</StigmerContext.Provider>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { result } = renderHook(() => useSession("ses_1"), {
|
|
167
|
+
wrapper: NoCacheWrapper,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(result.current.isLoading).toBe(true);
|
|
171
|
+
await flush();
|
|
172
|
+
expect(result.current.session).toBe(session);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("null id skips fetching and caching", async () => {
|
|
176
|
+
const sessionGet = vi.fn();
|
|
177
|
+
const cache = new FetchCache();
|
|
178
|
+
const wrapper = createWrapper(createMockStigmer(sessionGet), cache);
|
|
179
|
+
|
|
180
|
+
const { result } = renderHook(() => useSession(null), { wrapper });
|
|
181
|
+
|
|
182
|
+
expect(result.current.isLoading).toBe(false);
|
|
183
|
+
expect(result.current.session).toBeNull();
|
|
184
|
+
expect(sessionGet).not.toHaveBeenCalled();
|
|
185
|
+
expect(cache.size).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -5,6 +5,7 @@ import { getUserMessage, type McpServerUsageInput, type ResourceRef } from "@sti
|
|
|
5
5
|
import type { AgentResolution } from "../agent";
|
|
6
6
|
import { useDefaultAgent } from "../agent";
|
|
7
7
|
import { useModelRegistry } from "../models";
|
|
8
|
+
import { parseModelKey } from "../models/registry";
|
|
8
9
|
import { DEFAULT_HARNESS, type HarnessOption } from "../models/harness";
|
|
9
10
|
import { useWorkspaceEntries, type UseWorkspaceEntriesReturn } from "../workspace";
|
|
10
11
|
import { useSessionVariables, type UseSessionVariablesReturn } from "../execution/useSessionVariables";
|
|
@@ -184,9 +185,9 @@ export function useNewSessionFlow(
|
|
|
184
185
|
const setHarness = useCallback(
|
|
185
186
|
(h: HarnessOption) => {
|
|
186
187
|
setHarnessRaw(h);
|
|
187
|
-
// Restore per-harness model preference, or clear if none stored
|
|
188
188
|
const storedModel = localStorage.getItem(modelStorageKey(h));
|
|
189
|
-
|
|
189
|
+
const plain = storedModel ? (parseModelKey(storedModel)?.modelId ?? storedModel) : undefined;
|
|
190
|
+
setModelId(plain);
|
|
190
191
|
},
|
|
191
192
|
[],
|
|
192
193
|
);
|
|
@@ -194,15 +195,20 @@ export function useNewSessionFlow(
|
|
|
194
195
|
// Restore persisted model on mount (using current harness key)
|
|
195
196
|
useEffect(() => {
|
|
196
197
|
const stored = localStorage.getItem(modelStorageKey(harness));
|
|
197
|
-
if (stored
|
|
198
|
-
|
|
198
|
+
if (stored) {
|
|
199
|
+
const plain = parseModelKey(stored)?.modelId ?? stored;
|
|
200
|
+
if (getModel(plain)) {
|
|
201
|
+
setModelId(plain);
|
|
202
|
+
}
|
|
199
203
|
}
|
|
200
204
|
}, [getModel, harness]);
|
|
201
205
|
|
|
202
|
-
// Persist model on change (using current harness key)
|
|
206
|
+
// Persist model on change (using current harness key).
|
|
207
|
+
// Strip compound keys (e.g. "cursor/default") to plain modelId before storing.
|
|
203
208
|
useEffect(() => {
|
|
204
209
|
if (modelId) {
|
|
205
|
-
|
|
210
|
+
const plain = parseModelKey(modelId)?.modelId ?? modelId;
|
|
211
|
+
localStorage.setItem(modelStorageKey(harness), plain);
|
|
206
212
|
}
|
|
207
213
|
}, [modelId, harness]);
|
|
208
214
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { useModelRegistry } from "../models";
|
|
5
|
+
import { parseModelKey } from "../models/registry";
|
|
5
6
|
import type { HarnessOption } from "../models/harness";
|
|
6
7
|
|
|
7
8
|
/** Options for {@link usePersistedModel}. */
|
|
@@ -28,6 +29,16 @@ function storageKey(harness?: HarnessOption): string {
|
|
|
28
29
|
: "stigmer:session:model";
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Extract the plain modelId from a value that might be a compound key
|
|
34
|
+
* (e.g. `"cursor/default"` → `"default"`). Returns the value unchanged
|
|
35
|
+
* if it's already a plain ID.
|
|
36
|
+
*/
|
|
37
|
+
function extractPlainModelId(value: string): string {
|
|
38
|
+
const parsed = parseModelKey(value);
|
|
39
|
+
return parsed ? parsed.modelId : value;
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* Model selection with localStorage persistence.
|
|
33
44
|
*
|
|
@@ -39,6 +50,9 @@ function storageKey(harness?: HarnessOption): string {
|
|
|
39
50
|
* When `options.harness` is provided, the stored model is read from a
|
|
40
51
|
* harness-specific key and validated against the harness-filtered registry.
|
|
41
52
|
*
|
|
53
|
+
* Handles legacy compound keys (`"cursor/default"`) gracefully by
|
|
54
|
+
* extracting the plain modelId portion before validation.
|
|
55
|
+
*
|
|
42
56
|
* Used by both the session launcher (new session) and session page
|
|
43
57
|
* (follow-up messages) to maintain a consistent model preference.
|
|
44
58
|
*/
|
|
@@ -48,12 +62,24 @@ export function usePersistedModel(
|
|
|
48
62
|
const harness = options?.harness;
|
|
49
63
|
const { getModel } = useModelRegistry({ harness });
|
|
50
64
|
const key = storageKey(harness);
|
|
65
|
+
const prevKeyRef = useRef(key);
|
|
51
66
|
|
|
52
67
|
const [modelId, setModelId] = useState<string | undefined>(() => {
|
|
53
68
|
if (typeof window === "undefined") return undefined;
|
|
54
|
-
|
|
69
|
+
const raw = localStorage.getItem(key);
|
|
70
|
+
return raw ? extractPlainModelId(raw) : undefined;
|
|
55
71
|
});
|
|
56
72
|
|
|
73
|
+
// Re-read from localStorage when the storage key changes (harness transition).
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (prevKeyRef.current === key) return;
|
|
76
|
+
prevKeyRef.current = key;
|
|
77
|
+
|
|
78
|
+
if (typeof window === "undefined") return;
|
|
79
|
+
const raw = localStorage.getItem(key);
|
|
80
|
+
setModelId(raw ? extractPlainModelId(raw) : undefined);
|
|
81
|
+
}, [key]);
|
|
82
|
+
|
|
57
83
|
useEffect(() => {
|
|
58
84
|
if (modelId) {
|
|
59
85
|
localStorage.setItem(key, modelId);
|
|
@@ -62,6 +62,7 @@ export function useSession(id: string | null): UseSessionReturn {
|
|
|
62
62
|
id ? () => stigmer.session.get(id) : null,
|
|
63
63
|
[id, stigmer],
|
|
64
64
|
null as Session | null,
|
|
65
|
+
{ cacheKey: id ? `session:${id}` : undefined },
|
|
65
66
|
);
|
|
66
67
|
|
|
67
68
|
return { session, isLoading, isRefetching, error, refetch };
|