@theoremjs/react 0.3.0 → 0.3.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 (258) hide show
  1. package/README.md +728 -202
  2. package/client/composer-hints.js +1 -1
  3. package/client/decision-images.d.ts +76 -0
  4. package/client/decision-images.js +139 -0
  5. package/client/decision-transport.d.ts +26 -4
  6. package/client/decision-transport.js +79 -6
  7. package/client/embedding-input.d.ts +84 -0
  8. package/client/embedding-input.js +194 -0
  9. package/client/embedding-space.d.ts +82 -0
  10. package/client/embedding-space.js +389 -0
  11. package/client/embedding-transport.d.ts +40 -0
  12. package/client/embedding-transport.js +71 -0
  13. package/client/failure.d.ts +7 -10
  14. package/client/failure.js +9 -9
  15. package/client/host-call.d.ts +1 -1
  16. package/client/host-call.js +7 -2
  17. package/client/host-transport.d.ts +2 -0
  18. package/client/host-transport.js +3 -2
  19. package/client/index.d.ts +19 -11
  20. package/client/index.js +14 -10
  21. package/client/ink-waveform.d.ts +1 -1
  22. package/client/ink-waveform.js +1 -2
  23. package/client/live/apply-live-turn-tool-event.js +1 -1
  24. package/client/live/live-captions.d.ts +30 -0
  25. package/client/live/live-captions.js +105 -1
  26. package/client/live/live-mic-forward.d.ts +1 -0
  27. package/client/live/live-mic-forward.js +11 -2
  28. package/client/live/live-page-tool.d.ts +34 -0
  29. package/client/live/live-page-tool.js +11 -0
  30. package/client/live/live-state.d.ts +4 -2
  31. package/client/live/live-state.js +2 -0
  32. package/client/live/live-video.js +1 -1
  33. package/client/live/run-live-tool-call.d.ts +7 -2
  34. package/client/live/run-live-tool-call.js +48 -10
  35. package/client/live/use-live-caption-log.d.ts +17 -0
  36. package/client/live/use-live-caption-log.js +51 -0
  37. package/client/live-client.d.ts +52 -4
  38. package/client/live-client.js +250 -59
  39. package/client/live-messages.d.ts +51 -13
  40. package/client/live-messages.js +42 -9
  41. package/client/page-dispatch.d.ts +15 -0
  42. package/client/page-dispatch.js +63 -0
  43. package/client/run-commit.d.ts +16 -43
  44. package/client/run-commit.js +26 -101
  45. package/client/run-session.d.ts +40 -41
  46. package/client/run-session.js +104 -112
  47. package/client/schema-fields.js +27 -6
  48. package/client/shaped-data.js +37 -8
  49. package/client/source-citations.js +1 -1
  50. package/client/space-geometry.d.ts +39 -0
  51. package/client/space-geometry.js +183 -0
  52. package/client/state-fields.js +1 -1
  53. package/client/tool-result.js +30 -12
  54. package/client/tool-resume.d.ts +18 -1
  55. package/client/tool-resume.js +4 -1
  56. package/client/trace-feed.d.ts +1 -1
  57. package/client/trace-story.d.ts +2 -2
  58. package/client/trace-story.js +43 -19
  59. package/client/trace-view.d.ts +2 -2
  60. package/client/trace-view.js +56 -16
  61. package/client/transcript-groups.d.ts +28 -2
  62. package/client/transcript-groups.js +63 -13
  63. package/client/transcript-schema.d.ts +10 -0
  64. package/client/transcript-schema.js +120 -0
  65. package/client/transport.d.ts +114 -26
  66. package/client/transport.js +106 -32
  67. package/client/turn-client.d.ts +17 -8
  68. package/client/turn-client.js +26 -31
  69. package/client/voice-recorder.d.ts +1 -1
  70. package/client/voice-recorder.js +2 -5
  71. package/client/wire-line.js +5 -5
  72. package/client/worklets/mic-capture.js +9 -9
  73. package/components/InkWaveform.d.ts +12 -1
  74. package/components/InkWaveform.js +65 -42
  75. package/components/live/use-live-runner-controls.d.ts +3 -4
  76. package/components/live/use-live-runner-controls.js +6 -10
  77. package/components/live/use-live-runner-model.d.ts +10 -6
  78. package/components/live/use-live-runner-model.js +29 -14
  79. package/components/live/use-live-runner-ui.d.ts +10 -8
  80. package/components/live/use-live-runner-ui.js +19 -11
  81. package/components/live/use-live-session-client.d.ts +4 -4
  82. package/components/live/use-live-session-client.js +11 -8
  83. package/components/use-composer-voice.d.ts +4 -4
  84. package/components/use-composer-voice.js +8 -8
  85. package/components/use-editor-selection.js +4 -1
  86. package/components/use-voice-playback.d.ts +2 -1
  87. package/components/use-voice-playback.js +4 -4
  88. package/hooks/use-described.d.ts +5 -1
  89. package/hooks/use-described.js +11 -9
  90. package/hooks/use-embedding-index.d.ts +27 -0
  91. package/hooks/use-embedding-index.js +43 -0
  92. package/hooks/use-one-call.d.ts +31 -0
  93. package/hooks/use-one-call.js +76 -0
  94. package/hooks/use-page-tool-warnings.d.ts +6 -0
  95. package/hooks/use-page-tool-warnings.js +25 -0
  96. package/hooks/use-second-ticker.d.ts +2 -0
  97. package/hooks/use-second-ticker.js +13 -0
  98. package/hooks/use-theorem-chat-actions.d.ts +25 -21
  99. package/hooks/use-theorem-chat-actions.js +67 -45
  100. package/hooks/use-theorem-chat-state.d.ts +6 -6
  101. package/hooks/use-theorem-chat-state.js +13 -9
  102. package/hooks/use-theorem-chat.d.ts +48 -14
  103. package/hooks/use-theorem-chat.js +329 -141
  104. package/hooks/use-theorem-decision.d.ts +5 -10
  105. package/hooks/use-theorem-decision.js +4 -67
  106. package/hooks/use-theorem-embedding.d.ts +18 -0
  107. package/hooks/use-theorem-embedding.js +12 -0
  108. package/hooks/use-theorem-host.d.ts +5 -4
  109. package/hooks/use-theorem-host.js +26 -8
  110. package/hooks/use-theorem-interface.d.ts +6 -3
  111. package/hooks/use-theorem-interface.js +12 -22
  112. package/index.d.ts +6 -3
  113. package/index.js +5 -2
  114. package/live.d.ts +1 -0
  115. package/live.js +1 -1
  116. package/package.json +6 -5
  117. package/server/checkpoint-ownership.d.ts +6 -0
  118. package/server/checkpoint-ownership.js +42 -0
  119. package/server/conversation-store.d.ts +92 -0
  120. package/server/conversation-store.js +131 -0
  121. package/server/conversation.d.ts +193 -0
  122. package/server/conversation.js +552 -0
  123. package/server/decision-handler.d.ts +12 -4
  124. package/server/decision-handler.js +29 -38
  125. package/server/embedding-handler.d.ts +33 -0
  126. package/server/embedding-handler.js +45 -0
  127. package/server/handler.d.ts +58 -20
  128. package/server/handler.js +441 -238
  129. package/server/mod.d.ts +15 -6
  130. package/server/mod.js +13 -5
  131. package/server/page-dispatch.d.ts +22 -0
  132. package/server/page-dispatch.js +127 -0
  133. package/server/request-body.d.ts +2 -0
  134. package/server/request-body.js +42 -0
  135. package/server/request-check.d.ts +6 -1
  136. package/server/request-check.js +27 -12
  137. package/server/session-authority.d.ts +8 -0
  138. package/server/session-authority.js +78 -0
  139. package/server/session-boundary.d.ts +26 -0
  140. package/server/session-boundary.js +64 -0
  141. package/server/session-identity.d.ts +16 -0
  142. package/server/session-identity.js +41 -0
  143. package/server/session-limits.d.ts +12 -0
  144. package/server/session-limits.js +59 -0
  145. package/server/session-store.d.ts +69 -20
  146. package/server/session-store.js +220 -5
  147. package/server/single-action.d.ts +21 -0
  148. package/server/single-action.js +30 -0
  149. package/server/steer-inbox.js +1 -1
  150. package/server/tool-operations.d.ts +6 -0
  151. package/server/tool-operations.js +132 -0
  152. package/server/turn-input.d.ts +10 -0
  153. package/server/turn-input.js +29 -0
  154. package/styles/ink-controls.css +78 -78
  155. package/ui/CardWall.d.ts +10 -0
  156. package/ui/CardWall.js +61 -0
  157. package/ui/ChatComposerBar.d.ts +28 -5
  158. package/ui/ChatComposerBar.js +85 -30
  159. package/ui/ChatTranscript.d.ts +14 -1
  160. package/ui/ChatTranscript.js +110 -57
  161. package/ui/ConsoleFrame.d.ts +61 -4
  162. package/ui/ConsoleFrame.js +109 -9
  163. package/ui/DecisionAnswers.d.ts +40 -2
  164. package/ui/DecisionAnswers.js +166 -35
  165. package/ui/DecisionImages.d.ts +16 -0
  166. package/ui/DecisionImages.js +46 -0
  167. package/ui/EmbeddingDocuments.d.ts +78 -0
  168. package/ui/EmbeddingDocuments.js +278 -0
  169. package/ui/EmbeddingQuery.d.ts +27 -0
  170. package/ui/EmbeddingQuery.js +95 -0
  171. package/ui/EmbeddingResults.d.ts +41 -0
  172. package/ui/EmbeddingResults.js +165 -0
  173. package/ui/EmbeddingSpace.d.ts +26 -0
  174. package/ui/EmbeddingSpace.js +106 -0
  175. package/ui/LiveCaptionsPanel.d.ts +22 -0
  176. package/ui/LiveCaptionsPanel.js +27 -0
  177. package/ui/LiveRunner.d.ts +6 -4
  178. package/ui/LiveRunner.js +42 -55
  179. package/ui/PaneState.d.ts +33 -0
  180. package/ui/PaneState.js +50 -0
  181. package/ui/SchemaFields.d.ts +1 -1
  182. package/ui/SchemaFields.js +54 -26
  183. package/ui/ShapedData.js +73 -28
  184. package/ui/SidePanel.d.ts +74 -5
  185. package/ui/SidePanel.js +82 -9
  186. package/ui/StateFields.d.ts +10 -2
  187. package/ui/StateFields.js +56 -13
  188. package/ui/TheoremChat.d.ts +65 -14
  189. package/ui/TheoremChat.js +66 -78
  190. package/ui/TheoremDecision.d.ts +12 -20
  191. package/ui/TheoremDecision.js +78 -35
  192. package/ui/TheoremEmbedding.d.ts +27 -0
  193. package/ui/TheoremEmbedding.js +122 -0
  194. package/ui/TheoremHost.d.ts +22 -5
  195. package/ui/TheoremHost.js +127 -37
  196. package/ui/ToolGateCard.d.ts +5 -1
  197. package/ui/ToolGateCard.js +30 -23
  198. package/ui/ToolResult.js +12 -10
  199. package/ui/ToolResultCharts.js +29 -12
  200. package/ui/TraceCharts.d.ts +2 -2
  201. package/ui/TraceCharts.js +48 -17
  202. package/ui/TraceGuardrails.d.ts +3 -2
  203. package/ui/TraceGuardrails.js +24 -6
  204. package/ui/TraceInspector.d.ts +11 -1
  205. package/ui/TraceInspector.js +45 -28
  206. package/ui/TraceInspectorPanel.d.ts +31 -9
  207. package/ui/TraceInspectorPanel.js +77 -39
  208. package/ui/TraceOverview.d.ts +7 -3
  209. package/ui/TraceOverview.js +44 -14
  210. package/ui/TraceSpanDetail.js +24 -22
  211. package/ui/TraceStory.js +9 -8
  212. package/ui/TraceValues.js +19 -9
  213. package/ui/TraceWaterfall.js +12 -9
  214. package/ui/VoiceNote.js +4 -4
  215. package/ui/approval-heading.d.ts +4 -0
  216. package/ui/approval-heading.js +9 -0
  217. package/ui/arrive.d.ts +18 -0
  218. package/ui/arrive.js +46 -0
  219. package/ui/built/theme.css +86 -68
  220. package/ui/built/theme.css.d.ts +9 -0
  221. package/ui/built/theorem.d.ts +3 -3
  222. package/ui/built/theorem.js +34 -5
  223. package/ui/built/theorem.variants.d.ts +8 -2
  224. package/ui/code-language.d.ts +12 -0
  225. package/ui/code-language.js +50 -0
  226. package/ui/disclosure-motion.js +53 -9
  227. package/ui/embedding-space-gl.d.ts +37 -0
  228. package/ui/embedding-space-gl.js +663 -0
  229. package/ui/embedding-space-view.d.ts +8 -0
  230. package/ui/embedding-space-view.js +8 -0
  231. package/ui/glide.d.ts +7 -0
  232. package/ui/glide.js +44 -0
  233. package/ui/icons.js +2 -1
  234. package/ui/index.d.ts +20 -9
  235. package/ui/index.js +20 -10
  236. package/ui/labels-provider.js +5 -2
  237. package/ui/labels.d.ts +374 -85
  238. package/ui/labels.js +1028 -188
  239. package/ui/markdown-plugins.d.ts +9 -0
  240. package/ui/markdown-plugins.js +13 -0
  241. package/ui/mcp-icon.d.ts +6 -0
  242. package/ui/mcp-icon.js +8 -0
  243. package/ui/row-keys.d.ts +19 -0
  244. package/ui/row-keys.js +33 -0
  245. package/ui/theme.d.ts +1 -1
  246. package/ui/theme.js +2 -2
  247. package/ui/theorem-theme.js +32 -10
  248. package/ui/trace-palette.d.ts +2 -16
  249. package/ui/trace-palette.js +2 -16
  250. package/ui/trace-patterns.d.ts +11 -3
  251. package/ui/trace-patterns.js +16 -3
  252. package/ui/transcript-copy-text.js +1 -0
  253. package/ui/wall-columns.d.ts +8 -0
  254. package/ui/wall-columns.js +19 -0
  255. package/client/walk-away.d.ts +0 -34
  256. package/client/walk-away.js +0 -48
  257. package/server/walk-away.d.ts +0 -22
  258. package/server/walk-away.js +0 -41
