@kortix/sdk 0.12.7 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +13 -6
  2. package/dist/browser/session-sync/session-sync-registry.d.ts +7 -2
  3. package/dist/browser/session-sync/session-sync-registry.d.ts.map +1 -1
  4. package/dist/browser/session-sync/session-sync-registry.js +33 -44
  5. package/dist/browser/session-sync/session-sync-registry.js.map +1 -1
  6. package/dist/browser/stores/opencode-compaction-store.d.ts +9 -3
  7. package/dist/browser/stores/opencode-compaction-store.d.ts.map +1 -1
  8. package/dist/browser/stores/opencode-compaction-store.js +5 -12
  9. package/dist/browser/stores/opencode-compaction-store.js.map +1 -1
  10. package/dist/browser/stores/sandbox-connection-store.d.ts +28 -17
  11. package/dist/browser/stores/sandbox-connection-store.d.ts.map +1 -1
  12. package/dist/browser/stores/sandbox-connection-store.js +32 -84
  13. package/dist/browser/stores/sandbox-connection-store.js.map +1 -1
  14. package/dist/browser/stores/session-working-store.d.ts +73 -0
  15. package/dist/browser/stores/session-working-store.d.ts.map +1 -0
  16. package/dist/browser/stores/session-working-store.js +86 -0
  17. package/dist/browser/stores/session-working-store.js.map +1 -0
  18. package/dist/browser/stores/sync-store/types.d.ts +14 -4
  19. package/dist/browser/stores/sync-store/types.d.ts.map +1 -1
  20. package/dist/browser/stores/sync-store.d.ts +140 -3
  21. package/dist/browser/stores/sync-store.d.ts.map +1 -1
  22. package/dist/browser/stores/sync-store.js +425 -36
  23. package/dist/browser/stores/sync-store.js.map +1 -1
  24. package/dist/core/client/kortix.d.ts +102 -1
  25. package/dist/core/client/kortix.d.ts.map +1 -1
  26. package/dist/core/client/kortix.js +78 -0
  27. package/dist/core/client/kortix.js.map +1 -1
  28. package/dist/core/files/client.d.ts +50 -2
  29. package/dist/core/files/client.d.ts.map +1 -1
  30. package/dist/core/files/client.js +253 -44
  31. package/dist/core/files/client.js.map +1 -1
  32. package/dist/core/files/types.d.ts +11 -0
  33. package/dist/core/files/types.d.ts.map +1 -1
  34. package/dist/core/http/abort-error.d.ts +89 -0
  35. package/dist/core/http/abort-error.d.ts.map +1 -0
  36. package/dist/core/http/abort-error.js +190 -0
  37. package/dist/core/http/abort-error.js.map +1 -0
  38. package/dist/core/http/api-client.d.ts.map +1 -1
  39. package/dist/core/http/api-client.js +28 -4
  40. package/dist/core/http/api-client.js.map +1 -1
  41. package/dist/core/http/impersonation.d.ts +58 -0
  42. package/dist/core/http/impersonation.d.ts.map +1 -0
  43. package/dist/core/http/impersonation.js +174 -0
  44. package/dist/core/http/impersonation.js.map +1 -0
  45. package/dist/core/rest/platform-client/github-app.d.ts +13 -0
  46. package/dist/core/rest/platform-client/github-app.d.ts.map +1 -1
  47. package/dist/core/rest/platform-client/github-app.js +2 -0
  48. package/dist/core/rest/platform-client/github-app.js.map +1 -1
  49. package/dist/core/rest/platform-client/members.d.ts +10 -64
  50. package/dist/core/rest/platform-client/members.d.ts.map +1 -1
  51. package/dist/core/rest/platform-client/members.js +10 -28
  52. package/dist/core/rest/platform-client/members.js.map +1 -1
  53. package/dist/core/rest/projects-client/accounts.d.ts +30 -1
  54. package/dist/core/rest/projects-client/accounts.d.ts.map +1 -1
  55. package/dist/core/rest/projects-client/accounts.js +1 -1
  56. package/dist/core/rest/projects-client/accounts.js.map +1 -1
  57. package/dist/core/rest/projects-client/agent-config.d.ts +21 -0
  58. package/dist/core/rest/projects-client/agent-config.d.ts.map +1 -1
  59. package/dist/core/rest/projects-client/agent-config.js +14 -0
  60. package/dist/core/rest/projects-client/agent-config.js.map +1 -1
  61. package/dist/core/rest/projects-client/apps.d.ts +13 -0
  62. package/dist/core/rest/projects-client/apps.d.ts.map +1 -1
  63. package/dist/core/rest/projects-client/apps.js.map +1 -1
  64. package/dist/core/rest/projects-client/assignments.d.ts +114 -0
  65. package/dist/core/rest/projects-client/assignments.d.ts.map +1 -0
  66. package/dist/core/rest/projects-client/assignments.js +91 -0
  67. package/dist/core/rest/projects-client/assignments.js.map +1 -0
  68. package/dist/core/rest/projects-client/billing.d.ts +80 -1
  69. package/dist/core/rest/projects-client/billing.d.ts.map +1 -1
  70. package/dist/core/rest/projects-client/billing.js +61 -2
  71. package/dist/core/rest/projects-client/billing.js.map +1 -1
  72. package/dist/core/rest/projects-client/connectors.d.ts +2 -0
  73. package/dist/core/rest/projects-client/connectors.d.ts.map +1 -1
  74. package/dist/core/rest/projects-client/connectors.js.map +1 -1
  75. package/dist/core/rest/projects-client/files.js +2 -2
  76. package/dist/core/rest/projects-client/files.js.map +1 -1
  77. package/dist/core/rest/projects-client/gateway.d.ts +51 -0
  78. package/dist/core/rest/projects-client/gateway.d.ts.map +1 -1
  79. package/dist/core/rest/projects-client/gateway.js.map +1 -1
  80. package/dist/core/rest/projects-client/iam.d.ts +51 -3
  81. package/dist/core/rest/projects-client/iam.d.ts.map +1 -1
  82. package/dist/core/rest/projects-client/iam.js +18 -1
  83. package/dist/core/rest/projects-client/iam.js.map +1 -1
  84. package/dist/core/rest/projects-client/index.d.ts +1 -0
  85. package/dist/core/rest/projects-client/index.d.ts.map +1 -1
  86. package/dist/core/rest/projects-client/index.js +1 -0
  87. package/dist/core/rest/projects-client/index.js.map +1 -1
  88. package/dist/core/rest/projects-client/projects.d.ts +1 -1
  89. package/dist/core/rest/projects-client/projects.d.ts.map +1 -1
  90. package/dist/core/rest/projects-client/projects.js +3 -0
  91. package/dist/core/rest/projects-client/projects.js.map +1 -1
  92. package/dist/core/rest/projects-client/secrets.d.ts +11 -0
  93. package/dist/core/rest/projects-client/secrets.d.ts.map +1 -1
  94. package/dist/core/rest/projects-client/secrets.js +1 -1
  95. package/dist/core/rest/projects-client/secrets.js.map +1 -1
  96. package/dist/core/rest/projects-client/session-costs.d.ts +33 -0
  97. package/dist/core/rest/projects-client/session-costs.d.ts.map +1 -1
  98. package/dist/core/rest/projects-client/session-costs.js.map +1 -1
  99. package/dist/core/rest/projects-client/sessions.d.ts +253 -4
  100. package/dist/core/rest/projects-client/sessions.d.ts.map +1 -1
  101. package/dist/core/rest/projects-client/sessions.js +101 -4
  102. package/dist/core/rest/projects-client/sessions.js.map +1 -1
  103. package/dist/core/rest/projects-client/shared.d.ts +10 -1
  104. package/dist/core/rest/projects-client/shared.d.ts.map +1 -1
  105. package/dist/core/rest/projects-client/shared.js.map +1 -1
  106. package/dist/core/rest/projects-client/tokens.d.ts +13 -1
  107. package/dist/core/rest/projects-client/tokens.d.ts.map +1 -1
  108. package/dist/core/rest/projects-client/tokens.js +7 -2
  109. package/dist/core/rest/projects-client/tokens.js.map +1 -1
  110. package/dist/core/rest/projects-client/triggers.d.ts +69 -2
  111. package/dist/core/rest/projects-client/triggers.d.ts.map +1 -1
  112. package/dist/core/rest/projects-client/triggers.js +1 -1
  113. package/dist/core/rest/projects-client/triggers.js.map +1 -1
  114. package/dist/core/session/compaction.d.ts +98 -0
  115. package/dist/core/session/compaction.d.ts.map +1 -0
  116. package/dist/core/session/compaction.js +108 -0
  117. package/dist/core/session/compaction.js.map +1 -0
  118. package/dist/core/session/health.d.ts +22 -0
  119. package/dist/core/session/health.d.ts.map +1 -1
  120. package/dist/core/session/health.js +31 -5
  121. package/dist/core/session/health.js.map +1 -1
  122. package/dist/core/session/index.d.ts +2 -0
  123. package/dist/core/session/index.d.ts.map +1 -1
  124. package/dist/core/session/index.js +2 -0
  125. package/dist/core/session/index.js.map +1 -1
  126. package/dist/core/session/message-queue.d.ts +79 -15
  127. package/dist/core/session/message-queue.d.ts.map +1 -1
  128. package/dist/core/session/message-queue.js +73 -27
  129. package/dist/core/session/message-queue.js.map +1 -1
  130. package/dist/core/session/rewind.d.ts +76 -10
  131. package/dist/core/session/rewind.d.ts.map +1 -1
  132. package/dist/core/session/rewind.js +68 -11
  133. package/dist/core/session/rewind.js.map +1 -1
  134. package/dist/core/session/working.d.ts +241 -0
  135. package/dist/core/session/working.d.ts.map +1 -0
  136. package/dist/core/session/working.js +261 -0
  137. package/dist/core/session/working.js.map +1 -0
  138. package/dist/core/session-sync/session-sync-controller.d.ts +35 -20
  139. package/dist/core/session-sync/session-sync-controller.d.ts.map +1 -1
  140. package/dist/core/session-sync/session-sync-controller.js +48 -115
  141. package/dist/core/session-sync/session-sync-controller.js.map +1 -1
  142. package/dist/core/stream/event-stream.d.ts +22 -0
  143. package/dist/core/stream/event-stream.d.ts.map +1 -1
  144. package/dist/core/stream/event-stream.js +132 -36
  145. package/dist/core/stream/event-stream.js.map +1 -1
  146. package/dist/core/turns/classify.d.ts +5 -2
  147. package/dist/core/turns/classify.d.ts.map +1 -1
  148. package/dist/core/turns/classify.js +1 -0
  149. package/dist/core/turns/classify.js.map +1 -1
  150. package/dist/core/turns/errors.d.ts +11 -0
  151. package/dist/core/turns/errors.d.ts.map +1 -1
  152. package/dist/core/turns/errors.js +49 -2
  153. package/dist/core/turns/errors.js.map +1 -1
  154. package/dist/core/turns/grouping.d.ts +32 -2
  155. package/dist/core/turns/grouping.d.ts.map +1 -1
  156. package/dist/core/turns/grouping.js +31 -2
  157. package/dist/core/turns/grouping.js.map +1 -1
  158. package/dist/core/turns/index.d.ts +1 -0
  159. package/dist/core/turns/index.d.ts.map +1 -1
  160. package/dist/core/turns/index.js +1 -0
  161. package/dist/core/turns/index.js.map +1 -1
  162. package/dist/core/turns/open-turn.d.ts +81 -0
  163. package/dist/core/turns/open-turn.d.ts.map +1 -0
  164. package/dist/core/turns/open-turn.js +77 -0
  165. package/dist/core/turns/open-turn.js.map +1 -0
  166. package/dist/core/turns/state.d.ts +1 -1
  167. package/dist/core/turns/state.d.ts.map +1 -1
  168. package/dist/core/turns/state.js +29 -27
  169. package/dist/core/turns/state.js.map +1 -1
  170. package/dist/core/turns/types.d.ts +8 -0
  171. package/dist/core/turns/types.d.ts.map +1 -1
  172. package/dist/index.d.ts +3 -1
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.js +3 -1
  175. package/dist/index.js.map +1 -1
  176. package/dist/kortix.esm.min.js +25 -25
  177. package/dist/kortix.global.js +25 -25
  178. package/dist/node/server.d.ts.map +1 -1
  179. package/dist/node/server.js +14 -2
  180. package/dist/node/server.js.map +1 -1
  181. package/dist/react/index.d.ts +3 -1
  182. package/dist/react/index.d.ts.map +1 -1
  183. package/dist/react/index.js +8 -1
  184. package/dist/react/index.js.map +1 -1
  185. package/dist/react/initial-session-pin.d.ts +17 -0
  186. package/dist/react/initial-session-pin.d.ts.map +1 -1
  187. package/dist/react/initial-session-pin.js +58 -0
  188. package/dist/react/initial-session-pin.js.map +1 -1
  189. package/dist/react/opencode.d.ts +6 -2
  190. package/dist/react/opencode.d.ts.map +1 -1
  191. package/dist/react/opencode.js +5 -1
  192. package/dist/react/opencode.js.map +1 -1
  193. package/dist/react/prefetch-session-start.d.ts +10 -4
  194. package/dist/react/prefetch-session-start.d.ts.map +1 -1
  195. package/dist/react/prefetch-session-start.js +10 -4
  196. package/dist/react/prefetch-session-start.js.map +1 -1
  197. package/dist/react/query-keys.d.ts +8 -2
  198. package/dist/react/query-keys.d.ts.map +1 -1
  199. package/dist/react/query-keys.js +8 -2
  200. package/dist/react/query-keys.js.map +1 -1
  201. package/dist/react/session-start-stash.d.ts +25 -0
  202. package/dist/react/session-start-stash.d.ts.map +1 -1
  203. package/dist/react/session-start-stash.js +34 -3
  204. package/dist/react/session-start-stash.js.map +1 -1
  205. package/dist/react/session-title-sync.d.ts +5 -1
  206. package/dist/react/session-title-sync.d.ts.map +1 -1
  207. package/dist/react/session-title-sync.js +16 -4
  208. package/dist/react/session-title-sync.js.map +1 -1
  209. package/dist/react/use-admin-accounts.d.ts +121 -0
  210. package/dist/react/use-admin-accounts.d.ts.map +1 -1
  211. package/dist/react/use-admin-accounts.js +92 -0
  212. package/dist/react/use-admin-accounts.js.map +1 -1
  213. package/dist/react/use-can.d.ts +105 -0
  214. package/dist/react/use-can.d.ts.map +1 -0
  215. package/dist/react/use-can.js +190 -0
  216. package/dist/react/use-can.js.map +1 -0
  217. package/dist/react/use-canonical-opencode-session.d.ts +6 -1
  218. package/dist/react/use-canonical-opencode-session.d.ts.map +1 -1
  219. package/dist/react/use-canonical-opencode-session.js +46 -3
  220. package/dist/react/use-canonical-opencode-session.js.map +1 -1
  221. package/dist/react/use-impersonation.d.ts +54 -0
  222. package/dist/react/use-impersonation.d.ts.map +1 -0
  223. package/dist/react/use-impersonation.js +91 -0
  224. package/dist/react/use-impersonation.js.map +1 -0
  225. package/dist/react/use-opencode-events/handle-event.d.ts +10 -0
  226. package/dist/react/use-opencode-events/handle-event.d.ts.map +1 -1
  227. package/dist/react/use-opencode-events/handle-event.js +76 -7
  228. package/dist/react/use-opencode-events/handle-event.js.map +1 -1
  229. package/dist/react/use-opencode-events/helpers.d.ts +21 -8
  230. package/dist/react/use-opencode-events/helpers.d.ts.map +1 -1
  231. package/dist/react/use-opencode-events/helpers.js +25 -26
  232. package/dist/react/use-opencode-events/helpers.js.map +1 -1
  233. package/dist/react/use-opencode-events/index.d.ts.map +1 -1
  234. package/dist/react/use-opencode-events/index.js +43 -26
  235. package/dist/react/use-opencode-events/index.js.map +1 -1
  236. package/dist/react/use-opencode-events/use-event-stream-refs.d.ts.map +1 -1
  237. package/dist/react/use-opencode-events/use-event-stream-refs.js +66 -3
  238. package/dist/react/use-opencode-events/use-event-stream-refs.js.map +1 -1
  239. package/dist/react/use-opencode-local.d.ts +17 -0
  240. package/dist/react/use-opencode-local.d.ts.map +1 -1
  241. package/dist/react/use-opencode-local.js +41 -7
  242. package/dist/react/use-opencode-local.js.map +1 -1
  243. package/dist/react/use-opencode-sessions/messages.d.ts +101 -1
  244. package/dist/react/use-opencode-sessions/messages.d.ts.map +1 -1
  245. package/dist/react/use-opencode-sessions/messages.js +481 -63
  246. package/dist/react/use-opencode-sessions/messages.js.map +1 -1
  247. package/dist/react/use-project-apps.d.ts +24 -1
  248. package/dist/react/use-project-apps.d.ts.map +1 -1
  249. package/dist/react/use-project-apps.js +5 -3
  250. package/dist/react/use-project-apps.js.map +1 -1
  251. package/dist/react/use-runtime-boot-stalled.d.ts +32 -0
  252. package/dist/react/use-runtime-boot-stalled.d.ts.map +1 -0
  253. package/dist/react/use-runtime-boot-stalled.js +48 -0
  254. package/dist/react/use-runtime-boot-stalled.js.map +1 -0
  255. package/dist/react/use-runtime-reconnect.d.ts +67 -1
  256. package/dist/react/use-runtime-reconnect.d.ts.map +1 -1
  257. package/dist/react/use-runtime-reconnect.js +116 -18
  258. package/dist/react/use-runtime-reconnect.js.map +1 -1
  259. package/dist/react/use-session-prompts.d.ts +113 -0
  260. package/dist/react/use-session-prompts.d.ts.map +1 -0
  261. package/dist/react/use-session-prompts.js +242 -0
  262. package/dist/react/use-session-prompts.js.map +1 -0
  263. package/dist/react/use-session-send.d.ts +169 -20
  264. package/dist/react/use-session-send.d.ts.map +1 -1
  265. package/dist/react/use-session-send.js +178 -29
  266. package/dist/react/use-session-send.js.map +1 -1
  267. package/dist/react/use-session-sync.d.ts +35 -2
  268. package/dist/react/use-session-sync.d.ts.map +1 -1
  269. package/dist/react/use-session-sync.js +46 -9
  270. package/dist/react/use-session-sync.js.map +1 -1
  271. package/dist/react/use-session-working.d.ts +57 -0
  272. package/dist/react/use-session-working.d.ts.map +1 -0
  273. package/dist/react/use-session-working.js +178 -0
  274. package/dist/react/use-session-working.js.map +1 -0
  275. package/dist/react/use-session.d.ts +122 -5
  276. package/dist/react/use-session.d.ts.map +1 -1
  277. package/dist/react/use-session.js +343 -69
  278. package/dist/react/use-session.js.map +1 -1
  279. package/package.json +2 -2
  280. package/dist/browser/stores/tab-store.d.ts +0 -91
  281. package/dist/browser/stores/tab-store.d.ts.map +0 -1
  282. package/dist/browser/stores/tab-store.js +0 -369
  283. package/dist/browser/stores/tab-store.js.map +0 -1
  284. package/dist/core/session/send-queue.d.ts +0 -70
  285. package/dist/core/session/send-queue.d.ts.map +0 -1
  286. package/dist/core/session/send-queue.js +0 -116
  287. package/dist/core/session/send-queue.js.map +0 -1
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+ /**
3
+ * `useCan` / `useCans` — the ONLY client-side authorization read.
4
+ *
5
+ * Relocated from `apps/web/src/lib/use-permission.ts` + `use-project-can.ts`.
6
+ * They were host-local, so every other host (the whitelabel demo, mobile) had
7
+ * no probe at all and fell back to reading a role literal off a row — which is
8
+ * exactly the bug the canonical model deletes: a member holding a custom role
9
+ * that grants `project.members.manage` is invisible to `role === 'manager'`.
10
+ *
11
+ * Ask for the LEAF the route asserts (`project.gitops.push`), never for a role.
12
+ * The engine answers; the client renders the answer.
13
+ *
14
+ * NOT a security boundary — the API re-checks every mutating route. This only
15
+ * decides what to show and enable.
16
+ *
17
+ * ### Cache contract
18
+ *
19
+ * A verdict is cached for 5 minutes. That is only safe because every
20
+ * assignment write calls `invalidatePermissionProbes`, so a revoke is visible
21
+ * on the next render rather than up to five minutes later. If you add a write
22
+ * path that changes an assignment, it calls that function. No exceptions.
23
+ */
24
+ import { useQuery } from '@tanstack/react-query';
25
+ import { getProject, probeEffectivePermission, probeEffectivePermissions, validateToken, } from '../core/rest/projects-client/index.js';
26
+ import { contract } from './query-contracts.js';
27
+ import { qk } from './query-keys.js';
28
+ const PROBE_STALE_MS = 5 * 60_000;
29
+ /** The single-probe cache key. Kept as a flat literal (not under `qk`) because
30
+ * it predates that factory and both `apps/web` and its tests address it. */
31
+ export const permissionProbeKey = (accountId, userId, action, target) => [
32
+ 'iam-permission',
33
+ accountId,
34
+ userId,
35
+ action,
36
+ target?.resourceType ?? null,
37
+ target?.resourceId ?? null,
38
+ ];
39
+ /** The batch cache key. Keyed on the probe LIST so adding or removing a probe
40
+ * refetches; the signature is action+target per probe, never object identity,
41
+ * which React swaps every render. */
42
+ export const permissionProbeBatchKey = (accountId, userId, probes) => [
43
+ 'iam-permission-batch',
44
+ accountId,
45
+ userId,
46
+ probes.map((p) => `${p.action}|${p.resourceType ?? ''}|${p.resourceId ?? ''}`).join(','),
47
+ ];
48
+ /** Identity of the calling principal, for hosts that do not inject one. */
49
+ const identityKey = ['iam-identity', 'me'];
50
+ function useProbeUserId(explicit) {
51
+ const query = useQuery({
52
+ queryKey: identityKey,
53
+ queryFn: async () => (await validateToken()).identity ?? null,
54
+ enabled: !explicit,
55
+ staleTime: Infinity,
56
+ });
57
+ return explicit ?? query.data?.user_id;
58
+ }
59
+ /**
60
+ * Resolve the account that owns a project. A DISABLED react-query query reports
61
+ * `isLoading === false` in v5, so callers must not read the unresolved window
62
+ * as a denial — `pending()` below converts it to "still loading".
63
+ */
64
+ function useOwningAccountId(projectId, accountIdHint) {
65
+ const { data } = useQuery({
66
+ queryKey: qk.project.summary(projectId ?? ''),
67
+ queryFn: () => getProject(projectId),
68
+ enabled: !!projectId && !accountIdHint,
69
+ ...contract('config'),
70
+ });
71
+ return projectId ? (accountIdHint ?? data?.account_id) : undefined;
72
+ }
73
+ const pending = () => ({
74
+ allowed: false,
75
+ reason: null,
76
+ isLoading: true,
77
+ isError: false,
78
+ });
79
+ /** Everything a target resolves to: the account to ask, and the probe target. */
80
+ function useResolvedTarget(target) {
81
+ const isProject = 'projectId' in target && target.projectId !== undefined;
82
+ const projectId = 'projectId' in target ? target.projectId : undefined;
83
+ const owningAccountId = useOwningAccountId(projectId, target.accountId);
84
+ const accountId = isProject ? owningAccountId : target.accountId;
85
+ const probeTarget = isProject
86
+ ? { resourceType: 'project', resourceId: projectId }
87
+ : target.resource
88
+ ? { resourceType: target.resource.type, resourceId: target.resource.id }
89
+ : undefined;
90
+ return { accountId, probeTarget, resolved: !!accountId && (!isProject || !!projectId) };
91
+ }
92
+ /** Can this principal perform `action` on this target? */
93
+ export function useCan(target, action, options) {
94
+ const userId = useProbeUserId(options?.userId);
95
+ const { accountId, probeTarget, resolved } = useResolvedTarget(target);
96
+ const enabled = resolved && !!userId;
97
+ const query = useQuery({
98
+ queryKey: permissionProbeKey(accountId, userId, action, probeTarget),
99
+ queryFn: () => probeEffectivePermission(accountId, userId, (probeTarget ? { action, ...probeTarget } : { action })),
100
+ enabled,
101
+ staleTime: PROBE_STALE_MS,
102
+ });
103
+ if (!enabled)
104
+ return pending();
105
+ return {
106
+ allowed: query.data?.allowed === true,
107
+ reason: query.data?.reason ?? null,
108
+ isLoading: query.isLoading,
109
+ isError: query.isError,
110
+ };
111
+ }
112
+ /**
113
+ * Batch variant — ONE roundtrip for N actions on the same target. Returns a map
114
+ * keyed by action, so a caller reads `caps[ACTION].allowed`.
115
+ *
116
+ * `actions` must be stable across renders (module-level or memoized): the query
117
+ * key is derived from the list.
118
+ */
119
+ export function useCans(target, actions, options) {
120
+ const userId = useProbeUserId(options?.userId);
121
+ const { accountId, probeTarget, resolved } = useResolvedTarget(target);
122
+ const probes = actions.map((action) => (probeTarget ? { action, ...probeTarget } : { action }));
123
+ const enabled = resolved && !!userId && probes.length > 0;
124
+ const query = useQuery({
125
+ queryKey: permissionProbeBatchKey(accountId, userId, probes),
126
+ queryFn: () => probeEffectivePermissions(accountId, userId, probes),
127
+ enabled,
128
+ staleTime: PROBE_STALE_MS,
129
+ });
130
+ const map = {};
131
+ actions.forEach((action, i) => {
132
+ if (!enabled) {
133
+ map[action] = pending();
134
+ return;
135
+ }
136
+ const hit = query.data?.[i];
137
+ map[action] = {
138
+ allowed: hit?.allowed === true,
139
+ reason: hit?.reason ?? null,
140
+ isLoading: query.isLoading,
141
+ isError: query.isError,
142
+ };
143
+ });
144
+ return map;
145
+ }
146
+ /**
147
+ * Probe on behalf of ANOTHER member — the "what can this person actually do"
148
+ * admin panels. Returns one result per input, in order. The caller needs
149
+ * `member.read` on the account; the server enforces it.
150
+ *
151
+ * Kept probe-shaped (not action-shaped like `useCans`) because these panels mix
152
+ * account and project targets in one request.
153
+ */
154
+ export function usePermissionsFor(accountId, memberUserId, probes) {
155
+ const enabled = !!accountId && !!memberUserId && probes.length > 0;
156
+ const query = useQuery({
157
+ queryKey: permissionProbeBatchKey(accountId, memberUserId, probes),
158
+ queryFn: () => probeEffectivePermissions(accountId, memberUserId, probes),
159
+ enabled,
160
+ staleTime: PROBE_STALE_MS,
161
+ });
162
+ return probes.map((_, i) => {
163
+ if (!enabled)
164
+ return pending();
165
+ const hit = query.data?.[i];
166
+ return {
167
+ allowed: hit?.allowed === true,
168
+ reason: hit?.reason ?? null,
169
+ isLoading: query.isLoading,
170
+ isError: query.isError,
171
+ };
172
+ });
173
+ }
174
+ /**
175
+ * Bust the probe cache after an assignment write. **Every** write that creates,
176
+ * changes or revokes an assignment calls this — a 5-minute stale window on an
177
+ * authorization verdict is a revoke that has not happened yet.
178
+ *
179
+ * Scope it as tightly as you honestly can: an account+user pair when you know
180
+ * whose access changed, the account when a group or a role definition changed
181
+ * (that moves verdicts for principals you cannot enumerate client-side), and
182
+ * everything only when you know nothing.
183
+ */
184
+ export function invalidatePermissionProbes(queryClient, scope) {
185
+ const suffix = [scope.accountId, scope.userId].filter((s) => !!s);
186
+ queryClient.invalidateQueries({ queryKey: ['iam-permission', ...suffix] });
187
+ queryClient.invalidateQueries({ queryKey: ['iam-permission-batch', ...suffix] });
188
+ return Promise.resolve();
189
+ }
190
+ //# sourceMappingURL=use-can.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-can.js","sourceRoot":"","sources":["../../src/react/use-can.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,12 @@ import { type Session } from './use-opencode-sessions/index.js';
8
8
  * the client (the old client-side `ensure-opencode` mutation caused the
9
9
  * "session replaced / data lost" drift). It just surfaces the pin:
