@theoremjs/react 0.3.4-next.649.gf83cea1 → 0.3.4-next.671.gfa09ab2

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
@@ -203,8 +203,8 @@ A profile whose `providerContinuation.history` is `'stateless'` keeps no convers
203
203
 
204
204
  ### Send a message from your code
205
205
 
206
- `chatRef` gives you `send(text, files?)`. It sends the text and optional array of browser files as the user. Files use the same profile validation and encoding as composer attachments. When the reply is complete, it returns the blocks that the turn added. It returns `null` if the chat cannot accept a message now. This happens while a reply streams or waits for a gate.
207
- `chatRef` also gives you `stop()`. It stops the reply that streams, as the composer's stop button does. And it gives you `focus()`, which puts the keyboard in the composer, for example after your own control clears the chat.
206
+ `chatRef` gives you `send(text, options?)`. It sends the text as the user. `options` can add `files` and `voice`, arrays of browser files, which use the same profile validation and encoding as composer attachments. It can also name a `model` and its `effort` for the turn. The model becomes the chat's pick, as if chosen in the composer. A model or file the profile does not take is refused, as the composer refuses it, and the refusal shows in the chat. When the reply is complete, it returns the blocks that the turn added. It returns `null` if the chat cannot accept a message now. This happens while a reply streams or waits for a gate.
207
+ `chatRef` also gives you `stop()`. It stops the reply that streams, as the composer's stop button does. It gives you `focus()`, which puts the keyboard in the composer, for example after your own control clears the chat. And it gives you `controller`, the chat's `ChatController`, for a host that drives the chat from outside, as a composer shared by several chats does.
208
208
 
