@theoremjs/react 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -2
  3. package/client/attachment-hover-preview.d.ts +11 -0
  4. package/client/attachment-hover-preview.js +36 -0
  5. package/client/audio-level.d.ts +5 -0
  6. package/client/audio-level.js +24 -0
  7. package/client/composer-attachments.d.ts +30 -0
  8. package/client/composer-attachments.js +33 -0
  9. package/client/composer-drawer.d.ts +19 -0
  10. package/client/composer-drawer.js +15 -0
  11. package/client/composer-hints.d.ts +25 -0
  12. package/client/composer-hints.js +20 -0
  13. package/client/composer-primary.d.ts +16 -0
  14. package/client/composer-primary.js +25 -0
  15. package/client/decision-transport.d.ts +48 -0
  16. package/client/decision-transport.js +102 -0
  17. package/client/decode-composer-draft.d.ts +11 -0
  18. package/client/decode-composer-draft.js +12 -0
  19. package/client/encode-composer-draft.d.ts +9 -0
  20. package/client/encode-composer-draft.js +34 -0
  21. package/client/encode-files.d.ts +18 -0
  22. package/client/encode-files.js +54 -0
  23. package/client/failure.d.ts +32 -0
  24. package/client/failure.js +34 -0
  25. package/client/generation-selection.d.ts +15 -0
  26. package/client/generation-selection.js +24 -0
  27. package/client/host-call.d.ts +51 -0
  28. package/client/host-call.js +42 -0
  29. package/client/host-transport.d.ts +68 -0
  30. package/client/host-transport.js +74 -0
  31. package/client/image-output.d.ts +6 -0
  32. package/client/image-output.js +13 -0
  33. package/client/index.d.ts +20 -0
  34. package/client/index.js +18 -0
  35. package/client/ink-waveform.d.ts +17 -0
  36. package/client/ink-waveform.js +69 -0
  37. package/client/live/apply-live-turn-tool-event.d.ts +14 -0
  38. package/client/live/apply-live-turn-tool-event.js +39 -0
  39. package/client/live/live-captions.d.ts +23 -0
  40. package/client/live/live-captions.js +70 -0
  41. package/client/live/live-mic-forward.d.ts +29 -0
  42. package/client/live/live-mic-forward.js +37 -0
  43. package/client/live/live-state.d.ts +11 -0
  44. package/client/live/live-state.js +20 -0
  45. package/client/live/live-tool.d.ts +11 -0
  46. package/client/live/live-tool.js +1 -0
  47. package/client/live/live-video.d.ts +10 -0
  48. package/client/live/live-video.js +71 -0
  49. package/client/live/run-live-tool-call.d.ts +17 -0
  50. package/client/live/run-live-tool-call.js +32 -0
  51. package/client/live-client.d.ts +141 -0
  52. package/client/live-client.js +638 -0
  53. package/client/live-errors.d.ts +2 -0
  54. package/client/live-errors.js +9 -0
  55. package/client/live-messages.d.ts +110 -0
  56. package/client/live-messages.js +67 -0
  57. package/client/mic-capture.d.ts +4 -0
  58. package/client/mic-capture.js +6 -0
  59. package/client/oauth-popup.d.ts +26 -0
  60. package/client/oauth-popup.js +30 -0
  61. package/client/pcm-downsample.d.ts +4 -0
  62. package/client/pcm-downsample.js +45 -0
  63. package/client/run-commit.d.ts +61 -0
  64. package/client/run-commit.js +124 -0
  65. package/client/run-session.d.ts +80 -0
  66. package/client/run-session.js +215 -0
  67. package/client/schema-fields.d.ts +63 -0
  68. package/client/schema-fields.js +178 -0
  69. package/client/shaped-data.d.ts +118 -0
  70. package/client/shaped-data.js +314 -0
  71. package/client/source-citations.d.ts +13 -0
  72. package/client/source-citations.js +56 -0
  73. package/client/state-fields.d.ts +16 -0
  74. package/client/state-fields.js +44 -0
  75. package/client/tool-result.d.ts +78 -0
  76. package/client/tool-result.js +275 -0
  77. package/client/tool-resume.d.ts +33 -0
  78. package/client/tool-resume.js +11 -0
  79. package/client/trace-feed.d.ts +13 -0
  80. package/client/trace-feed.js +18 -0
  81. package/client/trace-story.d.ts +163 -0
  82. package/client/trace-story.js +416 -0
  83. package/client/trace-view.d.ts +130 -0
  84. package/client/trace-view.js +293 -0
  85. package/client/transcript-groups.d.ts +114 -0
  86. package/client/transcript-groups.js +167 -0
  87. package/client/transcript-scroll.d.ts +7 -0
  88. package/client/transcript-scroll.js +6 -0
  89. package/client/transport.d.ts +176 -0
  90. package/client/transport.js +256 -0
  91. package/client/turn-client.d.ts +35 -0
  92. package/client/turn-client.js +93 -0
  93. package/client/voice-label.d.ts +2 -0
  94. package/client/voice-label.js +15 -0
  95. package/client/voice-recorder.d.ts +27 -0
  96. package/client/voice-recorder.js +165 -0
  97. package/client/walk-away.d.ts +34 -0
  98. package/client/walk-away.js +48 -0
  99. package/client/wire-line.d.ts +56 -0
  100. package/client/wire-line.js +90 -0
  101. package/client/worklets/mic-capture.js +16 -0
  102. package/components/InkWaveform.d.ts +10 -0
  103. package/components/InkWaveform.js +108 -0
  104. package/components/live/use-live-runner-controls.d.ts +48 -0
  105. package/components/live/use-live-runner-controls.js +100 -0
  106. package/components/live/use-live-runner-model.d.ts +45 -0
  107. package/components/live/use-live-runner-model.js +175 -0
  108. package/components/live/use-live-runner-ui.d.ts +65 -0
  109. package/components/live/use-live-runner-ui.js +159 -0
  110. package/components/live/use-live-session-client.d.ts +41 -0
  111. package/components/live/use-live-session-client.js +123 -0
  112. package/components/use-composer-voice.d.ts +28 -0
  113. package/components/use-composer-voice.js +90 -0
  114. package/components/use-editor-selection.d.ts +3 -0
  115. package/components/use-editor-selection.js +16 -0
  116. package/components/use-voice-playback.d.ts +16 -0
  117. package/components/use-voice-playback.js +68 -0
  118. package/hooks/use-described.d.ts +8 -0
  119. package/hooks/use-described.js +23 -0
  120. package/hooks/use-theorem-chat-actions.d.ts +75 -0
  121. package/hooks/use-theorem-chat-actions.js +265 -0
  122. package/hooks/use-theorem-chat-state.d.ts +66 -0
  123. package/hooks/use-theorem-chat-state.js +118 -0
  124. package/hooks/use-theorem-chat.d.ts +63 -0
  125. package/hooks/use-theorem-chat.js +340 -0
  126. package/hooks/use-theorem-decision.d.ts +23 -0
  127. package/hooks/use-theorem-decision.js +75 -0
  128. package/hooks/use-theorem-host.d.ts +44 -0
  129. package/hooks/use-theorem-host.js +117 -0
  130. package/hooks/use-theorem-interface.d.ts +19 -0
  131. package/hooks/use-theorem-interface.js +25 -0
  132. package/hooks/use-trace-records.d.ts +4 -0
  133. package/hooks/use-trace-records.js +7 -0
  134. package/index.d.ts +14 -0
  135. package/index.js +14 -0
  136. package/live.d.ts +8 -0
  137. package/live.js +10 -0
  138. package/package.json +73 -4
  139. package/server/credential-store.d.ts +24 -0
  140. package/server/credential-store.js +14 -0
  141. package/server/decision-handler.d.ts +34 -0
  142. package/server/decision-handler.js +55 -0
  143. package/server/handler.d.ts +114 -0
  144. package/server/handler.js +568 -0
  145. package/server/json-response.d.ts +2 -0
  146. package/server/json-response.js +7 -0
  147. package/server/mod.d.ts +16 -0
  148. package/server/mod.js +15 -0
  149. package/server/request-check.d.ts +16 -0
  150. package/server/request-check.js +34 -0
  151. package/server/session-store.d.ts +74 -0
  152. package/server/session-store.js +49 -0
  153. package/server/steer-inbox.d.ts +35 -0
  154. package/server/steer-inbox.js +52 -0
  155. package/server/walk-away.d.ts +22 -0
  156. package/server/walk-away.js +41 -0
  157. package/styles/ink-controls.css +134 -0
  158. package/ui/ChatComposerBar.d.ts +39 -0
  159. package/ui/ChatComposerBar.js +251 -0
  160. package/ui/ChatTranscript.d.ts +30 -0
  161. package/ui/ChatTranscript.js +474 -0
  162. package/ui/ConsoleFrame.d.ts +47 -0
  163. package/ui/ConsoleFrame.js +46 -0
  164. package/ui/DecisionAnswers.d.ts +15 -0
  165. package/ui/DecisionAnswers.js +102 -0
  166. package/ui/LiveRunner.d.ts +28 -0
  167. package/ui/LiveRunner.js +193 -0
  168. package/ui/SchemaFields.d.ts +10 -0
  169. package/ui/SchemaFields.js +122 -0
  170. package/ui/ShapedData.d.ts +11 -0
  171. package/ui/ShapedData.js +151 -0
  172. package/ui/SidePanel.d.ts +57 -0
  173. package/ui/SidePanel.js +50 -0
  174. package/ui/StateFields.d.ts +36 -0
  175. package/ui/StateFields.js +96 -0
  176. package/ui/TheoremChat.d.ts +71 -0
  177. package/ui/TheoremChat.js +146 -0
  178. package/ui/TheoremDecision.d.ts +38 -0
  179. package/ui/TheoremDecision.js +87 -0
  180. package/ui/TheoremHost.d.ts +38 -0
  181. package/ui/TheoremHost.js +150 -0
  182. package/ui/ToolGateCard.d.ts +28 -0
  183. package/ui/ToolGateCard.js +100 -0
  184. package/ui/ToolResult.d.ts +10 -0
  185. package/ui/ToolResult.js +60 -0
  186. package/ui/ToolResultCharts.d.ts +5 -0
  187. package/ui/ToolResultCharts.js +86 -0
  188. package/ui/TraceCharts.d.ts +13 -0
  189. package/ui/TraceCharts.js +235 -0
  190. package/ui/TraceGuardrails.d.ts +10 -0
  191. package/ui/TraceGuardrails.js +56 -0
  192. package/ui/TraceInspector.d.ts +6 -0
  193. package/ui/TraceInspector.js +189 -0
  194. package/ui/TraceInspectorPanel.d.ts +24 -0
  195. package/ui/TraceInspectorPanel.js +57 -0
  196. package/ui/TraceOverview.d.ts +34 -0
  197. package/ui/TraceOverview.js +96 -0
  198. package/ui/TraceSpanDetail.d.ts +10 -0
  199. package/ui/TraceSpanDetail.js +188 -0
  200. package/ui/TraceStory.d.ts +20 -0
  201. package/ui/TraceStory.js +107 -0
  202. package/ui/TraceValues.d.ts +36 -0
  203. package/ui/TraceValues.js +113 -0
  204. package/ui/TraceWaterfall.d.ts +13 -0
  205. package/ui/TraceWaterfall.js +132 -0
  206. package/ui/VoiceNote.d.ts +12 -0
  207. package/ui/VoiceNote.js +28 -0
  208. package/ui/built/theme.css +774 -0
  209. package/ui/built/theorem.d.ts +13 -0
  210. package/ui/built/theorem.js +631 -0
  211. package/ui/built/theorem.variants.d.ts +16 -0
  212. package/ui/disclosure-motion.d.ts +6 -0
  213. package/ui/disclosure-motion.js +163 -0
  214. package/ui/icons.d.ts +4 -0
  215. package/ui/icons.js +37 -0
  216. package/ui/index.d.ts +25 -0
  217. package/ui/index.js +29 -0
  218. package/ui/labels-provider.d.ts +16 -0
  219. package/ui/labels-provider.js +61 -0
  220. package/ui/labels.d.ts +1153 -0
  221. package/ui/labels.js +603 -0
  222. package/ui/theme.d.ts +10 -0
  223. package/ui/theme.js +13 -0
  224. package/ui/theorem-theme.d.ts +10 -0
  225. package/ui/theorem-theme.js +83 -0
  226. package/ui/trace-palette.d.ts +24 -0
  227. package/ui/trace-palette.js +24 -0
  228. package/ui/trace-patterns.d.ts +32 -0
  229. package/ui/trace-patterns.js +44 -0
  230. package/ui/transcript-copy-text.d.ts +4 -0
  231. package/ui/transcript-copy-text.js +39 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ORCHID AI LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,247 @@