10
10
  * 1. the value /start handed us this render (`pinFromStart`), else
11
- * 2. the persisted pin on the Kortix session row (`getProjectSession`).
11
+ * 2. the host's own warm-list pin (`initialPin`), else
12
+ * 3. the persisted row (`getProjectSession`) once its REST read resolves,
13
+ * or — before it does — this browser's own synchronous local mirror of
14
+ * the last canonical id seen for this session (T6). The local
15
+ * mirror exists ONLY to cover the gap before (3)'s network read lands;
16
+ * it never outranks it once loaded, and neither ever outranks (1).
12
17
  *
13
18
  * The OpenCode session list is still read (read-only) for ?oc deep-links and
14
19
  * sidebar sub-session rendering.
@@ -1 +1 @@
1
- {"version":3,"file":"use-canonical-opencode-session.d.ts","sourceRoot":"","sources":["../../src/react/use-canonical-opencode-session.ts"],"names":[],"mappings":"AAGA,OAAO,EAAuB,KAAK,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAG5E;;;;;;;;;;;;;GAaG;AAEH;gFACgF;AAChF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,MAAM,WAAW,wBAAwB;IACvC,kFAAkF;IAClF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,GAAG,wBAAwB,CAuC3B"}
1
+ {"version":3,"file":"use-canonical-opencode-session.d.ts","sourceRoot":"","sources":["../../src/react/use-canonical-opencode-session.ts"],"names":[],"mappings":"AAUA,OAAO,EAAuB,KAAK,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAa5E;;;;;;;;;;;;;;;;;;GAkBG;AAEH;gFACgF;AAChF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,MAAM,WAAW,wBAAwB;IACvC,kFAAkF;IAClF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,GAAG,wBAAwB,CAqE3B"}
@@ -1,7 +1,17 @@
1
1
  'use client';
