@opengeni/react 0.46.1 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/README.md +158 -14
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +170 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/chunk-6BBWWOOU.js +82 -0
  15. package/dist/chunk-6BBWWOOU.js.map +1 -0
  16. package/dist/{chunk-HFO4ERGQ.js → chunk-724BW4TW.js} +586 -35
  17. package/dist/chunk-724BW4TW.js.map +1 -0
  18. package/dist/chunk-7I2KB3OK.js +3895 -0
  19. package/dist/chunk-7I2KB3OK.js.map +1 -0
  20. package/dist/chunk-7WCLAEFV.js +2828 -0
  21. package/dist/chunk-7WCLAEFV.js.map +1 -0
  22. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  23. package/dist/chunk-FK6VG4LL.js.map +1 -0
  24. package/dist/{chunk-SRFUT2ZU.js → chunk-FWS3KC3Z.js} +120 -64
  25. package/dist/chunk-FWS3KC3Z.js.map +1 -0
  26. package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
  27. package/dist/chunk-HDBG7XGO.js.map +1 -0
  28. package/dist/chunk-HUIKIDM5.js +2148 -0
  29. package/dist/chunk-HUIKIDM5.js.map +1 -0
  30. package/dist/{chunk-LGVMUIRV.js → chunk-JAGDUCZQ.js} +1053 -390
  31. package/dist/chunk-JAGDUCZQ.js.map +1 -0
  32. package/dist/{chunk-LWR4MXSS.js → chunk-JR3QFY6B.js} +420 -35
  33. package/dist/chunk-JR3QFY6B.js.map +1 -0
  34. package/dist/chunk-JVYQRRC4.js +53 -0
  35. package/dist/chunk-JVYQRRC4.js.map +1 -0
  36. package/dist/chunk-KCGXPP5V.js +157 -0
  37. package/dist/chunk-KCGXPP5V.js.map +1 -0
  38. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  39. package/dist/chunk-KZ73RL76.js.map +1 -0
  40. package/dist/chunk-PEI6RANI.js +3234 -0
  41. package/dist/chunk-PEI6RANI.js.map +1 -0
  42. package/dist/{chunk-3PB7MIT6.js → chunk-ROIRTM37.js} +220 -72
  43. package/dist/chunk-ROIRTM37.js.map +1 -0
  44. package/dist/chunk-VZTQ73XT.js +86 -0
  45. package/dist/chunk-VZTQ73XT.js.map +1 -0
  46. package/dist/{chunk-VCBFUSUZ.js → chunk-WSK7G5LE.js} +72 -4
  47. package/dist/chunk-WSK7G5LE.js.map +1 -0
  48. package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
  49. package/dist/chunk-WYA65Y3F.js.map +1 -0
  50. package/dist/{chunk-6EDV6YFE.js → chunk-X4S56Y2H.js} +147 -35
  51. package/dist/chunk-X4S56Y2H.js.map +1 -0
  52. package/dist/client.d.ts +7 -1
  53. package/dist/clipboard.d.ts +1 -0
  54. package/dist/clipboard.js +7 -0
  55. package/dist/clipboard.js.map +1 -0
  56. package/dist/commands/index.d.ts +3 -3
  57. package/dist/commands/registry.d.ts +1 -1
  58. package/dist/commands/types.d.ts +1 -1
  59. package/dist/components/approval-surface.d.ts +2 -2
  60. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  61. package/dist/components/artifacts/document-editor.d.ts +172 -0
  62. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  63. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  64. package/dist/components/artifacts/editable-document.d.ts +24 -0
  65. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  66. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  67. package/dist/components/artifacts/index.d.ts +8 -0
  68. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  69. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  70. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  71. package/dist/components/browser-viewer.d.ts +29 -0
  72. package/dist/components/chat-composer.d.ts +13 -8
  73. package/dist/components/code-editor.d.ts +25 -3
  74. package/dist/components/command-palette.d.ts +2 -2
  75. package/dist/components/composer-transcription-control.d.ts +4 -2
  76. package/dist/components/composer.d.ts +29 -15
  77. package/dist/components/computer-viewer.d.ts +23 -0
  78. package/dist/components/copy-button.d.ts +2 -2
  79. package/dist/components/desktop-viewer.d.ts +25 -21
  80. package/dist/components/diff-view.d.ts +1 -1
  81. package/dist/components/enrollment-consent.d.ts +1 -1
  82. package/dist/components/enrollment-device-flow.d.ts +1 -1
  83. package/dist/components/file-browser.d.ts +2 -2
  84. package/dist/components/fleet-tile.d.ts +1 -1
  85. package/dist/components/generated-video-player.d.ts +10 -0
  86. package/dist/components/human-input-form.d.ts +1 -1
  87. package/dist/components/human-input-surface.d.ts +2 -2
  88. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  89. package/dist/components/machine-card.d.ts +2 -2
  90. package/dist/components/machine-dock-bar.d.ts +3 -3
  91. package/dist/components/machine-health-pill.d.ts +2 -2
  92. package/dist/components/machine-input-display.d.ts +1 -1
  93. package/dist/components/machine-metrics.d.ts +2 -2
  94. package/dist/components/machine-status-pill.d.ts +4 -4
  95. package/dist/components/machines/health.d.ts +1 -1
  96. package/dist/components/machines/machine-detail.d.ts +5 -3
  97. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  98. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  99. package/dist/components/machines/series.d.ts +2 -2
  100. package/dist/components/machines-dashboard.d.ts +5 -5
  101. package/dist/components/markdown.d.ts +1 -1
  102. package/dist/components/message-timeline.d.ts +22 -5
  103. package/dist/components/model-picker.d.ts +2 -2
  104. package/dist/components/model-policy-picker.d.ts +7 -7
  105. package/dist/components/pierre-diff.d.ts +1 -1
  106. package/dist/components/pierre-file.d.ts +1 -1
  107. package/dist/components/queue-draft-policy.d.ts +1 -1
  108. package/dist/components/queue-surface-implementation.d.ts +3 -3
  109. package/dist/components/queue-surface-state.d.ts +4 -4
  110. package/dist/components/queue-surface.d.ts +5 -5
  111. package/dist/components/sandbox-files.d.ts +5 -5
  112. package/dist/components/sandbox-terminal.d.ts +50 -4
  113. package/dist/components/sandbox-workspace.d.ts +30 -10
  114. package/dist/components/session-chrome.d.ts +4 -4
  115. package/dist/components/session-status.d.ts +2 -2
  116. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  117. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  118. package/dist/components/timeline-annotations.d.ts +13 -0
  119. package/dist/components/tooltip.d.ts +4 -4
  120. package/dist/components/user-message-body.d.ts +2 -2
  121. package/dist/components/workbench-changes.d.ts +2 -2
  122. package/dist/components/workspace-dock.d.ts +3 -1
  123. package/dist/composer.d.ts +11 -11
  124. package/dist/composer.js +7 -7
  125. package/dist/hooks/internal.d.ts +7 -1
  126. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  127. package/dist/hooks/use-available-models.d.ts +2 -2
  128. package/dist/hooks/use-billing-usage.d.ts +1 -1
  129. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  130. package/dist/hooks/use-browser-identities.d.ts +26 -0
  131. package/dist/hooks/use-browser-session.d.ts +28 -0
  132. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  133. package/dist/hooks/use-codex-accounts.d.ts +3 -3
  134. package/dist/hooks/use-composer.d.ts +36 -9
  135. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  136. package/dist/hooks/use-computer-session.d.ts +28 -0
  137. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  138. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  139. package/dist/hooks/use-environments.d.ts +11 -5
  140. package/dist/hooks/use-file-attachments.d.ts +1 -1
  141. package/dist/hooks/use-goal.d.ts +2 -2
  142. package/dist/hooks/use-human-input.d.ts +2 -2
  143. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  144. package/dist/hooks/use-machine-chip.d.ts +2 -2
  145. package/dist/hooks/use-machines.d.ts +10 -3
  146. package/dist/hooks/use-packs.d.ts +7 -2
  147. package/dist/hooks/use-rigs.d.ts +1 -1
  148. package/dist/hooks/use-sandbox-files.d.ts +25 -4
  149. package/dist/hooks/use-sandbox-git.d.ts +13 -5
  150. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  151. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  152. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  153. package/dist/hooks/use-session-control.d.ts +1 -1
  154. package/dist/hooks/use-session-events.d.ts +2 -2
  155. package/dist/hooks/use-session-lineage.d.ts +1 -1
  156. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  157. package/dist/hooks/use-session.d.ts +2 -2
  158. package/dist/hooks/use-slash-commands.d.ts +1 -1
  159. package/dist/hooks/use-terminal-stream.d.ts +8 -2
  160. package/dist/hooks/use-transcription.d.ts +2 -2
  161. package/dist/hooks/use-turn-queue.d.ts +2 -2
  162. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  163. package/dist/hooks/use-voice-input.d.ts +6 -4
  164. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  165. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  166. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  167. package/dist/hooks/use-workspaces.d.ts +1 -1
  168. package/dist/index.d.ts +158 -154
  169. package/dist/index.js +1718 -908
  170. package/dist/index.js.map +1 -1
  171. package/dist/interaction.d.ts +23 -0
  172. package/dist/interaction.js +36 -0
  173. package/dist/interaction.js.map +1 -0
  174. package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
  175. package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
  176. package/dist/lib/composer-responsive-context.d.ts +9 -0
  177. package/dist/lib/relay-wire.d.ts +4 -0
  178. package/dist/lib/sandbox-liveness.d.ts +9 -0
  179. package/dist/lib/terminal-capability.d.ts +5 -0
  180. package/dist/lib/use-portal-token-style.d.ts +14 -3
  181. package/dist/lib/xterm-theme.d.ts +1 -1
  182. package/dist/machines.d.ts +30 -30
  183. package/dist/machines.js +3 -3
  184. package/dist/provider.d.ts +4 -4
  185. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-2KVJTYXH.js} +117 -64
  186. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  187. package/dist/realtime/dropdown-menu.d.ts +8 -8
  188. package/dist/realtime/realtime-control.d.ts +15 -6
  189. package/dist/realtime.d.ts +4 -4
  190. package/dist/realtime.js +30 -33
  191. package/dist/realtime.js.map +1 -1
  192. package/dist/session-context.d.ts +19 -1
  193. package/dist/session-ui.d.ts +14 -14
  194. package/dist/session-ui.js +6 -5
  195. package/dist/session.d.ts +28 -28
  196. package/dist/session.js +5 -5
  197. package/dist/timeline/activity-rail.d.ts +6 -3
  198. package/dist/timeline/disclosure-context.d.ts +1 -1
  199. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  200. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  201. package/dist/timeline/index.d.ts +19 -19
  202. package/dist/timeline/parsers.d.ts +6 -3
  203. package/dist/timeline/projection.d.ts +3 -3
  204. package/dist/timeline/registry.d.ts +10 -1
  205. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  206. package/dist/timeline/shared.d.ts +21 -10
  207. package/dist/timeline/tool-diff.d.ts +2 -2
  208. package/dist/timeline/tool-renderers.d.ts +1 -1
  209. package/dist/timeline/turn-summary.d.ts +3 -3
  210. package/dist/timeline/types.d.ts +42 -2
  211. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  212. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  213. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  214. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  215. package/dist/voice-recording-store.d.ts +8 -2
  216. package/package.json +52 -7
  217. package/src/artifacts-document.ts +31 -0
  218. package/src/artifacts-presentation.ts +36 -0
  219. package/src/artifacts-spreadsheet.ts +21 -0
  220. package/src/artifacts.ts +86 -0
  221. package/src/client.ts +90 -1
  222. package/src/clipboard.ts +2 -0
  223. package/src/components/artifacts/artifact-surface.tsx +93 -0
  224. package/src/components/artifacts/document-editor.tsx +2255 -0
  225. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  226. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  227. package/src/components/artifacts/editable-document.tsx +657 -0
  228. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  229. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  230. package/src/components/artifacts/index.ts +99 -0
  231. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  232. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  233. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  234. package/src/components/browser-viewer.tsx +1592 -0
  235. package/src/components/chat-composer.tsx +25 -2
  236. package/src/components/code-editor.tsx +90 -31
  237. package/src/components/command-palette.tsx +1 -1
  238. package/src/components/composer-transcription-control.tsx +51 -22
  239. package/src/components/composer.tsx +49 -23
  240. package/src/components/computer-viewer.tsx +1139 -0
  241. package/src/components/copy-button.tsx +1 -1
  242. package/src/components/desktop-viewer.tsx +103 -75
  243. package/src/components/file-browser.tsx +39 -17
  244. package/src/components/generated-video-player.tsx +89 -0
  245. package/src/components/human-input-form.tsx +2 -2
  246. package/src/components/interactive-workbench-viewers.ts +2 -0
  247. package/src/components/machine-card.tsx +1 -1
  248. package/src/components/machine-input-display.ts +3 -0
  249. package/src/components/machines/machine-detail.tsx +39 -15
  250. package/src/components/machines-dashboard.tsx +7 -6
  251. package/src/components/message-timeline.tsx +327 -37
  252. package/src/components/model-picker.tsx +2 -2
  253. package/src/components/model-policy-picker.tsx +16 -15
  254. package/src/components/queue-surface-implementation.tsx +135 -75
  255. package/src/components/queue-surface-state.tsx +2 -2
  256. package/src/components/sandbox-files.tsx +142 -62
  257. package/src/components/sandbox-terminal.tsx +119 -35
  258. package/src/components/sandbox-workspace.tsx +394 -228
  259. package/src/components/session-chrome.tsx +67 -27
  260. package/src/components/timeline-annotation-selection.tsx +221 -0
  261. package/src/components/timeline-annotations-dialog.tsx +213 -0
  262. package/src/components/timeline-annotations.tsx +84 -0
  263. package/src/components/tooltip.tsx +13 -16
  264. package/src/components/user-message-body.tsx +1 -1
  265. package/src/components/workbench-changes.tsx +192 -35
  266. package/src/components/workspace-dock.tsx +13 -5
  267. package/src/composer.ts +3 -0
  268. package/src/hooks/internal.ts +132 -38
  269. package/src/hooks/use-attached-browsers.ts +152 -0
  270. package/src/hooks/use-browser-frame-stream.ts +316 -0
  271. package/src/hooks/use-browser-identities.ts +225 -0
  272. package/src/hooks/use-browser-session.ts +476 -0
  273. package/src/hooks/use-browser-sessions.ts +281 -0
  274. package/src/hooks/use-codex-accounts.ts +4 -4
  275. package/src/hooks/use-composer.ts +769 -24
  276. package/src/hooks/use-computer-frame-stream.ts +311 -0
  277. package/src/hooks/use-computer-session.ts +442 -0
  278. package/src/hooks/use-computer-sessions.ts +245 -0
  279. package/src/hooks/use-environments.ts +24 -5
  280. package/src/hooks/use-file-attachments.ts +70 -32
  281. package/src/hooks/use-goal.ts +17 -5
  282. package/src/hooks/use-machine-chip.ts +5 -3
  283. package/src/hooks/use-machines.ts +79 -3
  284. package/src/hooks/use-packs.ts +51 -0
  285. package/src/hooks/use-sandbox-files.ts +382 -88
  286. package/src/hooks/use-sandbox-git.ts +208 -46
  287. package/src/hooks/use-sandbox-terminal.ts +3 -2
  288. package/src/hooks/use-session-capabilities.ts +94 -70
  289. package/src/hooks/use-session-events.ts +18 -4
  290. package/src/hooks/use-terminal-stream.ts +102 -23
  291. package/src/hooks/use-transcription.ts +4 -14
  292. package/src/hooks/use-turn-queue.ts +17 -5
  293. package/src/hooks/use-video-artifact-playback.ts +28 -0
  294. package/src/hooks/use-voice-input.ts +163 -19
  295. package/src/hooks/use-windowed-sections.ts +9 -7
  296. package/src/hooks/use-workspace-edit.ts +2 -1
  297. package/src/index.ts +21 -6
  298. package/src/interaction.ts +71 -0
  299. package/src/lib/composer-responsive-context.ts +15 -0
  300. package/src/lib/relay-wire.ts +4 -0
  301. package/src/lib/sandbox-liveness.ts +11 -0
  302. package/src/lib/terminal-capability.ts +10 -0
  303. package/src/lib/use-portal-token-style.ts +60 -15
  304. package/src/provider.tsx +52 -7
  305. package/src/realtime/dropdown-menu.tsx +15 -19
  306. package/src/realtime/realtime-control.tsx +16 -5
  307. package/src/session-context.ts +90 -0
  308. package/src/timeline/activity-rail.tsx +31 -6
  309. package/src/timeline/index.ts +4 -1
  310. package/src/timeline/parsers.ts +46 -20
  311. package/src/timeline/projection.ts +233 -13
  312. package/src/timeline/registry.ts +19 -0
  313. package/src/timeline/screenshot-lightbox.tsx +30 -9
  314. package/src/timeline/shared.tsx +59 -11
  315. package/src/timeline/tool-renderers.tsx +421 -24
  316. package/src/timeline/turn-summary.tsx +2 -2
  317. package/src/timeline/types.ts +58 -2
  318. package/src/voice-recording-store.ts +15 -0
  319. package/styles/compiled.css +5298 -0
  320. package/styles/compiled.d.ts +2 -0
  321. package/styles/effective-tokens.css +27 -0
  322. package/styles/index.css +18 -0
  323. package/styles/responsive.css +208 -0
  324. package/styles/responsive.d.ts +2 -0
  325. package/styles/tokens.css +50 -17
  326. package/dist/chunk-3PB7MIT6.js.map +0 -1
  327. package/dist/chunk-6EDV6YFE.js.map +0 -1
  328. package/dist/chunk-EB67J347.js.map +0 -1
  329. package/dist/chunk-EKZH6IDG.js.map +0 -1
  330. package/dist/chunk-FGZUCXZF.js.map +0 -1
  331. package/dist/chunk-HFO4ERGQ.js.map +0 -1
  332. package/dist/chunk-JB5725BI.js +0 -48
  333. package/dist/chunk-JB5725BI.js.map +0 -1
  334. package/dist/chunk-LGVMUIRV.js.map +0 -1
  335. package/dist/chunk-LWR4MXSS.js.map +0 -1
  336. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  337. package/dist/chunk-U3CSCMWA.js.map +0 -1
  338. package/dist/chunk-VCBFUSUZ.js.map +0 -1
  339. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  340. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  341. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  DEFAULT_FILE_RESOURCE_MOUNT_ROOT,
