@beeuni/sdk 0.1.0 → 0.3.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 (291) hide show
  1. package/README.md +53 -92
  2. package/dist/beeuni.esm.min.js +36 -34
  3. package/dist/beeuni.global.js +36 -34
  4. package/dist/browser/cache/idb-sync-cache.js +15 -5
  5. package/dist/browser/session-sync/server-transcript-mirror.d.ts +7 -1
  6. package/dist/browser/session-sync/server-transcript-mirror.js +30 -24
  7. package/dist/browser/session-sync/session-sync-registry.d.ts +3 -1
  8. package/dist/browser/session-sync/session-sync-registry.js +48 -3
  9. package/dist/browser/stores/sync-store/keep-unchanged.d.ts +4 -0
  10. package/dist/browser/stores/sync-store/keep-unchanged.js +34 -0
  11. package/dist/browser/stores/sync-store.d.ts +7 -0
  12. package/dist/browser/stores/sync-store.js +122 -35
  13. package/dist/core/attachments/prompt-attachments.d.ts +4 -0
  14. package/dist/core/attachments/prompt-attachments.js +34 -0
  15. package/dist/core/capture/rules.d.ts +17 -0
  16. package/dist/core/capture/rules.js +27 -0
  17. package/dist/core/client/beeuni.d.ts +99 -34
  18. package/dist/core/client/beeuni.js +5 -11
  19. package/dist/core/client/capture.d.ts +105 -0
  20. package/dist/core/client/capture.js +48 -0
  21. package/dist/core/client/facade-catalog.d.ts +0 -4
  22. package/dist/core/client/facade-catalog.js +1 -5
  23. package/dist/core/client/facade-identity.d.ts +1 -0
  24. package/dist/core/client/facade-identity.js +1 -0
  25. package/dist/core/client/project-access-resources.d.ts +3 -1
  26. package/dist/core/client/project-access-resources.js +1 -1
  27. package/dist/core/client/project-connections.d.ts +2 -2
  28. package/dist/core/client/project-connections.js +2 -2
  29. package/dist/core/client/project-operations-admin.d.ts +4 -4
  30. package/dist/core/client/project-operations-review.d.ts +2 -3
  31. package/dist/core/client/project-operations-review.js +1 -0
  32. package/dist/core/client/project-operations-workspace.d.ts +4 -4
  33. package/dist/core/client/project-operations-workspace.js +1 -1
  34. package/dist/core/client/project-operations.d.ts +9 -19
  35. package/dist/core/client/project-operations.js +2 -6
  36. package/dist/core/client/project-platform-security.d.ts +1 -1
  37. package/dist/core/client/project-platform-settings.d.ts +1 -1
  38. package/dist/core/client/session-context.d.ts +2 -0
  39. package/dist/core/client/session-handle.d.ts +13 -0
  40. package/dist/core/client/session-lifecycle-resources.d.ts +11 -0
  41. package/dist/core/client/session-lifecycle-resources.js +4 -0
  42. package/dist/core/client/session-lifecycle-security.d.ts +2 -0
  43. package/dist/core/client/session-lifecycle-security.js +1 -0
  44. package/dist/core/client/session-readiness.d.ts +2 -0
  45. package/dist/core/client/session-readiness.js +20 -1
  46. package/dist/core/http/api-client.d.ts +1 -7
  47. package/dist/core/http/api-client.js +2 -37
  48. package/dist/core/http/auth.d.ts +2 -4
  49. package/dist/core/http/auth.js +2 -8
  50. package/dist/core/http/config.d.ts +2 -0
  51. package/dist/core/http/response-error.js +26 -11
  52. package/dist/core/http/transport.js +6 -1
  53. package/dist/core/models/composer-model.d.ts +14 -0
  54. package/dist/core/models/composer-model.js +21 -1
  55. package/dist/core/rest/platform-client/capture.d.ts +363 -0
  56. package/dist/core/rest/platform-client/capture.js +116 -0
  57. package/dist/core/rest/platform-client/client-policy.d.ts +8 -0
  58. package/dist/core/rest/platform-client/client-policy.js +30 -0
  59. package/dist/core/rest/platform-client/index.d.ts +4 -4
  60. package/dist/core/rest/platform-client/index.js +4 -4
  61. package/dist/core/rest/platform-client/lifecycle.js +18 -12
  62. package/dist/core/rest/platform-client/shared.d.ts +5 -0
  63. package/dist/core/rest/platform-client/shared.js +18 -0
  64. package/dist/core/rest/platform-client/types.d.ts +1 -1
  65. package/dist/core/rest/projects-client/account-secret-resources.d.ts +2 -1
  66. package/dist/core/rest/projects-client/accounts.d.ts +34 -0
  67. package/dist/core/rest/projects-client/accounts.js +10 -0
  68. package/dist/core/rest/projects-client/admin-providers.d.ts +0 -61
  69. package/dist/core/rest/projects-client/admin-providers.js +0 -6
  70. package/dist/core/rest/projects-client/agent-config.d.ts +3 -0
  71. package/dist/core/rest/projects-client/apps-fleet.d.ts +5 -1
  72. package/dist/core/rest/projects-client/apps-fleet.js +2 -2
  73. package/dist/core/rest/projects-client/apps.d.ts +11 -1
  74. package/dist/core/rest/projects-client/change-requests.d.ts +2 -0
  75. package/dist/core/rest/projects-client/change-requests.js +2 -0
  76. package/dist/core/rest/projects-client/connectors.d.ts +13 -55
  77. package/dist/core/rest/projects-client/connectors.js +5 -29
  78. package/dist/core/rest/projects-client/files.d.ts +3 -0
  79. package/dist/core/rest/projects-client/files.js +4 -0
  80. package/dist/core/rest/projects-client/gateway.d.ts +5 -0
  81. package/dist/core/rest/projects-client/git-backend.d.ts +1 -10
  82. package/dist/core/rest/projects-client/git-backend.js +0 -10
  83. package/dist/core/rest/projects-client/iam-actions.d.ts +76 -0
  84. package/dist/core/rest/projects-client/iam-actions.js +101 -0
  85. package/dist/core/rest/projects-client/iam-sso.d.ts +0 -10
  86. package/dist/core/rest/projects-client/iam-sso.js +0 -3
  87. package/dist/core/rest/projects-client/iam.d.ts +1 -0
  88. package/dist/core/rest/projects-client/iam.js +1 -0
  89. package/dist/core/rest/projects-client/index.d.ts +1 -4
  90. package/dist/core/rest/projects-client/index.js +1 -4
  91. package/dist/core/rest/projects-client/notifications.d.ts +1 -1
  92. package/dist/core/rest/projects-client/notifications.js +4 -0
  93. package/dist/core/rest/projects-client/projects.d.ts +4 -2
  94. package/dist/core/rest/projects-client/projects.js +4 -2
  95. package/dist/core/rest/projects-client/review-items.d.ts +2 -0
  96. package/dist/core/rest/projects-client/review-items.js +4 -0
  97. package/dist/core/rest/projects-client/review.d.ts +17 -3
  98. package/dist/core/rest/projects-client/review.js +11 -0
  99. package/dist/core/rest/projects-client/sandbox.d.ts +8 -9
  100. package/dist/core/rest/projects-client/secrets.d.ts +3 -0
  101. package/dist/core/rest/projects-client/session-attachments.js +14 -1
  102. package/dist/core/rest/projects-client/session-sandbox.d.ts +1 -1
  103. package/dist/core/rest/projects-client/sessions.d.ts +31 -4
  104. package/dist/core/rest/projects-client/sessions.js +34 -4
  105. package/dist/core/runtime/runtime-rest-client.js +1 -1
  106. package/dist/core/runtime/runtime-types.d.ts +1 -0
  107. package/dist/core/session/control-stream.d.ts +1 -0
  108. package/dist/core/session/control-stream.js +18 -1
  109. package/dist/core/session/open-bundle.d.ts +4 -4
  110. package/dist/core/session/open-bundle.js +29 -46
  111. package/dist/core/session/turn-end-watch.d.ts +47 -0
  112. package/dist/core/session/turn-end-watch.js +141 -0
  113. package/dist/core/session-log/guard.d.ts +6 -0
  114. package/dist/core/session-log/guard.js +109 -0
  115. package/dist/core/session-log/index.d.ts +7 -0
  116. package/dist/core/session-log/index.js +5 -0
  117. package/dist/core/session-log/saved-log.d.ts +10 -0
  118. package/dist/core/session-log/saved-log.js +25 -0
  119. package/dist/core/session-log/source.d.ts +25 -0
  120. package/dist/core/session-log/source.js +30 -0
  121. package/dist/core/session-log/store.d.ts +21 -0
  122. package/dist/core/session-log/store.js +126 -0
  123. package/dist/core/session-log/tool-call.d.ts +11 -0
  124. package/dist/core/session-log/tool-call.js +65 -0
  125. package/dist/core/session-log/turns/blocks.d.ts +60 -0
  126. package/dist/core/session-log/turns/blocks.js +124 -0
  127. package/dist/core/session-log/turns/history.d.ts +32 -0
  128. package/dist/core/session-log/turns/history.js +99 -0
  129. package/dist/core/session-log/turns/index.d.ts +4 -0
  130. package/dist/core/session-log/turns/index.js +4 -0
  131. package/dist/core/session-log/turns/metrics.d.ts +21 -0
  132. package/dist/core/session-log/turns/metrics.js +29 -0
  133. package/dist/core/session-log/turns/turn.d.ts +55 -0
  134. package/dist/core/session-log/turns/turn.js +279 -0
  135. package/dist/core/session-log/turns/v1-parts.d.ts +8 -0
  136. package/dist/core/session-log/turns/v1-parts.js +43 -0
  137. package/dist/core/session-log/types.d.ts +264 -0
  138. package/dist/core/session-log/types.js +6 -0
  139. package/dist/core/session-log/v1-view.d.ts +13 -0
  140. package/dist/core/session-log/v1-view.js +215 -0
  141. package/dist/core/session-log/v1.d.ts +31 -0
  142. package/dist/core/session-log/v1.js +473 -0
  143. package/dist/core/session-sync/copy-store-types.d.ts +19 -0
  144. package/dist/core/session-sync/copy-store-types.js +1 -0
  145. package/dist/core/session-sync/saved-copy-store.js +25 -4
  146. package/dist/core/session-sync/saved-transcript.d.ts +15 -1
  147. package/dist/core/session-sync/saved-transcript.js +55 -0
  148. package/dist/core/session-sync/session-sync-controller.js +3 -0
  149. package/dist/core/stream/event-stream.js +2 -2
  150. package/dist/core/turns/classify.d.ts +2 -1
  151. package/dist/core/turns/classify.js +7 -3
  152. package/dist/core/turns/edit-blocks.d.ts +1 -0
  153. package/dist/core/turns/edit-blocks.js +15 -0
  154. package/dist/core/turns/parts.d.ts +3 -2
  155. package/dist/core/turns/parts.js +4 -2
  156. package/dist/core/turns/segments/burst-summary.d.ts +1 -0
  157. package/dist/core/turns/segments/burst-summary.js +24 -0
  158. package/dist/core/turns/segments/group-steps.d.ts +4 -3
  159. package/dist/core/turns/segments/group-steps.js +9 -6
  160. package/dist/core/turns/segments/index.d.ts +1 -0
  161. package/dist/core/turns/segments/index.js +1 -0
  162. package/dist/core/turns/segments/narration.d.ts +5 -4
  163. package/dist/core/turns/segments/narration.js +12 -3
  164. package/dist/core/turns/segments/session-activity-groups.d.ts +2 -1
  165. package/dist/core/turns/segments/session-activity-groups.js +3 -1
  166. package/dist/core/turns/segments/step-label.d.ts +2 -1
  167. package/dist/core/turns/segments/step-label.js +3 -1
  168. package/dist/core/turns/segments/work-split.d.ts +10 -0
  169. package/dist/core/turns/segments/work-split.js +40 -0
  170. package/dist/core/turns/segments/working-turn.d.ts +4 -2
  171. package/dist/core/turns/segments/working-turn.js +9 -8
  172. package/dist/core/turns/state.d.ts +2 -7
  173. package/dist/core/turns/state.js +28 -134
  174. package/dist/core/turns/tool-kind.d.ts +1 -1
  175. package/dist/core/turns/tool-kind.js +3 -2
  176. package/dist/core/turns/tool-registry.js +1 -0
  177. package/dist/core/turns/tools/tool-meta.d.ts +3 -2
  178. package/dist/core/turns/tools/tool-meta.js +5 -2
  179. package/dist/core/turns/tools/tool-outcome.d.ts +2 -1
  180. package/dist/core/turns/tools/tool-outcome.js +3 -1
  181. package/dist/core/turns/types.d.ts +1 -1
  182. package/dist/core/turns/view-model.js +1 -13
  183. package/dist/core/warm-session-pool.d.ts +29 -0
  184. package/dist/core/warm-session-pool.js +127 -0
  185. package/dist/genui/catalog.d.ts +28 -0
  186. package/dist/genui/catalog.js +396 -0
  187. package/dist/genui/expansion.d.ts +3 -0
  188. package/dist/genui/expansion.js +295 -0
  189. package/dist/genui/fence-entry.d.ts +2 -0
  190. package/dist/genui/fence-entry.js +1 -0
  191. package/dist/genui/fence.d.ts +21 -0
  192. package/dist/genui/fence.js +138 -0
  193. package/dist/genui/index.d.ts +6 -0
  194. package/dist/genui/index.js +6 -0
  195. package/dist/genui/markdown.d.ts +12 -0
  196. package/dist/genui/markdown.js +104 -0
  197. package/dist/genui/parse.d.ts +6 -0
  198. package/dist/genui/parse.js +75 -0
  199. package/dist/genui/prompt.d.ts +2 -0
  200. package/dist/genui/prompt.js +206 -0
  201. package/dist/genui/react/default-components.d.ts +2 -0
  202. package/dist/genui/react/default-components.js +72 -0
  203. package/dist/genui/react/default-components.test.d.ts +1 -0
  204. package/dist/genui/react/default-components.test.js +94 -0
  205. package/dist/genui/react/genui-block-disabled.test.d.ts +1 -0
  206. package/dist/genui/react/genui-block-disabled.test.js +50 -0
  207. package/dist/genui/react/genui-block.d.ts +28 -0
  208. package/dist/genui/react/genui-block.js +91 -0
  209. package/dist/genui/react/genui-block.test.d.ts +1 -0
  210. package/dist/genui/react/genui-block.test.js +205 -0
  211. package/dist/genui/react/index.d.ts +2 -0
  212. package/dist/genui/react/index.js +2 -0
  213. package/dist/genui/share.d.ts +5 -0
  214. package/dist/genui/share.js +30 -0
  215. package/dist/genui/test-fixtures.d.ts +2 -0
  216. package/dist/genui/test-fixtures.js +16 -0
  217. package/dist/genui/types.d.ts +29 -0
  218. package/dist/genui/types.js +1 -0
  219. package/dist/genui/urls.d.ts +1 -0
  220. package/dist/genui/urls.js +18 -0
  221. package/dist/genui/validate.d.ts +9 -0
  222. package/dist/genui/validate.js +116 -0
  223. package/dist/index.d.ts +4 -4
  224. package/dist/index.js +4 -4
  225. package/dist/internal/connector-catalog.d.ts +48 -0
  226. package/dist/internal/connector-catalog.js +27 -0
  227. package/dist/internal/host-configuration.d.ts +22 -0
  228. package/dist/internal/host-configuration.js +28 -0
  229. package/dist/internal/platform-admin-react.d.ts +12 -0
  230. package/dist/internal/platform-admin-react.js +13 -0
  231. package/dist/internal/platform-admin.d.ts +9 -0
  232. package/dist/internal/platform-admin.js +9 -0
  233. package/dist/platform/ui.d.ts +8 -2
  234. package/dist/platform/ui.js +19 -4
  235. package/dist/react/chat/use-chat-turns.d.ts +1 -1
  236. package/dist/react/human-presence.d.ts +9 -0
  237. package/dist/react/human-presence.js +12 -0
  238. package/dist/react/index.d.ts +4 -13
  239. package/dist/react/index.js +4 -13
  240. package/dist/react/opencode.d.ts +1 -0
  241. package/dist/react/opencode.js +1 -0
  242. package/dist/react/query-contracts.d.ts +6 -0
  243. package/dist/react/query-contracts.js +6 -0
  244. package/dist/react/query-keys.d.ts +17 -2
  245. package/dist/react/query-keys.js +24 -2
  246. package/dist/react/reset-identity-state.js +2 -0
  247. package/dist/react/session-cache-write.d.ts +1 -0
  248. package/dist/react/session-cache-write.js +20 -0
  249. package/dist/react/session-transcript-subscription.js +3 -1
  250. package/dist/react/sign-in-with-beeuni.d.ts +6 -6
  251. package/dist/react/turn-end-handoff.d.ts +13 -0
  252. package/dist/react/turn-end-handoff.js +101 -0
  253. package/dist/react/use-admin-providers.d.ts +3 -6
  254. package/dist/react/use-admin-providers.js +1 -11
  255. package/dist/react/use-capture.d.ts +47 -0
  256. package/dist/react/use-capture.js +173 -0
  257. package/dist/react/use-feature-flag.js +1 -0
  258. package/dist/react/use-opencode-events/handle-event.d.ts +1 -0
  259. package/dist/react/use-opencode-events/handle-event.js +5 -0
  260. package/dist/react/use-opencode-events/handle-interaction-event.js +8 -3
  261. package/dist/react/use-opencode-events/handle-session-event.js +13 -8
  262. package/dist/react/use-opencode-events/handler-context.d.ts +1 -0
  263. package/dist/react/use-opencode-events/helpers.d.ts +1 -0
  264. package/dist/react/use-opencode-events/helpers.js +11 -1
  265. package/dist/react/use-opencode-events/index.js +1 -0
  266. package/dist/react/use-opencode-local.d.ts +5 -1
  267. package/dist/react/use-opencode-local.js +15 -2
  268. package/dist/react/use-opencode-sessions/commands.d.ts +11 -1
  269. package/dist/react/use-opencode-sessions/commands.js +21 -1
  270. package/dist/react/use-opencode-sessions/permissions.js +28 -1
  271. package/dist/react/use-project-sessions.d.ts +1 -1
  272. package/dist/react/use-project-sessions.js +6 -5
  273. package/dist/react/use-prompt-attachments.js +1 -0
  274. package/dist/react/use-review-summary.d.ts +13 -0
  275. package/dist/react/use-review-summary.js +32 -0
  276. package/dist/react/use-runtime-reconnect.js +1 -1
  277. package/dist/react/use-session-list-state.d.ts +6 -0
  278. package/dist/react/use-session-list-state.js +33 -0
  279. package/dist/react/use-session-log.d.ts +16 -0
  280. package/dist/react/use-session-log.js +143 -0
  281. package/dist/react/use-session-messages.d.ts +2 -1
  282. package/dist/react/use-session-messages.js +15 -14
  283. package/dist/react/use-session-stream.js +12 -1
  284. package/dist/react/use-session-sync.d.ts +2 -2
  285. package/dist/react/use-session-sync.js +10 -8
  286. package/dist/react/use-session-transcript-history.js +2 -12
  287. package/dist/react/use-session-working.d.ts +2 -0
  288. package/dist/react/use-session-working.js +10 -0
  289. package/dist/react/use-session.d.ts +6 -0
  290. package/dist/react/use-session.js +88 -6
  291. package/package.json +62 -8
