@theoremjs/react 0.3.4-next.679.gcdf1a3a → 0.3.4-next.711.g78e504a

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.
package/README.md CHANGED
@@ -369,6 +369,8 @@ Three more props give the call values from the page. The profile declares each o
369
369
  | `context` | `inputs.context` with `from: ['client']` | Any JSON the agent should know, such as the page the person is on. |
370
370
  | `pageTools` | A function tool with `answeredBy: 'page'` | A function for each page tool, by name. It returns `{ output }`. |
371
371
 
372
+ `stated` is a fourth prop, for a page you trust as yourself. It says the `path` and `conditions` that choose the call's prompt lines, and the server reads it only with `pageStatesTurn` set. A change during a call takes the call up again, as a slot does. `TheoremChat` takes the same prop, on each turn.
373
+
372
374
  ```tsx
373
375
  <LiveRunner
374
376
  iface={iface}
@@ -10,7 +10,7 @@ import { type JsonSchema } from '@theoremjs/web/client';
10
10
  export type ToolCall = Extract<TranscriptBlock, {
11
11
  kind: 'tool';
12
12
  }>['tool'];
13
- export type HostCallStatus = 'running' | 'gate' | 'complete' | 'error' | 'cancel';
13
+ export type HostCallStatus = 'running' | 'gate' | 'background' | 'complete' | 'error' | 'cancel';
14
14
  /** What a call's standing reads from. */
15
15
  export type HostCallState = {
16
16
  call: ToolCall | null;
@@ -7,6 +7,7 @@
7
7
  import { json, sampleFromSchema } from '@theoremjs/web/client';
8
8
  const SETTLED = new Set([
9
9
  'gate',
10
+ 'background',
10
11
  'complete',
11
12
  'error',
12
13
  'cancel',
@@ -311,6 +311,9 @@ const STORED_FORMATS = new Set([
311
311
  function isStringList(value) {
312
312
  return Array.isArray(value) && value.every((item) => typeof item === 'string');
313
313
  }
314
+ function isNumberList(value) {
315
+ return Array.isArray(value) && value.every((item) => typeof item === 'number');
316
+ }
314
317
  /** An object, or a list of objects, as items to read by a catalog entry's fields. */
315
318
  function fieldItems(value) {
316
319
  if (isRecord(value))
@@ -326,6 +329,9 @@ export function traceValueShape(meta, value) {
326
329
  return { kind: 'scalar', value };
327
330
  if (isStringList(value))
328
331
  return { kind: 'list', value };
332
+ if (meta?.format === 'list' && isNumberList(value)) {
333
+ return { kind: 'list', value: value.map(String) };
334
+ }
329
335
  const items = meta?.fields ? fieldItems(value) : undefined;
330
336
  return meta?.fields && items
331
337
  ? { kind: 'fields', fields: meta.fields, items }
@@ -5,7 +5,7 @@ import { type LiveCaptionState, type LiveCaptionTurn } from '../../client/live/l
5
5
  * Owns LiveRunner state, session client, and stage callbacks.
6
6
  * `connection` resolves what the call opens: a host with a fixed profile returns
7
7
  * its id; the studio returns its draft as the relay's open message. `options`
8
- * are the host's part in the call: its slots, the page's context, and its page tools.
8
+ * are the host's part in the call: its slots, its stated turn, the page's context, and its page tools.
9
9
  */
10
10
  export declare function useLiveRunnerModel(iface: LiveProfileInterface, connection: () => LiveConnection | Promise<LiveConnection>, options?: LiveCallOptions): {
11
11
  handle: string;
@@ -53,7 +53,7 @@ function useLiveCallLifecycle(controls, captionsRef, sessionActive, videoAvailab
53
53
  * Owns LiveRunner state, session client, and stage callbacks.
54
54
  * `connection` resolves what the call opens: a host with a fixed profile returns
55
55
  * its id; the studio returns its draft as the relay's open message. `options`
56
- * are the host's part in the call: its slots, the page's context, and its page tools.
56
+ * are the host's part in the call: its slots, its stated turn, the page's context, and its page tools.
57
57
  */
58
58
  export function useLiveRunnerModel(iface, connection, options = {}) {
59
59
  const ui = useLiveRunnerUiState(iface.labels);
@@ -78,6 +78,7 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
78
78
  const traces = useMemo(createTraceFeed, []);
79
79
  const { clientRef, ensureClient, clearClient } = useLiveSessionClient({
80
80
  slots: options.slots,
81
+ stated: options.stated,
81
82
  context: options.context,
82
83
  pageTools: options.pageTools,
83
84
  voiceAvailable,
@@ -148,6 +149,11 @@ export function useLiveRunnerModel(iface, connection, options = {}) {
148
149
  useEffect(() => {
149
150
  clientRef.current?.setSlots(slotsJson === undefined ? undefined : JSON.parse(slotsJson));
150
151
  }, [clientRef, slotsJson]);
152
+ // why: As slots: a changed pick takes an open call up again with the lines it chooses.
153
+ const statedJson = JSON.stringify(options.stated);
154
+ useEffect(() => {
155
+ clientRef.current?.setStated(statedJson === undefined ? undefined : JSON.parse(statedJson));
156
+ }, [clientRef, statedJson]);
151
157
  usePageToolWarnings(iface.tools.page, options.pageTools);
152
158
  return {
153
159
  handle: iface.identity.handle,
@@ -53,6 +53,7 @@ export function useLiveSessionClient(bindings) {
53
53
  const client = new LiveSessionClient({
54
54
  ...connection,
55
55
  slots: b.slots,
56
+ stated: b.stated,
56
57
  context: b.context,
57
58
  voiceIngress: b.voiceAvailable,
58
59
  onConnectPhase: (phase) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/react",
3
- "version": "0.3.4-next.679.gcdf1a3a",
3
+ "version": "0.3.4-next.711.g78e504a",
4
4
  "description": "React bindings for Theorem: headless hooks + transport, an Astryx chat UI, and a Web-standard server handler.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,8 +60,8 @@
60
60
  "@stylexjs/stylex": "0.19.0",
61
61
  "react": "^19.0.0",
62
62
  "react-dom": "^19.0.0",
63
- "@theoremjs/agents": "^0.3.4-next.679.gcdf1a3a",
64
- "@theoremjs/web": "^0.3.4-next.679.gcdf1a3a"
63
+ "@theoremjs/agents": "^0.3.4-next.711.g78e504a",
64
+ "@theoremjs/web": "^0.3.4-next.711.g78e504a"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@astryxdesign/core": {
@@ -464,6 +464,7 @@ function toolCallItem(t, id, tool, showUsage) {
464
464
  resultDetail: toolDetail(t, tool, state.output === undefined ? undefined : (_jsx(ShapedData, { value: state.output, title: t('@theorem.transcript.tool_output') }))),
465
465
  };
466
466
  case 'running':
467
+ case 'background':
467
468
  return { ...base, status: 'running', resultDetail: toolDetail(t, tool) };
468
469
  default:
469
470
  return { ...base, status: 'pending', resultDetail: toolDetail(t, tool) };
package/ui/LiveRunner.js CHANGED
@@ -38,9 +38,9 @@ const WAVE_HEIGHT = 320;
38
38
  export function LiveRunner({ theme, mode, labels, ...props }) {
39
39
  return (_jsx(TheoremThemeProvider, { theme: theme, mode: mode, children: _jsx(TheoremLabelsProvider, { labels: labels, children: _jsx(LiveRunnerBody, { ...props }) }) }));
40
40
  }
41
- function LiveRunnerBody({ iface, connection, trace, slots, context, pageTools, }) {
41
+ function LiveRunnerBody({ iface, connection, trace, slots, stated, context, pageTools, }) {
42
42
  const t = useLabels();
43
- const model = useLiveRunnerModel(iface, connection, { slots, context, pageTools });
43
+ const model = useLiveRunnerModel(iface, connection, { slots, stated, context, pageTools });
44
44
  // why: Both panels size against the whole live layout, so a third means the same for each.
45
45
  const layoutRef = useRef(null);
46
46
  const inspector = useTraceInspector(iface, model.traces, trace);
@@ -1,6 +1,6 @@
1
1
  import type { DefinedTheme } from '@astryxdesign/core/theme';
2
2
  import { type ComposerPendingMessage } from '@theoremjs/agents/interface';
3
- import type { ChatController, PageTools } from '@theoremjs/web/client';
3
+ import type { ChatController, PageTools, StatedTurn } from '@theoremjs/web/client';
4
4
  import { type ConversationView, type HttpTransportOptions, type TheoremTransport } from '@theoremjs/web/client';
5
5
  import { type ReactNode, type Ref } from 'react';
6
6
  import { type SendTextOptions, type SentTurn, type UseTheoremChatOptions } from '../hooks/use-theorem-chat.ts';
@@ -35,6 +35,8 @@ export type TheoremChatProps = {
35
35
  slots?: Record<string, string>;
36
36
  /** Told each time the visitor chooses a slot in the composer, with every slot's value. */
37
37
  onSlotsChange?: (slots: Record<string, string>) => void;
38
+ /** Which prompt lines each turn gets, for a host that lets its page choose (`pageStatesTurn`). */
39
+ stated?: StatedTurn;
38
40
  /**
39
41
  * What the page wants the agent to know: any JSON. The latest value goes with every
40
42
  * turn, and the profile's `inputs.context.from` must list `client`.
@@ -48,7 +50,7 @@ export type TheoremChatProps = {
48
50
  /** Every event of every turn, as it arrives: also the ones the chat shows nothing for. */
49
51
  onTurnEvent?: UseTheoremChatOptions['onTurnEvent'];
50
52
  placeholder?: string;
51
- /** Shown above the centred composer before the first message. Default: the agent's handle and a prompt. */
53
+ /** Shown above the centred composer before the first message. Default: the agent's handle and a prompt; `null` shows nothing. */
52
54
  emptyState?: ReactNode;
53
55
  density?: 'compact' | 'balanced' | 'spacious';
54
56
  /**
package/ui/TheoremChat.js CHANGED
@@ -51,7 +51,7 @@ function ChatComposerForChat({ chat, iface, placeholder, inputRef, }) {
51
51
  void chat.handleSendNow(message);
52
52
  } }));
53
53
  }
54
- function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth = DEFAULT_CHAT_MAX_WIDTH, scrollRef, trace, usage, className, style, conversation, conversationView, initialText, initialFiles, initialVoice, initialPending, onConversationChange, chatRef, composer: hasComposer = true, slots, onSlotsChange, context, pageTools, onTurnEvent, }) {
54
+ function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth = DEFAULT_CHAT_MAX_WIDTH, scrollRef, trace, usage, className, style, conversation, conversationView, initialText, initialFiles, initialVoice, initialPending, onConversationChange, chatRef, composer: hasComposer = true, slots, onSlotsChange, stated, context, pageTools, onTurnEvent, }) {
55
55
  const t = useLabels();
56
56
  const chat = useTheoremChat({
57
57
  transport,
@@ -65,6 +65,7 @@ function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth
65
65
  onConversationChange,
66
66
  slots,
67
67
  onSlotsChange,
68
+ stated,
68
69
  context,
69
70
  pageTools,
70
71
  onTurnEvent,
@@ -97,7 +98,7 @@ const CHAT_SCROLL = { overscrollBehavior: 'none' };
97
98
  */
98
99
  function ChatLanding({ composer, composerRef, maxWidth, handle, emptyState, }) {
99
100
  const t = useLabels();
100
- return (_jsxs(VStack, { minHeight: "100%", vAlign: "center", gap: 8, paddingInline: 3, children: [_jsx(ChatColumn, { maxWidth: maxWidth, children: emptyState ?? (_jsxs(VStack, { gap: 1, children: [_jsx(Text, { type: "supporting", as: "h2", children: handle }), _jsx(Text, { type: "display-2", as: "h1", children: t('@theorem.chat.greeting') })] })) }), composer && (_jsx(ChatColumn, { ref: composerRef, maxWidth: maxWidth, children: composer }))] }));
101
+ return (_jsxs(VStack, { minHeight: "100%", vAlign: "center", gap: 8, paddingInline: 3, children: [_jsx(ChatColumn, { maxWidth: maxWidth, children: emptyState === undefined ? (_jsxs(VStack, { gap: 1, children: [_jsx(Text, { type: "supporting", as: "h2", children: handle }), _jsx(Text, { type: "display-2", as: "h1", children: t('@theorem.chat.greeting') })] })) : (emptyState) }), composer && (_jsx(ChatColumn, { ref: composerRef, maxWidth: maxWidth, children: composer }))] }));
101
102
  }
102
103
  /**
103
104
  * The chat once it has messages: the transcript, with the composer in ChatLayout's own dock.
@@ -105,15 +106,15 @@ function ChatLanding({ composer, composerRef, maxWidth, handle, emptyState, }) {
105
106
  * bottom margin.
106
107
  */
107
108
  function ChatThread({ composer, composerRef, maxWidth, chat, iface, blocks, handle, density, scrollRef, usage, }) {
108
- return (_jsx(VStack, { height: "100%", children: _jsx(ChatLayout, { density: density, scrollRef: scrollRef,
109
- // why: Scrolling past either end does not bounce the transcript and its docked composer.
110
- style: CHAT_SCROLL, composer: composer && (_jsx(VStack, { paddingBlockEnd: 3, children: _jsx(ChatColumn, { ref: composerRef, maxWidth: maxWidth, children: composer }) })), children: _jsx(ChatColumn, { maxWidth: maxWidth, children: _jsx(ChatTranscript, { blocks: blocks, handle: handle, streaming: chat.streaming, delivery: chat.delivery, answering: chat.answering, usage: usage, labels: iface.labels, imageOutput: iface.type === 'image'
111
- ? { ratio: parseAspectRatio(iface.image.aspectRatio) }
112
- : undefined, onToolDecision: (index, action) => {
113
- void chat.handleToolDecision(index, action);
114
- }, onAuthenticated: (index, secret) => {
115
- void chat.handleAuthenticated(index, secret);
116
- } }) }) }) }));
109
+ return (_jsxs(VStack, { height: "100%", children: [_jsx("style", { href: "theorem-chat-thread", precedence: "default", children: '.theorem-chat-thread > :last-child > div:empty { display: none; }' }), _jsx(ChatLayout, { className: "theorem-chat-thread", density: density, scrollRef: scrollRef,
110
+ // why: Scrolling past either end does not bounce the transcript and its docked composer.
111
+ style: CHAT_SCROLL, composer: composer && (_jsx(VStack, { paddingBlockEnd: 3, children: _jsx(ChatColumn, { ref: composerRef, maxWidth: maxWidth, children: composer }) })), children: _jsx(ChatColumn, { maxWidth: maxWidth, children: _jsx(ChatTranscript, { blocks: blocks, handle: handle, streaming: chat.streaming, delivery: chat.delivery, answering: chat.answering, usage: usage, labels: iface.labels, imageOutput: iface.type === 'image'
112
+ ? { ratio: parseAspectRatio(iface.image.aspectRatio) }
113
+ : undefined, onToolDecision: (index, action) => {
114
+ void chat.handleToolDecision(index, action);
115
+ }, onAuthenticated: (index, secret) => {
116
+ void chat.handleAuthenticated(index, secret);
117
+ } }) }) })] }));
117
118
  }
118
119
  function ChatForTransport(props) {
119
120
  const t = useLabels();
package/ui/TheoremHost.js CHANGED
@@ -32,6 +32,7 @@ import { TheoremThemeProvider } from "./theme.js";
32
32
  const MAX_EARLIER = 8;
33
33
  const STATUS_DOT = {
34
34
  running: 'accent',
35
+ background: 'accent',
35
36
  gate: 'warning',
36
37
  complete: 'success',
37
38
  error: 'error',
package/ui/labels.d.ts CHANGED
@@ -903,6 +903,10 @@ export declare const THEOREM_UI_CATALOG: {
903
903
  readonly defaultMessage: "running";
904
904
  readonly description: "A tool call in progress.";
905
905
  };
906
+ readonly '@theorem.host.status.background': {
907
+ readonly defaultMessage: "in background";
908
+ readonly description: "A tool call that replied and goes on after the turn.";
909
+ };
906
910
  readonly '@theorem.host.status.gate': {
907
911
  readonly defaultMessage: "waiting";
908
912
  readonly description: "A tool call paused on approval or sign-in.";
package/ui/labels.js CHANGED
@@ -860,6 +860,10 @@ export const THEOREM_UI_CATALOG = {
860
860
  defaultMessage: 'running',
861
861
  description: 'A tool call in progress.',
862
862
  },
863
+ '@theorem.host.status.background': {
864
+ defaultMessage: 'in background',
865
+ description: 'A tool call that replied and goes on after the turn.',
866
+ },
863
867
  '@theorem.host.status.gate': {
864
868
  defaultMessage: 'waiting',
865
869
  description: 'A tool call paused on approval or sign-in.',