@@ -1,10 +1,62 @@
1
- import { useCallback, useEffect, useMemo, useRef } from 'react';
2
- import { branchInterfaceTurnSession, consumeNextComposerQueue, convertSteersToFrontQueued, defaultInterfaceEffort, orderComposerPendingMessages, promoteComposerPendingKind, removeLandedSteers, } from '@theoremjs/agents/interface';
3
- import { clientFailure } from "../client/failure.js";
1
+ import { branchInterfaceTurnSession, consumeNextComposerQueue, convertSteersToFrontQueued, defaultInterfaceEffort, orderComposerPendingMessages, promoteComposerPendingKind, removeComposerPendingMessage, removeLandedSteers, } from '@theoremjs/agents/interface';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { clientFailure, turnFailure } from "../client/failure.js";
4
4
  import { followGenerationDefaults } from "../client/generation-selection.js";
5
- import { applyTurnResultToTranscript } from "../client/index.js";
5
+ import { withPageToolDispatch } from "../client/page-dispatch.js";
6
+ import { sessionAfter, stampWorked } from "../client/run-commit.js";
7
+ import { keepsConversation } from "../client/run-session.js";
8
+ import { foldAssistantTurn } from "../client/turn-client.js";
9
+ import { usePageToolWarnings } from "./use-page-tool-warnings.js";
6
10
  import { useTheoremChatActions } from "./use-theorem-chat-actions.js";