3
3
  type ComposerDraft,
4
+ type DraftTimelineAnnotation,
4
5
  type EffectiveControlResumeOption,
5
6
  type EffectiveSessionControl,
6
7
  type OpenGeniApiError,
@@ -13,10 +14,12 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react
13
14
  import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
14
15
  import { useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
15
16
 
16
- export type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId">;
17
+ export type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId" | "annotations">;
17
18
 
18
19
  export type UseComposerOptions = EmbeddedSessionClientOverride &
19
20
  SessionEventFeedOptions & {
21
+ /** Called synchronously after an ordinary Send is accepted by the local UI. */
22
+ onSubmitted?: ((text: string, input: SendMessageInput) => void) | undefined;
20
23
  /** Called with the exact accepted wire input after a successful send. */
21
24
  onSent?: ((text: string, input: SendMessageInput) => void) | undefined;
22
25
  /**
@@ -42,6 +45,7 @@ export type UseComposerOptions = EmbeddedSessionClientOverride &
42
45
  type ComposerDraftShadow = {
43
46
  text: string;
44
47
  resources: ResourceRef[];
48
+ annotations: DraftTimelineAnnotation[];
45
49
  };
46
50
 
47
51
  type PendingComposerOperation = {
@@ -49,6 +53,7 @@ type PendingComposerOperation = {
49
53
  input: SendMessageInput;
50
54
  draftAtSend: string;
51
55
  resourcesAtSend: ResourceRef[];
56
+ annotationsAtSend: DraftTimelineAnnotation[];
52
57
  /** Latest local text/resources that must survive an uncertain delivery. */
53
58
  newerShadow: ComposerDraftShadow;
54
59
  clearDraftOnAccept: boolean;
@@ -61,13 +66,39 @@ type StoredPendingComposerOperation = Omit<PendingComposerOperation, "input" | "
61
66
  hasMcpCredentialUpdates: boolean;
62
67
  };
63
68
 
69
+ export type ComposerOptimisticMessage = {
70
+ clientEventId: string;
71
+ text: string;
72
+ annotations: DraftTimelineAnnotation[];
73
+ resources: ResourceRef[];
74
+ occurredAt: string;
75
+ state: "sending" | "queued" | "failed";
76
+ error?: string | undefined;
77
+ outcomeUnknown?: boolean | undefined;
78
+ };
79
+
80
+ type OptimisticSendOperation = ComposerOptimisticMessage & {
81
+ input: SendMessageInput;
82
+ draftPayload: SaveComposerDraftRequest | null;
83
+ /** Latest unsent local draft that must survive this operation and remounts. */
84
+ newerShadow: ComposerDraftShadow;
85
+ canRetry: boolean;
86
+ };
87
+
88
+ type StoredOptimisticSendOperation = Omit<OptimisticSendOperation, "input" | "canRetry"> & {
89
+ input: Omit<SendMessageInput, "mcpCredentialUpdates">;
90
+ hasMcpCredentialUpdates: boolean;
91
+ };
92
+
64
93
  const PENDING_COMPOSER_STORAGE_PREFIX = "opengeni.pending-composer.v1:";
94
+ const OPTIMISTIC_SEND_STORAGE_PREFIX = "opengeni.optimistic-sends.v1:";
65
95
 
66
96
  // A remount must not manufacture a new operation while the previous mutation
67
97
  // is still outcome-unknown. Keep only non-credential request fields here; the
68
98
  // mounted hook retains the exact input, including any credential updates. The
69
99
  // safe shadow is also session-scoped so a refresh cannot lose newer edits.
70
100
  const pendingComposerOperations = new Map<string, StoredPendingComposerOperation>();
101
+ const optimisticSendOperations = new Map<string, StoredOptimisticSendOperation[]>();
71
102
 
72
103
  function pendingComposerOperationKey(
73
104
  workspaceId: string,
@@ -88,6 +119,78 @@ function pendingComposerStorageKey(key: string): string {
88
119
  return `${PENDING_COMPOSER_STORAGE_PREFIX}${encodeURIComponent(key)}`;
89
120
  }
90
121
 
122
+ function optimisticSendStorageKey(key: string): string {
123
+ return `${OPTIMISTIC_SEND_STORAGE_PREFIX}${encodeURIComponent(key)}`;
124
+ }
125
+
126
+ function restoreOptimisticSendOperations(key: string | null): OptimisticSendOperation[] {
127
+ if (!key) return [];
128
+ let stored = optimisticSendOperations.get(key);
129
+ if (!stored) {
130
+ const storage = pendingComposerStorage();
131
+ try {
132
+ const parsed: unknown = JSON.parse(storage?.getItem(optimisticSendStorageKey(key)) ?? "[]");
133
+ stored = Array.isArray(parsed)
134
+ ? (parsed.filter(
135
+ (candidate): candidate is StoredOptimisticSendOperation =>
136
+ typeof candidate === "object" &&
137
+ candidate !== null &&
138
+ typeof candidate.clientEventId === "string" &&
139
+ typeof candidate.text === "string" &&
140
+ typeof candidate.occurredAt === "string" &&
141
+ (candidate.state === "sending" || candidate.state === "failed") &&
142
+ typeof candidate.input === "object" &&
143
+ candidate.input !== null &&
144
+ typeof candidate.input.text === "string" &&
145
+ typeof candidate.hasMcpCredentialUpdates === "boolean",
146
+ ) as StoredOptimisticSendOperation[])
147
+ : [];
148
+ } catch {
149
+ stored = [];
150
+ }
151
+ }
152
+ return stored.map(({ hasMcpCredentialUpdates, ...operation }) => ({
153
+ ...operation,
154
+ newerShadow: operation.newerShadow ?? { text: "", resources: [], annotations: [] },
155
+ state:
156
+ operation.state === "sending" || operation.state === "queued" ? "sending" : operation.state,
157
+ outcomeUnknown:
158
+ operation.state === "sending" || operation.state === "queued"
159
+ ? true
160
+ : operation.outcomeUnknown,
161
+ error:
162
+ operation.state === "sending"
163
+ ? "Delivery was interrupted; retry to reconcile this message."
164
+ : operation.error,
165
+ input: operation.input,
166
+ canRetry: !hasMcpCredentialUpdates,
167
+ }));
168
+ }
169
+
170
+ function rememberOptimisticSendOperations(
171
+ key: string | null,
172
+ operations: OptimisticSendOperation[],
173
+ ): void {
174
+ if (!key) return;
175
+ const stored = operations
176
+ .filter((operation) => operation.state !== "queued")
177
+ .map((operation) => {
178
+ const { canRetry: _canRetry, input, ...rest } = operation;
179
+ const { mcpCredentialUpdates: _credentials, ...safeInput } = input;
180
+ return {
181
+ ...rest,
182
+ input: safeInput,
183
+ hasMcpCredentialUpdates: input.mcpCredentialUpdates !== undefined,
184
+ } satisfies StoredOptimisticSendOperation;
185
+ });
186
+ optimisticSendOperations.set(key, stored);
187
+ try {
188
+ pendingComposerStorage()?.setItem(optimisticSendStorageKey(key), JSON.stringify(stored));
189
+ } catch {
190
+ // Best effort; the in-memory queue still owns this mount.
191
+ }
192
+ }
193
+
91
194
  function resourceList(value: unknown): value is ResourceRef[] {
92
195
  return (
93
196
  Array.isArray(value) &&
@@ -103,6 +206,41 @@ function resourceList(value: unknown): value is ResourceRef[] {
103
206
  );
104
207
  }
105
208
 
209
+ function timelineAnnotationList(value: unknown): value is DraftTimelineAnnotation[] {
210
+ return (
211
+ Array.isArray(value) &&
212
+ value.every((candidate) => {
213
+ if (typeof candidate !== "object" || candidate === null) return false;
214
+ const annotation = candidate as Record<string, unknown>;
215
+ const source = annotation.source;
216
+ if (typeof source !== "object" || source === null) return false;
217
+ const sourceRecord = source as Record<string, unknown>;
218
+ return (
219
+ typeof annotation.id === "string" &&
220
+ typeof annotation.quote === "string" &&
221
+ typeof annotation.note === "string" &&
222
+ typeof sourceRecord.eventId === "string" &&
223
+ typeof sourceRecord.eventType === "string" &&
224
+ typeof sourceRecord.sequence === "number" &&
225
+ (typeof sourceRecord.turnId === "string" || sourceRecord.turnId === null) &&
226
+ typeof sourceRecord.startOffset === "number" &&
227
+ typeof sourceRecord.endOffset === "number" &&
228
+ typeof sourceRecord.contextBefore === "string" &&
229
+ typeof sourceRecord.contextAfter === "string"
230
+ );
231
+ })
232
+ );
233
+ }
234
+
235
+ function cloneAnnotations(
236
+ annotations: readonly DraftTimelineAnnotation[],
237
+ ): DraftTimelineAnnotation[] {
238
+ return annotations.map((annotation) => ({
239
+ ...annotation,
240
+ source: { ...annotation.source },
241
+ }));
242
+ }
243
+
106
244
  function readStoredPendingComposerOperation(
107
245
  key: string | null,
108
246
  ): StoredPendingComposerOperation | null {
@@ -128,18 +266,34 @@ function readStoredPendingComposerOperation(
128
266
  (record.delivery !== "send" && record.delivery !== "steer") ||
129
267
  typeof record.draftAtSend !== "string" ||
130
268
  !resourceList(record.resourcesAtSend) ||
269
+ (record.annotationsAtSend !== undefined &&
270
+ !timelineAnnotationList(record.annotationsAtSend)) ||
131
271
  typeof shadowRecord.text !== "string" ||
132
272
  !resourceList(shadowRecord.resources) ||
273
+ (shadowRecord.annotations !== undefined &&
274
+ !timelineAnnotationList(shadowRecord.annotations)) ||
133
275
  typeof record.clearDraftOnAccept !== "boolean" ||
134
276
  typeof record.hasMcpCredentialUpdates !== "boolean" ||
135
277
  typeof inputRecord.text !== "string" ||
136
278
  typeof inputRecord.clientEventId !== "string" ||
137
279
  "mcpCredentialUpdates" in inputRecord ||
138
- ("resources" in inputRecord && !resourceList(inputRecord.resources))
280
+ ("resources" in inputRecord && !resourceList(inputRecord.resources)) ||
281
+ ("annotations" in inputRecord && !timelineAnnotationList(inputRecord.annotations))
139
282
  ) {
140
283
  return null;
141
284
  }
142
- return record as StoredPendingComposerOperation;
285
+ return {
286
+ ...(record as StoredPendingComposerOperation),
287
+ annotationsAtSend: timelineAnnotationList(record.annotationsAtSend)
288
+ ? record.annotationsAtSend
289
+ : [],
290
+ newerShadow: {
291
+ ...(shadow as ComposerDraftShadow),
292
+ annotations: timelineAnnotationList(shadowRecord.annotations)
293
+ ? shadowRecord.annotations
294
+ : [],
295
+ },
296
+ };
143
297
  } catch {
144
298
  return null;
145
299
  }
@@ -168,6 +322,7 @@ function restorePendingComposerOperation(key: string | null): PendingComposerOpe
168
322
  newerShadow: stored.newerShadow ?? {
169
323
  text: stored.draftAtSend,
170
324
  resources: stored.resourcesAtSend,
325
+ annotations: stored.annotationsAtSend,
171
326
  },
172
327
  canRetry: !stored.hasMcpCredentialUpdates,
173
328
  };
@@ -188,6 +343,7 @@ function rememberPendingComposerOperation(
188
343
  newerShadow: {
189
344
  text: operation.newerShadow.text,
190
345
  resources: [...operation.newerShadow.resources],
346
+ annotations: cloneAnnotations(operation.newerShadow.annotations),
191
347
  },
192
348
  } satisfies StoredPendingComposerOperation;
193
349
  pendingComposerOperations.set(key, stored);
@@ -212,7 +368,14 @@ function updatePendingComposerShadow(
212
368
  shadow: ComposerDraftShadow,
213
369
  ): PendingComposerOperation | null {
214
370
  if (!key || !operation) return operation;
215
- const next = { ...operation, newerShadow: { ...shadow, resources: [...shadow.resources] } };
371
+ const next = {
372
+ ...operation,
373
+ newerShadow: {
374
+ ...shadow,
375
+ resources: [...shadow.resources],
376
+ annotations: cloneAnnotations(shadow.annotations),
377
+ },
378
+ };
216
379
  rememberPendingComposerOperation(key, next);
217
380
  return next;
218
381
  }
@@ -220,14 +383,27 @@ function updatePendingComposerShadow(
220
383
  export type ComposerState = {
221
384
  value: string;
222
385
  setValue: (value: string) => void;
386
+ annotations?: DraftTimelineAnnotation[] | undefined;
387
+ addAnnotation?: ((annotation: DraftTimelineAnnotation) => void) | undefined;
388
+ updateAnnotation?: ((id: string, note: string) => void) | undefined;
389
+ removeAnnotation?: ((id: string) => void) | undefined;
390
+ /** Newly captured annotation the review surface should focus. */
391
+ annotationReviewTargetId?: string | null | undefined;
392
+ clearAnnotationReviewTarget?: (() => void) | undefined;
223
393
  /** Read the current draft synchronously before a destructive replacement. */
224
394
  hasDraftContent: () => boolean;
225
395
  /** Append the draft behind prompts already visible in the queue. */
226
396
  send: (text?: string) => Promise<boolean>;
397
+ /** Locally acknowledged ordinary sends awaiting durable timeline reconciliation. */
398
+ optimisticMessages?: ComposerOptimisticMessage[] | undefined;
399
+ retryOptimisticMessage?: ((clientEventId: string) => void) | undefined;
400
+ removeOptimisticMessage?: ((clientEventId: string) => void) | undefined;
227
401
  /** Supersede current direction with the draft. */
228
402
  steer: (text?: string) => Promise<boolean>;
229
403
  /** Optimistic-to-durable projection for a Steer that has not started yet. */
230
404
  steering?: ComposerSteeringState | null | undefined;
405
+ /** Immediate mutation-receipt projection while physical cancellation settles. */
406
+ stoppingAttempt?: "current" | "previous" | null | undefined;
231
407
  sending: boolean;
232
408
  canSend: boolean;
233
409
  /** Pause the session without deleting its prompt queue. */
@@ -259,6 +435,13 @@ export type ComposerSteeringState = {
259
435
  clientEventId: string | null;
260
436
  triggerEventId: string | null;
261
437
  turnId: string | null;
438
+ /** True only when the accepted Steer durably interrupted a live attempt. */
439
+ stoppingPreviousAttempt?: boolean | undefined;
440
+ };
441
+
442
+ type ComposerControlStoppingState = {
443
+ controlVersion: number;
444
+ controlEtag: string;
262
445
  };
263
446
 
264
447
  const STEERING_SETTLEMENT_EVENT_TYPES = new Set([
@@ -321,12 +504,20 @@ export function useComposer(
321
504
  const targetKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${durableDrafts ? "durable" : "disabled"}`;
322
505
  const pendingOperationKey = pendingComposerOperationKey(workspaceId, sessionId);
323
506
  const initialPendingOperation = restorePendingComposerOperation(pendingOperationKey);
324
- const initialShadow = initialPendingOperation?.newerShadow;
507
+ const initialOptimisticSends = restoreOptimisticSendOperations(pendingOperationKey);
508
+ const initialShadow =
509
+ initialPendingOperation?.newerShadow ?? initialOptimisticSends.at(-1)?.newerShadow;
325
510
  const [value, setValue] = useState(() => initialShadow?.text ?? "");
511
+ const [annotations, setAnnotations] = useState<DraftTimelineAnnotation[]>(
512
+ () => initialShadow?.annotations ?? [],
513
+ );
514
+ const [annotationReviewTargetId, setAnnotationReviewTargetId] = useState<string | null>(null);
326
515
  // Keep rendered state behind the committed target identity for one frame:
327
516
  // a parent may switch sessionId without remounting this public hook.
328
517
  const [stateTargetKey, setStateTargetKey] = useState(targetKey);
329
518
  const [sending, setSending] = useState(false);
519
+ const [optimisticSends, setOptimisticSends] =
520
+ useState<OptimisticSendOperation[]>(initialOptimisticSends);
330
521
  const [steering, setSteering] = useState<ComposerSteeringState | null>(() =>
331
522
  initialPendingOperation?.delivery === "steer"
332
523
  ? {
@@ -339,6 +530,7 @@ export function useComposer(
339
530
  : null,
340
531
  );
341
532
  const [pausing, setPausing] = useState(false);
533
+ const [controlStopping, setControlStopping] = useState<ComposerControlStoppingState | null>(null);
342
534
  const [resuming, setResuming] = useState(false);
343
535
  const [error, setError] = useState<Error | null>(null);
344
536
  const [draft, setDraft] = useState<ComposerDraft | null>(null);
@@ -349,12 +541,16 @@ export function useComposer(
349
541
  () => initialShadow?.resources ?? [],
350
542
  );
351
543
  const pendingOperationRef = useRef<PendingComposerOperation | null>(initialPendingOperation);
544
+ const optimisticSendsRef = useRef<OptimisticSendOperation[]>(initialOptimisticSends);
545
+ const optimisticProcessorBusyRef = useRef(false);
546
+ const optimisticCallbackIdsRef = useRef(new Set<string>());
352
547
  const steeringSettlementEventsRef = useRef<SessionEvent[]>([]);
353
548
  const steeringRef = useRef(steering);
354
549
  const pendingClientEventId = useRef<string | null>(
355
550
  initialPendingOperation?.input.clientEventId ?? null,
356
551
  );
357
552
  const valueRef = useRef(initialShadow?.text ?? "");
553
+ const annotationsRef = useRef<DraftTimelineAnnotation[]>(initialShadow?.annotations ?? []);
358
554
  const draftRef = useRef<ComposerDraft | null>(null);
359
555
  const restoredResourcesRef = useRef<ResourceRef[]>(initialShadow?.resources ?? []);
360
556
  const localEditRevision = useRef(initialShadow ? 1 : 0);
@@ -363,6 +559,7 @@ export function useComposer(
363
559
  const lastSavedSignature = useRef<string | null>(null);
364
560
  const saveChain = useRef<Promise<void>>(Promise.resolve());
365
561
  const onSent = options.onSent;
562
+ const onSubmitted = options.onSubmitted;
366
563
  const onDraftApplied = options.onDraftApplied;
367
564
  // Read through a ref so live session/policy projections can replace their
368
565
  // apply callback without invalidating the draft loader and re-running its
@@ -392,11 +589,17 @@ export function useComposer(
392
589
  targetGeneration.current += 1;
393
590
  draftReadGeneration.current += 1;
394
591
  pendingOperationRef.current = restorePendingComposerOperation(pendingOperationKey);
592
+ const restoredOptimisticSends = restoreOptimisticSendOperations(pendingOperationKey);
593
+ optimisticSendsRef.current = restoredOptimisticSends;
594
+ optimisticProcessorBusyRef.current = false;
595
+ optimisticCallbackIdsRef.current = new Set();
395
596
  steeringSettlementEventsRef.current = [];
396
597
  pendingClientEventId.current = pendingOperationRef.current?.input.clientEventId ?? null;
397
- const shadow = pendingOperationRef.current?.newerShadow;
598
+ const shadow =
599
+ pendingOperationRef.current?.newerShadow ?? restoredOptimisticSends.at(-1)?.newerShadow;
398
600
  localEditRevision.current = shadow ? 1 : 0;
399
601
  valueRef.current = shadow?.text ?? "";
602
+ annotationsRef.current = shadow?.annotations ?? [];
400
603
  draftRef.current = null;
401
604
  restoredResourcesRef.current = shadow?.resources ?? [];
402
605
  lastSavedSignature.current = null;
@@ -405,7 +608,10 @@ export function useComposer(
405
608
  saveChain.current = Promise.resolve();
406
609
  setStateTargetKey(targetKey);
407
610
  setValue(shadow?.text ?? "");
611
+ setAnnotations(shadow?.annotations ?? []);
612
+ setAnnotationReviewTargetId(null);
408
613
  setSending(false);
614
+ setOptimisticSends(restoredOptimisticSends);
409
615
  setSteering(
410
616
  pendingOperationRef.current?.delivery === "steer"
411
617
  ? {
@@ -418,6 +624,7 @@ export function useComposer(
418
624
  : null,
419
625
  );
420
626
  setPausing(false);
627
+ setControlStopping(null);
421
628
  setResuming(false);
422
629
  setError(null);
423
630
  setDraft(null);
@@ -427,17 +634,37 @@ export function useComposer(
427
634
  setRestoredResources(shadow?.resources ?? []);
428
635
  }, [durableDrafts, pendingOperationKey, sessionId, targetKey]);
429
636
 
637
+ const setOptimisticDraftShadow = useCallback(
638
+ (shadow: ComposerDraftShadow): void => {
639
+ if (optimisticSendsRef.current.length === 0) return;
640
+ const next = optimisticSendsRef.current.map((operation) => ({
641
+ ...operation,
642
+ newerShadow: {
643
+ text: shadow.text,
644
+ resources: [...shadow.resources],
645
+ annotations: cloneAnnotations(shadow.annotations),
646
+ },
647
+ }));
648
+ optimisticSendsRef.current = next;
649
+ rememberOptimisticSendOperations(pendingOperationKey, next);
650
+ setOptimisticSends(next);
651
+ },
652
+ [pendingOperationKey],
653
+ );
654
+
430
655
  const applyDraft = useCallback(
431
656
  (next: ComposerDraft): void => {
432
657
  if (targetKeyRef.current !== targetKey) return;
433
658
  if (!durableDrafts) {
434
659
  localEditRevision.current += 1;
435
660
  valueRef.current = next.text;
661
+ annotationsRef.current = next.annotations ?? [];
436
662
  restoredResourcesRef.current = next.resources;
437
663
  draftRef.current = null;
438
664
  lastSavedSignature.current = null;
439
665
  setDraft(null);
440
666
  setValue(next.text);
667
+ setAnnotations(next.annotations ?? []);
441
668
  setRestoredResources(next.resources);
442
669
  pendingOperationRef.current = updatePendingComposerShadow(
443
670
  pendingOperationKey,
@@ -448,18 +675,29 @@ export function useComposer(
448
675
  next.resources,
449
676
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
450
677
  ),
678
+ annotations: next.annotations ?? [],
451
679
  },
452
680
  );
681
+ setOptimisticDraftShadow({
682
+ text: next.text,
683
+ resources: mergeResources(
684
+ next.resources,
685
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
686
+ ),
687
+ annotations: next.annotations ?? [],
688
+ });
453
689
  setDraftConflict(null);
454
690
  return;
455
691
  }
456
692
  valueRef.current = next.text;
693
+ annotationsRef.current = next.annotations ?? [];
457
694
  draftRef.current = next;
458
695
  restoredResourcesRef.current = next.resources;
459
696
  lastSavedSignature.current = draftSignature(draftPayload(next));
460
697
  localEditRevision.current += 1;
461
698
  setDraft(next);
462
699
  setValue(next.text);
700
+ setAnnotations(next.annotations ?? []);
463
701
  setRestoredResources(next.resources);
464
702
  pendingOperationRef.current = updatePendingComposerShadow(
465
703
  pendingOperationKey,
@@ -470,12 +708,21 @@ export function useComposer(
470
708
  next.resources,
471
709
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
472
710
  ),
711
+ annotations: next.annotations ?? [],
473
712
  },
474
713
  );
714
+ setOptimisticDraftShadow({
715
+ text: next.text,
716
+ resources: mergeResources(
717
+ next.resources,
718
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
719
+ ),
720
+ annotations: next.annotations ?? [],
721
+ });
475
722
  setDraftConflict(null);
476
723
  onDraftAppliedRef.current?.(next);
477
724
  },
478
- [durableDrafts, pendingOperationKey, targetKey],
725
+ [durableDrafts, pendingOperationKey, setOptimisticDraftShadow, targetKey],
479
726
  );
480
727
 
481
728
  const loadDraft = useCallback(
@@ -496,6 +743,7 @@ export function useComposer(
496
743
  baseAtStart,
497
744
  valueRef.current,
498
745
  restoredResourcesRef.current,
746
+ annotationsRef.current,
499
747
  extrasAtStart,
500
748
  ),
501
749
  )
@@ -525,15 +773,19 @@ export function useComposer(
525
773
  draftRef.current = fetched;
526
774
  setDraft(fetched);
527
775
  setDraftConflict(null);
528
- const shadow = pendingOperationRef.current?.newerShadow;
776
+ const shadow =
777
+ pendingOperationRef.current?.newerShadow ??
778
+ optimisticSendsRef.current.at(-1)?.newerShadow;
529
779
  if (shadow) {
530
780
  // The server can only know the original operation. Never replace
531
781
  // newer local edits while that operation is still uncertain.
532
782
  valueRef.current = shadow.text;
533
783
  restoredResourcesRef.current = shadow.resources;
784
+ annotationsRef.current = shadow.annotations;
534
785
  localEditRevision.current ||= 1;
535
786
  setValue(shadow.text);
536
787
  setRestoredResources(shadow.resources);
788
+ setAnnotations(shadow.annotations);
537
789
  } else if (
538
790
  replaceLocal ||
539
791
  (!localWasDirtyAtStart && localAtStart === localEditRevision.current)
@@ -549,9 +801,11 @@ export function useComposer(
549
801
  extrasNow.latencyMode !== extrasAtStart.latencyMode;
550
802
  valueRef.current = fetched.text;
551
803
  restoredResourcesRef.current = fetched.resources;
804
+ annotationsRef.current = fetched.annotations ?? [];
552
805
  lastSavedSignature.current = draftSignature(draftPayload(fetched));
553
806
  setValue(fetched.text);
554
807
  setRestoredResources(fetched.resources);
808
+ setAnnotations(fetched.annotations ?? []);
555
809
  if (replaceLocal || !pickerChangedDuringFetch) {
556
810
  onDraftAppliedRef.current?.(fetched);
557
811
  }
@@ -675,13 +929,32 @@ export function useComposer(
675
929
  setSteering(null);
676
930
  }, [options.events, steering]);
677
931
 
932
+ useEffect(() => {
933
+ if (!controlStopping) return;
934
+ const effectiveControl = options.effectiveControl;
935
+ if (!effectiveControl || effectiveControl.controlVersion < controlStopping.controlVersion) {
936
+ return;
937
+ }
938
+ if (
939
+ effectiveControl.controlVersion === controlStopping.controlVersion &&
940
+ effectiveControl.controlEtag === controlStopping.controlEtag &&
941
+ effectiveControl.settlement !== null
942
+ ) {
943
+ return;
944
+ }
945
+ // A newer control revision, an impossible same-version/different-etag
946
+ // projection, or quiescence at this exact revision supersedes the local
947
+ // mutation receipt. SessionChrome then falls back to durable queue truth.
948
+ setControlStopping((current) => (current === controlStopping ? null : current));
949
+ }, [controlStopping, options.effectiveControl]);
950
+
678
951
  const currentDraftPayload = useCallback((): SaveComposerDraftRequest | null => {
679
952
  if (!durableDrafts || targetKeyRef.current !== targetKey) return null;
680
953
  const base = draftRef.current;
681
954
  if (!base) return null;
682
955
  const extras = resolveSendExtras(sendExtrasRef.current);
683
- return composerDraftPayload(base, value, restoredResources, extras);
684
- }, [durableDrafts, restoredResources, targetKey, value]);
956
+ return composerDraftPayload(base, value, restoredResources, annotations, extras);
957
+ }, [annotations, durableDrafts, restoredResources, targetKey, value]);
685
958
 
686
959
  const persistPayload = useCallback(
687
960
  async (payload: SaveComposerDraftRequest): Promise<boolean> => {
@@ -758,6 +1031,177 @@ export function useComposer(
758
1031
  [client, durableDrafts, sessionId, targetKey, workspaceId],
759
1032
  );
760
1033
 
1034
+ const replaceOptimisticSends = useCallback(
1035
+ (
1036
+ update: (current: OptimisticSendOperation[]) => OptimisticSendOperation[],
1037
+ ): OptimisticSendOperation[] => {
1038
+ const next = update(optimisticSendsRef.current);
1039
+ optimisticSendsRef.current = next;
1040
+ rememberOptimisticSendOperations(pendingOperationKey, next);
1041
+ setOptimisticSends(next);
1042
+ return next;
1043
+ },
1044
+ [pendingOperationKey],
1045
+ );
1046
+
1047
+ const markOptimisticAccepted = useCallback(
1048
+ (operation: OptimisticSendOperation): void => {
1049
+ if (!optimisticCallbackIdsRef.current.has(operation.clientEventId)) {
1050
+ optimisticCallbackIdsRef.current.add(operation.clientEventId);
1051
+ onSent?.(operation.input.text, operation.input);
1052
+ }
1053
+ replaceOptimisticSends((current) =>
1054
+ current.filter((candidate) => candidate.clientEventId !== operation.clientEventId),
1055
+ );
1056
+ },
1057
+ [onSent, replaceOptimisticSends],
1058
+ );
1059
+
1060
+ useEffect(() => {
1061
+ const acceptedIds = new Set(
1062
+ (options.events ?? [])
1063
+ .filter((event) => event.type === "user.message" && event.clientEventId)
1064
+ .map((event) => event.clientEventId as string),
1065
+ );
1066
+ if (acceptedIds.size === 0) return;
1067
+ const accepted = optimisticSendsRef.current.filter((operation) =>
1068
+ acceptedIds.has(operation.clientEventId),
1069
+ );
1070
+ for (const operation of accepted) markOptimisticAccepted(operation);
1071
+ }, [markOptimisticAccepted, options.events]);
1072
+
1073
+ const processOptimisticSends = useCallback((): void => {
1074
+ if (!sessionId || optimisticProcessorBusyRef.current) return;
1075
+ const currentOperations = optimisticSendsRef.current;
1076
+ const operationIndex = currentOperations.findIndex(
1077
+ (candidate) => candidate.state === "sending",
1078
+ );
1079
+ const operation = operationIndex < 0 ? undefined : currentOperations[operationIndex];
1080
+ if (!operation) return;
1081
+ const ownedTargetKey = targetKey;
1082
+ const ownedGeneration = targetGeneration.current;
1083
+ optimisticProcessorBusyRef.current = true;
1084
+ void (async () => {
1085
+ try {
1086
+ if (operation.outcomeUnknown) {
1087
+ const events = await client.listEvents(workspaceId, sessionId, {
1088
+ includeTypes: ["user.message"],
1089
+ limit: 100,
1090
+ payloadMode: "none",
1091
+ });
1092
+ if (
1093
+ events.some(
1094
+ (event) =>
1095
+ event.type === "user.message" && event.clientEventId === operation.clientEventId,
1096
+ )
1097
+ ) {
1098
+ markOptimisticAccepted(operation);
1099
+ return;
1100
+ }
1101
+ if (!operation.canRetry) {
1102
+ throw new Error(
1103
+ "OpenGeni cannot safely retry this uncertain request after remount; reconcile the session before sending again.",
1104
+ );
1105
+ }
1106
+ }
1107
+ let expectedDraftRevision: number | undefined;
1108
+ if (durableDrafts && operation.draftPayload) {
1109
+ if (!(await persistPayload(operation.draftPayload))) {
1110
+ throw new Error("The message draft could not be saved before delivery.");
1111
+ }
1112
+ expectedDraftRevision = draftRef.current?.revision;
1113
+ }
1114
+ if (
1115
+ targetKeyRef.current !== ownedTargetKey ||
1116
+ targetGeneration.current !== ownedGeneration
1117
+ ) {
1118
+ return;
1119
+ }
1120
+ const wireInput = {
1121
+ ...operation.input,
1122
+ ...(expectedDraftRevision !== undefined ? { expectedDraftRevision } : {}),
1123
+ };
1124
+ await client.sendMessage(workspaceId, sessionId, wireInput);
1125
+ if (
1126
+ targetKeyRef.current !== ownedTargetKey ||
1127
+ targetGeneration.current !== ownedGeneration
1128
+ ) {
1129
+ return;
1130
+ }
1131
+ if (options.events === undefined) {
1132
+ markOptimisticAccepted({ ...operation, input: wireInput });
1133
+ } else {
1134
+ replaceOptimisticSends((current) =>
1135
+ current.map((candidate) =>
1136
+ candidate.clientEventId === operation.clientEventId
1137
+ ? { ...candidate, input: wireInput, state: "queued", error: undefined }
1138
+ : candidate,
1139
+ ),
1140
+ );
1141
+ }
1142
+ if (!optimisticCallbackIdsRef.current.has(operation.clientEventId)) {
1143
+ optimisticCallbackIdsRef.current.add(operation.clientEventId);
1144
+ onSent?.(wireInput.text, wireInput);
1145
+ }
1146
+ if (durableDrafts && draftRef.current) {
1147
+ const cleared = {
1148
+ ...draftRef.current,
1149
+ revision: 0,
1150
+ text: "",
1151
+ resources: [],
1152
+ annotations: [],
1153
+ sourceTurnId: null,
1154
+ sourceTurnVersion: null,
1155
+ updatedAt: null,
1156
+ };
1157
+ draftRef.current = cleared;
1158
+ setDraft(cleared);
1159
+ lastSavedSignature.current = draftSignature(draftPayload(cleared));
1160
+ }
1161
+ } catch (cause) {
1162
+ const problem = asError(cause);
1163
+ const outcomeUnknown = isOutcomeUnknownError(cause) || operation.outcomeUnknown === true;
1164
+ if (
1165
+ targetKeyRef.current === ownedTargetKey &&
1166
+ targetGeneration.current === ownedGeneration
1167
+ ) {
1168
+ replaceOptimisticSends((current) =>
1169
+ current.map((candidate) =>
1170
+ candidate.clientEventId === operation.clientEventId
1171
+ ? {
1172
+ ...candidate,
1173
+ state: "failed",
1174
+ error: problem.message,
1175
+ outcomeUnknown,
1176
+ }
1177
+ : candidate,
1178
+ ),
1179
+ );
1180
+ }
1181
+ } finally {
1182
+ optimisticProcessorBusyRef.current = false;
1183
+ if (targetKeyRef.current === ownedTargetKey) {
1184
+ setOptimisticSends([...optimisticSendsRef.current]);
1185
+ }
1186
+ }
1187
+ })();
1188
+ }, [
1189
+ client,
1190
+ durableDrafts,
1191
+ markOptimisticAccepted,
1192
+ onSent,
1193
+ options.events,
1194
+ persistPayload,
1195
+ replaceOptimisticSends,
1196
+ sessionId,
1197
+ targetKey,
1198
+ workspaceId,
1199
+ ]);
1200
+
1201
+ useEffect(() => {
1202
+ processOptimisticSends();
1203
+ }, [optimisticSends, processOptimisticSends]);
1204
+
761
1205
  // Private durable autosave. A newer local edit is never replaced by an older
762
1206
  // response; saves serialize and each reads the latest acknowledged revision.
763
1207
  useEffect(() => {
@@ -769,10 +1213,12 @@ export function useComposer(
769
1213
  restoredResourcesRef.current,
770
1214
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
771
1215
  ),
1216
+ annotations: annotationsRef.current,
772
1217
  };
773
1218
  if (
774
1219
  pending.newerShadow.text !== shadow.text ||
775
- JSON.stringify(pending.newerShadow.resources) !== JSON.stringify(shadow.resources)
1220
+ JSON.stringify(pending.newerShadow.resources) !== JSON.stringify(shadow.resources) ||
1221
+ JSON.stringify(pending.newerShadow.annotations) !== JSON.stringify(shadow.annotations)
776
1222
  ) {
777
1223
  pendingOperationRef.current = updatePendingComposerShadow(
778
1224
  pendingOperationKey,
@@ -782,6 +1228,27 @@ export function useComposer(
782
1228
  }
783
1229
  return;
784
1230
  }
1231
+ const optimistic = optimisticSendsRef.current.at(-1);
1232
+ if (optimistic) {
1233
+ const shadow = {
1234
+ text: valueRef.current,
1235
+ resources: mergeResources(
1236
+ restoredResourcesRef.current,
1237
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
1238
+ ),
1239
+ annotations: annotationsRef.current,
1240
+ };
1241
+ if (
1242
+ optimistic.newerShadow.text !== shadow.text ||
1243
+ JSON.stringify(optimistic.newerShadow.resources) !== JSON.stringify(shadow.resources) ||
1244
+ JSON.stringify(optimistic.newerShadow.annotations) !== JSON.stringify(shadow.annotations)
1245
+ ) {
1246
+ setOptimisticDraftShadow(shadow);
1247
+ }
1248
+ }
1249
+ if (optimisticSendsRef.current.some((operation) => operation.state === "sending")) {
1250
+ return;
1251
+ }
785
1252
  if (
786
1253
  !durableDrafts ||
787
1254
  !sessionId ||
@@ -803,6 +1270,7 @@ export function useComposer(
803
1270
  liveExtrasVersion,
804
1271
  pendingOperationKey,
805
1272
  persistPayload,
1273
+ setOptimisticDraftShadow,
806
1274
  sending,
807
1275
  sessionId,
808
1276
  ]);
@@ -818,14 +1286,19 @@ export function useComposer(
818
1286
  pendingClientEventId.current = pending.input.clientEventId ?? null;
819
1287
  }
820
1288
  const draftAtSend = value;
1289
+ const annotationsAtSend = annotations;
821
1290
  const rawText = explicit ?? draftAtSend;
822
1291
  const hasText = rawText.trim().length > 0;
1292
+ const hasAnnotations = annotationsAtSend.length > 0;
1293
+ const annotationsComplete = annotationsAtSend.every(
1294
+ (annotation) => annotation.note.trim().length > 0,
1295
+ );
823
1296
  // Resolve the extras once: a file-only message (empty text + ≥1 ready
824
1297
  // resource) is legitimate, so we must not bail on empty text alone.
825
1298
  const extras = pending ? {} : resolveSendExtras(sendExtrasRef.current);
826
1299
  const hasResources = restoredResources.length > 0 || (extras.resources?.length ?? 0) > 0;
827
1300
  if (
828
- (!pending && !hasText && !hasResources) ||
1301
+ (!pending && (!annotationsComplete || (!hasText && !hasResources && !hasAnnotations))) ||
829
1302
  !sessionId ||
830
1303
  sending ||
831
1304
  sendBlockedRef.current?.() === true ||
@@ -848,6 +1321,8 @@ export function useComposer(
848
1321
  const resourcesWereUnchanged =
849
1322
  JSON.stringify(restoredResourcesRef.current) ===
850
1323
  JSON.stringify(operation.resourcesAtSend);
1324
+ const annotationsWereUnchanged =
1325
+ JSON.stringify(annotationsRef.current) === JSON.stringify(operation.annotationsAtSend);
851
1326
  const previousDraft = draftRef.current;
852
1327
  if (previousDraft) {
853
1328
  const cleared = {
@@ -855,6 +1330,7 @@ export function useComposer(
855
1330
  revision: 0,
856
1331
  text: "",
857
1332
  resources: [],
1333
+ annotations: [],
858
1334
  sourceTurnId: null,
859
1335
  sourceTurnVersion: null,
860
1336
  updatedAt: null,
@@ -867,6 +1343,11 @@ export function useComposer(
867
1343
  restoredResourcesRef.current = [];
868
1344
  setRestoredResources([]);
869
1345
  }
1346
+ if (annotationsWereUnchanged) {
1347
+ annotationsRef.current = [];
1348
+ setAnnotations([]);
1349
+ setAnnotationReviewTargetId(null);
1350
+ }
870
1351
  if (operation.clearDraftOnAccept && draftWasUnchanged) {
871
1352
  valueRef.current = "";
872
1353
  setValue("");
@@ -955,6 +1436,8 @@ export function useComposer(
955
1436
  clientEventId: pending.input.clientEventId ?? null,
956
1437
  triggerEventId: result.accepted.id,
957
1438
  turnId: result.turn.id,
1439
+ stoppingPreviousAttempt:
1440
+ result.replay !== true && (result.interruptionCount ?? 0) > 0,
958
1441
  });
959
1442
  }
960
1443
  } catch (cause) {
@@ -981,7 +1464,7 @@ export function useComposer(
981
1464
  // and submitted byte-for-byte, while file-only sends use the same
982
1465
  // placeholder for both operations so the server content fence cannot
983
1466
  // reject its own client.
984
- const sendText = hasText ? rawText : FILE_ONLY_MESSAGE_TEXT;
1467
+ const sendText = hasText ? rawText : hasAnnotations ? "" : FILE_ONLY_MESSAGE_TEXT;
985
1468
  const currentPayload = currentDraftPayload();
986
1469
  const payload = currentPayload ? { ...currentPayload, text: sendText } : null;
987
1470
  if (payload && !(await persistPayload(payload))) return false;
@@ -1003,15 +1486,18 @@ export function useComposer(
1003
1486
  ? { expectedDraftRevision: draftRef.current.revision }
1004
1487
  : {}),
1005
1488
  resources: mergeResources(restoredResources, extras.resources ?? []),
1489
+ annotations: cloneAnnotations(annotationsAtSend),
1006
1490
  });
1007
1491
  const operation: PendingComposerOperation = {
1008
1492
  delivery,
1009
1493
  input,
1010
1494
  draftAtSend,
1011
1495
  resourcesAtSend: [...restoredResources],
1496
+ annotationsAtSend: cloneAnnotations(annotationsAtSend),
1012
1497
  newerShadow: {
1013
1498
  text: draftAtSend,
1014
1499
  resources: mergeResources(restoredResources, extras.resources ?? []),
1500
+ annotations: cloneAnnotations(annotationsAtSend),
1015
1501
  },
1016
1502
  clearDraftOnAccept: explicit === undefined,
1017
1503
  canRetry: true,
@@ -1037,6 +1523,8 @@ export function useComposer(
1037
1523
  clientEventId: input.clientEventId ?? null,
1038
1524
  triggerEventId: result.accepted.id,
1039
1525
  turnId: result.turn.id,
1526
+ stoppingPreviousAttempt:
1527
+ result.replay !== true && (result.interruptionCount ?? 0) > 0,
1040
1528
  });
1041
1529
  }
1042
1530
  } catch (cause) {
@@ -1079,6 +1567,7 @@ export function useComposer(
1079
1567
  options.effectiveControl?.controlEtag,
1080
1568
  persistPayload,
1081
1569
  restoredResources,
1570
+ annotations,
1082
1571
  sending,
1083
1572
  sessionId,
1084
1573
  targetKey,
@@ -1087,9 +1576,140 @@ export function useComposer(
1087
1576
  ],
1088
1577
  );
1089
1578
 
1090
- const send = useCallback(async (text?: string) => await dispatch("send", text), [dispatch]);
1579
+ const send = useCallback(
1580
+ async (explicit?: string): Promise<boolean> => {
1581
+ if (pendingOperationRef.current?.delivery === "send") {
1582
+ return await dispatch("send", explicit);
1583
+ }
1584
+ const rawText = explicit ?? valueRef.current;
1585
+ const annotationsAtSend = cloneAnnotations(annotationsRef.current);
1586
+ const hasText = rawText.trim().length > 0;
1587
+ const hasAnnotations = annotationsAtSend.length > 0;
1588
+ const annotationsComplete = annotationsAtSend.every(
1589
+ (annotation) => annotation.note.trim().length > 0,
1590
+ );
1591
+ const extras = resolveSendExtras(sendExtrasRef.current);
1592
+ const resources = mergeResources(restoredResourcesRef.current, extras.resources ?? []);
1593
+ if (
1594
+ !sessionId ||
1595
+ sending ||
1596
+ !annotationsComplete ||
1597
+ (!hasText && !hasAnnotations && resources.length === 0) ||
1598
+ sendBlockedRef.current?.() === true ||
1599
+ targetKeyRef.current !== targetKey
1600
+ ) {
1601
+ return false;
1602
+ }
1603
+ const sendText = hasText ? rawText : hasAnnotations ? "" : FILE_ONLY_MESSAGE_TEXT;
1604
+ const clientEventId = generateClientEventId();
1605
+ const input = composeSendInput(sendText, clientEventId, extras, {
1606
+ ...(options.effectiveControl?.controlEtag
1607
+ ? { controlEtag: options.effectiveControl.controlEtag }
1608
+ : {}),
1609
+ resources,
1610
+ annotations: annotationsAtSend,
1611
+ });
1612
+ const currentPayload = currentDraftPayload();
1613
+ const operation: OptimisticSendOperation = {
1614
+ clientEventId,
1615
+ text: sendText,
1616
+ annotations: annotationsAtSend,
1617
+ resources,
1618
+ occurredAt: new Date().toISOString(),
1619
+ state: "sending",
1620
+ input,
1621
+ draftPayload: currentPayload ? { ...currentPayload, text: sendText } : null,
1622
+ newerShadow: {
1623
+ text: explicit === undefined ? "" : valueRef.current,
1624
+ resources: explicit === undefined ? [] : [...restoredResourcesRef.current],
1625
+ annotations: explicit === undefined ? [] : cloneAnnotations(annotationsRef.current),
1626
+ },
1627
+ canRetry: true,
1628
+ };
1629
+ replaceOptimisticSends((current) => [...current, operation]);
1630
+ queueMicrotask(processOptimisticSends);
1631
+ setError(null);
1632
+ onSubmitted?.(sendText, input);
1633
+ if (explicit === undefined) {
1634
+ valueRef.current = "";
1635
+ annotationsRef.current = [];
1636
+ restoredResourcesRef.current = [];
1637
+ localEditRevision.current += 1;
1638
+ setValue("");
1639
+ setAnnotations([]);
1640
+ setAnnotationReviewTargetId(null);
1641
+ setRestoredResources([]);
1642
+ }
1643
+ return true;
1644
+ },
1645
+ [
1646
+ currentDraftPayload,
1647
+ dispatch,
1648
+ onSubmitted,
1649
+ options.effectiveControl?.controlEtag,
1650
+ processOptimisticSends,
1651
+ replaceOptimisticSends,
1652
+ sending,
1653
+ sessionId,
1654
+ targetKey,
1655
+ ],
1656
+ );
1091
1657
  const steer = useCallback(async (text?: string) => await dispatch("steer", text), [dispatch]);
1092
1658
 
1659
+ const retryOptimisticMessage = useCallback(
1660
+ (clientEventId: string): void => {
1661
+ replaceOptimisticSends((current) =>
1662
+ current.map((operation) => {
1663
+ if (operation.clientEventId !== clientEventId || operation.state !== "failed") {
1664
+ return operation;
1665
+ }
1666
+ if (operation.outcomeUnknown) {
1667
+ return { ...operation, state: "sending", error: undefined };
1668
+ }
1669
+ const nextClientEventId = generateClientEventId();
1670
+ const retryExtras = resolveSendExtras(sendExtrasRef.current);
1671
+ const retryInput = composeSendInput(operation.text, nextClientEventId, retryExtras, {
1672
+ ...(options.effectiveControl?.controlEtag
1673
+ ? { controlEtag: options.effectiveControl.controlEtag }
1674
+ : {}),
1675
+ resources: operation.resources,
1676
+ annotations: operation.annotations,
1677
+ });
1678
+ return {
1679
+ ...operation,
1680
+ clientEventId: nextClientEventId,
1681
+ input: retryInput,
1682
+ draftPayload: operation.draftPayload
1683
+ ? {
1684
+ ...operation.draftPayload,
1685
+ model: retryExtras.model ?? operation.draftPayload.model,
1686
+ reasoningEffort:
1687
+ retryExtras.reasoningEffort ?? operation.draftPayload.reasoningEffort,
1688
+ latencyMode: retryExtras.latencyMode ?? operation.draftPayload.latencyMode,
1689
+ }
1690
+ : null,
1691
+ occurredAt: new Date().toISOString(),
1692
+ state: "sending",
1693
+ error: undefined,
1694
+ outcomeUnknown: false,
1695
+ canRetry: true,
1696
+ };
1697
+ }),
1698
+ );
1699
+ queueMicrotask(processOptimisticSends);
1700
+ },
1701
+ [options.effectiveControl?.controlEtag, processOptimisticSends, replaceOptimisticSends],
1702
+ );
1703
+
1704
+ const removeOptimisticMessage = useCallback(
1705
+ (clientEventId: string): void => {
1706
+ replaceOptimisticSends((current) =>
1707
+ current.filter((operation) => operation.clientEventId !== clientEventId),
1708
+ );
1709
+ },
1710
+ [replaceOptimisticSends],
1711
+ );
1712
+
1093
1713
  // A send is possible with non-empty text OR with ≥1 attached resource (a
1094
1714
  // file-only message). Resources ride in `sendExtras`, so we resolve them here
1095
1715
  // — keeping useComposer attachment-agnostic while still lighting up the send
@@ -1099,6 +1719,7 @@ export function useComposer(
1099
1719
  restoredResources.length > 0 ||
1100
1720
  (resolveSendExtras(sendExtrasRef.current).resources?.length ?? 0) > 0;
1101
1721
  const hasPendingOperation = pendingOperationRef.current !== null;
1722
+ const annotationsComplete = annotations.every((annotation) => annotation.note.trim().length > 0);
1102
1723
 
1103
1724
  const pause = useCallback(
1104
1725
  async (reason?: string): Promise<void> => {
@@ -1110,12 +1731,27 @@ export function useComposer(
1110
1731
  setPausing(true);
1111
1732
  setError(null);
1112
1733
  try {
1113
- await client.pauseSession(workspaceId, sessionId, {
1734
+ const result = await client.pauseSession(workspaceId, sessionId, {
1114
1735
  ...(reason !== undefined ? { reason } : {}),
1115
1736
  ...(options.effectiveControl?.controlEtag
1116
1737
  ? { expectedControlEtag: options.effectiveControl.controlEtag }
1117
1738
  : {}),
1118
1739
  });
1740
+ if (
1741
+ targetKeyRef.current === ownedTargetKey &&
1742
+ targetGeneration.current === ownedGeneration
1743
+ ) {
1744
+ setControlStopping(
1745
+ result.interruptionCount > 0 &&
1746
+ result.effectiveControl.state === "paused" &&
1747
+ result.effectiveControl.settlement !== null
1748
+ ? {
1749
+ controlVersion: result.effectiveControl.controlVersion,
1750
+ controlEtag: result.effectiveControl.controlEtag,
1751
+ }
1752
+ : null,
1753
+ );
1754
+ }
1119
1755
  } catch (cause) {
1120
1756
  if (
1121
1757
  targetKeyRef.current === ownedTargetKey &&
@@ -1241,13 +1877,87 @@ export function useComposer(
1241
1877
  restoredResourcesRef.current,
1242
1878
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
1243
1879
  ),
1880
+ annotations: annotationsRef.current,
1244
1881
  },
1245
1882
  );
1883
+ setOptimisticDraftShadow({
1884
+ text: next,
1885
+ resources: mergeResources(
1886
+ restoredResourcesRef.current,
1887
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
1888
+ ),
1889
+ annotations: annotationsRef.current,
1890
+ });
1246
1891
  setValue(next);
1247
1892
  },
1248
- [pendingOperationKey, targetKey],
1893
+ [pendingOperationKey, setOptimisticDraftShadow, targetKey],
1249
1894
  );
1250
1895
 
1896
+ const updateAnnotations = useCallback(
1897
+ (next: DraftTimelineAnnotation[], reviewTargetId: string | null = null) => {
1898
+ if (targetKeyRef.current !== targetKey) return;
1899
+ localEditRevision.current += 1;
1900
+ annotationsRef.current = next;
1901
+ pendingOperationRef.current = updatePendingComposerShadow(
1902
+ pendingOperationKey,
1903
+ pendingOperationRef.current,
1904
+ {
1905
+ text: valueRef.current,
1906
+ resources: mergeResources(
1907
+ restoredResourcesRef.current,
1908
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
1909
+ ),
1910
+ annotations: next,
1911
+ },
1912
+ );
1913
+ setOptimisticDraftShadow({
1914
+ text: valueRef.current,
1915
+ resources: mergeResources(
1916
+ restoredResourcesRef.current,
1917
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
1918
+ ),
1919
+ annotations: next,
1920
+ });
1921
+ setAnnotations(next);
1922
+ setAnnotationReviewTargetId(reviewTargetId);
1923
+ },
1924
+ [pendingOperationKey, setOptimisticDraftShadow, targetKey],
1925
+ );
1926
+
1927
+ const addAnnotation = useCallback(
1928
+ (annotation: DraftTimelineAnnotation) => {
1929
+ if (annotationsRef.current.length >= 12) {
1930
+ setError(new Error("A message can include at most 12 timeline annotations."));
1931
+ return;
1932
+ }
1933
+ const next = [...annotationsRef.current, annotation];
1934
+ updateAnnotations(next, annotation.id);
1935
+ },
1936
+ [updateAnnotations],
1937
+ );
1938
+
1939
+ const updateAnnotation = useCallback(
1940
+ (id: string, note: string) => {
1941
+ updateAnnotations(
1942
+ annotationsRef.current.map((annotation) =>
1943
+ annotation.id === id ? { ...annotation, note } : annotation,
1944
+ ),
1945
+ );
1946
+ },
1947
+ [updateAnnotations],
1948
+ );
1949
+
1950
+ const removeAnnotation = useCallback(
1951
+ (id: string) => {
1952
+ updateAnnotations(annotationsRef.current.filter((annotation) => annotation.id !== id));
1953
+ },
1954
+ [updateAnnotations],
1955
+ );
1956
+
1957
+ const clearAnnotationReviewTarget = useCallback(() => {
1958
+ setAnnotationReviewTargetId(null);
1959
+ }, []);
1960
+
1251
1961
  const removeRestoredResource = useCallback(
1252
1962
  (index: number) => {
1253
1963
  if (targetKeyRef.current !== targetKey) return;
@@ -1260,11 +1970,17 @@ export function useComposer(
1260
1970
  {
1261
1971
  text: valueRef.current,
1262
1972
  resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? []),
1973
+ annotations: annotationsRef.current,
1263
1974
  },
1264
1975
  );
1976
+ setOptimisticDraftShadow({
1977
+ text: valueRef.current,
1978
+ resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? []),
1979
+ annotations: annotationsRef.current,
1980
+ });
1265
1981
  setRestoredResources(next);
1266
1982
  },
1267
- [pendingOperationKey, targetKey],
1983
+ [pendingOperationKey, setOptimisticDraftShadow, targetKey],
1268
1984
  );
1269
1985
 
1270
1986
  const hasDraftContent = useCallback((): boolean => {
@@ -1272,6 +1988,7 @@ export function useComposer(
1272
1988
  const extras = resolveSendExtras(sendExtrasRef.current);
1273
1989
  return (
1274
1990
  valueRef.current.length > 0 ||
1991
+ annotationsRef.current.length > 0 ||
1275
1992
  restoredResourcesRef.current.length > 0 ||
1276
1993
  (extras.resources?.length ?? 0) > 0 ||
1277
1994
  (current?.sourceTurnId !== null && current?.sourceTurnId !== undefined)
@@ -1312,6 +2029,7 @@ export function useComposer(
1312
2029
  );
1313
2030
 
1314
2031
  const identityMatches = stateTargetKey === targetKey;
2032
+ const visibleSteering = identityMatches ? steering : null;
1315
2033
  const reloadDraft = useCallback(async () => await loadDraft(true), [loadDraft]);
1316
2034
  const clearError = useCallback(() => {
1317
2035
  if (targetKeyRef.current !== targetKey) return;
@@ -1322,17 +2040,41 @@ export function useComposer(
1322
2040
  return {
1323
2041
  value: identityMatches ? value : "",
1324
2042
  setValue: updateValue,
2043
+ annotations: identityMatches ? annotations : [],
2044
+ addAnnotation,
2045
+ updateAnnotation,
2046
+ removeAnnotation,
2047
+ annotationReviewTargetId: identityMatches ? annotationReviewTargetId : null,
2048
+ clearAnnotationReviewTarget,
1325
2049
  hasDraftContent,
1326
2050
  send,
2051
+ optimisticMessages: identityMatches
2052
+ ? optimisticSends.map(
2053
+ ({ input: _input, draftPayload: _payload, canRetry: _retry, ...item }) => item,
2054
+ )
2055
+ : [],
2056
+ retryOptimisticMessage,
2057
+ removeOptimisticMessage,
1327
2058
  steer,
1328
- steering: identityMatches ? steering : null,
2059
+ steering: visibleSteering,
2060
+ stoppingAttempt: identityMatches
2061
+ ? controlStopping
2062
+ ? "current"
2063
+ : visibleSteering?.phase === "accepted" && visibleSteering.stoppingPreviousAttempt === true
2064
+ ? "previous"
2065
+ : null
2066
+ : null,
1329
2067
  sending: identityMatches ? sending : false,
1330
2068
  canSend:
1331
2069
  identityMatches &&
1332
2070
  Boolean(sessionId) &&
1333
2071
  !sending &&
1334
2072
  sendBlockedRef.current?.() !== true &&
1335
- (hasPendingOperation || value.trim().length > 0 || hasReadyResources),
2073
+ annotationsComplete &&
2074
+ (hasPendingOperation ||
2075
+ value.trim().length > 0 ||
2076
+ hasReadyResources ||
2077
+ annotations.length > 0),
1336
2078
  pause,
1337
2079
  pausing: identityMatches ? pausing : false,
1338
2080
  resume,
@@ -1360,12 +2102,12 @@ export function isComposerDraftEvent(event: Pick<SessionEvent, "type">): boolean
1360
2102
  }
1361
2103
 
1362
2104
  /**
1363
- * Default text for a file-only message (attachment(s) present, no typed draft).
1364
- * Kept non-empty so the wire contract (`text: z.string().min(1)`) and the
1365
- * worker's non-whitespace guard accept it; the attached files still ride in
1366
- * `resources`. Exported for tests.
2105
+ * Default text for a resource-only message (attachments present, no typed
2106
+ * draft). Kept non-empty so the wire contract (`text: z.string().min(1)`) and
2107
+ * the worker's non-whitespace guard accept it. The export name is retained for
2108
+ * compatibility now that repositories can ride beside files in `resources`.
1367
2109
  */
1368
- export const FILE_ONLY_MESSAGE_TEXT = "(see attached files)";
2110
+ export const FILE_ONLY_MESSAGE_TEXT = "(see attached context)";
1369
2111
 
1370
2112
  /** Resolve possibly-deferred extras to a concrete bag (function evaluated now). */
1371
2113
  export function resolveSendExtras(
@@ -1477,6 +2219,7 @@ function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
1477
2219
  expectedRevision: draft.revision,
1478
2220
  text: draft.text,
1479
2221
  resources: draft.resources,
2222
+ annotations: draft.annotations ?? [],
1480
2223
  model: draft.model,
1481
2224
  reasoningEffort: draft.reasoningEffort,
1482
2225
  latencyMode: draft.latencyMode ?? "standard",
@@ -1487,12 +2230,14 @@ function composerDraftPayload(
1487
2230
  base: ComposerDraft,
1488
2231
  text: string,
1489
2232
  restoredResources: ResourceRef[],
2233
+ annotations: DraftTimelineAnnotation[],
1490
2234
  extras: ComposerSendExtras,
1491
2235
  ): SaveComposerDraftRequest {
1492
2236
  return {
1493
2237
  expectedRevision: base.revision,
1494
2238
  text,
1495
2239
  resources: mergeResources(restoredResources, extras.resources ?? []),
2240
+ annotations,
1496
2241
  model: extras.model ?? base.model,
1497
2242
  reasoningEffort: extras.reasoningEffort ?? base.reasoningEffort,
1498
2243
  latencyMode: extras.latencyMode ?? base.latencyMode ?? "standard",