1
- # Temporary Holding Version
1
+ # `@theoremjs/react`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React projection of the headless interface (`@theoremjs/agents/interface`) — runners, transcript, composer, live stage.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @theoremjs/react @theoremjs/agents zod react react-dom
9
+ # for the chat and live UI (`/ui`, `/live`):
10
+ npm install @astryxdesign/core@0.6.3 @stylexjs/stylex@0.19.0
11
+ # only to build your own theme from the Theorem one (`/ui/theme`):
12
+ npm install @astryxdesign/theme-neutral@0.6.3
13
+ ```
14
+
15
+ Published to npm only: the package ships built JavaScript, declarations and
16
+ stylesheets. `@theoremjs/agents` is a peer, so the host and the UI share one
17
+ kernel. The Astryx peers are optional: the hooks, client and server entry
18
+ points run without them. The declarations need TypeScript 5.7 or later.
19
+
20
+ `@theoremjs/react/ui` and `@theoremjs/react/live` import their stylesheets
21
+ themselves, so a bundler that handles CSS imports (Vite does) needs no extra
22
+ step. The theme's type is Figtree with a system-font fallback;
23
+ the package does not ship the font, so load it yourself if you want it.
24
+
25
+ The live UI loads its microphone worklet as an asset
26
+ (`new URL('./worklets/mic-capture.js', import.meta.url)`); Vite ships it
27
+ without configuration, and any bundler that resolves that pattern does too.
28
+
29
+ No Svelte. The playground site (`theoremai-frontend`) hosts a thin Vite SPA at `apps/run` that imports this package; the info-site graph stays Svelte and only writes a `PlaygroundRunPayload` handoff.
30
+
31
+ ## Imports
32
+
33
+ ```ts
34
+ import { useTheoremChat, useTheoremInterface } from '@theoremjs/react'; // headless hooks + transport
35
+ import { TheoremChat } from '@theoremjs/react/ui'; // Astryx chat UI
36
+ import { LiveRunner } from '@theoremjs/react/live'; // Astryx voice / video UI
37
+ import { createTheoremHandler } from '@theoremjs/react/server'; // host side
38
+ ```
39
+
40
+ A `host` profile runs no model: the page calls its tools directly. Serve it
41
+ with `createTheoremHostHandler({ profile })` and render `<TheoremHost
42
+ endpoint="/api/host" />` (`createHostTransport` and `useTheoremHost` for a UI
43
+ of your own). `GET` describes each allowed tool with its input and output JSON
44
+ Schema, never its endpoint or credentials; `POST /call` `{ name, input }`
45
+ streams the call's events, and `POST /invoke` answers a gate it paused on, as
46
+ in chat. The console draws the form from the input schema and lays the result
47
+ out from its value: figures, charts, tables, images, audio and Markdown, with
48
+ the raw JSON beside them.
49
+
50
+ The playground's run-tab handoff (`savePlaygroundRunPayload`,
51
+ `readPlaygroundRunIdFromUrl`, …) lives in `@theoremjs/playground`; see
52
+ [`playground/README.md`](../playground/README.md).
53
+
54
+ ## The wire
55
+
56
+ Turn lines, live envelopes, request bodies, live messages and the profile
57
+ interface `describe` returns are each checked against their schema, both ways:
58
+
59
+ - **Browser → host.** `createTheoremHandler` reads `/turn`, `/invoke` and
60
+ `/steer` bodies with `theoremTurnRequestSchema`, `theoremInvokeRequestSchema`
61
+ and `theoremSteerRequestSchema`; a missing or malformed field is a `request`
62
+ error (400). `/invoke` answers a paused call by its `gateId` with a
63
+ `decision`: `approve` (with `input` when the user edited it, `secret` at a
64
+ sign-in gate) or `deny`; the host settles each one. An answer whose request
65
+ ends before its call settles (the network drops, the host fails) puts the
66
+ call back to wait, to be answered again; once it settled, the session keeps
67
+ the settle (`TheoremSessionState.settled`, pruned with the gates): a second
68
+ answer is refused with `session.gate_expired`, and a walk-away naming the
69
+ call reads its result. A message sent while
70
+ its reply waits walks away in its own `/turn`: `abandon` names the waiting
71
+ calls, its history leaves exactly those open, and the host settles each
72
+ cancelled ahead of the message's reply (`checkWalkAway` and `walkAway` for a
73
+ host with routes of its own). A relay reads the live client's messages with
74
+ `parseLiveClientMessage`. A host with routes of its own reads a body with
75
+ `checkRequest(schema, body, what)`, and answers a paused call with
76
+ `answerGatedCall` (`@theoremjs/agents/kernel`), the rule the handler uses.
77
+ - **Host → browser.** `describe` returns the profile interface as
78
+ `profileInterfaceSchema` names it: tool ids, never a tool's definition, and
79
+ no host functions. The transport and the live client read each line or
80
+ envelope against its kind's schema, and `describe` against
81
+ `profileInterfaceSchema`. A kind the client does not know reaches
82
+ the event handler as `unsupported`, and one that fails its check (not JSON,
83
+ no kind, or a known kind that fails its schema) as `malformed`: a
84
+ `bad_response` naming what broke, never the value. Either way the reply or
85
+ call goes on without it. The chat names a skipped part in the composer and
86
+ the live call in its failure banner, both with the lexicon's
87
+ `session.part_skipped`; a run built on `streamInterfaceTurn` hears of it
88
+ through its `view.skipped`. A `describe` reply that fails its schema is
89
+ `bad_response`.
90
+
91
+ ## Local layout
92
+
93
+ ```text
94
+ Development/
95
+ theoremai/ # kernel + this package
96
+ react/
97
+ theoremai-frontend/ # site; apps/run consumes file:../../../theoremai/react
98
+ ```
99
+
100
+ ## Tool credentials
101
+
102
+ Tool credentials live on the server, never in the browser. `createTheoremHandler`
103
+ keeps them in a `credentialStore` keyed by session id (default: process memory;
104
+ use a vault or an encrypted row in production) and loads them into every turn
105
+ and every resumed call.
106
+
107
+ - **Typed keys** — at a bearer or API-key gate, the user types the key into the
108
+ gate card. It goes up once as `secret` on the resume request, and the server
109
+ saves it under the gate's slot; the card keeps no copy and no event carries it.
110
+ The server decides the credential's kind and header from the tool, not the
111
+ request.
112
+ - **OAuth** — pass `authorizationUrl`: begin the flow with `createOAuthPkceFlow`,
113
+ binding it to the `sessionId` you're given, and return its URL; the gate card
114
+ opens it in a popup. Your callback route exchanges the code, saves the
115
+ credential under that session, then its page calls `notifyOAuthComplete(slot)`.
116
+ The card takes that message only from its own popup and origin, and resumes the
117
+ gate with its id alone.
118
+ - **Refresh** — a refreshed OAuth token is saved to the store before the call
119
+ goes on, so a rotated refresh token is never lost.
120
+
121
+ ```ts
122
+ const credentialStore = createMemoryCredentialStore();
123
+ const secret = secrets.oauthStateSecret;
124
+ const redirectUri = 'https://app.example/oauth/callback';
125
+
126
+ export const theorem = createTheoremHandler({
127
+ profile,
128
+ provider,
129
+ credentialStore,
130
+ authorizationUrl: async (challenge, { sessionId }) =>
131
+ (
132
+ await createOAuthPkceFlow({
133
+ resourceServerUrl: challenge.resource ?? 'https://api.tracker.example',
134
+ clientId: 'https://app.example/oauth/client.json',
135
+ redirectUri,
136
+ scopes: challenge.requiredScopes,
137
+ signingSecret: secret,
138
+ sessionBinding: sessionId,
139
+ })
140
+ ).authorizationUrl,
141
+ });
142
+
143
+ // The callback route: the handler's session cookie comes with the redirect.
144
+ export async function oauthCallback(request: Request): Promise<Response> {
145
+ const sessionId = theoremSessionId(request);
146
+ if (!sessionId) return new Response(null, { status: 401 });
147
+ const params = new URL(request.url).searchParams;
148
+ const { credential } = await exchangeOAuthPkce({
149
+ code: params.get('code') ?? '',
150
+ state: params.get('state') ?? '',
151
+ iss: params.get('iss') ?? undefined,
152
+ redirectUri,
153
+ signingSecret: secret,
154
+ sessionBinding: sessionId,
155
+ });
156
+ const saved = (await credentialStore.load(sessionId)) ?? {};
157
+ await credentialStore.save(sessionId, { ...saved, tracker: credential });
158
+ // That page runs `notifyOAuthComplete('tracker')` from `@theoremjs/react`.
159
+ return Response.redirect(new URL('/oauth/done?slot=tracker', request.url), 303);
160
+ }
161
+ ```
162
+
163
+ A host with its own `session` resolver passes its own session id instead of
164
+ `theoremSessionId`. For voice, the relay you host receives a typed key as `secret`
165
+ on the `executeTool` message: save it with `credentialFromTypedSecret` (from
166
+ `@theoremjs/agents/kernel`) under the gate's slot, and pass a
167
+ `ToolCredentialSource` over your store as `credentials` on every `executeTool`.
168
+
169
+ ## Composer pending intents
170
+
171
+ `src/interface/` owns stash / queue / steer list ops and the action matrix.
172
+ This package wires AbortSignal, the pending bar, and playground turn/steer HTTP.
173
+
174
+ | Phase | Empty composer | Filled composer |
175
+ | --- | --- | --- |
176
+ | idle | disabled | Send (+ Stash menu) |
177
+ | streaming | Stop | Queue (+ Steer / Send now / Stash) |
178
+ | gated | disabled | Queue (+ Send now / Stash; no Steer) |
179
+
180
+ Enter matches the primary action. No keyboard shortcuts for stash/steer.
181
+
182
+ Send now while gated walks away from every waiting gate in the message's own request
183
+ (`walkAway` on `streamInterfaceDraftTurn`): the host settles each call cancelled, the
184
+ paused reply commits, and the message's reply follows in the same stream. The message
185
+ leaves the composer only once it posts; if the request fails first, the reply still waits.
186
+ An answer at a gate that fails before its call settles leaves the gate to answer again,
187
+ the failure in the composer; one that fails after keeps the call's result. The gate cards
188
+ show the answer on its way from their owner (`useTheoremChat().answering`, passed as
189
+ `ChatTranscript`'s `answering`, `ApprovalCard`'s `decided`, `AuthChallengeCard`'s
190
+ `submitted`), so a failed answer brings the gate's actions back. Steer POSTs use the Cache API
191
+ on Cloudflare (process Map locally) so mid-turn injects work across isolates.
192
+ Live sessions key the same inbox by `sessionId` from relay `ready`.
193
+
194
+ A chat can be kept and resumed: `onChatChange` (or `useTheoremChat`'s `onChange`) reports the
195
+ `{ blocks, session }` each time the conversation comes to rest (a turn finished, a message
196
+ added or removed; never while a reply streams or waits on a gate), and `initialChat` (`initial`)
197
+ resumes from one, the session carrying what the next turn is sent with. `chatRef` (`sendText`
198
+ on the hook) sends a message as the composer would and resolves with the blocks the turn added
199
+ once its reply is done, or `null` while a reply streams or waits on a gate.
200
+
201
+ Pending rows show attachment / voice previews, text, **Queue** (stash → queue),
202
+ and **Send now**. Clicking the text restores the full draft (text + files + voice)
203
+ into the composer; if the composer already had a payload, that payload is re-stashed.
204
+
205
+ ## Wording
206
+
207
+ The headless layer (hooks, client, components, server handler) writes no
208
+ English. It hands the builder kinds, codes, and states:
209
+
210
+ - Failures are `ClientFailure` (`{ error, errorKind, errorInternal? }`). `error`
211
+ is the profile's wording (`iface.lexicon`, resolved on the host); show it to
212
+ the user. `errorKind` and `errorInternal` are for the builder.
213
+ - Attachment problems are `AttachmentValidationIssue`s; word one with
214
+ `attachmentIssueText(issue, iface.lexicon)` from `@theoremjs/agents`.
215
+ - A Live call the provider ended after warning it would is not a failure:
216
+ `LiveSessionClient`'s `onSessionEnded(session)` gives `session.message`, the
217
+ profile's `live.session_ended` wording, and `session.ended` (close code,
218
+ timing, the code's kind) for the builder.
219
+ - Chrome is semantic: `liveState`, `workStatus`, drawer `parts`, hint `id`.
220
+
221
+ `@theoremjs/react/ui` is the default UI. Every line it shows is an Astryx i18n
222
+ message: Theorem's under `@theorem.*` keys (`THEOREM_UI_CATALOG`, each with a
223
+ description and the ICU values it takes), Astryx's own under `@astryx.*`. The
224
+ builder owns all of them; `labels` replaces any line, per locale:
225
+
226
+ ```tsx
227
+ <TheoremChat
228
+ labels={{
229
+ en: {
230
+ '@theorem.chat.greeting': 'What shall we plan?',
231
+ '@theorem.composer.placeholder': 'Write to @{handle}',
232
+ '@astryx.chatSendButton.send': 'Go',
233
+ },
234
+ de: { '@theorem.chat.greeting': 'Woran arbeiten wir?' },
235
+ }}
236
+ />
237
+ ```
238
+
239
+ `LiveRunner` takes the same prop. The locale is the host's Astryx
240
+ `InternationalizationProvider` locale (`en` without one); a host's own Astryx
241
+ `messages` and `overrides` also apply, and win over the defaults. Labels are
242
+ checked on mount: an unknown `@theorem.*` key, a message that is not valid ICU,
243
+ a value the line is not given, or a key outside `@theorem.*` / `@astryx.*`
244
+ throws, naming the locale and key. Label values render as text, never HTML.
245
+
246
+ A builder with their own UI replaces all of it.
247
+ `scripts/docs-truth/copy-lint.mjs` keeps prose out of the headless directories.
@@ -0,0 +1,11 @@
1
+ /** Hover-card geometry for composer attachment pills (model-sculpt agent_chat parity). */
2
+ export type AttachPreviewStyle = {
3
+ left: number;
4
+ top?: number;
5
+ bottom?: number;
6
+ };
7
+ export declare function formatAttachmentSize(bytes?: number): string;
8
+ export declare function resolveAttachPreviewStyle(anchor: Pick<DOMRect, 'left' | 'top' | 'bottom'>, viewport?: {
9
+ width: number;
10
+ height: number;
11
+ }): AttachPreviewStyle;
@@ -0,0 +1,36 @@
1
+ /** Hover-card geometry for composer attachment pills (model-sculpt agent_chat parity). */
2
+ const ATTACH_PREVIEW_WIDTH_PX = 240;
3
+ const ATTACH_PREVIEW_OFFSET_PX = 6;
4
+ const ATTACH_PREVIEW_VIEWPORT_PAD_PX = 8;
5
+ /** Prefer anchoring above the chip when this much space exists. */
6
+ const ATTACH_PREVIEW_ABOVE_THRESHOLD_PX = 190;
7
+ export function formatAttachmentSize(bytes) {
8
+ if (!bytes || bytes <= 0)
9
+ return '';
10
+ if (bytes < 1024)
11
+ return `${String(bytes)} B`;
12
+ if (bytes < 1024 * 1024)
13
+ return `${(bytes / 1024).toFixed(1)} KB`;
14
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
15
+ }
16
+ function clampAttachPreviewLeft(anchorLeft, viewportWidth) {
17
+ const maxLeft = Math.max(ATTACH_PREVIEW_VIEWPORT_PAD_PX, viewportWidth - ATTACH_PREVIEW_WIDTH_PX - ATTACH_PREVIEW_VIEWPORT_PAD_PX);
18
+ return Math.min(Math.max(ATTACH_PREVIEW_VIEWPORT_PAD_PX, anchorLeft), maxLeft);
19
+ }
20
+ export function resolveAttachPreviewStyle(anchor, viewport = {
21
+ width: globalThis.innerWidth ?? 0,
22
+ height: globalThis.innerHeight ?? 0,
23
+ }) {
24
+ const left = clampAttachPreviewLeft(anchor.left, viewport.width);
25
+ const hasRoomAbove = anchor.top > ATTACH_PREVIEW_ABOVE_THRESHOLD_PX;
26
+ if (hasRoomAbove) {
27
+ return {
28
+ left,
29
+ bottom: Math.max(ATTACH_PREVIEW_VIEWPORT_PAD_PX, viewport.height - anchor.top + ATTACH_PREVIEW_OFFSET_PX),
30
+ };
31
+ }
32
+ return {
33
+ left,
34
+ top: Math.min(viewport.height - ATTACH_PREVIEW_VIEWPORT_PAD_PX, anchor.bottom + ATTACH_PREVIEW_OFFSET_PX),
35
+ };
36
+ }
@@ -0,0 +1,5 @@
1
+ /** Shared mic RMS → waveform level scaling for live and turn-composer paths. */
2
+ export declare const INPUT_LEVEL_GAIN = 4;
3
+ export declare function float32Rms(samples: Float32Array): number;
4
+ export declare function float32RmsToLevel(samples: Float32Array): number;
5
+ export declare function timeDomainBytesToLevel(samples: Uint8Array<ArrayBufferLike>): number;
@@ -0,0 +1,24 @@
1
+ /** Shared mic RMS → waveform level scaling for live and turn-composer paths. */
2
+ export const INPUT_LEVEL_GAIN = 4;
3
+ export function float32Rms(samples) {
4
+ let sum = 0;
5
+ for (const sample of samples) {
6
+ sum += sample * sample;
7
+ }
8
+ if (samples.length === 0)
9
+ return 0;
10
+ return Math.sqrt(sum / samples.length);
11
+ }
12
+ export function float32RmsToLevel(samples) {
13
+ return Math.min(1, float32Rms(samples) * INPUT_LEVEL_GAIN);
14
+ }
15
+ export function timeDomainBytesToLevel(samples) {
16
+ let sum = 0;
17
+ for (const sample of samples) {
18
+ const normalized = (sample - 128) / 128;
19
+ sum += normalized * normalized;
20
+ }
21
+ if (samples.length === 0)
22
+ return 0;
23
+ return Math.min(1, Math.sqrt(sum / samples.length) * INPUT_LEVEL_GAIN);
24
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Stage file picks against profile `maxFiles` (shared with voice). Excess
3
+ * incoming files are dropped; the limit hit is an issue the caller words
4
+ * (`attachmentIssueText`).
5
+ */
6
+ import type { AttachmentValidationIssue } from '@theoremjs/agents/interface';
7
+ export type StageComposerFilesArgs = {
8
+ existing: readonly File[];
9
+ incoming: readonly File[];
10
+ /** Profile `inputs.maxFiles`. When omitted, no cap. */
11
+ maxFiles?: number;
12
+ /** Staged voice notes that share the same maxFiles budget. */
13
+ voiceCount?: number;
14
+ };
15
+ export type StageComposerFilesResult = {
16
+ files: File[];
17
+ dropped: number;
18
+ /** `too_many_files` when the cap dropped a file. */
19
+ issues: AttachmentValidationIssue[];
20
+ };
21
+ /** Append `incoming` to `existing`, dropping overflow past maxFiles − voice. */
22
+ export declare function stageComposerFiles(args: StageComposerFilesArgs): StageComposerFilesResult;
23
+ /**
24
+ * Whether a new voice note fits under maxFiles.
25
+ * Recording clears any prior staged voice first, so only `fileCount` matters.
26
+ */
27
+ export declare function canStageVoice(args: {
28
+ fileCount: number;
29
+ maxFiles?: number;
30
+ }): boolean;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Stage file picks against profile `maxFiles` (shared with voice). Excess
3
+ * incoming files are dropped; the limit hit is an issue the caller words
4
+ * (`attachmentIssueText`).
5
+ */
6
+ /** Append `incoming` to `existing`, dropping overflow past maxFiles − voice. */
7
+ export function stageComposerFiles(args) {
8
+ const { existing, incoming, maxFiles } = args;
9
+ const voiceCount = Math.max(0, args.voiceCount ?? 0);
10
+ if (maxFiles === undefined || !Number.isFinite(maxFiles) || maxFiles < 0) {
11
+ return { files: [...existing, ...incoming], dropped: 0, issues: [] };
12
+ }
13
+ const room = Math.max(0, maxFiles - voiceCount - existing.length);
14
+ if (incoming.length <= room) {
15
+ return { files: [...existing, ...incoming], dropped: 0, issues: [] };
16
+ }
17
+ const kept = room === 0 ? [] : incoming.slice(0, room);
18
+ return {
19
+ files: [...existing, ...kept],
20
+ dropped: incoming.length - kept.length,
21
+ issues: [{ code: 'too_many_files', params: { maxFiles } }],
22
+ };
23
+ }
24
+ /**
25
+ * Whether a new voice note fits under maxFiles.
26
+ * Recording clears any prior staged voice first, so only `fileCount` matters.
27
+ */
28
+ export function canStageVoice(args) {
29
+ const maxFiles = args.maxFiles;
30
+ if (maxFiles === undefined || !Number.isFinite(maxFiles))
31
+ return true;
32
+ return args.fileCount < maxFiles;
33
+ }
@@ -0,0 +1,19 @@
1
+ import type { ComposerPendingMessage } from '@theoremjs/agents/interface';
2
+ /** What waits in the composer, by kind; the UI words each kind. */
3
+ export type ComposerDrawerKind = 'steer' | 'queue' | 'stash' | 'attached';
4
+ /** Collapsed-drawer header: the total, then each waiting kind's count, in display order. */
5
+ export type ComposerDrawerSummary = {
6
+ count: number;
7
+ parts: {
8
+ kind: ComposerDrawerKind;
9
+ n: number;
10
+ }[];
11
+ };
12
+ /**
13
+ * What's waiting in the composer, by kind: steering, queued, stashed, attached
14
+ * (files and voice notes). Null when nothing waits.
15
+ */
16
+ export declare function composerDrawerSummary(args: {
17
+ pendingMessages: readonly Pick<ComposerPendingMessage, 'kind'>[];
18
+ attachmentCount: number;
19
+ }): ComposerDrawerSummary | null;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What's waiting in the composer, by kind: steering, queued, stashed, attached
3
+ * (files and voice notes). Null when nothing waits.
4
+ */
5
+ export function composerDrawerSummary(args) {
6
+ const countOf = (kind) => args.pendingMessages.filter((message) => message.kind === kind).length;
7
+ const parts = [
8
+ { kind: 'steer', n: countOf('steer') },
9
+ { kind: 'queue', n: countOf('queue') },
10
+ { kind: 'stash', n: countOf('stash') },
11
+ { kind: 'attached', n: args.attachmentCount },
12
+ ].filter((part) => part.n > 0);
13
+ const count = parts.reduce((sum, part) => sum + part.n, 0);
14
+ return count === 0 ? null : { count, parts };
15
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Composer hints — ephemeral guidance about the current composer interaction
3
+ * (not pending-message state). Mirrors seance's `stash-selected-draft` hint.
4
+ */
5
+ /** Stash shortcut in Astryx `Kbd` notation (`mod` is ⌘ on macOS, Ctrl elsewhere). */
6
+ export declare const STASH_SHORTCUT = "mod+shift+s";
7
+ /** Which hint applies, and its shortcut; the UI words it. */
8
+ export type ComposerHint = {
9
+ id: 'stash-selected-draft';
10
+ shortcut: string;
11
+ };
12
+ /** Whether a keydown is the stash shortcut (⌘⇧S / Ctrl+Shift+S). */
13
+ export declare function isStashShortcut(event: {
14
+ code: string;
15
+ metaKey: boolean;
16
+ ctrlKey: boolean;
17
+ shiftKey: boolean;
18
+ altKey: boolean;
19
+ }): boolean;
20
+ /** The stash hint — shown while the whole draft is selected in the focused input and it can be stashed. */
21
+ export declare function resolveComposerHint(args: {
22
+ draftText: string;
23
+ selectedText: string;
24
+ canStash: boolean;
25
+ }): ComposerHint | null;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Composer hints — ephemeral guidance about the current composer interaction
3
+ * (not pending-message state). Mirrors seance's `stash-selected-draft` hint.
4
+ */
5
+ /** Stash shortcut in Astryx `Kbd` notation (`mod` is ⌘ on macOS, Ctrl elsewhere). */
6
+ export const STASH_SHORTCUT = 'mod+shift+s';
7
+ /** Whether a keydown is the stash shortcut (⌘⇧S / Ctrl+Shift+S). */
8
+ export function isStashShortcut(event) {
9
+ return event.code === 'KeyS' && event.shiftKey && !event.altKey && (event.metaKey || event.ctrlKey);
10
+ }
11
+ /** The stash hint — shown while the whole draft is selected in the focused input and it can be stashed. */
12
+ export function resolveComposerHint(args) {
13
+ const draft = args.draftText.trim();
14
+ if (!args.canStash || !draft || args.selectedText.trim() !== draft)
15
+ return null;
16
+ return {
17
+ id: 'stash-selected-draft',
18
+ shortcut: STASH_SHORTCUT,
19
+ };
20
+ }
@@ -0,0 +1,16 @@
1
+ import { type ComposerMenuAction, type ComposerPrimaryAction, type ComposerProfileInterface, type ComposerRunPhase } from '@theoremjs/agents/interface';
2
+ export type ComposerActionState = {
3
+ primary: ComposerPrimaryAction;
4
+ menuActions: ComposerMenuAction[];
5
+ /** Send is off while recording, with nothing to act on, or with an empty draft to send or queue. */
6
+ primaryDisabled: boolean;
7
+ };
8
+ /** The composer's primary button and send menu for the current draft and run phase. */
9
+ export declare function composerActionState(args: {
10
+ iface: ComposerProfileInterface;
11
+ phase: ComposerRunPhase;
12
+ draftText: string;
13
+ pendingFiles: readonly File[];
14
+ pendingVoice: readonly File[];
15
+ recording: boolean;
16
+ }): ComposerActionState;
@@ -0,0 +1,25 @@
1
+ import { resolveComposerMenuActions, resolveComposerPrimary, userDraftHasPayload, } from '@theoremjs/agents/interface';
2
+ function fileSpecs(files) {
3
+ return files.map((file) => ({
4
+ name: file.name,
5
+ mimeType: file.type || 'application/octet-stream',
6
+ sizeBytes: file.size,
7
+ }));
8
+ }
9
+ /** The composer's primary button and send menu for the current draft and run phase. */
10
+ export function composerActionState(args) {
11
+ const allowSteering = 'allowSteering' in args.iface && Boolean(args.iface.allowSteering);
12
+ const hasPayload = userDraftHasPayload({
13
+ ...(args.draftText.trim() ? { text: args.draftText } : {}),
14
+ ...(args.pendingFiles.length ? { attachments: fileSpecs(args.pendingFiles) } : {}),
15
+ ...(args.pendingVoice.length ? { voice: fileSpecs(args.pendingVoice) } : {}),
16
+ });
17
+ const context = { phase: args.phase, hasPayload, allowSteering };
18
+ const primary = resolveComposerPrimary(context);
19
+ const needsPayload = primary === 'send' || primary === 'queue';
20
+ return {
21
+ primary,
22
+ menuActions: resolveComposerMenuActions(context),
23
+ primaryDisabled: args.recording || primary === 'none' || (needsPayload && !hasPayload),
24
+ };
25
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The browser side of a decision profile: what the host lets the page see of
3
+ * it, and the one call that asks its questions about some state.
4
+ *
5
+ * @module
6
+ */
7
+ import { type DecisionJson, type DecisionProfile, type DecisionQuestion, type DecisionResult, type TraceRecord } from '@theoremjs/agents';
8
+ import type { TraceFeed } from './trace-feed.ts';
9
+ import { type HttpOptions } from './transport.ts';
10
+ /** One question as the page sees it: its answer type and the names of its options or levels. Instructions stay on the host. */
11
+ export type DecisionQuestionView = {
12
+ id: string;
13
+ type: DecisionQuestion['type'];
14
+ /** A choice's labels, a score's levels from 0 up; empty for a number. */
15
+ labels: string[];
16
+ };
17
+ /** What a decision profile shows the page. */
18
+ export type DecisionInterface = {
19
+ type: 'decision';
20
+ identity: {
21
+ handle: string;
22
+ };
23
+ contract: string;
24
+ maxStateBytes?: number;
25
+ questions: DecisionQuestionView[];
26
+ observability?: {
27
+ record: boolean;
28
+ };
29
+ };
30
+ /** The page's view of a decision profile and the questions its host asks. */
31
+ export declare function decisionInterface(profile: Pick<DecisionProfile, 'identity' | 'decision' | 'inputs' | 'observability'>, questions: Record<string, DecisionQuestion>): DecisionInterface;
32
+ /** A decision's reply: the answers, and the trace records a host that delivers them sent back. */
33
+ export type DecisionReply = {
34
+ result: DecisionResult;
35
+ traces?: TraceRecord[];
36
+ };
37
+ export interface DecisionTransport {
38
+ describe(signal?: AbortSignal): Promise<DecisionInterface>;
39
+ decide(state: Exclude<DecisionJson, null>, signal?: AbortSignal): Promise<DecisionReply>;
40
+ /** Where the host's trace records land, for the inspector. */
41
+ traces?: TraceFeed;
42
+ }
43
+ /** A host's decide reply, checked: the answers, and any trace records it delivers. */
44
+ export declare function readDecisionReply(raw: unknown): DecisionReply;
45
+ /** Transport for a host mounted with `createTheoremDecisionHandler`. Default endpoint `/api/decision`. */
46
+ export declare function createDecisionTransport(options?: HttpOptions & {
47
+ endpoint?: string;
48
+ }): DecisionTransport;