7
11
  import { useTheoremChatState, } from "./use-theorem-chat-state.js";
12
+ /**
13
+ * The slots the profile declares and the value of each: the page's where it names one, else what
14
+ * the visitor chose, else the first allowed value.
15
+ */
16
+ function useSlotChoices(iface, fixed, onSlotsChange) {
17
+ const [picked, setPicked] = useState({});
18
+ const declared = iface?.inputs.slots;
19
+ const values = useMemo(() => {
20
+ const out = { ...fixed };
21
+ for (const [name, allowed] of Object.entries(declared ?? {})) {
22
+ const value = fixed?.[name] ?? picked[name] ?? allowed[0];
23
+ if (value !== undefined)
24
+ out[name] = value;
25
+ }
26
+ return out;
27
+ }, [declared, fixed, picked]);
28
+ const choose = useCallback((name, value) => {
29
+ setPicked((prev) => ({ ...prev, [name]: value }));
30
+ onSlotsChange?.({ ...values, [name]: value });
31
+ }, [values, onSlotsChange]);
32
+ const isChosen = declared !== undefined && Object.keys(declared).length > 0;
33
+ return {
34
+ values: isChosen || fixed ? values : undefined,
35
+ choices: isChosen ? { declared, values, onChange: choose } : undefined,
36
+ };
37
+ }
38
+ /**
39
+ * Settles legacy page gates as unanswered. Browser callbacks require an active dispatch grant.
40
+ */
41
+ function usePageAnswers(gated, pageTools, answer) {
42
+ const answered = useRef(null);
43
+ const latest = useRef({ pageTools, answer });
44
+ latest.current = { pageTools, answer };
45
+ const waiting = gated?.gateKind === 'page' ? gated : null;
46
+ useEffect(() => {
47
+ if (!waiting?.operationId || answered.current === waiting.operationId)
48
+ return;
49
+ const operationId = waiting.operationId;
50
+ answered.current = operationId;
51
+ // invariant: A restored page gate cannot authorize callback execution. Only active dispatch grants do.
52
+ void latest.current.answer({ action: 'page', page: { unanswered: true } });
53
+ }, [waiting]);
54
+ }
55
+ /** The page's side of a chat: answers the calls that wait on it, and warns of a tool it cannot answer. */
56
+ function usePageTools(iface, waitingOn, pageTools, answer) {
57
+ usePageAnswers(waitingOn, pageTools, answer);
58
+ usePageToolWarnings(iface && 'tools' in iface ? iface.tools.page : undefined, pageTools);
59
+ }
8
60
  /**
9
61
  * Seed the session with the profile's default model / effort once the interface loads, and keep
10
62
  * it valid as the interface changes: a pick still on the old defaults follows the new ones, and a
@@ -22,43 +74,52 @@ function useDefaultGeneration(iface, session, setSession) {
22
74
  setSession((prev) => ({ ...prev, selectedModel: next.model, selectedEffort: next.effort }));
23
75
  }, [session.selectedEffort, session.selectedModel, setSession, iface]);
24
76
  }
77
+ function useConversationRef(initial, onChange) {
78
+ const ref = useRef(initial);
79
+ const onChangeRef = useRef(onChange);
80
+ onChangeRef.current = onChange;
81
+ const adopt = useCallback((next) => {
82
+ if (ref.current === next)
83
+ return;
84
+ ref.current = next;
85
+ onChangeRef.current?.(next);
86
+ }, []);
87
+ return useMemo(() => ({ ref, adopt }), [adopt]);
88
+ }
89
+ /** Draws the conversation as the host shows it: its transcript, and the reply that waits on gates. */
90
+ function drawConversation(state, view, session, events) {
91
+ state.setBlocks(view.transcript);
92
+ state.setStreamBlocks(view.paused?.blocks ?? []);
93
+ // why: A reply at rest leaves the run's own events (tokens, an ask); a paused one, the host's.
94
+ state.setSession(sessionAfter(session, view.paused?.events ?? events));
95
+ state.setChatStarted(view.transcript.length > 0 || view.paused !== undefined);
96
+ }
25
97
  /** The streamed blocks, ending on the turn's error unless the stream already carried it. */