209
209
  ```tsx
210
210
  const chat = useRef<TheoremChatHandle>(null);
@@ -1,4 +1,4 @@
1
- import { type SessionEventOf, type TurnEvent } from '@theoremjs/agents';
1
+ import type { SessionEventOf, TurnEvent } from '@theoremjs/agents';
2
2
  import type { LiveCallOptions, LiveConnectPhase, LiveGateAnswer, LiveSessionStatus, LiveToolGatePrompt, TraceFeed } from '@theoremjs/web/client';
3
3
  import { type LiveConnection, LiveSessionClient } from '@theoremjs/web/client';
4
4
  import { type Dispatch, type MutableRefObject, type SetStateAction } from 'react';
@@ -1,8 +1,6 @@
1
- import { TheoremError } from '@theoremjs/agents';
2
- import { LiveSessionClient } from '@theoremjs/web/client';
1
+ import { continueGatedToolInvocation, LiveSessionClient, } from '@theoremjs/web/client';
3
2
  import { useCallback, useRef, } from 'react';
4
3
  import { applyLiveTranscript } from "../../client/live/live-captions.js";
5
- import { runLiveToolCall } from "../../client/live/run-live-tool-call.js";
6
4
  function onLiveStatusChange(next, bindings) {
7
5
  bindings.setStatus(next);
8
6
  if (next === 'listening' || next === 'ready') {
@@ -26,38 +24,22 @@ function onLiveTranscript(text, isUser, meta, bindings) {
26
24
  // why: A thought or a tool call in the same batch is already queued; each folds onto the last.
27
25
  bindings.setCaptions((prev) => applyLiveTranscript(prev, text, isUser, meta?.interim));
28
26
  }
29
- async function onLiveToolCall(name, toolArgs, meta, bindings, clientRef) {
30
- bindings.setActiveTool(name);
31
- bindings.clearFailure();
32
- const client = clientRef.current;
33
- if (!client) {
34
- // labels-exempt: internal diagnostic; the user reads error.request
35
- bindings.reportFailure(new TheoremError('request', 'live tool call with no session client'));
36
- return;
37
- }
38
- try {
39
- await runLiveToolCall({
40
- pageTools: bindings.pageTools,
41
- executeToolOnRelay: (call) => client.executeToolOnRelay(call),
42
- name,
43
- toolArgs,
44
- callId: meta.callId,
45
- operationId: meta.operationId,
46
- sessionPermissions: bindings.sessionPermissionsRef.current,
47
- setSessionPermissions: (next) => {
48
- bindings.sessionPermissionsRef.current = next;
49
- bindings.setSessionPermissions(next);
50
- },
51
- waitForGateDecision: bindings.waitForGateDecision,
52
- });
53
- }
54
- catch (err) {
55
- bindings.reportFailure(err);
56
- }
57
- finally {
58
- if (!bindings.gatePromptRef.current)
59
- bindings.setActiveTool(null);
27
+ /** The user's answer to a gate, as the session takes it; an approval records the permissions it grants. */
28
+ async function onLiveGate(prompt, bindings) {
29
+ const resolution = await bindings.waitForGateDecision(prompt);
30
+ if (resolution === 'withdrawn')
31
+ return 'withdrawn';
32
+ const reply = continueGatedToolInvocation({
33
+ toolName: prompt.toolName,
34
+ gate: prompt.gate,
35
+ sessionPermissions: bindings.sessionPermissionsRef.current,
36
+ resolution,
37
+ });
38
+ if (reply.decision === 'approve') {
39
+ bindings.sessionPermissionsRef.current = reply.sessionPermissions;
40
+ bindings.setSessionPermissions(reply.sessionPermissions);
60
41
  }
42
+ return reply;
61
43
  }
62
44
  /** Owns LiveSessionClient construction + reconnect identity. */
63
45
  export function useLiveSessionClient(bindings) {
@@ -91,6 +73,9 @@ export function useLiveSessionClient(bindings) {
91
73
  bindingsRef.current.reportFailure(event.error);
92
74
  return;
93
75
  }
76
+ // why: A new call from the model clears the last failure.
77
+ if (event.type === 'tool' && event.tool.phase === undefined)
78
+ bindingsRef.current.clearFailure();
94
79
  bindingsRef.current.handleLiveTurnEvent(event);
95
80
  },
96
81
  onTrace: (record) => {
@@ -111,7 +96,11 @@ export function useLiveSessionClient(bindings) {
111
96
  onSessionEnded: (session) => {
112
97
  bindingsRef.current.reportSessionEnded(session);
113
98
  },
114
- onToolCall: (name, args, meta) => onLiveToolCall(name, args, meta, bindingsRef.current, clientRef),
99
+ // why: Read at each call, so a handler the host renders anew is the one that answers.
100
+ get pageTools() {
101
+ return bindingsRef.current.pageTools;
102
+ },
103
+ onGate: (prompt) => onLiveGate(prompt, bindingsRef.current),
115
104
  });
116
105
  clientRef.current = client;
117
106
  return client;
@@ -1,5 +1,5 @@
1
- import { type ChatOptions, type SentTurn, type ToolDecisionAction } from '@theoremjs/web/client';
2
- export type { SentTurn };
1
+ import { type ChatOptions, type SendTextOptions, type SentTurn, type ToolDecisionAction } from '@theoremjs/web/client';
2
+ export type { SendTextOptions, SentTurn };
3
3
  export type UseTheoremChatOptions = ChatOptions;
4
4
  /**
5
5
  * Headless chat model: transcript, streaming, composer drafts, pending
@@ -32,7 +32,9 @@ export declare function useTheoremChat(options: UseTheoremChatOptions): {
32
32
  modelId: string;
33
33
  effort?: string;
34
34
  }) => void;
35
- sendText: (text: string, files?: readonly File[]) => Promise<SentTurn | null>;
35
+ sendText: (text: string, options?: SendTextOptions) => Promise<SentTurn | null>;
36
+ /** The controller under it all, for a host that drives the chat from outside. */
37
+ controller: import("@theoremjs/web/client").ChatController;
36
38
  iface: import("../../../src/interface/types").ComposerProfileInterface | null;
37
39
  blocks: import("../../../src/interface/types").TranscriptBlock[];
38
40
  streamBlocks: import("../../../src/interface/types").TranscriptBlock[];
@@ -36,5 +36,7 @@ export function useTheoremChat(options) {
36
36
  handleToolDecision,
37
37
  handleGenerationChange: chat.setGeneration,
38
38
  sendText: chat.sendText,
39
+ /** The controller under it all, for a host that drives the chat from outside. */
40
+ controller: chat,
39
41
  };
40
42
  }
package/index.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export * from '@theoremjs/web/client';
11
11
  export { type EmbeddingIndex, type IndexProgress, useEmbeddingIndex, } from './hooks/use-embedding-index.ts';
12
- export { type SentTurn, type UseTheoremChatOptions, useTheoremChat, } from './hooks/use-theorem-chat.ts';
12
+ export { type SendTextOptions, type SentTurn, type UseTheoremChatOptions, useTheoremChat, } from './hooks/use-theorem-chat.ts';
13
13
  export { type TheoremDecisionState, useTheoremDecision } from './hooks/use-theorem-decision.ts';
14
14
  export { type TheoremEmbeddingState, useTheoremEmbedding, } from './hooks/use-theorem-embedding.ts';
15
15
  export { type TheoremHostCall, type TheoremHostState, useTheoremHost, } from './hooks/use-theorem-host.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/react",
3
- "version": "0.3.4-next.649.gf83cea1",
3
+ "version": "0.3.4-next.671.gfa09ab2",
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.649.gf83cea1",
64
- "@theoremjs/web": "^0.3.4-next.649.gf83cea1"
63
+ "@theoremjs/agents": "^0.3.4-next.671.gfa09ab2",
64
+ "@theoremjs/web": "^0.3.4-next.671.gfa09ab2"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@astryxdesign/core": {
@@ -1,9 +1,9 @@
1
1
  import type { DefinedTheme } from '@astryxdesign/core/theme';
2
2
  import { type ComposerPendingMessage } from '@theoremjs/agents/interface';
3
- import type { PageTools } from '@theoremjs/web/client';
3
+ import type { ChatController, PageTools } 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
- import { type SentTurn, type UseTheoremChatOptions } from '../hooks/use-theorem-chat.ts';
6
+ import { type SendTextOptions, type SentTurn, type UseTheoremChatOptions } from '../hooks/use-theorem-chat.ts';
7
7
  import type { TheoremLabels } from './labels.ts';
8
8
  export type TheoremChatProps = {
9
9
  /** Where `createTheoremHandler` is mounted. Default `/api/theorem`. Ignored when `transport` is set. */
@@ -112,14 +112,19 @@ export type TheoremChatProps = {
112
112
  /** What a host can do to a mounted {@link TheoremChat}. */
113
113
  export type TheoremChatHandle = {
114
114
  /**
115
- * Sends `text` and optional `files` as the user, and resolves with the blocks the turn added once the reply is done;
115
+ * Sends `text`, and what `options` adds, as the user, and resolves with the blocks the turn added once the reply is done;
116
116
  * `null` when the chat can't take a message now (a reply is streaming, or waits on a gate).
117
117
  */
118
- send: (text: string, files?: readonly File[]) => Promise<SentTurn | null>;
118
+ send: (text: string, options?: SendTextOptions) => Promise<SentTurn | null>;
119
119
  /** Stops the reply that is streaming, as the composer's stop button does. */
120
120
  stop: () => void;
121
121
  /** Puts the keyboard in the composer. */
122
122
  focus: () => void;
123
+ /**
124
+ * The chat's controller, for a host that drives it from outside, as a composer shared by
125
+ * several chats does. See `createChatController` in `@theoremjs/web/client`.
126
+ */
127
+ controller: ChatController;
123
128
  };
124
129
  /**
125
130
  * 50% of the chat's width, but never narrower than 640px (or the full width,
package/ui/TheoremChat.js CHANGED
@@ -69,7 +69,7 @@ function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth
69
69
  pageTools,
70
70
  onTurnEvent,
71
71
  });
72
- const { sendText, handleStop } = chat;
72
+ const { sendText, handleStop, controller } = chat;
73
73
  const blocks = useMemo(() => [...chat.blocks, ...chat.streamBlocks], [chat.blocks, chat.streamBlocks]);
74
74
  const handle = t('@theorem.agent.handle', { handle: iface.identity.handle });
75
75
  const landing = blocks.length === 0;
@@ -80,7 +80,8 @@ function ChatBody({ transport, iface, placeholder, emptyState, density, maxWidth
80
80
  focus: () => {
81
81
  inputRef.current?.focus();
82
82
  },
83
- }), [sendText, handleStop]);
83
+ controller,
84
+ }), [sendText, handleStop, controller]);
84
85
  const composerRef = useGlide(landing, () => inputRef.current?.focus());
85
86
  const inspector = useTraceInspector(iface, transport.traces, trace);
86
87
  const header = _jsx(SidePanelHeader, { floating: true, children: inspector.toggle });
@@ -1,20 +0,0 @@
1
- import type { ExecuteToolOnRelay, LiveGateAnswer, LiveToolGatePrompt, PageTools } from '@theoremjs/web/client';
2
- /**
3
- * Live provider tool-call handler: the page answers the call if it is the
4
- * page's; else run the model's call on the relay; at a gate, ask the user and
5
- * send their decision. The session answers the model
6
- * however the call ends; its tool events tell the user. A call the model
7
- * cancels while its gate is open is gone: nothing is sent.
8
- */
9
- export declare function runLiveToolCall(args: {
10
- executeToolOnRelay: ExecuteToolOnRelay;
11
- /** The host's page tools, if it has any. */
12
- pageTools?: PageTools;
13
- name: string;
14
- toolArgs: Record<string, unknown>;
15
- callId: string;
16
- operationId: string;
17
- sessionPermissions: string[];
18
- setSessionPermissions: (next: string[]) => void;
19
- waitForGateDecision: (prompt: LiveToolGatePrompt) => Promise<LiveGateAnswer>;
20
- }): Promise<void>;
@@ -1,70 +0,0 @@
1
- import { continueGatedToolInvocation } from '@theoremjs/web/client';
2
- /**
3
- * Live provider tool-call handler: the page answers the call if it is the
4
- * page's; else run the model's call on the relay; at a gate, ask the user and
5
- * send their decision. The session answers the model
6
- * however the call ends; its tool events tell the user. A call the model
7
- * cancels while its gate is open is gone: nothing is sent.
8
- */
9
- export async function runLiveToolCall(args) {
10
- const { executeToolOnRelay, name, toolArgs, callId, operationId } = args;
11
- let sessionPermissions = args.sessionPermissions;
12
- // why: A name from the model is looked up among the host's own keys only.
13
- const pageTool = args.pageTools && Object.hasOwn(args.pageTools, name) ? args.pageTools[name] : undefined;
14
- let step = await executeToolOnRelay({ callId, operationId });
15
- const dispatched = new Set();
16
- while (step.status !== 'settled') {
17
- if (step.status === 'dispatched') {
18
- if (dispatched.has(step.grantId))
19
- return;
20
- dispatched.add(step.grantId);
21
- const grantId = step.grantId;
22
- const input = step.arguments;
23
- const answer = pageTool
24
- ? await Promise.resolve()
25
- .then(() => pageTool(input, { callId, operationId }))
26
- .catch(() => undefined)
27
- : undefined;
28
- step = await executeToolOnRelay({
29
- callId,
30
- operationId,
31
- grantId,
32
- ...(answer ? { output: answer.output } : { unanswered: true }),
33
- });
34
- continue;
35
- }
36
- const { gate } = step;
37
- if (gate.kind === 'page') {
38
- await executeToolOnRelay({ callId, operationId, decision: 'deny' });
39
- return;
40
- }
41
- const resolution = await args.waitForGateDecision({
42
- callId,
43
- toolName: name,
44
- input: gate.preparedArguments ?? toolArgs,
45
- gate,
46
- });
47
- if (resolution === 'withdrawn')
48
- return;
49
- const reply = continueGatedToolInvocation({
50
- toolName: name,
51
- gate,
52
- sessionPermissions,
53
- resolution,
54
- });
55
- if (reply.decision === 'deny') {
56
- // why: The session settles the refusal; its tool event tells the user.
57
- await executeToolOnRelay({ callId, operationId, decision: 'deny' });
58
- return;
59
- }
60
- sessionPermissions = reply.sessionPermissions;
61
- args.setSessionPermissions(reply.sessionPermissions);
62
- // why: Signed in: a typed key goes once, with the approval; after an OAuth callback there is none.
63
- step = await executeToolOnRelay({
64
- callId,
65
- decision: 'approve',
66
- operationId,
67
- ...(reply.secret !== undefined ? { secret: reply.secret } : {}),
68
- });
69
- }
70
- }