@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.
Files changed (450) hide show
  1. package/billing/AutoRechargeCard.d.ts +38 -0
  2. package/billing/AutoRechargeCard.d.ts.map +1 -0
  3. package/billing/AutoRechargeCard.js +90 -0
  4. package/billing/AutoRechargeCard.js.map +1 -0
  5. package/billing/BillingSection.d.ts +32 -0
  6. package/billing/BillingSection.d.ts.map +1 -0
  7. package/billing/BillingSection.js +81 -0
  8. package/billing/BillingSection.js.map +1 -0
  9. package/billing/CreditBalanceCard.d.ts +25 -0
  10. package/billing/CreditBalanceCard.d.ts.map +1 -0
  11. package/billing/CreditBalanceCard.js +28 -0
  12. package/billing/CreditBalanceCard.js.map +1 -0
  13. package/billing/CreditLedgerTable.d.ts +22 -0
  14. package/billing/CreditLedgerTable.d.ts.map +1 -0
  15. package/billing/CreditLedgerTable.js +75 -0
  16. package/billing/CreditLedgerTable.js.map +1 -0
  17. package/billing/CreditPackGrid.d.ts +31 -0
  18. package/billing/CreditPackGrid.d.ts.map +1 -0
  19. package/billing/CreditPackGrid.js +35 -0
  20. package/billing/CreditPackGrid.js.map +1 -0
  21. package/billing/LowBalanceBanner.d.ts +26 -0
  22. package/billing/LowBalanceBanner.d.ts.map +1 -0
  23. package/billing/LowBalanceBanner.js +33 -0
  24. package/billing/LowBalanceBanner.js.map +1 -0
  25. package/billing/PaymentMethodCard.d.ts +35 -0
  26. package/billing/PaymentMethodCard.d.ts.map +1 -0
  27. package/billing/PaymentMethodCard.js +48 -0
  28. package/billing/PaymentMethodCard.js.map +1 -0
  29. package/billing/credit-packs.d.ts +25 -0
  30. package/billing/credit-packs.d.ts.map +1 -0
  31. package/billing/credit-packs.js +39 -0
  32. package/billing/credit-packs.js.map +1 -0
  33. package/billing/format.d.ts +39 -0
  34. package/billing/format.d.ts.map +1 -0
  35. package/billing/format.js +90 -0
  36. package/billing/format.js.map +1 -0
  37. package/billing/index.d.ts +32 -0
  38. package/billing/index.d.ts.map +1 -0
  39. package/billing/index.js +21 -0
  40. package/billing/index.js.map +1 -0
  41. package/billing/useBillingAccount.d.ts +40 -0
  42. package/billing/useBillingAccount.d.ts.map +1 -0
  43. package/billing/useBillingAccount.js +35 -0
  44. package/billing/useBillingAccount.js.map +1 -0
  45. package/billing/useBillingUsageReport.d.ts +42 -0
  46. package/billing/useBillingUsageReport.d.ts.map +1 -0
  47. package/billing/useBillingUsageReport.js +43 -0
  48. package/billing/useBillingUsageReport.js.map +1 -0
  49. package/billing/useCreateBillingPortalSession.d.ts +35 -0
  50. package/billing/useCreateBillingPortalSession.d.ts.map +1 -0
  51. package/billing/useCreateBillingPortalSession.js +50 -0
  52. package/billing/useCreateBillingPortalSession.js.map +1 -0
  53. package/billing/useCreateCheckoutSession.d.ts +54 -0
  54. package/billing/useCreateCheckoutSession.d.ts.map +1 -0
  55. package/billing/useCreateCheckoutSession.js +58 -0
  56. package/billing/useCreateCheckoutSession.js.map +1 -0
  57. package/billing/useCreditLedger.d.ts +48 -0
  58. package/billing/useCreditLedger.d.ts.map +1 -0
  59. package/billing/useCreditLedger.js +39 -0
  60. package/billing/useCreditLedger.js.map +1 -0
  61. package/billing/useCustomerModelPricing.d.ts +41 -0
  62. package/billing/useCustomerModelPricing.d.ts.map +1 -0
  63. package/billing/useCustomerModelPricing.js +37 -0
  64. package/billing/useCustomerModelPricing.js.map +1 -0
  65. package/billing/useSetAutoRechargeConfig.d.ts +50 -0
  66. package/billing/useSetAutoRechargeConfig.d.ts.map +1 -0
  67. package/billing/useSetAutoRechargeConfig.js +53 -0
  68. package/billing/useSetAutoRechargeConfig.js.map +1 -0
  69. package/composer/ComposerToolbar.js +1 -1
  70. package/composer/ComposerToolbar.js.map +1 -1
  71. package/composer/SessionComposer.d.ts +1 -1
  72. package/composer/SessionComposer.d.ts.map +1 -1
  73. package/composer/SessionComposer.js +19 -4
  74. package/composer/SessionComposer.js.map +1 -1
  75. package/composer/__tests__/SessionComposer-memo.test.d.ts +2 -0
  76. package/composer/__tests__/SessionComposer-memo.test.d.ts.map +1 -0
  77. package/composer/__tests__/SessionComposer-memo.test.js +23 -0
  78. package/composer/__tests__/SessionComposer-memo.test.js.map +1 -0
  79. package/execution/ApprovalCard.d.ts +5 -1
  80. package/execution/ApprovalCard.d.ts.map +1 -1
  81. package/execution/ApprovalCard.js +7 -3
  82. package/execution/ApprovalCard.js.map +1 -1
  83. package/execution/ExecutionPhaseBadge.d.ts +1 -1
  84. package/execution/ExecutionPhaseBadge.d.ts.map +1 -1
  85. package/execution/ExecutionPhaseBadge.js +3 -2
  86. package/execution/ExecutionPhaseBadge.js.map +1 -1
  87. package/execution/MessageEntry.d.ts +7 -3
  88. package/execution/MessageEntry.d.ts.map +1 -1
  89. package/execution/MessageEntry.js +19 -8
  90. package/execution/MessageEntry.js.map +1 -1
  91. package/execution/MessageThread.d.ts +84 -3
  92. package/execution/MessageThread.d.ts.map +1 -1
  93. package/execution/MessageThread.js +113 -65
  94. package/execution/MessageThread.js.map +1 -1
  95. package/execution/SetupProgress.d.ts +1 -1
  96. package/execution/SetupProgress.d.ts.map +1 -1
  97. package/execution/SetupProgress.js +3 -3
  98. package/execution/SetupProgress.js.map +1 -1
  99. package/execution/SubAgentSection.d.ts +5 -1
  100. package/execution/SubAgentSection.d.ts.map +1 -1
  101. package/execution/SubAgentSection.js +13 -7
  102. package/execution/SubAgentSection.js.map +1 -1
  103. package/execution/ThreadSkeleton.d.ts +22 -0
  104. package/execution/ThreadSkeleton.d.ts.map +1 -0
  105. package/execution/ThreadSkeleton.js +26 -0
  106. package/execution/ThreadSkeleton.js.map +1 -0
  107. package/execution/ToolCallGroup.d.ts +16 -1
  108. package/execution/ToolCallGroup.d.ts.map +1 -1
  109. package/execution/ToolCallGroup.js +31 -3
  110. package/execution/ToolCallGroup.js.map +1 -1
  111. package/execution/UsageWidget.d.ts +1 -1
  112. package/execution/__tests__/message-entry.test.d.ts +2 -0
  113. package/execution/__tests__/message-entry.test.d.ts.map +1 -0
  114. package/execution/__tests__/message-entry.test.js +178 -0
  115. package/execution/__tests__/message-entry.test.js.map +1 -0
  116. package/execution/__tests__/thread-keys.test.d.ts +2 -0
  117. package/execution/__tests__/thread-keys.test.d.ts.map +1 -0
  118. package/execution/__tests__/thread-keys.test.js +289 -0
  119. package/execution/__tests__/thread-keys.test.js.map +1 -0
  120. package/execution/__tests__/thread-memoization.test.d.ts +2 -0
  121. package/execution/__tests__/thread-memoization.test.d.ts.map +1 -0
  122. package/execution/__tests__/thread-memoization.test.js +262 -0
  123. package/execution/__tests__/thread-memoization.test.js.map +1 -0
  124. package/execution/__tests__/thread-skeleton.test.d.ts +2 -0
  125. package/execution/__tests__/thread-skeleton.test.d.ts.map +1 -0
  126. package/execution/__tests__/thread-skeleton.test.js +35 -0
  127. package/execution/__tests__/thread-skeleton.test.js.map +1 -0
  128. package/execution/__tests__/useExecutionStream.test.js +73 -10
  129. package/execution/__tests__/useExecutionStream.test.js.map +1 -1
  130. package/execution/__tests__/useSessionVariables-stability.test.d.ts +2 -0
  131. package/execution/__tests__/useSessionVariables-stability.test.d.ts.map +1 -0
  132. package/execution/__tests__/useSessionVariables-stability.test.js +69 -0
  133. package/execution/__tests__/useSessionVariables-stability.test.js.map +1 -0
  134. package/execution/__tests__/virtualized-thread.test.d.ts +2 -0
  135. package/execution/__tests__/virtualized-thread.test.d.ts.map +1 -0
  136. package/execution/__tests__/virtualized-thread.test.js +274 -0
  137. package/execution/__tests__/virtualized-thread.test.js.map +1 -0
  138. package/execution/index.d.ts +2 -0
  139. package/execution/index.d.ts.map +1 -1
  140. package/execution/index.js +1 -0
  141. package/execution/index.js.map +1 -1
  142. package/execution/useExecutionStream.d.ts +35 -10
  143. package/execution/useExecutionStream.d.ts.map +1 -1
  144. package/execution/useExecutionStream.js +79 -40
  145. package/execution/useExecutionStream.js.map +1 -1
  146. package/execution/useSessionVariables.d.ts.map +1 -1
  147. package/execution/useSessionVariables.js +4 -3
  148. package/execution/useSessionVariables.js.map +1 -1
  149. package/github/useGitHubConnection.d.ts.map +1 -1
  150. package/github/useGitHubConnection.js +5 -4
  151. package/github/useGitHubConnection.js.map +1 -1
  152. package/identity-account/index.d.ts +2 -0
  153. package/identity-account/index.d.ts.map +1 -0
  154. package/identity-account/index.js +2 -0
  155. package/identity-account/index.js.map +1 -0
  156. package/identity-account/useIdentityAccountGate.d.ts +81 -0
  157. package/identity-account/useIdentityAccountGate.d.ts.map +1 -0
  158. package/identity-account/useIdentityAccountGate.js +100 -0
  159. package/identity-account/useIdentityAccountGate.js.map +1 -0
  160. package/index.d.ts +10 -4
  161. package/index.d.ts.map +1 -1
  162. package/index.js +8 -2
  163. package/index.js.map +1 -1
  164. package/internal/FetchCacheProvider.d.ts +44 -0
  165. package/internal/FetchCacheProvider.d.ts.map +1 -0
  166. package/internal/FetchCacheProvider.js +61 -0
  167. package/internal/FetchCacheProvider.js.map +1 -0
  168. package/internal/JumpToLatestButton.d.ts +14 -0
  169. package/internal/JumpToLatestButton.d.ts.map +1 -0
  170. package/internal/JumpToLatestButton.js +19 -0
  171. package/internal/JumpToLatestButton.js.map +1 -0
  172. package/internal/ThreadItemWrapper.d.ts +20 -0
  173. package/internal/ThreadItemWrapper.d.ts.map +1 -0
  174. package/internal/ThreadItemWrapper.js +44 -0
  175. package/internal/ThreadItemWrapper.js.map +1 -0
  176. package/internal/VirtualizedThread.d.ts +25 -0
  177. package/internal/VirtualizedThread.d.ts.map +1 -0
  178. package/internal/VirtualizedThread.js +58 -0
  179. package/internal/VirtualizedThread.js.map +1 -0
  180. package/internal/__tests__/fetch-cache.test.d.ts +2 -0
  181. package/internal/__tests__/fetch-cache.test.d.ts.map +1 -0
  182. package/internal/__tests__/fetch-cache.test.js +182 -0
  183. package/internal/__tests__/fetch-cache.test.js.map +1 -0
  184. package/internal/__tests__/stream-controller.test.d.ts +2 -0
  185. package/internal/__tests__/stream-controller.test.d.ts.map +1 -0
  186. package/internal/__tests__/stream-controller.test.js +294 -0
  187. package/internal/__tests__/stream-controller.test.js.map +1 -0
  188. package/internal/__tests__/thread-animation.test.d.ts +2 -0
  189. package/internal/__tests__/thread-animation.test.d.ts.map +1 -0
  190. package/internal/__tests__/thread-animation.test.js +79 -0
  191. package/internal/__tests__/thread-animation.test.js.map +1 -0
  192. package/internal/__tests__/useAutoScroll.test.d.ts +2 -0
  193. package/internal/__tests__/useAutoScroll.test.d.ts.map +1 -0
  194. package/internal/__tests__/useAutoScroll.test.js +188 -0
  195. package/internal/__tests__/useAutoScroll.test.js.map +1 -0
  196. package/internal/__tests__/useFetch-cache.test.d.ts +2 -0
  197. package/internal/__tests__/useFetch-cache.test.d.ts.map +1 -0
  198. package/internal/__tests__/useFetch-cache.test.js +137 -0
  199. package/internal/__tests__/useFetch-cache.test.js.map +1 -0
  200. package/internal/dev/__tests__/use-key-stability.test.d.ts +2 -0
  201. package/internal/dev/__tests__/use-key-stability.test.d.ts.map +1 -0
  202. package/internal/dev/__tests__/use-key-stability.test.js +72 -0
  203. package/internal/dev/__tests__/use-key-stability.test.js.map +1 -0
  204. package/internal/dev/__tests__/use-render-tracer.test.d.ts +2 -0
  205. package/internal/dev/__tests__/use-render-tracer.test.d.ts.map +1 -0
  206. package/internal/dev/__tests__/use-render-tracer.test.js +55 -0
  207. package/internal/dev/__tests__/use-render-tracer.test.js.map +1 -0
  208. package/internal/dev/dom-counter.d.ts +14 -0
  209. package/internal/dev/dom-counter.d.ts.map +1 -0
  210. package/internal/dev/dom-counter.js +39 -0
  211. package/internal/dev/dom-counter.js.map +1 -0
  212. package/internal/dev/index.d.ts +6 -0
  213. package/internal/dev/index.d.ts.map +1 -0
  214. package/internal/dev/index.js +6 -0
  215. package/internal/dev/index.js.map +1 -0
  216. package/internal/dev/profiler-wrapper.d.ts +16 -0
  217. package/internal/dev/profiler-wrapper.d.ts.map +1 -0
  218. package/internal/dev/profiler-wrapper.js +31 -0
  219. package/internal/dev/profiler-wrapper.js.map +1 -0
  220. package/internal/dev/use-key-stability.d.ts +22 -0
  221. package/internal/dev/use-key-stability.d.ts.map +1 -0
  222. package/internal/dev/use-key-stability.js +67 -0
  223. package/internal/dev/use-key-stability.js.map +1 -0
  224. package/internal/dev/use-render-tracer.d.ts +13 -0
  225. package/internal/dev/use-render-tracer.d.ts.map +1 -0
  226. package/internal/dev/use-render-tracer.js +57 -0
  227. package/internal/dev/use-render-tracer.js.map +1 -0
  228. package/internal/dev/use-stream-rate.d.ts +23 -0
  229. package/internal/dev/use-stream-rate.d.ts.map +1 -0
  230. package/internal/dev/use-stream-rate.js +94 -0
  231. package/internal/dev/use-stream-rate.js.map +1 -0
  232. package/internal/fetch-cache.d.ts +72 -0
  233. package/internal/fetch-cache.d.ts.map +1 -0
  234. package/internal/fetch-cache.js +118 -0
  235. package/internal/fetch-cache.js.map +1 -0
  236. package/internal/store/__tests__/conversation-store.test.d.ts +2 -0
  237. package/internal/store/__tests__/conversation-store.test.d.ts.map +1 -0
  238. package/internal/store/__tests__/conversation-store.test.js +200 -0
  239. package/internal/store/__tests__/conversation-store.test.js.map +1 -0
  240. package/internal/store/__tests__/structural-share.test.d.ts +2 -0
  241. package/internal/store/__tests__/structural-share.test.d.ts.map +1 -0
  242. package/internal/store/__tests__/structural-share.test.js +368 -0
  243. package/internal/store/__tests__/structural-share.test.js.map +1 -0
  244. package/internal/store/conversation-store.d.ts +62 -0
  245. package/internal/store/conversation-store.d.ts.map +1 -0
  246. package/internal/store/conversation-store.js +95 -0
  247. package/internal/store/conversation-store.js.map +1 -0
  248. package/internal/store/index.d.ts +31 -0
  249. package/internal/store/index.d.ts.map +1 -0
  250. package/internal/store/index.js +54 -0
  251. package/internal/store/index.js.map +1 -0
  252. package/internal/store/structural-share.d.ts +13 -0
  253. package/internal/store/structural-share.d.ts.map +1 -0
  254. package/internal/store/structural-share.js +240 -0
  255. package/internal/store/structural-share.js.map +1 -0
  256. package/internal/stream-controller.d.ts +85 -0
  257. package/internal/stream-controller.d.ts.map +1 -0
  258. package/internal/stream-controller.js +146 -0
  259. package/internal/stream-controller.js.map +1 -0
  260. package/internal/useAutoScroll.d.ts +32 -0
  261. package/internal/useAutoScroll.d.ts.map +1 -0
  262. package/internal/useAutoScroll.js +97 -0
  263. package/internal/useAutoScroll.js.map +1 -0
  264. package/internal/useFetch.d.ts +14 -0
  265. package/internal/useFetch.d.ts.map +1 -1
  266. package/internal/useFetch.js +32 -2
  267. package/internal/useFetch.js.map +1 -1
  268. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  269. package/mcp-server/McpServerDetailView.js +3 -3
  270. package/mcp-server/McpServerDetailView.js.map +1 -1
  271. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
  272. package/mcp-server/useMcpServerOAuthConnect.js +37 -9
  273. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
  274. package/package.json +7 -5
  275. package/session/__tests__/useNewSessionFlow.test.js +16 -0
  276. package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
  277. package/session/__tests__/usePersistedModel.test.d.ts +2 -0
  278. package/session/__tests__/usePersistedModel.test.d.ts.map +1 -0
  279. package/session/__tests__/usePersistedModel.test.js +82 -0
  280. package/session/__tests__/usePersistedModel.test.js.map +1 -0
  281. package/session/__tests__/useSession.test.d.ts +2 -0
  282. package/session/__tests__/useSession.test.d.ts.map +1 -0
  283. package/session/__tests__/useSession.test.js +130 -0
  284. package/session/__tests__/useSession.test.js.map +1 -0
  285. package/session/useNewSessionFlow.d.ts.map +1 -1
  286. package/session/useNewSessionFlow.js +12 -6
  287. package/session/useNewSessionFlow.js.map +1 -1
  288. package/session/usePersistedModel.d.ts +3 -0
  289. package/session/usePersistedModel.d.ts.map +1 -1
  290. package/session/usePersistedModel.js +27 -2
  291. package/session/usePersistedModel.js.map +1 -1
  292. package/session/useSession.d.ts.map +1 -1
  293. package/session/useSession.js +1 -1
  294. package/session/useSession.js.map +1 -1
  295. package/session/useSessionConversation.d.ts.map +1 -1
  296. package/session/useSessionConversation.js +9 -1
  297. package/session/useSessionConversation.js.map +1 -1
  298. package/session/useSessionExecutions.d.ts.map +1 -1
  299. package/session/useSessionExecutions.js +1 -1
  300. package/session/useSessionExecutions.js.map +1 -1
  301. package/session/useSessionPageFlow.js +1 -1
  302. package/session/useSessionPageFlow.js.map +1 -1
  303. package/session/useSessionUsage.d.ts +24 -40
  304. package/session/useSessionUsage.d.ts.map +1 -1
  305. package/session/useSessionUsage.js +64 -97
  306. package/session/useSessionUsage.js.map +1 -1
  307. package/settings/BillingSection.d.ts +3 -0
  308. package/settings/BillingSection.d.ts.map +1 -0
  309. package/settings/BillingSection.js +3 -0
  310. package/settings/BillingSection.js.map +1 -0
  311. package/settings/index.d.ts +2 -0
  312. package/settings/index.d.ts.map +1 -1
  313. package/settings/index.js +1 -0
  314. package/settings/index.js.map +1 -1
  315. package/settings/settings-nav.js +1 -1
  316. package/settings/settings-nav.js.map +1 -1
  317. package/src/billing/AutoRechargeCard.tsx +274 -0
  318. package/src/billing/BillingSection.tsx +255 -0
  319. package/src/billing/CreditBalanceCard.tsx +81 -0
  320. package/src/billing/CreditLedgerTable.tsx +281 -0
  321. package/src/billing/CreditPackGrid.tsx +132 -0
  322. package/src/billing/LowBalanceBanner.tsx +67 -0
  323. package/src/billing/PaymentMethodCard.tsx +133 -0
  324. package/src/billing/credit-packs.ts +54 -0
  325. package/src/billing/format.ts +97 -0
  326. package/src/billing/index.ts +51 -0
  327. package/src/billing/useBillingAccount.ts +64 -0
  328. package/src/billing/useBillingUsageReport.ts +73 -0
  329. package/src/billing/useCreateBillingPortalSession.ts +76 -0
  330. package/src/billing/useCreateCheckoutSession.ts +101 -0
  331. package/src/billing/useCreditLedger.ts +79 -0
  332. package/src/billing/useCustomerModelPricing.ts +67 -0
  333. package/src/billing/useSetAutoRechargeConfig.ts +90 -0
  334. package/src/composer/ComposerToolbar.tsx +1 -1
  335. package/src/composer/SessionComposer.tsx +22 -4
  336. package/src/composer/__tests__/SessionComposer-memo.test.ts +26 -0
  337. package/src/execution/ApprovalCard.tsx +7 -3
  338. package/src/execution/ExecutionPhaseBadge.tsx +3 -2
  339. package/src/execution/MessageEntry.tsx +27 -16
  340. package/src/execution/MessageThread.tsx +308 -131
  341. package/src/execution/SetupProgress.tsx +3 -3
  342. package/src/execution/SubAgentSection.tsx +14 -6
  343. package/src/execution/ThreadSkeleton.tsx +73 -0
  344. package/src/execution/ToolCallGroup.tsx +36 -3
  345. package/src/execution/UsageWidget.tsx +1 -1
  346. package/src/execution/__tests__/message-entry.test.tsx +236 -0
  347. package/src/execution/__tests__/thread-keys.test.ts +409 -0
  348. package/src/execution/__tests__/thread-memoization.test.ts +320 -0
  349. package/src/execution/__tests__/thread-skeleton.test.tsx +44 -0
  350. package/src/execution/__tests__/useExecutionStream.test.tsx +109 -12
  351. package/src/execution/__tests__/useSessionVariables-stability.test.ts +95 -0
  352. package/src/execution/__tests__/virtualized-thread.test.tsx +401 -0
  353. package/src/execution/index.ts +3 -0
  354. package/src/execution/useExecutionStream.ts +123 -48
  355. package/src/execution/useSessionVariables.ts +17 -12
  356. package/src/github/useGitHubConnection.ts +18 -13
  357. package/src/identity-account/index.ts +5 -0
  358. package/src/identity-account/useIdentityAccountGate.ts +163 -0
  359. package/src/index.ts +73 -0
  360. package/src/internal/FetchCacheProvider.tsx +74 -0
  361. package/src/internal/JumpToLatestButton.tsx +61 -0
  362. package/src/internal/ThreadItemWrapper.tsx +65 -0
  363. package/src/internal/VirtualizedThread.tsx +162 -0
  364. package/src/internal/__tests__/fetch-cache.test.ts +230 -0
  365. package/src/internal/__tests__/stream-controller.test.ts +395 -0
  366. package/src/internal/__tests__/thread-animation.test.tsx +121 -0
  367. package/src/internal/__tests__/useAutoScroll.test.tsx +261 -0
  368. package/src/internal/__tests__/useFetch-cache.test.ts +214 -0
  369. package/src/internal/dev/__tests__/use-key-stability.test.ts +124 -0
  370. package/src/internal/dev/__tests__/use-render-tracer.test.ts +78 -0
  371. package/src/internal/dev/dom-counter.ts +47 -0
  372. package/src/internal/dev/index.ts +5 -0
  373. package/src/internal/dev/profiler-wrapper.tsx +52 -0
  374. package/src/internal/dev/use-key-stability.ts +86 -0
  375. package/src/internal/dev/use-render-tracer.ts +70 -0
  376. package/src/internal/dev/use-stream-rate.ts +138 -0
  377. package/src/internal/fetch-cache.ts +155 -0
  378. package/src/internal/store/__tests__/conversation-store.test.ts +257 -0
  379. package/src/internal/store/__tests__/structural-share.test.ts +454 -0
  380. package/src/internal/store/conversation-store.ts +128 -0
  381. package/src/internal/store/index.ts +68 -0
  382. package/src/internal/store/structural-share.ts +318 -0
  383. package/src/internal/stream-controller.ts +201 -0
  384. package/src/internal/useAutoScroll.ts +121 -0
  385. package/src/internal/useFetch.ts +51 -2
  386. package/src/mcp-server/McpServerDetailView.tsx +15 -0
  387. package/src/mcp-server/useMcpServerOAuthConnect.ts +37 -9
  388. package/src/session/__tests__/useNewSessionFlow.test.tsx +22 -0
  389. package/src/session/__tests__/usePersistedModel.test.tsx +117 -0
  390. package/src/session/__tests__/useSession.test.tsx +187 -0
  391. package/src/session/useNewSessionFlow.ts +12 -6
  392. package/src/session/usePersistedModel.ts +28 -2
  393. package/src/session/useSession.ts +1 -0
  394. package/src/session/useSessionConversation.ts +11 -2
  395. package/src/session/useSessionExecutions.ts +1 -0
  396. package/src/session/useSessionPageFlow.ts +1 -1
  397. package/src/session/useSessionUsage.ts +102 -123
  398. package/src/settings/BillingSection.tsx +4 -0
  399. package/src/settings/index.ts +2 -0
  400. package/src/settings/settings-nav.ts +1 -1
  401. package/src/styles.css +31 -0
  402. package/src/usage/AgentBreakdownList.tsx +147 -0
  403. package/src/usage/CreditRunwayIndicator.tsx +71 -0
  404. package/src/usage/ExportButton.tsx +115 -0
  405. package/src/usage/HarnessSplitCard.tsx +103 -0
  406. package/src/usage/OrgUsagePanel.tsx +109 -45
  407. package/src/usage/index.ts +15 -0
  408. package/src/usage/useExportCSV.ts +115 -0
  409. package/src/usage/useOrgUsageReport.ts +2 -1
  410. package/src/workspace/__tests__/useWorkspaceEntries-stability.test.ts +76 -0
  411. package/src/workspace/useWorkspaceEntries.ts +16 -11
  412. package/styles.css +1 -1
  413. package/usage/AgentBreakdownList.d.ts +21 -0
  414. package/usage/AgentBreakdownList.d.ts.map +1 -0
  415. package/usage/AgentBreakdownList.js +44 -0
  416. package/usage/AgentBreakdownList.js.map +1 -0
  417. package/usage/CreditRunwayIndicator.d.ts +21 -0
  418. package/usage/CreditRunwayIndicator.d.ts.map +1 -0
  419. package/usage/CreditRunwayIndicator.js +38 -0
  420. package/usage/CreditRunwayIndicator.js.map +1 -0
  421. package/usage/ExportButton.d.ts +20 -0
  422. package/usage/ExportButton.d.ts.map +1 -0
  423. package/usage/ExportButton.js +36 -0
  424. package/usage/ExportButton.js.map +1 -0
  425. package/usage/HarnessSplitCard.d.ts +17 -0
  426. package/usage/HarnessSplitCard.d.ts.map +1 -0
  427. package/usage/HarnessSplitCard.js +38 -0
  428. package/usage/HarnessSplitCard.js.map +1 -0
  429. package/usage/OrgUsagePanel.d.ts.map +1 -1
  430. package/usage/OrgUsagePanel.js +30 -22
  431. package/usage/OrgUsagePanel.js.map +1 -1
  432. package/usage/index.d.ts +10 -0
  433. package/usage/index.d.ts.map +1 -1
  434. package/usage/index.js +5 -0
  435. package/usage/index.js.map +1 -1
  436. package/usage/useExportCSV.d.ts +23 -0
  437. package/usage/useExportCSV.d.ts.map +1 -0
  438. package/usage/useExportCSV.js +81 -0
  439. package/usage/useExportCSV.js.map +1 -0
  440. package/usage/useOrgUsageReport.d.ts +2 -1
  441. package/usage/useOrgUsageReport.d.ts.map +1 -1
  442. package/usage/useOrgUsageReport.js +2 -1
  443. package/usage/useOrgUsageReport.js.map +1 -1
  444. package/workspace/__tests__/useWorkspaceEntries-stability.test.d.ts +2 -0
  445. package/workspace/__tests__/useWorkspaceEntries-stability.test.d.ts.map +1 -0
  446. package/workspace/__tests__/useWorkspaceEntries-stability.test.js +57 -0
  447. package/workspace/__tests__/useWorkspaceEntries-stability.test.js.map +1 -0
  448. package/workspace/useWorkspaceEntries.d.ts.map +1 -1
  449. package/workspace/useWorkspaceEntries.js +5 -4
  450. package/workspace/useWorkspaceEntries.js.map +1 -1
@@ -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 [data, setData] = useState<T>(initialData);
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(false);
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
- setError(wrapped);
218
- setPhase("idle");
219
- closePopup(popup);
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. COOP providers sever the opener reference
245
- * immediately, making `popup.closed` appear `true` while the popup is still
246
- * active. The grace period lets the BroadcastChannel callback arrive.
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
- // COOP providers make popup.closed appear true immediately after
336
- // cross-origin navigation. Wait a grace period before treating it
337
- // as a real user-initiated close.
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
- setModelId(storedModel ?? undefined);
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 && getModel(stored)) {
198
- setModelId(stored);
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
- localStorage.setItem(modelStorageKey(harness), modelId);
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
- return localStorage.getItem(key) ?? undefined;
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 };