26
98
  function withTurnError(blocks, error) {
27
99
  if (blocks.some((block) => block.kind === 'error'))
28
100
  return blocks;
29
101
  return [...blocks, { id: crypto.randomUUID(), kind: 'error', message: error }];
30
102
  }
31
- /**
32
- * Shows a failed turn's error. Once the message is in the transcript, the
33
- * failure is too, closing the turn (the message, or the reply as far as it
34
- * got, shows it failed); a message that never went out (refused attachments)
35
- * is still in the composer, and a reply that still waits on a gate stays open,
36
- * so the failure shows in the composer.
37
- */
38
- function showTurnFailure(state, result, streamed, open) {
39
- const { error, errorKind, errorInternal } = result;
40
- if (open) {
41
- state.setFailure({ error, errorKind, ...(errorInternal ? { errorInternal } : {}) });
42
- }
43
- else {
44
- const failed = withTurnError(streamed, error);
45
- state.setBlocks((prev) => [...prev, ...failed]);
46
- state.setDelivery(null);
47
- }
48
- if (result.issues)
49
- state.setIssues(result.issues);
103
+ /** A failure the composer shows: none when the person stopped the run. */
104
+ function showComposerFailure(state, failure) {
105
+ if (!failure || failure.aborted)
106
+ return;
107
+ const { error, errorKind, errorInternal } = failure;
108
+ state.setFailure({ error, errorKind, ...(errorInternal ? { errorInternal } : {}) });
109
+ if (failure.issues)
110
+ state.setIssues(failure.issues);
50
111
  }
51
112
  /**
52
113
  * The run begins: busy, and streaming unless its message goes live when it
53
114
  * posts (onUserBlocks), so the previous reply never renders as streaming in
54
115
  * between. Returns the delivery before it, which a new message replaces.
55
116
  */
56
- function beginRun(state, userBlocksAlreadyApplied) {
117
+ function beginRun(state, message) {
57
118
  state.setFailure(null);
58
119
  const priorDelivery = state.deliveryRef.current;
59
120
  state.busyRef.current = true;
60
121
  state.setBusy(true);
61
- if (!userBlocksAlreadyApplied)
122
+ if (!message)
62
123
  state.setStreaming(true);
63
124
  state.allowQueueDrainRef.current = false;
64
125
  return priorDelivery;
@@ -72,108 +133,181 @@ function endRun(state) {
72
133
  state.abortRef.current = null;
73
134
  state.turnIdRef.current = null;
74
135
  }
136
+ /** A message that never reached the conversation leaves the transcript and goes back in the composer. */
137
+ async function takeBackMessage(state, start, restore) {
138
+ state.setBlocks(start.blocks);
139
+ state.setStreamBlocks(start.streamBlocks);
140
+ state.setChatStarted(start.blocks.length > 0 || start.streamBlocks.length > 0);
141
+ state.setDelivery(null);
142
+ await restore();
143
+ }
75
144
  /**
76
- * A failed run: its session (when it has one) and its error. A message that
77
- * never posted, or an answer that left its reply paused (as it was, or on its
78
- * next gate), keeps the transcript as it was, the paused reply waiting in it.
145
+ * A host that keeps the conversation is the truth: the chat draws it again
146
+ * from its ref, even after a message that never posted (a walk can settle
147
+ * calls before it fails). A failure shows in the composer unless the host's
148
+ * transcript closed on it. Returns whether the reply waits on gates.
79
149
  */
80
- function failRun(state, result, stream, unposted) {
81
- const waits = (result.session ?? state.sessionRef.current).gatedTool !== null;
82
- const open = unposted || waits;
83
- if (result.session)
84
- state.setSession(result.session);
85
- if (!result.aborted)
86
- showTurnFailure(state, result, stream.streamed, open);
87
- if (!open)
88
- state.setStreamBlocks([]);
89
- else
90
- state.setStreamBlocks(result.session ? stream.streamed : stream.before);
150
+ async function settleHosted(state, transport, conversation, outcome, start) {
151
+ const { failure } = outcome;
152
+ if (failure)
153
+ state.setDelivery(null);
154
+ if (conversation === undefined) {
155
+ state.setStreamBlocks(start.streamBlocks);
156
+ showComposerFailure(state, failure);
157
+ return state.sessionRef.current.gatedTool !== null;
158
+ }
159
+ const view = await transport.conversation({ conversation });
160
+ drawConversation(state, view, outcome.session, outcome.events);
161
+ if (!view.transcript.slice(start.blocks.length).some((b) => b.kind === 'error')) {
162
+ showComposerFailure(state, failure);
163
+ }
164
+ return view.paused !== undefined;
165
+ }
166
+ /**
167
+ * A stateless chat keeps the reply as the run streamed it: in the transcript
168
+ * once it ends (closed by its failure, or by the person's stop), or beside it
169
+ * while it waits on a gate. Returns whether it waits.
170
+ */
171
+ function settleLocal(state, iface, outcome, worked) {
172
+ const session = sessionAfter(outcome.session, outcome.events);
173
+ const { failure } = outcome;
174
+ const reply = stampWorked(foldAssistantTurn(iface, outcome.events), worked);
175
+ state.setSession(session);
176
+ if (session.gatedTool !== null) {
177
+ state.setStreamBlocks(reply);
178
+ showComposerFailure(state, failure);
179
+ return true;
180
+ }
181
+ state.setStreamBlocks([]);
182
+ if (!failure) {
183
+ state.setBlocks((prev) => [...prev, ...reply]);
184
+ return false;
185
+ }
186
+ state.setDelivery(null);
187
+ const closing = failure.aborted
188
+ ? [{ id: crypto.randomUUID(), kind: 'turn-done', stop: { kind: 'cancelled' } }]
189
+ : withTurnError(reply, failure.error);
190
+ state.setBlocks((prev) => [...prev, ...closing]);
191
+ if (failure.issues)
192
+ state.setIssues(failure.issues);
193
+ return false;
194
+ }
195
+ /**
196
+ * Draws the conversation the chat resumes, once the interface says the host
197
+ * keeps one. A run that starts first draws it itself.
198
+ */
199
+ function useResumedConversation(iface, transport, state, conversation, shown) {
200
+ const loaded = useRef(false);
201
+ const shownRef = useRef(shown);
202
+ useEffect(() => {
203
+ if (!iface || loaded.current)
204
+ return;
205
+ loaded.current = true;
206
+ const ref = conversation.ref.current;
207
+ if (ref === undefined || !keepsConversation(iface))
208
+ return;
209
+ if (shownRef.current) {
210
+ drawConversation(state, shownRef.current, state.sessionRef.current, []);
211
+ return;
212
+ }
213
+ void (async () => {
214
+ const before = state.blocksRef.current;
215
+ try {
216
+ const view = await transport.conversation({ conversation: ref });
217
+ // why: A message sent while it loaded posted onto the host's conversation; its run draws it.
218
+ if (state.blocksRef.current !== before || state.busyRef.current)
219
+ return;
220
+ drawConversation(state, view, state.sessionRef.current, []);
221
+ }
222
+ catch (err) {
223
+ showComposerFailure(state, turnFailure(err, iface.labels));
224
+ }
225
+ })();
226
+ }, [conversation, iface, state, transport]);
91
227
  }
92
228
  /**
93
229
  * Runs one turn's stream into the transcript: live partials while it streams,
94
- * then the committed result (or the error) and the pending queue's next step.
230
+ * then what the run left (or its failure) and the pending queue's next step.
95
231
  */
96
- function useRunTurnStream(iface, state) {
97
- // The current reply's work across its runs: a gate splits a reply into runs, and the wait between doesn't count.
232
+ function useRunTurnStream(iface, transport, state, conversation) {
233
+ // why: A stateless reply's work across its runs: a gate splits a reply into runs, and the wait between doesn't count.
98
234
  const replyWorkedMs = useRef(0);
99
- const onRunEnded = useCallback((nextPending, drain) => {
100
- const converted = convertSteersToFrontQueued(nextPending);
101
- state.setPendingMessages(orderComposerPendingMessages(converted));
235
+ // why: Steers the reply never took wait at the front of the queue.
236
+ const onRunEnded = useCallback((drain) => {
237
+ state.setPendingMessages((prev) => orderComposerPendingMessages(convertSteersToFrontQueued(prev)));
102
238
  state.allowQueueDrainRef.current = drain;
103
- return converted;
104
239
  }, [state]);
105
240
  return useCallback(async (run, options = {}) => {
106
241
  if (!iface || state.busyRef.current)
107
242
  return;
108
- const applied = options.userBlocksAlreadyApplied === true;
109
- const priorDelivery = beginRun(state, applied);
110
- const streamBefore = state.streamBlocksRef.current;
111
- const paused = { workedMs: state.sessionRef.current.gatedTool ? replyWorkedMs.current : 0 };
112
- // A run that answers a gate (a decision, sign-in) continues its reply; any other, walking away included, starts one.
113
- if (!state.sessionRef.current.gatedTool || options.walksAway)
243
+ const { message } = options;
244
+ const priorDelivery = beginRun(state, message !== undefined);
245
+ const start = {
246
+ blocks: state.blocksRef.current,
247
+ streamBlocks: state.streamBlocksRef.current,
248
+ };
249
+ // why: A run that answers a gate continues its reply; any other starts one.
250
+ if (!state.sessionRef.current.gatedTool)
114
251
  replyWorkedMs.current = 0;
115
252
  const runStartedAt = Date.now();
116
253
  const work = (async () => {
117
- let latestStream = [];
118
254
  const view = {
119
- blocks: (partial) => {
120
- latestStream = partial;
121
- state.scheduleStreamBlocks(partial);
122
- },
123
- // The reply goes on; the composer names what it left out.
124
- skipped: (error) => state.setFailure(clientFailure(error, iface.lexicon)),
255
+ blocks: (partial) => state.scheduleStreamBlocks(partial),
256
+ // why: The reply goes on; the composer names what it left out.
257
+ skipped: (error) => state.setFailure(clientFailure(error, iface.labels)),
125
258
  };
126
- const result = await run(view, paused);
259
+ const outcome = await run(view);
127
260
  const endedAt = Date.now();
128
261
  replyWorkedMs.current += endedAt - runStartedAt;
129
262
  endRun(state);
130
- if (!result.ok) {
131
- // A new message posts itself (onUserBlocks) with a fresh delivery.
132
- const unposted = applied && state.deliveryRef.current === priorDelivery;
133
- failRun(state, result, { streamed: latestStream, before: streamBefore }, unposted);
134
- onRunEnded(state.pendingRef.current, false);
135
- return;
263
+ const unposted = message !== undefined && !outcome.posted;
264
+ // why: A message that posts itself (onUserBlocks) gets a fresh delivery.
265
+ if (unposted && state.deliveryRef.current !== priorDelivery) {
266
+ await takeBackMessage(state, start, message.restore);
136
267
  }
137
- const merged = applyTurnResultToTranscript({
138
- blocks: state.blocksRef.current,
139
- streamBlocks: latestStream,
140
- session: result.session,
141
- userBlocks: applied ? undefined : result.userBlocks,
142
- assistantBlocks: result.assistantBlocks,
143
- worked: { workedMs: replyWorkedMs.current, endedAt },
144
- });
145
- state.setBlocks(merged.blocks);
146
- state.setStreamBlocks(merged.streamBlocks);
147
- state.setSession(merged.session);
148
- if (merged.session.gatedTool !== null)
149
- return;
150
- onRunEnded(state.pendingRef.current, true);
268
+ let waits;
269
+ if (keepsConversation(iface)) {
270
+ waits = await settleHosted(state, transport, conversation.ref.current, outcome, start);
271
+ }
272
+ else if (unposted) {
273
+ showComposerFailure(state, outcome.failure);
274
+ waits = state.sessionRef.current.gatedTool !== null;
275
+ }
276
+ else {
277
+ waits = settleLocal(state, iface, outcome, { workedMs: replyWorkedMs.current, endedAt });
278
+ }
279
+ onRunEnded(!outcome.failure && !waits);
151
280
  })();
152
281
  state.runPromiseRef.current = work;
153
282
  try {
154
283
  await work;
155
284
  }
285
+ catch (err) {
286
+ // why: The run left no view to draw; the composer says so, and the chat stays as it stood.
287
+ showComposerFailure(state, turnFailure(err, iface.labels));
288
+ onRunEnded(false);
289
+ }
156
290
  finally {
157
291
  if (state.runPromiseRef.current === work)
158
292
  state.runPromiseRef.current = null;
159
293
  }
160
- }, [iface, onRunEnded, state]);
294
+ }, [conversation, iface, onRunEnded, state, transport]);
161
295
  }
162
296
  /** Start the next queued message once the run goes idle, when the run that ended allows it. */
163
297
  function useQueueDrain(phase, state, startTurnFromDraft) {
164
298
  const drainQueue = useCallback(async () => {
165
- if (state.drainLockRef.current ||
166
- state.busyRef.current ||
167
- state.sessionRef.current.gatedTool) {
299
+ if (state.drainLockRef.current || state.busyRef.current || state.sessionRef.current.gatedTool) {
168
300
  return;
169
301
  }
170
- const { message, remaining } = consumeNextComposerQueue(state.pendingRef.current);
302
+ const { message } = consumeNextComposerQueue(state.pendingRef.current);
171
303
  if (!message)
172
304
  return;
173
305
  state.drainLockRef.current = true;
174
- state.setPendingMessages(remaining);
175
306
  try {
176
- await startTurnFromDraft(message.draft);
307
+ // why: The message leaves the queue once it posts; one that never starts stays queued.
308
+ await startTurnFromDraft(message.draft, {
309
+ onPosted: () => state.setPendingMessages((prev) => removeComposerPendingMessage(prev, message.id)),
310
+ });
177
311
  }
178
312
  finally {
179
313
  state.drainLockRef.current = false;
@@ -196,48 +330,91 @@ const SERVER_EVENTS = new Set(['stage', 'guardrail', 'session', 'error', 'done']
196
330
  /** The reply itself (its thinking and tool calls come before it). */
197
331
  const REPLY_EVENTS = new Set(['text', 'structured', 'media']);
198
332
  /**
199
- * How far an event shows the message got: any event means the server took it,
200
- * one from the model (thinking, a tool call) means the model has it, and the
201
- * reply means it was read.
333
+ * How far a line shows the message got: the host saving it, or any event,
334
+ * means the server took it; one from the model (thinking, a tool call) means
335
+ * the model has it, and the reply means it was read. A save without the
336
+ * message (a walk settling calls) says nothing of it.
202
337
  */
203
- function deliveryOf(event) {
204
- if (REPLY_EVENTS.has(event.type))
338
+ function deliveryOf(line) {
339
+ if (line.type === 'conversation')
340
+ return line.posted ? 'sent' : undefined;
341
+ if (REPLY_EVENTS.has(line.type))
205
342
  return 'read';
206
- return SERVER_EVENTS.has(event.type) ? 'sent' : 'delivered';
343
+ return SERVER_EVENTS.has(line.type) ? 'sent' : 'delivered';
344
+ }
345
+ /** Moves the message's delivery on to what `line` shows it reached, never back. */
346
+ function advanceDelivery(deliveryRef, setDelivery, line) {
347
+ const delivery = deliveryRef.current;
348
+ const reached = deliveryOf(line);
349
+ if (!delivery || !reached)
350
+ return;
351
+ if (DELIVERY_ORDER.indexOf(reached) > DELIVERY_ORDER.indexOf(delivery.status)) {
352
+ setDelivery({ status: reached });
353
+ }
207
354
  }
208
355
  /**
209
356
  * The transport, with each turn's events also clearing the steers they report
210
357
  * as landed (so the run's end requeues only the steers the agent never saw),
211
- * and moving the posted message's delivery forward.
358
+ * and moving the posted message's delivery forward. The chat adopts each
359
+ * conversation ref as it arrives, so a page that reloads mid-reply keeps the
360
+ * posted message. Each turn carries the page's slots and context as they are
361
+ * when it is sent.
212
362
  */
213
- function useTappedTransport(transport, state) {
363
+ function useTappedTransport(transport, state, conversation, values, onTurnEvent) {
364
+ const valuesRef = useRef(values);
365
+ valuesRef.current = values;
366
+ const onTurnEventRef = useRef(onTurnEvent);
367
+ onTurnEventRef.current = onTurnEvent;
214
368
  return useMemo(() => {
215
- const tap = (onEvent) => (event) => {
216
- if (event.type === 'stage' && event.injected?.length) {
217
- state.pendingRef.current = removeLandedSteers(state.pendingRef.current, event);
218
- state.setPendingMessages((prev) => removeLandedSteers(prev, event));
219
- }
220
- const delivery = state.deliveryRef.current;
221
- const reached = deliveryOf(event);
222
- if (delivery && DELIVERY_ORDER.indexOf(reached) > DELIVERY_ORDER.indexOf(delivery.status)) {
223
- state.setDelivery({ status: reached });
369
+ const withValues = (request) => {
370
+ const { slots, context } = valuesRef.current;
371
+ if (slots === undefined && context === undefined)
372
+ return request;
373
+ return {
374
+ ...request,
375
+ input: {
376
+ ...request.input,
377
+ ...(slots === undefined ? {} : { slots }),
378
+ ...(context === undefined ? {} : { context }),
379
+ },
380
+ };
381
+ };
382
+ const tap = (onLine) => (line) => {
383
+ if (line.type === 'stage' && line.injected?.length) {
384
+ state.pendingRef.current = removeLandedSteers(state.pendingRef.current, line);
385
+ state.setPendingMessages((prev) => removeLandedSteers(prev, line));
224
386
  }
225
- onEvent(event);
387
+ advanceDelivery(state.deliveryRef, state.setDelivery, line);
388
+ if (line.type === 'conversation')
389
+ conversation.adopt(line.ref);
390
+ else
391
+ onTurnEventRef.current?.(line);
392
+ onLine(line);
226
393
  };
227
394
  return {
228
395
  ...transport,
229
- turn: (request, onEvent, signal) => transport.turn(request, tap(onEvent), signal),
230
- invoke: (request, onEvent, signal) => transport.invoke(request, tap(onEvent), signal),
396
+ turn: (request, onLine, signal) => transport.turn(withValues(request), tap(onLine), signal),
397
+ invoke: (request, onLine, signal) => transport.invoke(request, tap(onLine), signal),
231
398
  };
232
- }, [transport, state.pendingRef, state.setPendingMessages, state.deliveryRef, state.setDelivery]);
399
+ }, [
400
+ transport,
401
+ conversation,
402
+ state.pendingRef,
403
+ state.setPendingMessages,
404
+ state.deliveryRef,
405
+ state.setDelivery,
406
+ ]);
233
407
  }
234
408
  /**
235
409
  * Headless chat model: transcript, streaming, composer drafts, pending
236
410
  * queue / steer / stash, tool gates. Render it with `@theoremjs/react/ui` or
237
411
  * your own components.
238
412
  */
239
- export function useTheoremChat({ transport, iface, initial, onChange }) {
240
- const state = useTheoremChatState(initial);
413
+ export function useTheoremChat({ transport, iface, conversation: initialConversation, conversationView, initialText, onConversationChange, slots, onSlotsChange, context, pageTools, onTurnEvent, }) {
414
+ const slotState = useSlotChoices(iface, slots, onSlotsChange);
415
+ const state = useTheoremChatState(initialText);
416
+ const conversation = useConversationRef(initialConversation, onConversationChange);
417
+ useResumedConversation(iface, transport, state, conversation, conversationView);
241
418
  useDefaultGeneration(iface, state.session, state.setSession);
242
419
  const gated = state.session.gatedTool !== null;
243
420
  const phase = state.busy ? 'streaming' : gated ? 'gated' : 'idle';
@@ -249,53 +426,63 @@ export function useTheoremChat({ transport, iface, initial, onChange }) {
249
426
  ...(effort ? { selectedEffort: effort } : { selectedEffort: undefined }),
250
427
  }));
251
428
  }, [iface, state]);
252
- const runTurnStream = useRunTurnStream(iface, state);
253
- const steerTransport = useTappedTransport(transport, state);
429
+ const runTurnStream = useRunTurnStream(iface, transport, state, conversation);
430
+ const pageTransport = useMemo(() => withPageToolDispatch(transport, pageTools), [transport, pageTools]);
431
+ const steerTransport = useTappedTransport(pageTransport, state, conversation, { slots: slotState.values, context }, onTurnEvent);
254
432
  const actions = useTheoremChatActions({
255
433
  ...state,
256
434
  iface,
257
435
  transport: steerTransport,
436
+ conversationRef: conversation.ref,
258
437
  phase,
259
438
  gated,
260
439
  runTurnStream,
261
440
  });
262
441
  useQueueDrain(phase, state, actions.startTurnFromDraft);
263
- const onChangeRef = useRef(onChange);
264
- onChangeRef.current = onChange;
265
- const restedOn = useRef(initial);
266
- useEffect(() => {
267
- if (state.busy || gated)
268
- return;
269
- const last = restedOn.current;
270
- if (last && last.blocks === state.blocks && last.session === state.session)
271
- return;
272
- const snapshot = { blocks: state.blocks, session: state.session };
273
- restedOn.current = snapshot;
274
- onChangeRef.current?.(snapshot);
275
- }, [state.busy, gated, state.blocks, state.session]);
276
- /** Sends `text` as the composer would, and resolves once the reply is done; `null` when it can't go now. */
277
- const sendText = useCallback(async (text) => {
442
+ usePageTools(iface, phase === 'gated' ? state.session.gatedTool : null, pageTools, actions.resumeGatedTool);
443
+ /** Sends `text` and optional files as the composer would, and resolves once the reply is done; `null` when it can't go now. */
444
+ const sendText = useCallback(async (text, files = []) => {
278
445
  if (!iface || state.busyRef.current || state.sessionRef.current.gatedTool !== null)
279
446
  return null;
280
447
  const before = state.blocksRef.current.length;
281
- await actions.startTurnFromFields({ text, files: [], voice: [] });
448
+ await actions.startTurnFromFields({ text, files: [...files], voice: [] });
282
449
  return { blocks: state.blocksRef.current.slice(before) };
283
450
  }, [actions, iface, state]);
284
- const handleBranch = useCallback((index) => {
285
- const kept = [...state.blocks, ...state.streamBlocks].slice(0, index + 1);
286
- state.setBlocks(kept);
287
- state.setStreamBlocks([]);
288
- state.setStreaming(false);
289
- state.busyRef.current = false;
290
- state.setBusy(false);
291
- state.setChatStarted(kept.length > 0);
292
- // The kept transcript's last message isn't the one whose delivery was tracked.
293
- state.setDelivery(null);
294
- state.setSession((prevSession) => branchInterfaceTurnSession(prevSession, kept));
295
- }, [iface, state]);
451
+ /**
452
+ * Cut the conversation after transcript block `index`. A host that keeps
453
+ * the conversation cuts it (walking away from a reply that waits on gates)
454
+ * and the chat draws what it returns; a stateless chat cuts its own.
455
+ */
456
+ const handleBranch = useCallback(async (index) => {
457
+ if (!iface || state.busyRef.current)
458
+ return;
459
+ const ref = conversation.ref.current;
460
+ if (!keepsConversation(iface) || ref === undefined) {
461
+ const kept = [...state.blocksRef.current, ...state.streamBlocksRef.current].slice(0, index + 1);
462
+ state.setBlocks(kept);
463
+ state.setStreamBlocks([]);
464
+ state.setChatStarted(kept.length > 0);
465
+ // why: The kept transcript's last message isn't the one whose delivery was tracked.
466
+ state.setDelivery(null);
467
+ state.setSession((prev) => branchInterfaceTurnSession(prev, kept));
468
+ return;
469
+ }
470
+ state.setFailure(null);
471
+ try {
472
+ // why: The paused reply is not in the host's transcript; a cut inside it keeps the transcript whole.
473
+ const at = Math.min(index, state.blocksRef.current.length - 1);
474
+ const rewound = await transport.rewind({ conversation: ref, at });
475
+ conversation.adopt(rewound.conversation);
476
+ drawConversation(state, rewound.view, state.sessionRef.current, []);
477
+ state.setDelivery(null);
478
+ }
479
+ catch (err) {
480
+ showComposerFailure(state, turnFailure(err, iface.labels));
481
+ }
482
+ }, [conversation, iface, state, transport]);
296
483
  const handlePendingQueue = useCallback((id) => {
297
484
  const message = state.pendingRef.current.find((m) => m.id === id);
298
- if (!message || message.kind !== 'stash')
485
+ if (message?.kind !== 'stash')
299
486
  return;
300
487
  if (phase === 'idle') {
301
488
  state.allowQueueDrainRef.current = true;
@@ -335,6 +522,7 @@ export function useTheoremChat({ transport, iface, initial, onChange }) {
335
522
  handleSendNow: actions.handleSendNow,
336
523
  handleToolDecision: actions.handleToolDecision,
337
524
  handleGenerationChange,
525
+ slotChoices: slotState.choices,
338
526
  sendText,
339
527
  };
340
528
  }
@@ -1,23 +1,18 @@
1
- import type { DecisionJson, DecisionResult } from '@theoremjs/agents';
1
+ import type { DecisionAsset, DecisionJson, DecisionResult } from '@theoremjs/agents';
2
2
  import type { DecisionInterface, DecisionTransport } from '../client/decision-transport.ts';
3
- import { type ClientFailure } from '../client/failure.ts';
3
+ import type { ClientFailure } from '../client/failure.ts';
4
+ import { type OneCallState } from './use-one-call.ts';
4
5
  export type TheoremDecisionState = {
5
6
  /** The profile as the page sees it; null until the host describes it. */
6
7
  iface: DecisionInterface | null;
7
8
  /** Why the host could not describe the profile. */
8
9
  describeFailure: ClientFailure | null;
9
- status: 'idle' | 'deciding' | 'done' | 'error';
10
- /** The last answers; kept while the next decision runs and when it fails. */
11
- result: DecisionResult | null;
12
- /** How long the last decision took, round trip, in milliseconds. */
13
- elapsedMs: number | null;
14
- failure: ClientFailure | null;
15
- };
10
+ } & OneCallState<'deciding', DecisionResult>;
16
11
  /**
17
12
  * One decision profile: describe it once, then `decide(state)` as often as
18
13
  * needed. A new decision aborts the one still running.
19
14
  */
20
15
  export declare function useTheoremDecision(transport: DecisionTransport): TheoremDecisionState & {
21
- decide: (state: Exclude<DecisionJson, null>) => Promise<void>;
16
+ decide: (state: Exclude<DecisionJson, null>, assets?: Record<string, DecisionAsset>) => Promise<void>;
22
17
  cancel: () => void;
23
18
  };