@@ -0,0 +1,143 @@
1
+ 'use client';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useSandboxConnectionStore } from '../browser/stores/sandbox-connection-store.js';
4
+ import { useSyncStore } from '../browser/stores/sync-store.js';
5
+ import { openSessionStream } from '../core/session/control-stream.js';
6
+ import { savedSessionLogCopies } from '../core/session-log/saved-log.js';
7
+ import { sessionLogOfV1Rows, sessionLogThreadWithV1Rows } from '../core/session-log/v1-view.js';
8
+ import { fetchFirstSessionLogPage, loadSessionLogPage, negotiateSessionLog, } from '../core/session-log/source.js';
9
+ import { currentSavedCopyStore } from '../core/session-sync/saved-copy-store.js';
10
+ import { selectSessionLog, selectSessionLogThread } from '../core/session-log/store.js';
11
+ import { useFeatureFlag } from './use-feature-flag.js';
12
+ import { selectSessionRows } from './session-transcript-subscription.js';
13
+ import { usePacedStoreValue, useSessionMessages } from './use-session-messages.js';
14
+ const PACED = Symbol('paced');
15
+ const sourced = new WeakMap();
16
+ const withSource = (log, source) => {
17
+ let out = sourced.get(log);
18
+ if (!out)
19
+ sourced.set(log, (out = { ...log, source }));
20
+ return out;
21
+ };
22
+ function useV1SessionLog(session, threadId, enabled, options) {
23
+ const native = enabled ? threadId || session.runtimeSessionId || session.opencodeSessionId || null : null;
24
+ const rows = useSessionMessages({ ...session, runtimeSessionId: native, opencodeSessionId: null }, options);
25
+ return useMemo(() => (native && rows.length ? sessionLogOfV1Rows(session.sessionId, native, rows) : null), [session.sessionId, native, rows]);
26
+ }
27
+ export function useSessionLog(session) {
28
+ const v2 = useSyncStore((state) => {
29
+ const log = state.sessionLogs[session.sessionId];
30
+ return log ? selectSessionLog(log) : null;
31
+ });
32
+ const v1 = useV1SessionLog(session, undefined, !v2);
33
+ return v2 ? withSource(v2, 'v2') : v1 ? withSource(v1, 'v1') : null;
34
+ }
35
+ export function useSessionLogRecord(session) {
36
+ return useSyncStore((state) => state.sessionLogs[session.sessionId]?.session ?? null);
37
+ }
38
+ export function useSessionLogThread(session, threadId, options) {
39
+ const throttleMs = options?.throttleMs && options.throttleMs > 0 ? options.throttleMs : 0;
40
+ const select = (state) => {
41
+ const log = state.sessionLogs[session.sessionId];
42
+ if (!log)
43
+ return undefined;
44
+ const thread = selectSessionLogThread(log, threadId);
45
+ return thread && sessionLogThreadWithV1Rows(log, thread, selectSessionRows(state, thread.thread_id), (id) => state.optimisticEchoOf(thread.thread_id, id));
46
+ };
47
+ const live = useSyncStore((state) => (throttleMs ? PACED : select(state)));
48
+ const paced = usePacedStoreValue(select, `${session.sessionId}/${threadId ?? ''}`, throttleMs);
49
+ const v2 = live === PACED ? paced : live;
50
+ const v1 = useV1SessionLog(session, threadId, v2 === undefined, options);
51
+ return v2 !== undefined ? v2 : (v1?.threads[0] ?? null);
52
+ }
53
+ export function useSessionLogSync(projectId, sessionId, options) {
54
+ const loader = options.loader === undefined ? loadSessionLogPage : options.loader;
55
+ const joinStream = options.joinStream ?? openSessionStream;
56
+ const active = options.enabled && !!loader && !!projectId && !!sessionId;
57
+ const live = useSandboxConnectionStore((state) => state.runtimeCapabilities);
58
+ const served = useRef(null);
59
+ const key = `${projectId}/${sessionId}`;
60
+ if (live?.includes('session.log.v2'))
61
+ served.current = key;
62
+ const capabilities = served.current === key ? ['session.log.v2'] : live;
63
+ const flag = useFeatureFlag(active ? projectId : null, 'session_log').enabled;
64
+ const negotiated = active ? negotiateSessionLog({ capabilities, flag, loader }) : 'v1';
65
+ const [refusedKey, setRefusedKey] = useState(null);
66
+ const dialect = negotiated === 'v2' && refusedKey !== key ? 'v2' : 'v1';
67
+ useEffect(() => {
68
+ if (negotiated !== 'v2' || !loader)
69
+ return;
70
+ const controller = new AbortController();
71
+ const store = useSyncStore.getState;
72
+ const copies = savedSessionLogCopies(currentSavedCopyStore());
73
+ let stream = null;
74
+ let stopped = false;
75
+ let busy = false;
76
+ let lastEnded;
77
+ const join = (options) => {
78
+ stream?.close();
79
+ stream = joinStream({ projectId, sessionId, ...options });
80
+ };
81
+ const load = async () => {
82
+ const page = await fetchFirstSessionLogPage(loader, projectId, sessionId, controller.signal);
83
+ if (stopped)
84
+ return null;
85
+ if (page) {
86
+ store().applySessionLogPage(sessionId, page);
87
+ void copies?.write(projectId, sessionId, page);
88
+ }
89
+ return page;
90
+ };
91
+ const onReconnect = (run) => {
92
+ let connected = true;
93
+ return (up) => {
94
+ if (up && !connected)
95
+ run();
96
+ connected = up;
97
+ };
98
+ };
99
+ const attempt = async () => {
100
+ if (busy || stopped)
101
+ return;
102
+ busy = true;
103
+ const page = await load();
104
+ busy = false;
105
+ if (stopped)
106
+ return;
107
+ if (page) {
108
+ setRefusedKey(null);
109
+ join({ schema: 'v2', onConnectionChange: onReconnect(() => void load()), onRuntimeResync: () => void load() });
110
+ return;
111
+ }
112
+ store().dropSessionLog(sessionId);
113
+ setRefusedKey(`${projectId}/${sessionId}`);
114
+ join({
115
+ onControl: (frame) => {
116
+ if (frame.type !== 'beeuni.control.turn')
117
+ return;
118
+ const ended = frame.payload?.last_ended?.turn_token ?? null;
119
+ const seen = lastEnded;
120
+ lastEnded = ended;
121
+ if (seen !== undefined && ended !== seen)
122
+ void attempt();
123
+ },
124
+ onConnectionChange: onReconnect(() => void attempt()),
125
+ });
126
+ };
127
+ void (async () => {
128
+ const saved = await Promise.resolve(copies?.read(projectId, sessionId) ?? null).catch(() => null);
129
+ if (stopped)
130
+ return;
131
+ if (saved && !store().sessionLogs[sessionId])
132
+ store().applySessionLogPage(sessionId, saved);
133
+ await attempt();
134
+ })();
135
+ return () => {
136
+ stopped = true;
137
+ controller.abort();
138
+ stream?.close();
139
+ store().dropSessionLog(sessionId);
140
+ };
141
+ }, [negotiated, loader, joinStream, projectId, sessionId]);
142
+ return dialect;
143
+ }
@@ -1,4 +1,4 @@
1
- import { type MessageWithParts } from '../browser/stores/sync-store.js';
1
+ import { useSyncStore, type MessageWithParts } from '../browser/stores/sync-store.js';
2
2
  export interface SessionMessagesSource {
3
3
  projectId: string;
4
4
  sessionId: string;
@@ -9,3 +9,4 @@ export interface SessionMessagesOptions {
9
9
  throttleMs?: number;
10
10
  }
11
11
  export declare function useSessionMessages(session: SessionMessagesSource, options?: SessionMessagesOptions): MessageWithParts[];
12
+ export declare function usePacedStoreValue<T>(read: (state: ReturnType<typeof useSyncStore.getState>) => T, key: string, throttleMs: number): T;
@@ -16,31 +16,32 @@ export function useSessionMessages(session, options = {}) {
16
16
  return useMemo(() => messagesBeforeRewind(rows, rewind), [rows, rewind]);
17
17
  }
18
18
  function usePacedRows(readableSessionId, throttleMs) {
19
- const read = () => readableSessionId ? selectSessionRows(useSyncStore.getState(), readableSessionId) : EMPTY_ROWS;
20
- const [state, setState] = useState(() => ({ id: readableSessionId, rows: read() }));
19
+ return usePacedStoreValue((state) => (readableSessionId ? selectSessionRows(state, readableSessionId) : EMPTY_ROWS), readableSessionId, throttleMs);
20
+ }
21
+ export function usePacedStoreValue(read, key, throttleMs) {
22
+ const current = () => read(useSyncStore.getState());
23
+ const [state, setState] = useState(() => ({ key, value: current() }));
21
24
  useEffect(() => {
22
25
  if (!throttleMs)
23
26
  return;
24
27
  let lastShownAt = -Infinity;
25
28
  let timer = null;
26
- let shown = null;
29
+ let shown;
27
30
  const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
28
31
  const flush = () => {
29
32
  timer = null;
30
- const rows = read();
31
- if (rows === shown)
33
+ const value = current();
34
+ if (value === shown)
32
35
  return;
33
- shown = rows;
36
+ shown = value;
34
37
  lastShownAt = now();
35
- setState({ id: readableSessionId, rows });
38
+ setState({ key, value });
36
39
  };
37
- const initial = read();
40
+ const initial = current();
38
41
  shown = initial;
39
- setState((prev) => prev.id === readableSessionId && prev.rows === initial
40
- ? prev
41
- : { id: readableSessionId, rows: initial });
42
+ setState((prev) => (prev.key === key && prev.value === initial ? prev : { key, value: initial }));
42
43
  const unsubscribe = useSyncStore.subscribe(() => {
43
- if (read() === shown)
44
+ if (current() === shown)
44
45
  return;
45
46
  const wait = throttleMs - (now() - lastShownAt);
46
47
  if (wait <= 0) {
@@ -57,6 +58,6 @@ function usePacedRows(readableSessionId, throttleMs) {
57
58
  if (timer !== null)
58
59
  clearTimeout(timer);
59
60
  };
60
- }, [readableSessionId, throttleMs]);
61
- return state.id === readableSessionId ? state.rows : read();
61
+ }, [key, throttleMs]);
62
+ return state.key === key ? state.value : current();
62
63
  }
@@ -10,19 +10,30 @@ import { qk } from './query-keys.js';
10
10
  export function useSessionStreamConnected(projectId, sessionId) {
11
11
  return useSyncExternalStore(subscribeSessionStreamConnections, () => !!projectId && !!sessionId && sessionStreamConnected(projectId, sessionId), () => false);
12
12
  }
13
+ function openTurnsKey(turns) {
14
+ return (turns ?? []).map((turn) => turn.turn_token).sort().join(',');
15
+ }
13
16
  export function applySessionControlFrame(queryClient, projectId, sessionId, frame, memory, nowMs = Date.now()) {
14
17
  if (frame.type === 'beeuni.control.turn') {
15
18
  const payload = frame.payload;
16
19
  if (!payload?.known || !Array.isArray(payload.turns))
17
20
  return;
21
+ const key = qk.project.sessionTurn(projectId, sessionId);
22
+ const previous = queryClient.getQueryData(key);
23
+ const canStop = typeof payload.can_stop === 'boolean'
24
+ ? payload.can_stop
25
+ : previous && openTurnsKey(previous.turns) === openTurnsKey(payload.turns)
26
+ ? previous.can_stop
27
+ : undefined;
18
28
  const observation = {
19
29
  turns: payload.turns,
20
30
  ...(payload.last_ended ? { last_ended: payload.last_ended } : {}),
21
31
  ...(payload.recent_failures ? { recent_failures: payload.recent_failures } : {}),
32
+ ...(canStop !== undefined ? { can_stop: canStop } : {}),
22
33
  ...(payload.working ? { working: payload.working } : {}),
23
34
  atMs: nowMs,
24
35
  };
25
- queryClient.setQueryData(qk.project.sessionTurn(projectId, sessionId), observation);
36
+ queryClient.setQueryData(key, observation);
26
37
  return;
27
38
  }
28
39
  if (frame.type === 'beeuni.control.session') {
@@ -1,6 +1,6 @@
1
1
  import type { SessionTranscriptSyncEnvelope } from '../core/rest/projects-client/sessions.js';
2
- import { loadSessionRuntimeStatus, loadSessionTranscriptMessages } from '../browser/session-sync/session-sync-registry.js';
3
- export { loadSessionRuntimeStatus, loadSessionTranscriptMessages };
2
+ import { loadSessionExportMessages, loadSessionRuntimeStatus, loadSessionTranscriptMessages } from '../browser/session-sync/session-sync-registry.js';
3
+ export { loadSessionExportMessages, loadSessionRuntimeStatus, loadSessionTranscriptMessages };
4
4
  type FileDiff = Omit<import('../core/runtime/runtime-types.js').SnapshotFileDiff, 'patch'> & {
5
5
  patch?: string;
6
6
  before?: string;
@@ -1,18 +1,18 @@
1
1
  'use client';
2
2
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import { claimSessionCacheOwnership, getSessionCacheOwnership, resolveSessionCacheOwnerScope, sessionCacheOwnerScopesConflict, } from '../browser/session-sync/session-cache-ownership.js';
4
- import { getSessionSyncController, loadSessionRuntimeStatus, loadSessionTranscriptMessages, resetSessionSyncControllersForSession, retainSessionSyncController, } from '../browser/session-sync/session-sync-registry.js';
4
+ import { getSessionSyncController, loadSessionExportMessages, loadSessionRuntimeStatus, loadSessionTranscriptMessages, resetSessionSyncControllersForSession, retainSessionSyncController, } from '../browser/session-sync/session-sync-registry.js';
5
5
  import { loadOlderSessionTranscriptMirror, loadSessionTranscriptMirror, mirrorMessagesForHydrate, parseBeeuniSessionScope, refreshSavedCopy, SAVED_COPY_REFRESH_DELAY_MS, shouldHydrateFromMirror, } from '../browser/session-sync/server-transcript-mirror.js';
6
6
  import { currentSavedCopyStore } from '../core/session-sync/saved-copy-store.js';
7
7
  import { chooseOlderSource, mirrorCursorAfter } from '../browser/session-sync/mirror-paging.js';
8
8
  import { transcriptIsFragment } from '../core/session-sync/fragment.js';
9
9
  import { onTabVisible } from '../browser/session-sync/visibility.js';
10
10
  import { useSandboxConnectionStore } from '../browser/stores/sandbox-connection-store.js';
11
- import { hasOnlyCacheSourcedMessages, useSyncStore } from '../browser/stores/sync-store.js';
11
+ import { hasOnlyCacheSourcedMessages, hasRuntimeConfirmedMessage, useSyncStore } from '../browser/stores/sync-store.js';
12
12
  import { useCurrentRuntime } from './use-current-runtime.js';
13
13
  import { selectSessionRows } from './session-transcript-subscription.js';
14
14
  import { canQueryRuntimeSession } from './use-opencode-sessions/index.js';
15
- export { loadSessionRuntimeStatus, loadSessionTranscriptMessages };
15
+ export { loadSessionExportMessages, loadSessionRuntimeStatus, loadSessionTranscriptMessages };
16
16
  const EMPTY_DIFFS = [];
17
17
  const EMPTY_TODOS = [];
18
18
  const IDLE_STATUS = { type: 'idle' };
@@ -197,21 +197,23 @@ export function useSessionSync(sessionId, options = {}) {
197
197
  setIsLoadingOlderMirror(false);
198
198
  mirrorPageInFlight.current = null;
199
199
  }, [beeuniSessionScope, sessionId]);
200
+ const savedCursorCovered = useSyncStore(() => !!mirrorCursor && !sync.hasOlder && hasRuntimeConfirmedMessage(sessionId, mirrorCursor));
201
+ const savedCursor = savedCursorCovered ? null : mirrorCursor;
200
202
  const loadOlder = useCallback(() => {
201
203
  const source = chooseOlderSource({
202
204
  runtimeHasOlder: sync.hasOlder,
203
- mirrorCursor,
205
+ mirrorCursor: savedCursor,
204
206
  });
205
207
  if (source === 'runtime')
206
208
  return controller.loadOlder();
207
- if (source !== 'mirror' || !mirrorCursor)
209
+ if (source !== 'mirror' || !savedCursor)
208
210
  return Promise.resolve();
209
211
  if (mirrorPageInFlight.current)
210
212
  return mirrorPageInFlight.current;
211
213
  setIsLoadingOlderMirror(true);
212
214
  const pending = loadOlderSessionTranscriptMirror({
213
215
  beeuniSessionScope,
214
- before: mirrorCursor,
216
+ before: savedCursor,
215
217
  child: savedChild ? sessionId : undefined,
216
218
  })
217
219
  .then((envelope) => {
@@ -231,7 +233,7 @@ export function useSessionSync(sessionId, options = {}) {
231
233
  });
232
234
  mirrorPageInFlight.current = pending;
233
235
  return pending;
234
- }, [controller, beeuniSessionScope, mirrorCursor, savedChild, sessionId, sync.hasOlder]);
236
+ }, [controller, beeuniSessionScope, savedCursor, savedChild, sessionId, sync.hasOlder]);
235
237
  const retryTranscript = useCallback(() => {
236
238
  void controller.reconcile('manual');
237
239
  }, [controller]);
@@ -243,7 +245,7 @@ export function useSessionSync(sessionId, options = {}) {
243
245
  isBusy,
244
246
  isLoading,
245
247
  mirrorState,
246
- hasOlder: sync.hasOlder || Boolean(mirrorCursor),
248
+ hasOlder: sync.hasOlder || Boolean(savedCursor),
247
249
  isLoadingOlder: sync.isLoadingOlder || isLoadingOlderMirror,
248
250
  loadOlder,
249
251
  diffs: diffs ?? EMPTY_DIFFS,
@@ -1,21 +1,11 @@
1
1
  'use client';
2
2
  import { useQuery } from '@tanstack/react-query';
3
- import { getSessionTranscriptSync } from '../core/rest/projects-client/sessions.js';
4
- import { OPEN_BUNDLE_TRANSCRIPT_LIMIT, openBundleHistory, settledOpenBundle, } from '../core/session/open-bundle.js';
5
- import { savedCopyEmptyRoot } from '../core/session-sync/saved-transcript.js';
3
+ import { loadSavedTranscript, savedCopyEmptyRoot } from '../core/session-sync/saved-transcript.js';
6
4
  import { qk } from './query-keys.js';
7
5
  export function useSessionTranscriptHistory(projectId, sessionId, enabled) {
8
6
  const query = useQuery({
9
7
  queryKey: [...qk.project.session(projectId, sessionId), 'transcript-history'],
10
- queryFn: ({ signal }) => {
11
- const bundle = settledOpenBundle(projectId, sessionId);
12
- return ((bundle && openBundleHistory(bundle)) ??
13
- getSessionTranscriptSync(projectId, sessionId, {
14
- limit: OPEN_BUNDLE_TRANSCRIPT_LIMIT,
15
- history: true,
16
- signal,
17
- }));
18
- },
8
+ queryFn: ({ signal }) => loadSavedTranscript(projectId, sessionId, { signal }),
19
9
  enabled: enabled && !!projectId && !!sessionId,
20
10
  staleTime: 0,
21
11
  gcTime: 0,
@@ -11,6 +11,7 @@ export interface SessionTurnObservation {
11
11
  turns: SessionTurn[];
12
12
  last_ended?: SessionTurnEnded;
13
13
  recent_failures?: SessionTurnFailure[];
14
+ can_stop?: boolean;
14
15
  working?: SessionWorkingVerdict;
15
16
  atMs: number;
16
17
  }
@@ -35,3 +36,4 @@ export declare function readSessionTurnObservation(projectId: string, sessionId:
35
36
  }): Promise<SessionTurnObservation>;
36
37
  export declare function useSessionWorking(projectId: string, sessionId: string, options?: UseSessionWorkingOptions): WorkingProjection;
37
38
  export declare function useSessionTurnOutcome(projectId: string, sessionId: string): SessionTurnOutcome;
39
+ export declare function useSessionCanStop(projectId: string, sessionId: string): boolean;
@@ -59,6 +59,7 @@ export async function readSessionTurnObservation(projectId, sessionId, options =
59
59
  turns: turn.turns,
60
60
  last_ended: turn.last_ended,
61
61
  recent_failures: turn.recent_failures,
62
+ ...(turn.can_stop !== undefined ? { can_stop: turn.can_stop } : {}),
62
63
  atMs: turn.atMs,
63
64
  };
64
65
  }
@@ -70,6 +71,7 @@ export async function readSessionTurnObservation(projectId, sessionId, options =
70
71
  turns: status.turns ?? [],
71
72
  last_ended: status.last_ended,
72
73
  recent_failures: status.recent_failures,
74
+ ...(typeof status.can_stop === 'boolean' ? { can_stop: status.can_stop } : {}),
73
75
  atMs,
74
76
  };
75
77
  });
@@ -215,3 +217,11 @@ export function useSessionTurnOutcome(projectId, sessionId) {
215
217
  }, [settleInMs, queryClient, projectId, sessionId]);
216
218
  return useMemo(() => ({ last_ended: lastEnded, recent_failures: recentFailures, atMs }), [lastEnded, recentFailures, atMs]);
217
219
  }
220
+ export function useSessionCanStop(projectId, sessionId) {
221
+ const query = useQuery({
222
+ queryKey: qk.project.sessionTurn(projectId, sessionId),
223
+ queryFn: () => readSessionTurnObservation(projectId, sessionId, { bundle: false }),
224
+ enabled: false,
225
+ });
226
+ return query.data?.can_stop !== false;
227
+ }
@@ -6,6 +6,7 @@ import { type AbortSettlement, type PromptPart } from './use-opencode-sessions/i
6
6
  import { type SessionPicks } from './use-session-picks.js';
7
7
  export type SessionPhase = 'starting' | 'ready' | 'error';
8
8
  export declare function shouldRetrySessionStart(failureCount: number, error: unknown, sessionId: string): boolean;
9
+ export declare function presenceReturnReopensSession(heldByThisTab: boolean, cachedStage: string | null | undefined): boolean;
9
10
  export declare const SESSION_START_POLL_MS = 1500;
10
11
  export declare function resolveSessionRuntimeUrl(start: Pick<SessionStartResult, 'stage' | 'sandbox' | 'runtime_url'> | null | undefined): string | null;
11
12
  export interface SendOptions {
@@ -153,6 +154,11 @@ export declare function useSession(projectId: string, sessionId: string, options
153
154
  runtimePhase: import("./use-runtime-phase.js").RuntimePhase;
154
155
  working: import("../index.js").WorkingProjection;
155
156
  isBusy: boolean;
157
+ canStop: boolean;
158
+ presenceAwake: {
159
+ until: Date;
160
+ capAt: Date;
161
+ } | null;
156
162
  isCompacting: boolean;
157
163
  isLoading: boolean;
158
164
  savedTranscript: import("../core/session-sync/saved-transcript.js").SavedTranscript;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
3
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
4
4
  import { useOpenCodeCompactionStore } from '../browser/stores/opencode-compaction-store.js';
5
5
  import { useSessionWorkingStore } from '../browser/stores/session-working-store.js';
6
6
  import { compactionExpiryAtMs, projectCompacting, serverCompactionRevalidateAtMs, } from '../core/session/compaction.js';
@@ -29,7 +29,7 @@ import { useCanonicalRuntimeSession } from './use-canonical-opencode-session.js'
29
29
  import { useRuntimeEventStream } from './use-opencode-events/index.js';
30
30
  import { useSessionStream } from './use-session-stream.js';
31
31
  import { sessionStreamConnected } from '../core/session/control-stream.js';
32
- import { presenceReporter, watchHumanPresence } from './human-presence.js';
32
+ import { PRESENCE_AWAKE_REFRESH_MS, presenceAwakeFrom, presenceReporter, watchHumanPresence, } from './human-presence.js';
33
33
  import { formatModelString } from './use-opencode-local.js';
34
34
  import { runtimeKeys, rejectQuestion as rejectQuestionApi, replyToPermission, replyToQuestion, useAbortRuntimeSession, useExecuteRuntimeCommand, useRuntimeSession, } from './use-opencode-sessions/index.js';
35
35
  import { mintSessionWireMessageId } from './use-opencode-sessions/messages.js';
@@ -42,11 +42,13 @@ import { useRuntimePhase } from './use-runtime-phase.js';
42
42
  import { useSessionPicks } from './use-session-picks.js';
43
43
  import { derivePhase } from './use-session-phase.js';
44
44
  import { isEmptyConversation, resolveSavedTranscript } from '../core/session-sync/saved-transcript.js';
45
+ import { useSessionLogSync } from './use-session-log.js';
45
46
  import { useSessionSync } from './use-session-sync.js';
46
47
  import { selectTranscriptShapeKey } from './session-transcript-subscription.js';
47
48
  import { useSessionStartGiveUp } from './use-session-start-give-up.js';
48
- import { useSessionTurnOutcome, useSessionWorking } from './use-session-working.js';
49
+ import { useSessionCanStop, useSessionTurnOutcome, useSessionWorking } from './use-session-working.js';
49
50
  import { cancelSessionTurn } from './session-stop.js';
51
+ import { useTurnEndHandoff } from './turn-end-handoff.js';
50
52
  import { useVisibleAgents } from './use-visible-agents.js';
51
53
  import { createBeeuni } from '../core/client/beeuni.js';
52
54
  import { platformConfig } from '../core/http/config.js';
@@ -59,6 +61,9 @@ export function shouldRetrySessionStart(failureCount, error, sessionId) {
59
61
  }
60
62
  return !isSessionStartError(error) && failureCount < 3;
61
63
  }
64
+ export function presenceReturnReopensSession(heldByThisTab, cachedStage) {
65
+ return heldByThisTab || cachedStage === 'stopped';
66
+ }
62
67
  export const SESSION_START_POLL_MS = 1_500;
63
68
  export function resolveSessionRuntimeUrl(start) {
64
69
  if (!start || start.stage !== 'ready')
@@ -407,22 +412,53 @@ export function useSession(projectId, sessionId, options = {}) {
407
412
  const presenceAlertsRef = useRef(presenceAlerts);
408
413
  const presencePageExitRef = useRef(presencePageExit);
409
414
  const presenceReporterRef = useRef(null);
415
+ const lastPresenceActiveRef = useRef(null);
416
+ const [presenceAwake, setPresenceAwakeState] = useState(null);
417
+ const presenceAwakeRef = useRef(presenceAwake);
418
+ const setPresenceAwake = useCallback((next) => {
419
+ if (next === null && presenceAwakeRef.current === null)
420
+ return;
421
+ presenceAwakeRef.current = next;
422
+ setPresenceAwakeState(next);
423
+ }, []);
424
+ const presenceReportRef = useRef(0);
410
425
  useEffect(() => {
411
426
  if (!browserPresence || !presenceTabId || !projectId || !sessionId)
412
427
  return;
413
428
  const tab_id = presenceTabId;
414
429
  const handle = createBeeuni(platformConfig()).session(projectId, sessionId);
415
430
  const reporter = presenceReporter(({ active, alerts }) => {
431
+ if (active &&
432
+ lastPresenceActiveRef.current === false &&
433
+ presenceReturnReopensSession(stoppedByUserRef.current === sessionId, queryClient.getQueryData(sessionStartKey(projectId, sessionId))
434
+ ?.stage)) {
435
+ stoppedByUserRef.current = null;
436
+ openedAtRef.current = { sessionId, at: Date.now() };
437
+ void queryClient.invalidateQueries({ queryKey: sessionStartKey(projectId, sessionId) });
438
+ }
439
+ lastPresenceActiveRef.current = active;
440
+ const report = ++presenceReportRef.current;
441
+ if (!active)
442
+ setPresenceAwake(null);
416
443
  const keepalive = !active && presencePageExitRef.current;
417
- void handle.presence({ tab_id, active, alerts }, { keepalive }).catch(() => { });
444
+ void handle
445
+ .presence({ tab_id, active, alerts }, { keepalive })
446
+ .then((res) => {
447
+ if (active && report === presenceReportRef.current && res.data) {
448
+ setPresenceAwake(presenceAwakeFrom(res.data));
449
+ }
450
+ })
451
+ .catch(() => { });
418
452
  }, presenceAlertsRef.current);
419
453
  presenceReporterRef.current = reporter;
420
454
  const stop = watchHumanPresence({ doc: document, win: window }, reporter.report, () => sessionStreamConnected(projectId, sessionId), () => presencePageExitRef.current);
455
+ const refresh = window.setInterval(reporter.refresh, PRESENCE_AWAKE_REFRESH_MS);
421
456
  return () => {
457
+ window.clearInterval(refresh);
422
458
  presenceReporterRef.current = null;
423
459
  stop();
424
460
  };
425
- }, [browserPresence, presenceTabId, projectId, sessionId]);
461
+ }, [browserPresence, presenceTabId, projectId, sessionId, queryClient]);
426
462
  useEffect(() => {
427
463
  presenceAlertsRef.current = presenceAlerts;
428
464
  presenceReporterRef.current?.setAlerts(presenceAlerts);
@@ -485,6 +521,17 @@ export function useSession(projectId, sessionId, options = {}) {
485
521
  const sandbox = startData?.sandbox ?? null;
486
522
  const startReady = stage === 'ready';
487
523
  const terminal = stage === 'failed' || stage === 'stopped';
524
+ const startReadyRef = useRef(startReady);
525
+ useEffect(() => {
526
+ if (startReady && !startReadyRef.current) {
527
+ presenceReporterRef.current?.refresh();
528
+ }
529
+ else if (!startReady && startReadyRef.current) {
530
+ presenceReportRef.current += 1;
531
+ setPresenceAwake(null);
532
+ }
533
+ startReadyRef.current = startReady;
534
+ }, [startReady]);
488
535
  useIsomorphicLayoutEffect(() => {
489
536
  if (!startEnabled)
490
537
  return;
@@ -549,6 +596,7 @@ export function useSession(projectId, sessionId, options = {}) {
549
596
  }, [startEnabled, switchedBoxId, refetchStart]);
550
597
  useRuntimeEventStream({ enabled: switched, projectId, sessionId });
551
598
  const transcriptHistoryEnabled = startEnabled && chatEngine;
599
+ useSessionLogSync(projectId, sessionId, { enabled: startEnabled });
552
600
  const transcriptHistory = useSessionTranscriptHistory(projectId, sessionId, transcriptHistoryEnabled);
553
601
  const canonicalSession = useCanonicalRuntimeSession({
554
602
  projectId,
@@ -567,11 +615,24 @@ export function useSession(projectId, sessionId, options = {}) {
567
615
  enabled,
568
616
  runtimeSessionId: ocSessionId,
569
617
  });
618
+ const canStop = useSessionCanStop(projectId, sessionId);
619
+ const turnRunning = working.state === 'working';
620
+ const turnRanRef = useRef(turnRunning);
621
+ useEffect(() => {
622
+ if (turnRunning) {
623
+ presenceReportRef.current += 1;
624
+ setPresenceAwake(null);
625
+ }
626
+ else if (turnRanRef.current) {
627
+ presenceReporterRef.current?.refresh();
628
+ }
629
+ turnRanRef.current = turnRunning;
630
+ }, [turnRunning]);
570
631
  const { noteSendReceipt, acceptSendReceipt, clearSendReceipt } = useSessionWorkingStore.getState();
571
632
  const rawSync = useSessionSync(chatEngine ? ocSessionId : '', {
572
633
  beeuniSessionScope: `${projectId}/${sessionId}`,
573
634
  streamConnected,
574
- mirror: transcriptHistory.envelope ?? undefined,
635
+ mirror: transcriptHistory.envelope,
575
636
  networkEnabled: switched,
576
637
  working: working.state === 'working',
577
638
  serverHoldsTurn: working.serverOpenTurnToken !== null,
@@ -589,6 +650,25 @@ export function useSession(projectId, sessionId, options = {}) {
589
650
  const messages = useMemo(() => messagesBeforeRewind(sync.messages, restRewind), [sync.messages, restRewind]);
590
651
  const turnOutcome = useSessionTurnOutcome(projectId, sessionId);
591
652
  const turnRead = typeof turnOutcome.atMs === 'number';
653
+ const lastEndedToken = turnOutcome.last_ended?.turn_token ?? null;
654
+ const lastEndedSeenRef = useRef(undefined);
655
+ useEffect(() => {
656
+ if (!turnRead)
657
+ return;
658
+ const seen = lastEndedSeenRef.current;
659
+ lastEndedSeenRef.current = lastEndedToken;
660
+ if (seen !== undefined && seen !== lastEndedToken)
661
+ presenceReporterRef.current?.refresh();
662
+ }, [turnRead, lastEndedToken]);
663
+ useTurnEndHandoff({
664
+ enabled,
665
+ projectId,
666
+ sessionId,
667
+ runtimeSessionId: ocSessionId || null,
668
+ working: turnRunning,
669
+ openTurnToken: working.serverOpenTurnToken,
670
+ lastEndedToken,
671
+ });
592
672
  const savedEmptyRoot = transcriptHistoryEnabled ? transcriptHistory.emptyRootSessionId : null;
593
673
  const emptyProvenForRoot = savedEmptyRoot !== null && savedEmptyRoot === ocSessionId;
594
674
  const savedTranscript = resolveSavedTranscript({
@@ -855,6 +935,8 @@ export function useSession(projectId, sessionId, options = {}) {
855
935
  runtimePhase,
856
936
  working,
857
937
  isBusy: working.state === 'working',
938
+ canStop,
939
+ presenceAwake,
858
940
  isCompacting,
859
941
  isLoading: sync.isLoading,
860
942
  savedTranscript,