2
- import { resolveSessionPin } from './initial-session-pin.js';
2
+ import { useEffect, useLayoutEffect, useState } from 'react';
3
+ import { readPersistedSessionPin, resolvePersistedPin, resolveSessionPin, writePersistedSessionPin, } from './initial-session-pin.js';
3
4
  import { useOpenCodeSessions } from './use-opencode-sessions/index.js';
4
5
  import { useProjectSession } from './use-project-session.js';
6
+ // SSR-safe: `localStorage` does not exist on the server, and reading it
7
+ // during render (rather than an effect) would make the client's FIRST
8
+ // hydration render disagree with the server-rendered HTML. Mirrors
9
+ // `useSession`'s own `useIsomorphicLayoutEffect` (T5) for the exact
10
+ // same reason — a `useLayoutEffect` commits its state update before the
11
+ // browser paints, so the corrected value is on screen for the user's very
12
+ // first frame instead of trailing one visible "empty" frame behind a plain
13
+ // `useEffect`.
14
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
5
15
  /**
6
16
  * OpenCode ↔ Kortix session mapping — READ side.
7
17
  *
@@ -11,7 +21,12 @@ import { useProjectSession } from './use-project-session.js';
11
21
  * the client (the old client-side `ensure-opencode` mutation caused the
12
22
  * "session replaced / data lost" drift). It just surfaces the pin:
13
23
  * 1. the value /start handed us this render (`pinFromStart`), else
14
- * 2. the persisted pin on the Kortix session row (`getProjectSession`).
24
+ * 2. the host's own warm-list pin (`initialPin`), else
25
+ * 3. the persisted row (`getProjectSession`) once its REST read resolves,
26
+ * or — before it does — this browser's own synchronous local mirror of
27
+ * the last canonical id seen for this session (T6). The local
28
+ * mirror exists ONLY to cover the gap before (3)'s network read lands;
29
+ * it never outranks it once loaded, and neither ever outranks (1).
15
30
  *
16
31
  * The OpenCode session list is still read (read-only) for ?oc deep-links and
17
32
  * sidebar sub-session rendering.
@@ -44,12 +59,40 @@ export function useCanonicalOpenCodeSession(params) {
44
59
  const projectSessionQuery = useProjectSession(projectId, sessionId, {
45
60
  enabled: !pinFromStart && !initialPin,
46
61
  });
47
- const pin = projectSessionQuery.data?.opencode_session_id ?? null;
62
+ const networkPin = projectSessionQuery.data?.opencode_session_id ?? null;
63
+ // T6 — a synchronous local mirror of the persisted pin, so a cold
64
+ // mount (no /start response yet, no host-warm `initialPin`) can still
65
+ // resolve `rootSessionId` on frame one instead of waiting out a network
66
+ // round trip. `useState(null)` matches what a server render sees (no
67
+ // `localStorage`), so hydration never disagrees with the server-rendered
68
+ // HTML; the layout effect below then corrects it to the real cached value
69
+ // before the browser paints — see the module-level comment on
70
+ // `useIsomorphicLayoutEffect`.
71
+ const [cachedPin, setCachedPin] = useState(null);
72
+ useIsomorphicLayoutEffect(() => {
73
+ setCachedPin(readPersistedSessionPin(projectId, sessionId));
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
+ }, [projectId, sessionId]);
76
+ const pin = resolvePersistedPin({ networkPin, cachedPin });
48
77
  const rootSessionId = resolveSessionPin({
49
78
  startPin: pinFromStart,
50
79
  initialPin,
51
80
  persistedPin: pin,
52
81
  });
82
+ // Mirror a freshly-resolved canonical id back to disk so the NEXT mount of
83
+ // this (projectId, sessionId) can paint synchronously via `cachedPin`
84
+ // above. Scoped to the two AUTHORITATIVE, resolved-this-render sources —
85
+ // /start and the host's warm session-list pin — never to `pin` itself,
86
+ // which would just echo `cachedPin` back to its own key on every render
87
+ // this session is cold. This is also the write half of stale-pin
88
+ // convergence: a re-pinned session's fresh /start id overwrites whatever
89
+ // stale id a previous visit left behind, so the session AFTER this one
90
+ // reads the corrected value instead of the one that just got replaced.
91
+ const freshPin = pinFromStart ?? initialPin ?? null;
92
+ useEffect(() => {
93
+ if (freshPin)
94
+ writePersistedSessionPin(projectId, sessionId, freshPin);
95
+ }, [projectId, sessionId, freshPin]);
53
96
  return {
54
97
  rootSessionId,
55
98
  sessions: sessionsQuery.data ?? [],
@@ -1 +1 @@
1
- {"version":3,"file":"use-canonical-opencode-session.js","sourceRoot":"","sources":["../../src/react/use-canonical-opencode-session.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAgB,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D;;;;;;;;;;;;;GAaG;AAEH;gFACgF;AAChF,MAAM,UAAU,wBAAwB;IACtC,WAAW;AACb,CAAC;AAaD,MAAM,UAAU,2BAA2B,CAAC,MAS3C;IACC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,mBAAmB,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;IAC9F,MAAM,aAAa,GAAG,mBAAmB,CAAC,mBAAmB,CAAC,CAAC;IAE/D,8EAA8E;IAC9E,sEAAsE;IACtE,8EAA8E;IAC9E,gFAAgF;IAChF,gFAAgF;IAChF,kFAAkF;IAClF,4EAA4E;IAC5E,kEAAkE;IAClE,2EAA2E;IAC3E,0EAA0E;IAC1E,2EAA2E;IAC3E,sEAAsE;IACtE,4DAA4D;IAC5D,2EAA2E;IAC3E,yEAAyE;IACzE,wEAAwE;IACxE,yEAAyE;IACzE,MAAM,mBAAmB,GAAG,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE;QAClE,OAAO,EAAE,CAAC,YAAY,IAAI,CAAC,UAAU;KACtC,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,mBAAmB,CAAC,IAAI,EAAE,mBAAmB,IAAI,IAAI,CAAC;IAClE,MAAM,aAAa,GAAG,iBAAiB,CAAC;QACtC,QAAQ,EAAE,YAAY;QACtB,UAAU;QACV,YAAY,EAAE,GAAG;KAClB,CAAC,CAAC;IAEH,OAAO;QACL,aAAa;QACb,QAAQ,EAAE,aAAa,CAAC,IAAI,IAAI,EAAE;QAClC,SAAS,EAAE,aAAa,CAAC,SAAS;QAClC,OAAO,EAAE,aAAa,CAAC,OAAO;QAC9B,MAAM,EAAE,aAAa,CAAC,SAAS;QAC/B,KAAK,EAAE,aAAa,CAAC,KAAK,IAAI,IAAI;KACnC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"use-canonical-opencode-session.js","sourceRoot":"","sources":["../../src/react/use-canonical-opencode-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { type ImpersonationSession } from '../core/http/impersonation.js';
2
+ /**
3
+ * React surface over the act-as store. The data logic (storage, expiry,
4
+ * header injection) lives in `core/http/impersonation`; these three hooks are
5
+ * the only React that knows about it, so the host renders a banner and a
6
+ * button and nothing else.
7
+ */
8
+ /** Wire path of the mint route. */
9
+ export declare const ADMIN_IMPERSONATE_PATH = "/admin/api/impersonate";
10
+ /** Wire path of the revoke route for one grant. */
11
+ export declare function adminImpersonateRevokePath(grantId: string): string;
12
+ /** Wire path of the caller's live grants. */
13
+ export declare const ADMIN_IMPERSONATE_ACTIVE_PATH = "/admin/api/impersonate/active";
14
+ /** What `POST /admin/api/impersonate` answers. */
15
+ export interface AdminImpersonateResponse {
16
+ grant_id: string;
17
+ account_id: string;
18
+ account_name?: string | null;
19
+ /** ISO-8601. Server-capped at one hour — the client never chooses it. */
20
+ expires_at: string;
21
+ }
22
+ /**
23
+ * The current act-as session, or null. Subscribes to the store, so a banner
24
+ * built on it disappears by itself when the grant expires or is exited from
25
+ * another component.
26
+ */
27
+ export declare function useImpersonation(): ImpersonationSession | null;
28
+ /**
29
+ * Start acting as an account. On success the session is stored, which makes
30
+ * every subsequent request from this tab carry the grant header — so the
31
+ * caller navigates to the app shell and nothing else needs wiring.
32
+ *
33
+ * The whole React Query cache is cleared, not invalidated: every cached entry
34
+ * was fetched as the OPERATOR, and re-serving one inside a customer's session
35
+ * would show the operator's own data under the customer's banner.
36
+ */
37
+ export declare function useAdminImpersonate(): import("@tanstack/react-query").UseMutationResult<AdminImpersonateResponse, Error, {
38
+ accountId: string;
39
+ reason?: string;
40
+ }, unknown>;
41
+ /**
42
+ * Stop acting. Revokes server-side FIRST, then clears the local session — in
43
+ * that order, because a client that forgot the grant id can no longer revoke
44
+ * it, and the row would stay live for the rest of its hour.
45
+ *
46
+ * A failed revoke still clears locally: the operator asked to leave, and the
47
+ * grant expires on its own. The error is surfaced so the caller can say so.
48
+ */
49
+ export declare function useStopImpersonation(): import("@tanstack/react-query").UseMutationResult<{
50
+ ok: boolean;
51
+ } | null, Error, void | {
52
+ grantId?: string;
53
+ }, unknown>;
54
+ //# sourceMappingURL=use-impersonation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-impersonation.d.ts","sourceRoot":"","sources":["../../src/react/use-impersonation.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AAEpC;;;;;GAKG;AAEH,mCAAmC;AACnC,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAE/D,mDAAmD;AACnD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE;AAED,6CAA6C;AAC7C,eAAO,MAAM,6BAA6B,kCAAqC,CAAC;AAEhF,kDAAkD;AAClD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,oBAAoB,GAAG,IAAI,CAS9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB;eAEgC,MAAM;aAAW,MAAM;YAqBzF;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB;QAET,OAAO;;cAA8B,MAAM;YAgBrE"}
@@ -0,0 +1,91 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { useSyncExternalStore } from 'react';
3
+ import { backendApi } from '../core/http/api-client.js';
4
+ import { clearImpersonationSession, getImpersonationSession, setImpersonationSession, subscribeToImpersonation, } from '../core/http/impersonation.js';
5
+ /**
6
+ * React surface over the act-as store. The data logic (storage, expiry,
7
+ * header injection) lives in `core/http/impersonation`; these three hooks are
8
+ * the only React that knows about it, so the host renders a banner and a
9
+ * button and nothing else.
10
+ */
11
+ /** Wire path of the mint route. */
12
+ export const ADMIN_IMPERSONATE_PATH = '/admin/api/impersonate';
13
+ /** Wire path of the revoke route for one grant. */
14
+ export function adminImpersonateRevokePath(grantId) {
15
+ return `${ADMIN_IMPERSONATE_PATH}/${encodeURIComponent(grantId)}`;
16
+ }
17
+ /** Wire path of the caller's live grants. */
18
+ export const ADMIN_IMPERSONATE_ACTIVE_PATH = `${ADMIN_IMPERSONATE_PATH}/active`;
19
+ /**
20
+ * The current act-as session, or null. Subscribes to the store, so a banner
21
+ * built on it disappears by itself when the grant expires or is exited from
22
+ * another component.
23
+ */
24
+ export function useImpersonation() {
25
+ return useSyncExternalStore(subscribeToImpersonation, getImpersonationSession,
26
+ // Server render: never acting as an account. The store is per-tab
27
+ // `sessionStorage`, which does not exist during SSR, and claiming a session
28
+ // there would flash a banner for an operator who has none.
29
+ () => null);
30
+ }
31
+ /**
32
+ * Start acting as an account. On success the session is stored, which makes
33
+ * every subsequent request from this tab carry the grant header — so the
34
+ * caller navigates to the app shell and nothing else needs wiring.
35
+ *
36
+ * The whole React Query cache is cleared, not invalidated: every cached entry
37
+ * was fetched as the OPERATOR, and re-serving one inside a customer's session
38
+ * would show the operator's own data under the customer's banner.
39
+ */
40
+ export function useAdminImpersonate() {
41
+ const queryClient = useQueryClient();
42
+ return useMutation({
43
+ mutationFn: async ({ accountId, reason }) => {
44
+ const body = { account_id: accountId };
45
+ if (reason)
46
+ body.reason = reason;
47
+ const response = await backendApi.post(ADMIN_IMPERSONATE_PATH, body);
48
+ if (response.error)
49
+ throw new Error(response.error.message);
50
+ return response.data;
51
+ },
52
+ onSuccess: (data) => {
53
+ setImpersonationSession({
54
+ grantId: data.grant_id,
55
+ accountId: data.account_id,
56
+ accountName: data.account_name ?? null,
57
+ expiresAt: data.expires_at,
58
+ });
59
+ queryClient.clear();
60
+ },
61
+ });
62
+ }
63
+ /**
64
+ * Stop acting. Revokes server-side FIRST, then clears the local session — in
65
+ * that order, because a client that forgot the grant id can no longer revoke
66
+ * it, and the row would stay live for the rest of its hour.
67
+ *
68
+ * A failed revoke still clears locally: the operator asked to leave, and the
69
+ * grant expires on its own. The error is surfaced so the caller can say so.
70
+ */
71
+ export function useStopImpersonation() {
72
+ const queryClient = useQueryClient();
73
+ return useMutation({
74
+ mutationFn: async (variables) => {
75
+ const grantId = variables?.grantId ?? getImpersonationSession()?.grantId;
76
+ if (!grantId)
77
+ return null;
78
+ try {
79
+ const response = await backendApi.delete(adminImpersonateRevokePath(grantId));
80
+ if (response.error)
81
+ throw new Error(response.error.message);
82
+ return response.data ?? { ok: true };
83
+ }
84
+ finally {
85
+ clearImpersonationSession();
86
+ queryClient.clear();
87
+ }
88
+ },
89
+ });
90
+ }
91
+ //# sourceMappingURL=use-impersonation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-impersonation.js","sourceRoot":"","sources":["../../src/react/use-impersonation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,GAEzB,MAAM,4BAA4B,CAAC;AAEpC;;;;;GAKG;AAEH,mCAAmC;AACnC,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE/D,mDAAmD;AACnD,MAAM,UAAU,0BAA0B,CAAC,OAAe;IACxD,OAAO,GAAG,sBAAsB,IAAI,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,6CAA6C;AAC7C,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,sBAAsB,SAAS,CAAC;AAWhF;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,oBAAoB,CACzB,wBAAwB,EACxB,uBAAuB;IACvB,kEAAkE;IAClE,4EAA4E;IAC5E,2DAA2D;IAC3D,GAAG,EAAE,CAAC,IAAI,CACX,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IACrC,OAAO,WAAW,CAA0E;QAC1F,UAAU,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE;YAC1C,MAAM,IAAI,GAA4B,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;YAChE,IAAI,MAAM;gBAAE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACjC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,IAAI,CACpC,sBAAsB,EACtB,IAAI,CACL,CAAC;YACF,IAAI,QAAQ,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5D,OAAO,QAAQ,CAAC,IAAK,CAAC;QACxB,CAAC;QACD,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE;YAClB,uBAAuB,CAAC;gBACtB,OAAO,EAAE,IAAI,CAAC,QAAQ;gBACtB,SAAS,EAAE,IAAI,CAAC,UAAU;gBAC1B,WAAW,EAAE,IAAI,CAAC,YAAY,IAAI,IAAI;gBACtC,SAAS,EAAE,IAAI,CAAC,UAAU;aAC3B,CAAC,CAAC;YACH,WAAW,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IACrC,OAAO,WAAW,CAA6D;QAC7E,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;YAC9B,MAAM,OAAO,GAAG,SAAS,EAAE,OAAO,IAAI,uBAAuB,EAAE,EAAE,OAAO,CAAC;YACzE,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YAC1B,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,CACtC,0BAA0B,CAAC,OAAO,CAAC,CACpC,CAAC;gBACF,IAAI,QAAQ,CAAC,KAAK;oBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC5D,OAAO,QAAQ,CAAC,IAAI,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;YACvC,CAAC;oBAAS,CAAC;gBACT,yBAAyB,EAAE,CAAC;gBAC5B,WAAW,CAAC,KAAK,EAAE,CAAC;YACtB,CAAC;QACH,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -4,6 +4,8 @@ import type { RefObject } from 'react';
4
4
  import { getClient } from '../../core/runtime/client.js';
5
5
  import { type SessionSyncReason } from '../../core/session-sync/session-sync-controller.js';
6
6
  import type { NormalizeDiagnosticPaths, OpenCodeEvent } from './types.js';
7
+ /** See `createEventHandler`'s `userPartsGraceMs`. */
8
+ export declare const USER_PARTS_GRACE_MS = 1500;
7
9
  /** Builds the SSE event handler; all closure dependencies are injected. */
8
10
  export declare function createEventHandler(deps: {
9
11
  queryClient: QueryClient;
@@ -18,6 +20,14 @@ export declare function createEventHandler(deps: {
18
20
  markSessionAbortedLocally: RefObject<(sessionID: string, message?: string) => void>;
19
21
  fetchLspDiagnosticsDebounced: RefObject<() => void>;
20
22
  reconcileSessionTail?: (sessionID: string, reason: SessionSyncReason) => Promise<void>;
23
+ /**
24
+ * How long a USER `message.updated` may sit with no parts before the tail is
25
+ * re-read. The runtime emits the info frame and the text part separately;
26
+ * lose the part (a stream reconnect during the boot hand-off) and the
27
+ * transcript shows an empty bubble until a reload. Injected so tests can
28
+ * make it immediate.
29
+ */
30
+ userPartsGraceMs?: number;
21
31
  /** The route-scoped project this SSE connection belongs to — see
22
32
  * `refetchKortixSessionMirrors`'s doc comment for why this can't default
23
33
  * to "every project". Optional only so existing test harnesses that don't
@@ -1 +1 @@
1
- {"version":3,"file":"handle-event.d.ts","sourceRoot":"","sources":["../../../src/react/use-opencode-events/handle-event.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EACV,KAAK,IAAI,gBAAgB,EACzB,iBAAiB,EACjB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAUvC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,iDAAiD,CAAC;AAczD,OAAO,KAAK,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvE,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;IACrC,cAAc,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,aAAa,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,WAAW,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,wBAAwB,EAAE,SAAS,CAAC,wBAAwB,CAAC,CAAC;IAC9D,yBAAyB,EAAE,SAAS,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC;IACpF,4BAA4B,EAAE,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC;IACpD,oBAAoB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF;;;;6DAIyD;IACzD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,WAoC6B,aAAa,UAsf1C"}
1
+ {"version":3,"file":"handle-event.d.ts","sourceRoot":"","sources":["../../../src/react/use-opencode-events/handle-event.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EACV,KAAK,IAAI,gBAAgB,EACzB,iBAAiB,EACjB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAWvC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,iDAAiD,CAAC;AAazD,OAAO,KAAK,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvE,qDAAqD;AACrD,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;IACrC,cAAc,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,aAAa,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,WAAW,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,wBAAwB,EAAE,SAAS,CAAC,wBAAwB,CAAC,CAAC;IAC9D,yBAAyB,EAAE,SAAS,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC;IACpF,4BAA4B,EAAE,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC;IACpD,oBAAoB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;6DAIyD;IACzD,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,WAqC6B,aAAa,UA0jB1C"}
@@ -1,16 +1,19 @@
1
1
  import { infoToast, notifyPermissionRequest, notifyQuestion, notifySessionError, notifyTaskComplete, } from '../../platform/ui.js';
2
2
  import { deleteSessionFromIDB } from '../../browser/cache/idb-sync-cache.js';
3
3
  import { useSyncStore } from '../../browser/stores/sync-store.js';
4
+ import { isAbortError } from '../../core/http/abort-error.js';
4
5
  import { getClient } from '../../core/runtime/client.js';
5
6
  import { SESSION_SYNC_PAGE_SIZE, } from '../../core/session-sync/session-sync-controller.js';
6
7
  import { fileContentKeys, fileListKeys, gitStatusKeys } from '../file-keys.js';
7
8
  import { ptyKeys } from '../use-opencode-pty.js';
8
9
  import { opencodeKeys } from '../use-opencode-sessions/index.js';
9
10
  import { applyPartDiagnostics } from './diagnostics.js';
10
- import { asStringOrUndefined, looksLikeAbortError, patchKortixSessionTitleMirrors, readSessionInfo, realRuntimeTitle, refetchKortixSessionMirrors, scheduleProjectMetadataRefetch, } from './helpers.js';
11
+ import { asStringOrUndefined, patchKortixSessionTitleMirrors, readSessionInfo, realRuntimeTitle, refetchKortixSessionMirrors, scheduleProjectMetadataRefetch, } from './helpers.js';
12
+ /** See `createEventHandler`'s `userPartsGraceMs`. */
13
+ export const USER_PARTS_GRACE_MS = 1_500;
11
14
  /** Builds the SSE event handler; all closure dependencies are injected. */
12
15
  export function createEventHandler(deps) {
13
- const { queryClient, client, applySyncEvent, stopCompaction, addPermission, removePermission, addQuestion, removeQuestion, normalizeDiagnosticPaths, markSessionAbortedLocally, fetchLspDiagnosticsDebounced, projectId = null, } = deps;
16
+ const { queryClient, client, applySyncEvent, stopCompaction, addPermission, removePermission, addQuestion, removeQuestion, normalizeDiagnosticPaths, markSessionAbortedLocally, fetchLspDiagnosticsDebounced, projectId = null, userPartsGraceMs = USER_PARTS_GRACE_MS, } = deps;
14
17
  const reconcileTail = deps.reconcileSessionTail ??
15
18
  (async (sessionID) => {
16
19
  const result = await client.session.messages({
@@ -43,7 +46,24 @@ export function createEventHandler(deps) {
43
46
  applySyncEvent(event);
44
47
  switch (event.type) {
45
48
  // ---- Message events — handled by sync store only ----
46
- case 'message.updated':
49
+ case 'message.updated': {
50
+ // A user message is two frames — the info, then its text part. If the
51
+ // second never lands this tab renders an empty bubble for good; a
52
+ // tail re-read after a short grace repairs it from the server.
53
+ const info = event.properties.info;
54
+ const sessionID = info?.sessionID ?? event.properties.sessionID;
55
+ if (info?.role === 'user' && info.id && sessionID) {
56
+ const messageID = info.id;
57
+ setTimeout(() => {
58
+ const state = useSyncStore.getState();
59
+ const stillListed = state.messages[sessionID]?.some((m) => m.id === messageID);
60
+ if (stillListed && !(state.parts[messageID]?.length ?? 0)) {
61
+ void reconcileTail(sessionID, 'sse-gap');
62
+ }
63
+ }, userPartsGraceMs);
64
+ }
65
+ break;
66
+ }
47
67
  case 'message.removed':
48
68
  break;
49
69
  case 'message.part.updated': {
@@ -62,6 +82,15 @@ export function createEventHandler(deps) {
62
82
  case 'session.created': {
63
83
  const info = readSessionInfo(event);
64
84
  if (info) {
85
+ // T22 — reload/cross-tab recovery: a `Session.revert` field on the
86
+ // info this event carries is the only way a fresh mount (nothing
87
+ // staged locally yet, the `.staged` wire event already happened
88
+ // before this tab connected) rediscovers an already-staged revert.
89
+ // See `sync-store.ts`'s `syncSessionRevertFromInfo` doc comment —
90
+ // absence never clears an existing record; only the three
91
+ // dedicated wire events (also routed through `applySyncEvent`
92
+ // above) do that.
93
+ useSyncStore.getState().syncSessionRevertFromInfo(info.id, info.revert ?? null);
65
94
  queryClient.setQueryData(opencodeKeys.sessions(), (old) => {
66
95
  if (!old)
67
96
  return [info];
@@ -85,6 +114,8 @@ export function createEventHandler(deps) {
85
114
  case 'session.updated': {
86
115
  const info = readSessionInfo(event);
87
116
  if (info) {
117
+ // T22 — see the identical call in the `session.created` case above.
118
+ useSyncStore.getState().syncSessionRevertFromInfo(info.id, info.revert ?? null);
88
119
  // OpenCode auto-titles after the first message via session.updated.
89
120
  // Capture the previous title before local cache mutation so we only
90
121
  // force the server-owned mirror read when the title actually changed.
@@ -150,7 +181,21 @@ export function createEventHandler(deps) {
150
181
  const client = getClient();
151
182
  void reconcileTail(sessionID, 'compaction');
152
183
  // Refetch the individual session to clear time.compacting
153
- // (targeted refetch, not full session list invalidation)
184
+ // (targeted refetch, not full session list invalidation). This is
185
+ // the FAST path — it fires the instant the frame arrives, and on
186
+ // success also patches the session list mirror.
187
+ //
188
+ // It is not the ONLY path any more: a failure here (this fetch has
189
+ // no retry of its own) used to leave `time.compacting` stale in the
190
+ // `opencodeKeys.runtimeSession` cache FOREVER — `useOpenCodeSession`
191
+ // reads it with `staleTime: Infinity` and nothing else refetches it,
192
+ // so `projectCompacting`'s server-observed rule (`core/session/
193
+ // compaction.ts`) stayed pinned `true` for the rest of the tab's
194
+ // life. On failure, route the retry through the SAME query
195
+ // `useOpenCodeSession` registers (3 attempts, exponential backoff)
196
+ // instead of swallowing it silently. `use-session.ts` also arms a
197
+ // `serverCompactionRevalidateAtMs` timer as a second, frame-
198
+ // independent backstop — see that function's doc comment.
154
199
  client.session
155
200
  .get({ sessionID })
156
201
  .then((res) => {
@@ -169,8 +214,32 @@ export function createEventHandler(deps) {
169
214
  return next;
170
215
  });
171
216
  }
217
+ else {
218
+ void queryClient.invalidateQueries({
219
+ queryKey: opencodeKeys.runtimeSession(sessionID),
220
+ });
221
+ }
172
222
  })
173
- .catch(() => { });
223
+ .catch(() => {
224
+ void queryClient.invalidateQueries({
225
+ queryKey: opencodeKeys.runtimeSession(sessionID),
226
+ });
227
+ });
228
+ }
229
+ break;
230
+ }
231
+ // ---- Rewind commit in a view that never saw the staging ----
232
+ case 'session.next.revert.committed': {
233
+ // The store's `applyEvent` refuses to guess-delete when this tab has
234
+ // no tracked revert record (second tab / fresh mount) — guessing a
235
+ // watermark from the local tip can delete the user's REPLACEMENT
236
+ // prompt. It raises `sessionRevertNeedsTailReconcile` instead; this
237
+ // is the one consumer: fetch the server's already-truncated tail so
238
+ // truth arrives by read, not by guess.
239
+ const { sessionID } = event.properties;
240
+ if (sessionID && useSyncStore.getState().sessionRevertNeedsTailReconcile[sessionID]) {
241
+ useSyncStore.getState().clearSessionRevertNeedsTailReconcile(sessionID);
242
+ void reconcileTail(sessionID, 'manual');
174
243
  }
175
244
  break;
176
245
  }
@@ -270,8 +339,8 @@ export function createEventHandler(deps) {
270
339
  // may not have persisted the partial assistant response yet,
271
340
  // so hydrating would wipe the streamed content the user saw.
272
341
  // The error is already patched onto the message above.
273
- const isAbortError = looksLikeAbortError(error);
274
- if (!isAbortError) {
342
+ const aborted = isAbortError(error);
343
+ if (!aborted) {
275
344
  reconcileTail(sessionID, 'session-error')
276
345
  .then(() => {
277
346
  useSyncStore.getState().clearOptimisticMessages(sessionID);