@athenaintel/react 0.53.0 → 0.54.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.
- package/README.md +24 -1
- package/dist/agent-runtime/src/call-recap.cjs +2 -3
- package/dist/agent-runtime/src/call-recap.cjs.map +1 -1
- package/dist/agent-runtime/src/call-recap.d.ts +1 -2
- package/dist/agent-runtime/src/call-recap.js +1 -2
- package/dist/agent-runtime/src/call-recap.js.map +1 -1
- package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs +7 -3
- package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs.map +1 -1
- package/dist/agent-runtime/src/deep-agent-transport-recipe.d.ts +10 -2
- package/dist/agent-runtime/src/deep-agent-transport-recipe.js +8 -4
- package/dist/agent-runtime/src/deep-agent-transport-recipe.js.map +1 -1
- package/dist/agent-runtime/src/desktop-handoff.d.ts +7 -0
- package/dist/agent-runtime/src/dictation-session.d.ts +158 -0
- package/dist/agent-runtime/src/edit-command.cjs +6 -0
- package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
- package/dist/agent-runtime/src/edit-command.d.ts +8 -0
- package/dist/agent-runtime/src/edit-command.js +6 -0
- package/dist/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/agent-runtime/src/generated/runconfig-contract.cjs +6 -0
- package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
- package/dist/agent-runtime/src/generated/runconfig-contract.d.ts +13 -0
- package/dist/agent-runtime/src/generated/runconfig-contract.js +6 -0
- package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
- package/dist/agent-runtime/src/index.d.ts +6 -6
- package/dist/agent-runtime/src/message-window.cjs +111 -7
- package/dist/agent-runtime/src/message-window.cjs.map +1 -1
- package/dist/agent-runtime/src/message-window.d.ts +66 -6
- package/dist/agent-runtime/src/message-window.js +112 -8
- package/dist/agent-runtime/src/message-window.js.map +1 -1
- package/dist/agent-runtime/src/offline-status.cjs.map +1 -1
- package/dist/agent-runtime/src/offline-status.d.ts +33 -0
- package/dist/agent-runtime/src/offline-status.js.map +1 -1
- package/dist/agent-runtime/src/runtime.d.ts +4 -3
- package/dist/agent-runtime/src/subagent-transcripts.cjs +23 -1
- package/dist/agent-runtime/src/subagent-transcripts.cjs.map +1 -1
- package/dist/agent-runtime/src/subagent-transcripts.js +25 -3
- package/dist/agent-runtime/src/subagent-transcripts.js.map +1 -1
- package/dist/agent-runtime/src/subagents.cjs +112 -0
- package/dist/agent-runtime/src/subagents.cjs.map +1 -1
- package/dist/agent-runtime/src/subagents.d.ts +103 -0
- package/dist/agent-runtime/src/subagents.js +113 -1
- package/dist/agent-runtime/src/subagents.js.map +1 -1
- package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +37 -9
- package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
- package/dist/agent-runtime/src/tool-metadata.cjs +8 -0
- package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/agent-runtime/src/tool-metadata.js +8 -0
- package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-thread.cjs +6 -0
- package/dist/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-thread.js +7 -1
- package/dist/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +39 -10
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +14 -4
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +41 -12
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
- package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +5 -9
- package/dist/agent-runtime/src/voice-presentation.cjs +34 -0
- package/dist/agent-runtime/src/voice-presentation.cjs.map +1 -1
- package/dist/agent-runtime/src/voice-presentation.d.ts +35 -0
- package/dist/agent-runtime/src/voice-presentation.js +34 -0
- package/dist/agent-runtime/src/voice-presentation.js.map +1 -1
- package/dist/assets/AthenaAssetEmbed.cjs +4 -2
- package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
- package/dist/assets/AthenaAssetEmbed.d.ts +17 -8
- package/dist/assets/AthenaAssetEmbed.js +4 -2
- package/dist/assets/AthenaAssetEmbed.js.map +1 -1
- package/dist/assets/use-asset-embed.cjs.map +1 -1
- package/dist/assets/use-asset-embed.d.ts +7 -5
- package/dist/assets/use-asset-embed.js.map +1 -1
- package/dist/auth/account-identity.cjs +69 -0
- package/dist/auth/account-identity.cjs.map +1 -0
- package/dist/auth/account-identity.d.ts +57 -0
- package/dist/auth/account-identity.js +69 -0
- package/dist/auth/account-identity.js.map +1 -0
- package/dist/chat/AthenaChat.cjs +1 -1
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +1 -1
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/super-grouping/toolkit-map.cjs +1 -0
- package/dist/chat/super-grouping/toolkit-map.cjs.map +1 -1
- package/dist/chat/super-grouping/toolkit-map.js +1 -0
- package/dist/chat/super-grouping/toolkit-map.js.map +1 -1
- package/dist/chat/use-statewire-approval-request.cjs +5 -5
- package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
- package/dist/chat/use-statewire-approval-request.d.ts +7 -0
- package/dist/chat/use-statewire-approval-request.js +5 -5
- package/dist/chat/use-statewire-approval-request.js.map +1 -1
- package/dist/chat-ui/mentions/use-account-workspace-id.cjs +18 -58
- package/dist/chat-ui/mentions/use-account-workspace-id.cjs.map +1 -1
- package/dist/chat-ui/mentions/use-account-workspace-id.js +18 -58
- package/dist/chat-ui/mentions/use-account-workspace-id.js.map +1 -1
- package/dist/chat-ui/messages/voice-delegation-card.cjs +93 -25
- package/dist/chat-ui/messages/voice-delegation-card.cjs.map +1 -1
- package/dist/chat-ui/messages/voice-delegation-card.d.ts +16 -7
- package/dist/chat-ui/messages/voice-delegation-card.js +95 -27
- package/dist/chat-ui/messages/voice-delegation-card.js.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.cjs +30 -5
- package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.d.ts +13 -5
- package/dist/chat-ui/thread/handoff-cursor.js +32 -7
- package/dist/chat-ui/thread/handoff-cursor.js.map +1 -1
- package/dist/collab/client.cjs +94 -6
- package/dist/collab/client.cjs.map +1 -1
- package/dist/collab/client.d.ts +27 -0
- package/dist/collab/client.js +94 -6
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/index.d.ts +1 -1
- package/dist/keryx-http/src/initial-sync.cjs +121 -0
- package/dist/keryx-http/src/initial-sync.cjs.map +1 -0
- package/dist/keryx-http/src/initial-sync.d.ts +77 -0
- package/dist/keryx-http/src/initial-sync.js +104 -0
- package/dist/keryx-http/src/initial-sync.js.map +1 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +8 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +6 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.js +8 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/threads/api.cjs +1 -2
- package/dist/threads/api.cjs.map +1 -1
- package/dist/threads/api.d.ts +3 -2
- package/dist/threads/api.js +1 -2
- package/dist/threads/api.js.map +1 -1
- package/dist/threads/statewire-thread-list.cjs +5 -7
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.js +7 -9
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/dist/tools/subagent-task-tool-ui.cjs +15 -5
- package/dist/tools/subagent-task-tool-ui.cjs.map +1 -1
- package/dist/tools/subagent-task-tool-ui.d.ts +5 -4
- package/dist/tools/subagent-task-tool-ui.js +16 -6
- package/dist/tools/subagent-task-tool-ui.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { useAuiState } from "@assistant-ui/store";
|
|
2
2
|
import { useState, useRef, useEffect, useCallback, useMemo, useSyncExternalStore } from "react";
|
|
3
|
-
import { DEEP_AGENT_INTEREST_PROTOCOL, DEEP_AGENT_MAX_MESSAGE_WINDOW } from "./generated/runconfig-contract.js";
|
|
4
|
-
import {
|
|
3
|
+
import { DEEP_AGENT_INTEREST_PROTOCOL, DEEP_AGENT_OVERSIZED_INTEREST_CODE, DEEP_AGENT_MAX_MESSAGE_WINDOW } from "./generated/runconfig-contract.js";
|
|
4
|
+
import { messageWindowCarries, restatedInterestRequests, pageInterestRequests, readTruncatedToolOutputArtifact, buildToolOutputCommand, readToolOutputResult, readDeepAgentMessageWindow } from "./message-window.js";
|
|
5
5
|
import { hasStatewireThreadExtras } from "./thread-extras.js";
|
|
6
6
|
const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;
|
|
7
7
|
const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;
|
|
8
|
+
function isOversizedInterestRejection(error) {
|
|
9
|
+
const failure = error == null ? void 0 : error.failure;
|
|
10
|
+
return (failure == null ? void 0 : failure.fate) === "rejected" && failure.code === DEEP_AGENT_OVERSIZED_INTEREST_CODE;
|
|
11
|
+
}
|
|
8
12
|
const everyMessageMounted = () => true;
|
|
9
13
|
const defaultNow = () => Date.now();
|
|
10
14
|
const noopSubscribe = () => () => {
|
|
@@ -63,6 +67,10 @@ function useDeepAgentWindowSync({
|
|
|
63
67
|
const { sendCommand, invoke, connection } = useStatewireExtras();
|
|
64
68
|
const window = useDeepAgentMessageWindow();
|
|
65
69
|
const [loading, setLoading] = useState(false);
|
|
70
|
+
const [spentDepth, setSpentDepth] = useState(
|
|
71
|
+
null
|
|
72
|
+
);
|
|
73
|
+
const depthSpent = spentDepth !== null && window !== null && spentDepth.threadId === window.threadId && window.chain.length >= spentDepth.loaded;
|
|
66
74
|
const [requests, setRequests] = useState(0);
|
|
67
75
|
const servedRequestsRef = useRef(0);
|
|
68
76
|
const inflightRef = useRef(false);
|
|
@@ -70,7 +78,7 @@ function useDeepAgentWindowSync({
|
|
|
70
78
|
const openingFillRef = useRef(0);
|
|
71
79
|
const pageFromRef = useRef(null);
|
|
72
80
|
const aliveRef = useRef(true);
|
|
73
|
-
const
|
|
81
|
+
const heldRef = useRef(null);
|
|
74
82
|
const restatedConnectionRef = useRef(null);
|
|
75
83
|
const answeredRef = useRef(/* @__PURE__ */ new Map());
|
|
76
84
|
const toolOutputsInFlightRef = useRef(0);
|
|
@@ -89,19 +97,36 @@ function useDeepAgentWindowSync({
|
|
|
89
97
|
};
|
|
90
98
|
}, []);
|
|
91
99
|
useEffect(() => {
|
|
92
|
-
if (connection.status !== "connected" ||
|
|
93
|
-
if (
|
|
100
|
+
if (connection.status !== "connected" || window === null) return;
|
|
101
|
+
if (restatedConnectionRef.current === connection) {
|
|
102
|
+
heldRef.current = window;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
94
105
|
restatedConnectionRef.current = connection;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
const held = heldRef.current;
|
|
107
|
+
heldRef.current = window;
|
|
108
|
+
if (held === null || inflightRef.current || messageWindowCarries({ hello: window, held })) return;
|
|
109
|
+
if (held.threadId !== void 0 && window.threadId !== void 0 && held.threadId !== window.threadId) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
inflightRef.current = true;
|
|
113
|
+
setLoading(true);
|
|
114
|
+
invoke(DEEP_AGENT_INTEREST_PROTOCOL, "set", [restatedInterestRequests({ held, hello: window })]).catch((error) => {
|
|
98
115
|
var _a;
|
|
99
116
|
(_a = onErrorRef.current) == null ? void 0 : _a.call(onErrorRef, error, "athena/interest");
|
|
117
|
+
}).finally(() => {
|
|
118
|
+
inflightRef.current = false;
|
|
119
|
+
if (aliveRef.current) setLoading(false);
|
|
100
120
|
});
|
|
101
121
|
}, [connection, invoke, window]);
|
|
102
122
|
useEffect(() => {
|
|
103
123
|
var _a;
|
|
104
124
|
if (window === null || inflightRef.current) return;
|
|
125
|
+
if (depthSpent) {
|
|
126
|
+
servedRequestsRef.current = requests;
|
|
127
|
+
pageFromRef.current = null;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
105
130
|
const from = pageFromRef.current;
|
|
106
131
|
pageFromRef.current = null;
|
|
107
132
|
const lastPageUnseen = from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);
|
|
@@ -141,7 +166,6 @@ function useDeepAgentWindowSync({
|
|
|
141
166
|
const startedAt = nowRef.current();
|
|
142
167
|
invoke(DEEP_AGENT_INTEREST_PROTOCOL, "set", [stated]).then(() => {
|
|
143
168
|
var _a2;
|
|
144
|
-
statedRef.current = stated;
|
|
145
169
|
(_a2 = onHistoryLoadRef.current) == null ? void 0 : _a2.call(onHistoryLoadRef, {
|
|
146
170
|
page,
|
|
147
171
|
request,
|
|
@@ -151,6 +175,9 @@ function useDeepAgentWindowSync({
|
|
|
151
175
|
var _a2;
|
|
152
176
|
pageFromRef.current = null;
|
|
153
177
|
if (fillPage) openingFillRef.current = null;
|
|
178
|
+
if (isOversizedInterestRejection(error) && aliveRef.current) {
|
|
179
|
+
setSpentDepth({ threadId: window.threadId, loaded: window.chain.length });
|
|
180
|
+
}
|
|
154
181
|
(_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, error, "athena/interest");
|
|
155
182
|
}).finally(() => {
|
|
156
183
|
inflightRef.current = false;
|
|
@@ -166,7 +193,8 @@ function useDeepAgentWindowSync({
|
|
|
166
193
|
invoke,
|
|
167
194
|
maxChainedLoads,
|
|
168
195
|
maxUnseenLoads,
|
|
169
|
-
pageSize
|
|
196
|
+
pageSize,
|
|
197
|
+
depthSpent
|
|
170
198
|
]);
|
|
171
199
|
useEffect(() => {
|
|
172
200
|
var _a;
|
|
@@ -221,14 +249,15 @@ function useDeepAgentWindowSync({
|
|
|
221
249
|
setRequests((count) => count + 1);
|
|
222
250
|
}, []);
|
|
223
251
|
const canLoadOlder = useMemo(
|
|
224
|
-
() => window !== null && pageInterestRequests(window, pageSize) !== null,
|
|
225
|
-
[window, pageSize]
|
|
252
|
+
() => window !== null && !depthSpent && pageInterestRequests(window, pageSize) !== null,
|
|
253
|
+
[window, pageSize, depthSpent]
|
|
226
254
|
);
|
|
227
255
|
return { window, hasOlder: (window == null ? void 0 : window.hasOlder) ?? false, canLoadOlder, loading, loadOlder };
|
|
228
256
|
}
|
|
229
257
|
export {
|
|
230
258
|
DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,
|
|
231
259
|
DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,
|
|
260
|
+
isOversizedInterestRejection,
|
|
232
261
|
useDeepAgentMessageWindow,
|
|
233
262
|
useDeepAgentWindowSync
|
|
234
263
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-deep-agent-window-sync.js","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL, DEEP_AGENT_MAX_MESSAGE_WINDOW } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n *\n * A page loaded while the oldest loaded message is a reply lands inside that\n * reply's turn, so it asks for the host's largest window\n * (`DEEP_AGENT_MAX_MESSAGE_WINDOW`) instead of the thread's: a run of 373\n * tool calls is about 750 messages, nineteen round trips at the default\n * window and four at the largest, and the turn shows nothing new until its\n * prompt does. A page above a prompt keeps the thread's window.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n /**\n * Cut tool outputs this mount reads at once; the rest wait for one to\n * settle. One mounted message can hold a whole agent run — a hundred tool\n * steps — and the host rate-limits a client's frames, so a reader that sends\n * them all together gets refusals back for most of them. Omitted: every\n * mounted output is asked for together (renderers whose mounted set is\n * already small).\n */\n maxConcurrentToolOutputs?: number;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted, at most\n * `maxConcurrentToolOutputs` at a time; the converter puts it back\n * (`toolOutputs`), so the card re-renders complete. An output the host\n * replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/** Whether a converted message is a turn's reply rather than its prompt (unknown without a role). */\nfunction isReply(message: DeepAgentWindowSyncMessage | undefined): boolean {\n return message?.role !== undefined && message.role !== 'user';\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n // Reads this mount has in flight, and whether a pass left outputs waiting\n // for one to settle (`maxConcurrentToolOutputs`): only then does a settle\n // re-run the pass, so an unbounded mount renders no more than before.\n const toolOutputsInFlightRef = useRef(0);\n const toolOutputsWaitingRef = useRef(false);\n const [toolOutputSettles, setToolOutputSettles] = useState(0);\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n // The page continues the oldest loaded turn (see DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS).\n const insideTurn = lastPageUnseen || isReply(messages[0]);\n const stated =\n (insideTurn && pageInterestRequests(window, DEEP_AGENT_MAX_MESSAGE_WINDOW)) || pageRequests;\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = stated[stated.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated])\n .then(() => {\n statedRef.current = stated;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n toolOutputsWaitingRef.current = false;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {\n toolOutputsWaitingRef.current = true;\n return;\n }\n const { messageId, chars } = marker;\n toolOutputsInFlightRef.current += 1;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n })\n .finally(() => {\n toolOutputsInFlightRef.current -= 1;\n if (toolOutputsWaitingRef.current && aliveRef.current) {\n setToolOutputSettles((count) => count + 1);\n }\n });\n }\n }\n }, [\n messages,\n isMessageMounted,\n window,\n sendCommand,\n toolOutputs,\n maxConcurrentToolOutputs,\n toolOutputSettles,\n ]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-deep-agent-window-sync.js","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport {\n DEEP_AGENT_INTEREST_PROTOCOL,\n DEEP_AGENT_MAX_MESSAGE_WINDOW,\n DEEP_AGENT_OVERSIZED_INTEREST_CODE,\n} from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n messageWindowCarries,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n restatedInterestRequests,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n *\n * A page loaded while the oldest loaded message is a reply lands inside that\n * reply's turn, so it asks for the host's largest window\n * (`DEEP_AGENT_MAX_MESSAGE_WINDOW`) instead of the thread's: a run of 373\n * tool calls is about 750 messages, nineteen round trips at the default\n * window and four at the largest, and the turn shows nothing new until its\n * prompt does. A page above a prompt keeps the thread's window.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n /**\n * Cut tool outputs this mount reads at once; the rest wait for one to\n * settle. One mounted message can hold a whole agent run — a hundred tool\n * steps — and the host rate-limits a client's frames, so a reader that sends\n * them all together gets refusals back for most of them. Omitted: every\n * mounted output is asked for together (renderers whose mounted set is\n * already small).\n */\n maxConcurrentToolOutputs?: number;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * view has room for them. `hasOlder && !canLoadOlder` is the depth limit —\n * an edge row says so instead of offering a dead button. The host bounds\n * one attach's view twice: `MAX_INTERESTS` requests, and an interest\n * document that lists every loaded message within a byte budget (about a\n * thousand messages). The second is found when the host refuses a page as\n * `oversized-interest` ({@link isOversizedInterestRejection}).\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved, at the beginning, or once `canLoadOlder` is false. */\n loadOlder: () => void;\n}\n\n/**\n * Whether an `athena/interest` `set` came back refused because the windows it\n * states resolve past what one attach's interest document carries: the view\n * is as deep as this connection can load, and asking again only gets the same\n * refusal.\n */\nexport function isOversizedInterestRejection(error: unknown): boolean {\n const failure = (error as { failure?: { fate?: unknown; code?: unknown } } | null | undefined)\n ?.failure;\n return failure?.fate === 'rejected' && failure.code === DEEP_AGENT_OVERSIZED_INTEREST_CODE;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A re-attach's hello carries the view the client declares on the attach\n * (`useDeepAgentThread`), every page in it however it was loaded (this\n * hook, a handover's or a search's reveal, an edit's parent). A hello that\n * does not carry the view the last connected session held — a host that\n * reads no declaration and has no memo of the client id — gets it stated\n * again ({@link restatedInterestRequests}).\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted, at most\n * `maxConcurrentToolOutputs` at a time; the converter puts it back\n * (`toolOutputs`), so the card re-renders complete. An output the host\n * replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/** Whether a converted message is a turn's reply rather than its prompt (unknown without a role). */\nfunction isReply(message: DeepAgentWindowSyncMessage | undefined): boolean {\n return message?.role !== undefined && message.role !== 'user';\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n // The view the host refused a page over as deeper than one attach holds\n // (`oversized-interest`): its conversation and how many messages it covered.\n // A view at least that large gets the same refusal (a run's appends only\n // grow it); one that shrank — an edit rewound the transcript, a re-attach\n // came back smaller — may fit a page again.\n const [spentDepth, setSpentDepth] = useState<{ threadId: string | undefined; loaded: number } | null>(\n null\n );\n const depthSpent =\n spentDepth !== null &&\n window !== null &&\n spentDepth.threadId === window.threadId &&\n window.chain.length >= spentDepth.loaded;\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The window the last connected session held, whoever paged it in, and the\n // session whose hello it was last checked against (a hello resets the\n // documents to what the host seeded).\n const heldRef = useRef<DeepAgentMessageWindow | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n // Reads this mount has in flight, and whether a pass left outputs waiting\n // for one to settle (`maxConcurrentToolOutputs`): only then does a settle\n // re-run the pass, so an unbounded mount renders no more than before.\n const toolOutputsInFlightRef = useRef(0);\n const toolOutputsWaitingRef = useRef(false);\n const [toolOutputSettles, setToolOutputSettles] = useState(0);\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || window === null) return;\n if (restatedConnectionRef.current === connection) {\n heldRef.current = window;\n return;\n }\n // Decided once per connected session, on the hello's interest document.\n restatedConnectionRef.current = connection;\n const held = heldRef.current;\n heldRef.current = window;\n // A page still in flight was stated over the held window: it restates it.\n if (held === null || inflightRef.current || messageWindowCarries({ hello: window, held })) return;\n // Another conversation's hello (the runtime switched threads under this mount).\n if (held.threadId !== undefined && window.threadId !== undefined && held.threadId !== window.threadId) {\n return;\n }\n // Held like a page, so no page is stated over it while it is in flight.\n inflightRef.current = true;\n setLoading(true);\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [restatedInterestRequests({ held, hello: window })])\n .catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n if (depthSpent) {\n // An ask now would get the same refusal: answer it here, so it never\n // reaches another conversation this mount switches to.\n servedRequestsRef.current = requests;\n pageFromRef.current = null;\n return;\n }\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n // The page continues the oldest loaded turn (see DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS).\n const insideTurn = lastPageUnseen || isReply(messages[0]);\n const stated =\n (insideTurn && pageInterestRequests(window, DEEP_AGENT_MAX_MESSAGE_WINDOW)) || pageRequests;\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = stated[stated.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated])\n .then(() => {\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n if (isOversizedInterestRejection(error) && aliveRef.current) {\n setSpentDepth({ threadId: window.threadId, loaded: window.chain.length });\n }\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n depthSpent,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n toolOutputsWaitingRef.current = false;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {\n toolOutputsWaitingRef.current = true;\n return;\n }\n const { messageId, chars } = marker;\n toolOutputsInFlightRef.current += 1;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n })\n .finally(() => {\n toolOutputsInFlightRef.current -= 1;\n if (toolOutputsWaitingRef.current && aliveRef.current) {\n setToolOutputSettles((count) => count + 1);\n }\n });\n }\n }\n }, [\n messages,\n isMessageMounted,\n window,\n sendCommand,\n toolOutputs,\n maxConcurrentToolOutputs,\n toolOutputSettles,\n ]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && !depthSpent && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize, depthSpent]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["_a"],"mappings":"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"}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import { type HandoffLaunch, type HandoffMode, type HandoffOutcome, type HandoffSource, type OfficeLaunchMode, type ServerHandoffDestination, type ThreadHandoffDestination, type ThreadHandoffTransport, type ThreadHandoffView } from './thread-handoff-protocol';
|
|
2
|
-
/** Mobile: a push lands in seconds; the QR path needs time to unlock a phone. */
|
|
3
|
-
export declare const DEFAULT_MOBILE_HANDOFF_TIMEOUT_MS = 120000;
|
|
4
|
-
/** Office: launching a desktop host from cold takes tens of seconds. */
|
|
5
|
-
export declare const DEFAULT_OFFICE_HANDOFF_TIMEOUT_MS = 180000;
|
|
6
|
-
/** "Wherever I pick up next": the whole ten-minute life of the record. */
|
|
7
|
-
export declare const DEFAULT_PICKUP_HANDOFF_TIMEOUT_MS = 600000;
|
|
8
2
|
export type ThreadHandoffFlowPhase<D extends ThreadHandoffDestination = ServerHandoffDestination> = {
|
|
9
3
|
status: 'idle';
|
|
10
4
|
} | {
|
|
@@ -67,7 +61,10 @@ export interface ThreadHandoffFlowCallbacks<D extends ThreadHandoffDestination =
|
|
|
67
61
|
handoff: ThreadHandoffView;
|
|
68
62
|
launch: HandoffLaunch;
|
|
69
63
|
}) => void;
|
|
70
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Terminal result; `cancelled` is not reported. `timeout` means the ticket's
|
|
66
|
+
* life ran out with nobody picking it up: the source waits the whole of it.
|
|
67
|
+
*/
|
|
71
68
|
onSettled?: (context: {
|
|
72
69
|
destination: D;
|
|
73
70
|
threadId: string;
|
|
@@ -81,7 +78,6 @@ export interface UseThreadHandoffFlowOptions<D extends ThreadHandoffDestination
|
|
|
81
78
|
source: HandoffSource;
|
|
82
79
|
/** Change this with the person, workspace, environment or active thread to cancel the old flow. */
|
|
83
80
|
scopeKey?: string;
|
|
84
|
-
timeoutMs?: Partial<Record<ThreadHandoffDestination, number>>;
|
|
85
81
|
pollIntervalMs?: number;
|
|
86
82
|
}
|
|
87
83
|
export interface ThreadHandoffFlowController<D extends ThreadHandoffDestination = ServerHandoffDestination> {
|
|
@@ -94,4 +90,4 @@ export interface ThreadHandoffFlowController<D extends ThreadHandoffDestination
|
|
|
94
90
|
/** Back to idle after a terminal phase (dismisses the result UI). */
|
|
95
91
|
reset: () => void;
|
|
96
92
|
}
|
|
97
|
-
export declare function useThreadHandoffFlow<D extends ThreadHandoffDestination = ServerHandoffDestination>({ transport, source, scopeKey,
|
|
93
|
+
export declare function useThreadHandoffFlow<D extends ThreadHandoffDestination = ServerHandoffDestination>({ transport, source, scopeKey, pollIntervalMs, onLaunched, onSettled, }: UseThreadHandoffFlowOptions<D>): ThreadHandoffFlowController<D>;
|
|
@@ -5,6 +5,7 @@ const VOICE_SOURCE_SPOKEN = "voice";
|
|
|
5
5
|
const VOICE_SOURCE_TYPED = "text";
|
|
6
6
|
const VOICE_SOURCE_DELEGATION = "voice_delegation";
|
|
7
7
|
const VOICE_DELEGATION_ID_PREFIX = "voicetask_";
|
|
8
|
+
const VOICE_TYPED_ID_PREFIX = "voicetext_";
|
|
8
9
|
const VOICE_LIVE_KEY = "voice_live";
|
|
9
10
|
const VOICE_TURN_IDS_KEY = "voice_turn_ids";
|
|
10
11
|
const VOICE_UPDATE_TOOL_NAME = "send_voice_update";
|
|
@@ -12,6 +13,37 @@ const VOICE_LIVE_TYPED_HINT = "typed messages go to the voice conversation";
|
|
|
12
13
|
function isVoiceDelegationMessage({ messageId, custom }) {
|
|
13
14
|
return (custom == null ? void 0 : custom.source) === VOICE_SOURCE_DELEGATION || typeof messageId === "string" && messageId.startsWith(VOICE_DELEGATION_ID_PREFIX);
|
|
14
15
|
}
|
|
16
|
+
const DELEGATION_ECHO_LOOKBACK = 32;
|
|
17
|
+
const TYPED_TASK_ID_PREFIX = `${VOICE_DELEGATION_ID_PREFIX}typed_`;
|
|
18
|
+
function isPersonCallTurn(message) {
|
|
19
|
+
var _a;
|
|
20
|
+
if (message.role !== "user") return false;
|
|
21
|
+
const custom = (_a = message.metadata) == null ? void 0 : _a.custom;
|
|
22
|
+
if (isVoiceDelegationMessage({ messageId: message.id, custom })) return false;
|
|
23
|
+
const source = custom == null ? void 0 : custom.source;
|
|
24
|
+
return source === VOICE_SOURCE_SPOKEN || source === VOICE_SOURCE_TYPED || message.id.startsWith(VOICE_TYPED_ID_PREFIX);
|
|
25
|
+
}
|
|
26
|
+
function voiceDelegationEchoesRequest(messages, index) {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
const message = messages[index];
|
|
29
|
+
if (message === void 0 || message.role !== "user" || !isVoiceDelegationMessage({ messageId: message.id, custom: (_a = message.metadata) == null ? void 0 : _a.custom })) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const stop = Math.max(0, index - DELEGATION_ECHO_LOOKBACK);
|
|
33
|
+
if (message.id.startsWith(TYPED_TASK_ID_PREFIX)) {
|
|
34
|
+
const turnId = VOICE_TYPED_ID_PREFIX + message.id.slice(TYPED_TASK_ID_PREFIX.length);
|
|
35
|
+
for (let position = index - 1; position >= stop; position--) {
|
|
36
|
+
if (((_b = messages[position]) == null ? void 0 : _b.id) === turnId) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
for (let position = index - 1; position >= stop; position--) {
|
|
41
|
+
const earlier = messages[position];
|
|
42
|
+
if (earlier === void 0 || earlier.role !== "user") continue;
|
|
43
|
+
return isPersonCallTurn(earlier);
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
15
47
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
48
|
function isSpokenTurnTextPart(part) {
|
|
17
49
|
if (part.type !== "text") return false;
|
|
@@ -53,9 +85,11 @@ exports.VOICE_SOURCE_DELEGATION = VOICE_SOURCE_DELEGATION;
|
|
|
53
85
|
exports.VOICE_SOURCE_SPOKEN = VOICE_SOURCE_SPOKEN;
|
|
54
86
|
exports.VOICE_SOURCE_TYPED = VOICE_SOURCE_TYPED;
|
|
55
87
|
exports.VOICE_TURN_IDS_KEY = VOICE_TURN_IDS_KEY;
|
|
88
|
+
exports.VOICE_TYPED_ID_PREFIX = VOICE_TYPED_ID_PREFIX;
|
|
56
89
|
exports.VOICE_UPDATE_TOOL_NAME = VOICE_UPDATE_TOOL_NAME;
|
|
57
90
|
exports.describeVoiceUpdateToolCall = describeVoiceUpdateToolCall;
|
|
58
91
|
exports.isSpokenTurnTextPart = isSpokenTurnTextPart;
|
|
59
92
|
exports.isVoiceDelegationMessage = isVoiceDelegationMessage;
|
|
93
|
+
exports.voiceDelegationEchoesRequest = voiceDelegationEchoesRequest;
|
|
60
94
|
exports.voiceUpdateStatusLabel = voiceUpdateStatusLabel;
|
|
61
95
|
//# sourceMappingURL=voice-presentation.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-presentation.cjs","sources":["../../../../agent-runtime/src/voice-presentation.ts"],"sourcesContent":["/**\n * How voice-mode transcript entries present, shared by every chat surface\n * (Olympus, the Chrome side panel, the mobile app) so a spoken turn, a\n * delegation the live voice model handed off, and an interim spoken update\n * read the same everywhere. The wire contract is the deep-agent host's\n * `additional_kwargs.metadata` (exposed as `message.metadata.custom`):\n *\n * - `source: \"voice\"` — a spoken turn; `voice_live: true` while it is still\n * being heard or spoken (the ephemeral entry the durable row replaces);\n * - `source: \"text\"` — a message typed into the live conversation;\n * - `source: \"voice_delegation\"` (id prefix `voicetask_`) — the unit of work\n * the live voice model delegated to the thread.\n */\nimport { getExternalStoreMessages, type ThreadMessage } from '@assistant-ui/core';\n\n/** `metadata.custom.source` of a spoken turn. */\nexport const VOICE_SOURCE_SPOKEN = 'voice';\n/** `metadata.custom.source` of a message typed into the live conversation. */\nexport const VOICE_SOURCE_TYPED = 'text';\n/** `metadata.custom.source` of a voice delegation's anchor message. */\nexport const VOICE_SOURCE_DELEGATION = 'voice_delegation';\n/** Id prefix of a voice delegation's anchor message (the working signal on\n * hosts that drop the command's metadata). */\nexport const VOICE_DELEGATION_ID_PREFIX = 'voicetask_';\n/** `metadata.custom` flag on a spoken turn still in flight. */\nexport const VOICE_LIVE_KEY = 'voice_live';\n/** `metadata.custom` list of the wire ids a grouped spoken bubble stands for\n * (see `groupSpokenUserTurns`), so id-keyed lookups such as checkpoint markers\n * can resolve any member to the bubble. */\nexport const VOICE_TURN_IDS_KEY = 'voice_turn_ids';\n\n/** The delegated run's interim-update tool (agora `send_voice_update`). */\nexport const VOICE_UPDATE_TOOL_NAME = 'send_voice_update';\n\n/** Trailing hint on the live-session strip, worded once for every surface. */\nexport const VOICE_LIVE_TYPED_HINT = 'typed messages go to the voice conversation';\n\nexport interface VoiceMessageMarker {\n /** The thread message id; a host may not have assigned one yet. */\n messageId: string | undefined;\n /** The thread message's `metadata.custom`, when any. */\n custom: Record<string, unknown> | undefined;\n}\n\n/** Whether a thread message is a voice delegation's anchor (the ask the live\n * voice model handed to the thread), by source or by id prefix. */\nexport function isVoiceDelegationMessage({ messageId, custom }: VoiceMessageMarker): boolean {\n return (\n custom?.source === VOICE_SOURCE_DELEGATION ||\n (typeof messageId === 'string' && messageId.startsWith(VOICE_DELEGATION_ID_PREFIX))\n );\n}\n\nexport interface VoiceProvenance {\n /** Chip copy: \"Listening\", \"Speaking\", \"Spoken\", or \"Delegated from voice\". */\n readonly label: string;\n /** True while the turn is still being heard or spoken. */\n readonly live: boolean;\n}\n\n// These values are read directly by useAuiState/useSyncExternalStore selectors.\n// Equal presentation must retain its identity, even when a transport replaces\n// the message metadata. Fresh objects here cause React's update-depth crash.\nconst SPOKEN_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Spoken',\n live: false,\n});\nconst LISTENING_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Listening',\n live: true,\n});\nconst SPEAKING_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Speaking',\n live: true,\n});\nconst DELEGATED_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Delegated from voice',\n live: false,\n});\n\n/**\n * The provenance chip for a message, or `null` for ordinary chat messages so\n * the chip is free to mount on every row. A spoken turn still in flight reads\n * as what is happening (\"Listening\" for the user, \"Speaking\" for the\n * assistant) rather than as a finished \"Spoken\" turn; a typed-in-voice turn\n * is a normal message and gets no chip. Results are immutable and stable for\n * equal presentation, so all surfaces can use this function as a selector.\n */\nexport function voiceProvenanceLabel({\n messageId,\n custom,\n role,\n}: VoiceMessageMarker & { role: 'user' | 'assistant' | string }): VoiceProvenance | null {\n const source = custom?.source;\n if (source === VOICE_SOURCE_SPOKEN) {\n if (custom?.[VOICE_LIVE_KEY] === true) {\n return role === 'user' ? LISTENING_PROVENANCE : SPEAKING_PROVENANCE;\n }\n return SPOKEN_PROVENANCE;\n }\n if (isVoiceDelegationMessage({ messageId, custom })) {\n return DELEGATED_PROVENANCE;\n }\n return null;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Whether a part of an assistant message holds the words of a spoken\n * assistant turn.\n *\n * The converter joins consecutive assistant entries into one message. The\n * host appends a live turn while it is heard, and a delegated run keeps\n * appending its own steps after it, so a turn spoken during a run becomes a\n * text part inside the run's message, before the run's later tool calls.\n * Grouped views put the text before the last tool call in the details panel\n * as the run's narration; they use this to keep the spoken words visible.\n *\n * The joined message's `metadata.custom` merges every entry in it, so this\n * reads the part's own entry, which the converter binds to the part.\n */\nexport function isSpokenTurnTextPart(part: { readonly type: string }): boolean {\n if (part.type !== 'text') return false;\n const entries: readonly unknown[] = getExternalStoreMessages<unknown>(\n part as ThreadMessage['content'][number]\n );\n return entries.some((entry) => {\n if (!isRecord(entry) || entry.role !== 'assistant' || !isRecord(entry.metadata)) return false;\n const custom = entry.metadata.custom;\n return isRecord(custom) && custom.source === VOICE_SOURCE_SPOKEN;\n });\n}\n\nexport interface VoiceUpdateToolCall {\n /** `mode: \"speak\"` — read aloud to the user; otherwise a silent progress note. */\n spoken: boolean;\n /** The update's words. */\n text: string;\n /** Whether the live session took it; `null` while the call is still running. */\n delivered: boolean | null;\n}\n\n/**\n * What a `send_voice_update` tool call did, from its arguments and the tool's\n * teaching-text result (agora returns prose, not JSON, so the run can read it).\n */\nexport function describeVoiceUpdateToolCall({\n args,\n result,\n}: {\n args: Record<string, unknown> | undefined;\n result: unknown;\n}): VoiceUpdateToolCall {\n const text = typeof args?.text === 'string' ? args.text.trim() : '';\n const spoken = args?.mode === 'speak';\n if (typeof result !== 'string' || result.trim() === '') {\n return { spoken, text, delivered: null };\n }\n const outcome = result.toLowerCase();\n const failed =\n outcome.includes('could not be delivered') ||\n outcome.includes('was not delivered') ||\n outcome.includes('rejected the update') ||\n outcome.includes('has ended') ||\n outcome.includes('not configured') ||\n outcome.includes('no voice session');\n return { spoken, text, delivered: !failed };\n}\n\n/** The card's status line for a voice update in a given tool-call state. */\nexport function voiceUpdateStatusLabel(\n call: VoiceUpdateToolCall,\n status: 'running' | 'complete' | 'incomplete' | 'requires-action' | undefined\n): string {\n if (status === 'running' || (call.delivered === null && status !== 'incomplete')) {\n return call.spoken ? 'Saying to you…' : 'Noting progress…';\n }\n if (call.delivered === false || status === 'incomplete') {\n return call.spoken ? 'Could not say this to you' : 'Progress note not delivered';\n }\n return call.spoken ? 'Said aloud' : 'Progress noted silently';\n}\n"],"names":["getExternalStoreMessages"],"mappings":";;;AAgBO,MAAM,sBAAsB;AAE5B,MAAM,qBAAqB;AAE3B,MAAM,0BAA0B;AAGhC,MAAM,6BAA6B;AAEnC,MAAM,iBAAiB;AAIvB,MAAM,qBAAqB;AAG3B,MAAM,yBAAyB;AAG/B,MAAM,wBAAwB;AAW9B,SAAS,yBAAyB,EAAE,WAAW,UAAuC;AAC3F,UACE,iCAAQ,YAAW,2BAClB,OAAO,cAAc,YAAY,UAAU,WAAW,0BAA0B;AAErF;AAuDA,MAAM,WAAW,CAAC,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAgB9D,SAAS,qBAAqB,MAA0C;AAC7E,MAAI,KAAK,SAAS,OAAQ,QAAO;AACjC,QAAM,UAA8BA,KAAAA;AAAAA,IAClC;AAAA,EAAA;AAEF,SAAO,QAAQ,KAAK,CAAC,UAAU;AAC7B,QAAI,CAAC,SAAS,KAAK,KAAK,MAAM,SAAS,eAAe,CAAC,SAAS,MAAM,QAAQ,EAAG,QAAO;AACxF,UAAM,SAAS,MAAM,SAAS;AAC9B,WAAO,SAAS,MAAM,KAAK,OAAO,WAAW;AAAA,EAC/C,CAAC;AACH;AAeO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AACF,GAGwB;AACtB,QAAM,OAAO,QAAO,6BAAM,UAAS,WAAW,KAAK,KAAK,SAAS;AACjE,QAAM,UAAS,6BAAM,UAAS;AAC9B,MAAI,OAAO,WAAW,YAAY,OAAO,KAAA,MAAW,IAAI;AACtD,WAAO,EAAE,QAAQ,MAAM,WAAW,KAAA;AAAA,EACpC;AACA,QAAM,UAAU,OAAO,YAAA;AACvB,QAAM,SACJ,QAAQ,SAAS,wBAAwB,KACzC,QAAQ,SAAS,mBAAmB,KACpC,QAAQ,SAAS,qBAAqB,KACtC,QAAQ,SAAS,WAAW,KAC5B,QAAQ,SAAS,gBAAgB,KACjC,QAAQ,SAAS,kBAAkB;AACrC,SAAO,EAAE,QAAQ,MAAM,WAAW,CAAC,OAAA;AACrC;AAGO,SAAS,uBACd,MACA,QACQ;AACR,MAAI,WAAW,aAAc,KAAK,cAAc,QAAQ,WAAW,cAAe;AAChF,WAAO,KAAK,SAAS,mBAAmB;AAAA,EAC1C;AACA,MAAI,KAAK,cAAc,SAAS,WAAW,cAAc;AACvD,WAAO,KAAK,SAAS,8BAA8B;AAAA,EACrD;AACA,SAAO,KAAK,SAAS,eAAe;AACtC;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"voice-presentation.cjs","sources":["../../../../agent-runtime/src/voice-presentation.ts"],"sourcesContent":["/**\n * How voice-mode transcript entries present, shared by every chat surface\n * (Olympus, the Chrome side panel, the mobile app) so a spoken turn, a\n * delegation the live voice model handed off, and an interim spoken update\n * read the same everywhere. The wire contract is the deep-agent host's\n * `additional_kwargs.metadata` (exposed as `message.metadata.custom`):\n *\n * - `source: \"voice\"` — a spoken turn; `voice_live: true` while it is still\n * being heard or spoken (the ephemeral entry the durable row replaces);\n * - `source: \"text\"` (id prefix `voicetext_`) — a message typed into the live\n * conversation;\n * - `source: \"voice_delegation\"` (id prefix `voicetask_`) — the unit of work\n * the live voice model delegated to the thread (on GPT Live a typed request\n * is handed off by the host itself, `voicetask_typed_…`, with the typed\n * words as its text).\n *\n * A delegation's anchor is the delegated run's input, so it stays in the\n * transcript; what reads once is its presentation: right under the person's\n * own words it is only an echo of them, and every app shows just its header\n * ({@link voiceDelegationEchoesRequest}).\n */\nimport { getExternalStoreMessages, type ThreadMessage } from '@assistant-ui/core';\n\n/** `metadata.custom.source` of a spoken turn. */\nexport const VOICE_SOURCE_SPOKEN = 'voice';\n/** `metadata.custom.source` of a message typed into the live conversation. */\nexport const VOICE_SOURCE_TYPED = 'text';\n/** `metadata.custom.source` of a voice delegation's anchor message. */\nexport const VOICE_SOURCE_DELEGATION = 'voice_delegation';\n/** Id prefix of a voice delegation's anchor message (the working signal on\n * hosts that drop the command's metadata). */\nexport const VOICE_DELEGATION_ID_PREFIX = 'voicetask_';\n/** Id prefix of a message typed into a live call (`voice/user-text`). */\nexport const VOICE_TYPED_ID_PREFIX = 'voicetext_';\n/** `metadata.custom` flag on a spoken turn still in flight. */\nexport const VOICE_LIVE_KEY = 'voice_live';\n/** `metadata.custom` list of the wire ids a grouped spoken bubble stands for\n * (see `groupSpokenUserTurns`), so id-keyed lookups such as checkpoint markers\n * can resolve any member to the bubble. */\nexport const VOICE_TURN_IDS_KEY = 'voice_turn_ids';\n\n/** The delegated run's interim-update tool (agora `send_voice_update`). */\nexport const VOICE_UPDATE_TOOL_NAME = 'send_voice_update';\n\n/** Trailing hint on the live-session strip, worded once for every surface. */\nexport const VOICE_LIVE_TYPED_HINT = 'typed messages go to the voice conversation';\n\nexport interface VoiceMessageMarker {\n /** The thread message id; a host may not have assigned one yet. */\n messageId: string | undefined;\n /** The thread message's `metadata.custom`, when any. */\n custom: Record<string, unknown> | undefined;\n}\n\n/** Whether a thread message is a voice delegation's anchor (the ask the live\n * voice model handed to the thread), by source or by id prefix. */\nexport function isVoiceDelegationMessage({ messageId, custom }: VoiceMessageMarker): boolean {\n return (\n custom?.source === VOICE_SOURCE_DELEGATION ||\n (typeof messageId === 'string' && messageId.startsWith(VOICE_DELEGATION_ID_PREFIX))\n );\n}\n\n/** The fields of a transcript message {@link voiceDelegationEchoesRequest}\n * reads (structural, so any runtime's thread message fits). */\nexport interface VoiceTranscriptMessage {\n readonly id: string;\n readonly role: string;\n readonly metadata?: { readonly custom?: Record<string, unknown> } | undefined;\n}\n\n/** How far back {@link voiceDelegationEchoesRequest} looks for the person's\n * turn. Consecutive assistant entries are one message after conversion, so\n * the turn a delegation answers is a step or two away; past this it is not\n * \"right above\" any more. */\nconst DELEGATION_ECHO_LOOKBACK = 32;\n/** Id prefix of the task the host hands a typed request off as (GPT Live):\n * `voicetask_typed_<suffix>` for the turn `voicetext_<suffix>`. */\nconst TYPED_TASK_ID_PREFIX = `${VOICE_DELEGATION_ID_PREFIX}typed_`;\n\n/** Whether a message is the person's own turn in a call: spoken, or typed\n * into it (by source, or by the typed id on hosts that drop the metadata). */\nfunction isPersonCallTurn(message: VoiceTranscriptMessage): boolean {\n if (message.role !== 'user') return false;\n const custom = message.metadata?.custom;\n if (isVoiceDelegationMessage({ messageId: message.id, custom })) return false;\n const source = custom?.source;\n return (\n source === VOICE_SOURCE_SPOKEN ||\n source === VOICE_SOURCE_TYPED ||\n message.id.startsWith(VOICE_TYPED_ID_PREFIX)\n );\n}\n\n/**\n * Whether the voice delegation at `index` only repeats the person's request:\n * the nearest earlier user message is the person's own turn in the call\n * (spoken, or typed into it), which already shows the request. A typed request\n * on GPT Live is handed off with the very same words, and a spoken one in the\n * live model's paraphrase; either way the person reads it once, so the apps\n * draw such a delegation as its header alone, the task's wording one tap away.\n *\n * A typed request's task names its turn: `voicetask_typed_<suffix>` is the\n * hand-off of `voicetext_<suffix>`. Requests typed while a run works commit\n * together and their tasks run one after another, so the turns and tasks\n * interleave (`voicetext_A`, `voicetext_B`, `voicetask_typed_A`, A's run,\n * `voicetask_typed_B`); a typed task is an echo exactly when its own turn is\n * loaded within the look-back, never by whichever typed turn is nearest.\n *\n * Anything else keeps the full card: a spoken delegation whose nearest\n * earlier user message is another delegation (a follow-up spoken while a run\n * worked, whose spoken turn commits only after that run settles) or an\n * ordinary message, and a typed task whose turn is not loaded, are the only\n * place their ask shows. Assistant messages in between (a spoken\n * acknowledgement) are skipped. Pure and returns a boolean, so it is safe as\n * a selector.\n */\nexport function voiceDelegationEchoesRequest(\n messages: readonly VoiceTranscriptMessage[],\n index: number,\n): boolean {\n const message = messages[index];\n if (\n message === undefined ||\n message.role !== 'user' ||\n !isVoiceDelegationMessage({ messageId: message.id, custom: message.metadata?.custom })\n ) {\n return false;\n }\n const stop = Math.max(0, index - DELEGATION_ECHO_LOOKBACK);\n if (message.id.startsWith(TYPED_TASK_ID_PREFIX)) {\n const turnId = VOICE_TYPED_ID_PREFIX + message.id.slice(TYPED_TASK_ID_PREFIX.length);\n for (let position = index - 1; position >= stop; position--) {\n if (messages[position]?.id === turnId) return true;\n }\n return false;\n }\n for (let position = index - 1; position >= stop; position--) {\n const earlier = messages[position];\n if (earlier === undefined || earlier.role !== 'user') continue;\n return isPersonCallTurn(earlier);\n }\n return false;\n}\n\nexport interface VoiceProvenance {\n /** Chip copy: \"Listening\", \"Speaking\", \"Spoken\", or \"Delegated from voice\". */\n readonly label: string;\n /** True while the turn is still being heard or spoken. */\n readonly live: boolean;\n}\n\n// These values are read directly by useAuiState/useSyncExternalStore selectors.\n// Equal presentation must retain its identity, even when a transport replaces\n// the message metadata. Fresh objects here cause React's update-depth crash.\nconst SPOKEN_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Spoken',\n live: false,\n});\nconst LISTENING_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Listening',\n live: true,\n});\nconst SPEAKING_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Speaking',\n live: true,\n});\nconst DELEGATED_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Delegated from voice',\n live: false,\n});\n\n/**\n * The provenance chip for a message, or `null` for ordinary chat messages so\n * the chip is free to mount on every row. A spoken turn still in flight reads\n * as what is happening (\"Listening\" for the user, \"Speaking\" for the\n * assistant) rather than as a finished \"Spoken\" turn; a typed-in-voice turn\n * is a normal message and gets no chip. Results are immutable and stable for\n * equal presentation, so all surfaces can use this function as a selector.\n */\nexport function voiceProvenanceLabel({\n messageId,\n custom,\n role,\n}: VoiceMessageMarker & { role: 'user' | 'assistant' | string }): VoiceProvenance | null {\n const source = custom?.source;\n if (source === VOICE_SOURCE_SPOKEN) {\n if (custom?.[VOICE_LIVE_KEY] === true) {\n return role === 'user' ? LISTENING_PROVENANCE : SPEAKING_PROVENANCE;\n }\n return SPOKEN_PROVENANCE;\n }\n if (isVoiceDelegationMessage({ messageId, custom })) {\n return DELEGATED_PROVENANCE;\n }\n return null;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Whether a part of an assistant message holds the words of a spoken\n * assistant turn.\n *\n * The converter joins consecutive assistant entries into one message. The\n * host appends a live turn while it is heard, and a delegated run keeps\n * appending its own steps after it, so a turn spoken during a run becomes a\n * text part inside the run's message, before the run's later tool calls.\n * Grouped views put the text before the last tool call in the details panel\n * as the run's narration; they use this to keep the spoken words visible.\n *\n * The joined message's `metadata.custom` merges every entry in it, so this\n * reads the part's own entry, which the converter binds to the part.\n */\nexport function isSpokenTurnTextPart(part: { readonly type: string }): boolean {\n if (part.type !== 'text') return false;\n const entries: readonly unknown[] = getExternalStoreMessages<unknown>(\n part as ThreadMessage['content'][number]\n );\n return entries.some((entry) => {\n if (!isRecord(entry) || entry.role !== 'assistant' || !isRecord(entry.metadata)) return false;\n const custom = entry.metadata.custom;\n return isRecord(custom) && custom.source === VOICE_SOURCE_SPOKEN;\n });\n}\n\nexport interface VoiceUpdateToolCall {\n /** `mode: \"speak\"` — read aloud to the user; otherwise a silent progress note. */\n spoken: boolean;\n /** The update's words. */\n text: string;\n /** Whether the live session took it; `null` while the call is still running. */\n delivered: boolean | null;\n}\n\n/**\n * What a `send_voice_update` tool call did, from its arguments and the tool's\n * teaching-text result (agora returns prose, not JSON, so the run can read it).\n */\nexport function describeVoiceUpdateToolCall({\n args,\n result,\n}: {\n args: Record<string, unknown> | undefined;\n result: unknown;\n}): VoiceUpdateToolCall {\n const text = typeof args?.text === 'string' ? args.text.trim() : '';\n const spoken = args?.mode === 'speak';\n if (typeof result !== 'string' || result.trim() === '') {\n return { spoken, text, delivered: null };\n }\n const outcome = result.toLowerCase();\n const failed =\n outcome.includes('could not be delivered') ||\n outcome.includes('was not delivered') ||\n outcome.includes('rejected the update') ||\n outcome.includes('has ended') ||\n outcome.includes('not configured') ||\n outcome.includes('no voice session');\n return { spoken, text, delivered: !failed };\n}\n\n/** The card's status line for a voice update in a given tool-call state. */\nexport function voiceUpdateStatusLabel(\n call: VoiceUpdateToolCall,\n status: 'running' | 'complete' | 'incomplete' | 'requires-action' | undefined\n): string {\n if (status === 'running' || (call.delivered === null && status !== 'incomplete')) {\n return call.spoken ? 'Saying to you…' : 'Noting progress…';\n }\n if (call.delivered === false || status === 'incomplete') {\n return call.spoken ? 'Could not say this to you' : 'Progress note not delivered';\n }\n return call.spoken ? 'Said aloud' : 'Progress noted silently';\n}\n"],"names":["getExternalStoreMessages"],"mappings":"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"}
|
|
@@ -7,6 +7,8 @@ export declare const VOICE_SOURCE_DELEGATION = "voice_delegation";
|
|
|
7
7
|
/** Id prefix of a voice delegation's anchor message (the working signal on
|
|
8
8
|
* hosts that drop the command's metadata). */
|
|
9
9
|
export declare const VOICE_DELEGATION_ID_PREFIX = "voicetask_";
|
|
10
|
+
/** Id prefix of a message typed into a live call (`voice/user-text`). */
|
|
11
|
+
export declare const VOICE_TYPED_ID_PREFIX = "voicetext_";
|
|
10
12
|
/** `metadata.custom` flag on a spoken turn still in flight. */
|
|
11
13
|
export declare const VOICE_LIVE_KEY = "voice_live";
|
|
12
14
|
/** `metadata.custom` list of the wire ids a grouped spoken bubble stands for
|
|
@@ -26,6 +28,39 @@ export interface VoiceMessageMarker {
|
|
|
26
28
|
/** Whether a thread message is a voice delegation's anchor (the ask the live
|
|
27
29
|
* voice model handed to the thread), by source or by id prefix. */
|
|
28
30
|
export declare function isVoiceDelegationMessage({ messageId, custom }: VoiceMessageMarker): boolean;
|
|
31
|
+
/** The fields of a transcript message {@link voiceDelegationEchoesRequest}
|
|
32
|
+
* reads (structural, so any runtime's thread message fits). */
|
|
33
|
+
export interface VoiceTranscriptMessage {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly role: string;
|
|
36
|
+
readonly metadata?: {
|
|
37
|
+
readonly custom?: Record<string, unknown>;
|
|
38
|
+
} | undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether the voice delegation at `index` only repeats the person's request:
|
|
42
|
+
* the nearest earlier user message is the person's own turn in the call
|
|
43
|
+
* (spoken, or typed into it), which already shows the request. A typed request
|
|
44
|
+
* on GPT Live is handed off with the very same words, and a spoken one in the
|
|
45
|
+
* live model's paraphrase; either way the person reads it once, so the apps
|
|
46
|
+
* draw such a delegation as its header alone, the task's wording one tap away.
|
|
47
|
+
*
|
|
48
|
+
* A typed request's task names its turn: `voicetask_typed_<suffix>` is the
|
|
49
|
+
* hand-off of `voicetext_<suffix>`. Requests typed while a run works commit
|
|
50
|
+
* together and their tasks run one after another, so the turns and tasks
|
|
51
|
+
* interleave (`voicetext_A`, `voicetext_B`, `voicetask_typed_A`, A's run,
|
|
52
|
+
* `voicetask_typed_B`); a typed task is an echo exactly when its own turn is
|
|
53
|
+
* loaded within the look-back, never by whichever typed turn is nearest.
|
|
54
|
+
*
|
|
55
|
+
* Anything else keeps the full card: a spoken delegation whose nearest
|
|
56
|
+
* earlier user message is another delegation (a follow-up spoken while a run
|
|
57
|
+
* worked, whose spoken turn commits only after that run settles) or an
|
|
58
|
+
* ordinary message, and a typed task whose turn is not loaded, are the only
|
|
59
|
+
* place their ask shows. Assistant messages in between (a spoken
|
|
60
|
+
* acknowledgement) are skipped. Pure and returns a boolean, so it is safe as
|
|
61
|
+
* a selector.
|
|
62
|
+
*/
|
|
63
|
+
export declare function voiceDelegationEchoesRequest(messages: readonly VoiceTranscriptMessage[], index: number): boolean;
|
|
29
64
|
export interface VoiceProvenance {
|
|
30
65
|
/** Chip copy: "Listening", "Speaking", "Spoken", or "Delegated from voice". */
|
|
31
66
|
readonly label: string;
|
|
@@ -3,6 +3,7 @@ const VOICE_SOURCE_SPOKEN = "voice";
|
|
|
3
3
|
const VOICE_SOURCE_TYPED = "text";
|
|
4
4
|
const VOICE_SOURCE_DELEGATION = "voice_delegation";
|
|
5
5
|
const VOICE_DELEGATION_ID_PREFIX = "voicetask_";
|
|
6
|
+
const VOICE_TYPED_ID_PREFIX = "voicetext_";
|
|
6
7
|
const VOICE_LIVE_KEY = "voice_live";
|
|
7
8
|
const VOICE_TURN_IDS_KEY = "voice_turn_ids";
|
|
8
9
|
const VOICE_UPDATE_TOOL_NAME = "send_voice_update";
|
|
@@ -10,6 +11,37 @@ const VOICE_LIVE_TYPED_HINT = "typed messages go to the voice conversation";
|
|
|
10
11
|
function isVoiceDelegationMessage({ messageId, custom }) {
|
|
11
12
|
return (custom == null ? void 0 : custom.source) === VOICE_SOURCE_DELEGATION || typeof messageId === "string" && messageId.startsWith(VOICE_DELEGATION_ID_PREFIX);
|
|
12
13
|
}
|
|
14
|
+
const DELEGATION_ECHO_LOOKBACK = 32;
|
|
15
|
+
const TYPED_TASK_ID_PREFIX = `${VOICE_DELEGATION_ID_PREFIX}typed_`;
|
|
16
|
+
function isPersonCallTurn(message) {
|
|
17
|
+
var _a;
|
|
18
|
+
if (message.role !== "user") return false;
|
|
19
|
+
const custom = (_a = message.metadata) == null ? void 0 : _a.custom;
|
|
20
|
+
if (isVoiceDelegationMessage({ messageId: message.id, custom })) return false;
|
|
21
|
+
const source = custom == null ? void 0 : custom.source;
|
|
22
|
+
return source === VOICE_SOURCE_SPOKEN || source === VOICE_SOURCE_TYPED || message.id.startsWith(VOICE_TYPED_ID_PREFIX);
|
|
23
|
+
}
|
|
24
|
+
function voiceDelegationEchoesRequest(messages, index) {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
const message = messages[index];
|
|
27
|
+
if (message === void 0 || message.role !== "user" || !isVoiceDelegationMessage({ messageId: message.id, custom: (_a = message.metadata) == null ? void 0 : _a.custom })) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const stop = Math.max(0, index - DELEGATION_ECHO_LOOKBACK);
|
|
31
|
+
if (message.id.startsWith(TYPED_TASK_ID_PREFIX)) {
|
|
32
|
+
const turnId = VOICE_TYPED_ID_PREFIX + message.id.slice(TYPED_TASK_ID_PREFIX.length);
|
|
33
|
+
for (let position = index - 1; position >= stop; position--) {
|
|
34
|
+
if (((_b = messages[position]) == null ? void 0 : _b.id) === turnId) return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
for (let position = index - 1; position >= stop; position--) {
|
|
39
|
+
const earlier = messages[position];
|
|
40
|
+
if (earlier === void 0 || earlier.role !== "user") continue;
|
|
41
|
+
return isPersonCallTurn(earlier);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
13
45
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
46
|
function isSpokenTurnTextPart(part) {
|
|
15
47
|
if (part.type !== "text") return false;
|
|
@@ -52,10 +84,12 @@ export {
|
|
|
52
84
|
VOICE_SOURCE_SPOKEN,
|
|
53
85
|
VOICE_SOURCE_TYPED,
|
|
54
86
|
VOICE_TURN_IDS_KEY,
|
|
87
|
+
VOICE_TYPED_ID_PREFIX,
|
|
55
88
|
VOICE_UPDATE_TOOL_NAME,
|
|
56
89
|
describeVoiceUpdateToolCall,
|
|
57
90
|
isSpokenTurnTextPart,
|
|
58
91
|
isVoiceDelegationMessage,
|
|
92
|
+
voiceDelegationEchoesRequest,
|
|
59
93
|
voiceUpdateStatusLabel
|
|
60
94
|
};
|
|
61
95
|
//# sourceMappingURL=voice-presentation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-presentation.js","sources":["../../../../agent-runtime/src/voice-presentation.ts"],"sourcesContent":["/**\n * How voice-mode transcript entries present, shared by every chat surface\n * (Olympus, the Chrome side panel, the mobile app) so a spoken turn, a\n * delegation the live voice model handed off, and an interim spoken update\n * read the same everywhere. The wire contract is the deep-agent host's\n * `additional_kwargs.metadata` (exposed as `message.metadata.custom`):\n *\n * - `source: \"voice\"` — a spoken turn; `voice_live: true` while it is still\n * being heard or spoken (the ephemeral entry the durable row replaces);\n * - `source: \"text\"` — a message typed into the live conversation;\n * - `source: \"voice_delegation\"` (id prefix `voicetask_`) — the unit of work\n * the live voice model delegated to the thread.\n */\nimport { getExternalStoreMessages, type ThreadMessage } from '@assistant-ui/core';\n\n/** `metadata.custom.source` of a spoken turn. */\nexport const VOICE_SOURCE_SPOKEN = 'voice';\n/** `metadata.custom.source` of a message typed into the live conversation. */\nexport const VOICE_SOURCE_TYPED = 'text';\n/** `metadata.custom.source` of a voice delegation's anchor message. */\nexport const VOICE_SOURCE_DELEGATION = 'voice_delegation';\n/** Id prefix of a voice delegation's anchor message (the working signal on\n * hosts that drop the command's metadata). */\nexport const VOICE_DELEGATION_ID_PREFIX = 'voicetask_';\n/** `metadata.custom` flag on a spoken turn still in flight. */\nexport const VOICE_LIVE_KEY = 'voice_live';\n/** `metadata.custom` list of the wire ids a grouped spoken bubble stands for\n * (see `groupSpokenUserTurns`), so id-keyed lookups such as checkpoint markers\n * can resolve any member to the bubble. */\nexport const VOICE_TURN_IDS_KEY = 'voice_turn_ids';\n\n/** The delegated run's interim-update tool (agora `send_voice_update`). */\nexport const VOICE_UPDATE_TOOL_NAME = 'send_voice_update';\n\n/** Trailing hint on the live-session strip, worded once for every surface. */\nexport const VOICE_LIVE_TYPED_HINT = 'typed messages go to the voice conversation';\n\nexport interface VoiceMessageMarker {\n /** The thread message id; a host may not have assigned one yet. */\n messageId: string | undefined;\n /** The thread message's `metadata.custom`, when any. */\n custom: Record<string, unknown> | undefined;\n}\n\n/** Whether a thread message is a voice delegation's anchor (the ask the live\n * voice model handed to the thread), by source or by id prefix. */\nexport function isVoiceDelegationMessage({ messageId, custom }: VoiceMessageMarker): boolean {\n return (\n custom?.source === VOICE_SOURCE_DELEGATION ||\n (typeof messageId === 'string' && messageId.startsWith(VOICE_DELEGATION_ID_PREFIX))\n );\n}\n\nexport interface VoiceProvenance {\n /** Chip copy: \"Listening\", \"Speaking\", \"Spoken\", or \"Delegated from voice\". */\n readonly label: string;\n /** True while the turn is still being heard or spoken. */\n readonly live: boolean;\n}\n\n// These values are read directly by useAuiState/useSyncExternalStore selectors.\n// Equal presentation must retain its identity, even when a transport replaces\n// the message metadata. Fresh objects here cause React's update-depth crash.\nconst SPOKEN_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Spoken',\n live: false,\n});\nconst LISTENING_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Listening',\n live: true,\n});\nconst SPEAKING_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Speaking',\n live: true,\n});\nconst DELEGATED_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Delegated from voice',\n live: false,\n});\n\n/**\n * The provenance chip for a message, or `null` for ordinary chat messages so\n * the chip is free to mount on every row. A spoken turn still in flight reads\n * as what is happening (\"Listening\" for the user, \"Speaking\" for the\n * assistant) rather than as a finished \"Spoken\" turn; a typed-in-voice turn\n * is a normal message and gets no chip. Results are immutable and stable for\n * equal presentation, so all surfaces can use this function as a selector.\n */\nexport function voiceProvenanceLabel({\n messageId,\n custom,\n role,\n}: VoiceMessageMarker & { role: 'user' | 'assistant' | string }): VoiceProvenance | null {\n const source = custom?.source;\n if (source === VOICE_SOURCE_SPOKEN) {\n if (custom?.[VOICE_LIVE_KEY] === true) {\n return role === 'user' ? LISTENING_PROVENANCE : SPEAKING_PROVENANCE;\n }\n return SPOKEN_PROVENANCE;\n }\n if (isVoiceDelegationMessage({ messageId, custom })) {\n return DELEGATED_PROVENANCE;\n }\n return null;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Whether a part of an assistant message holds the words of a spoken\n * assistant turn.\n *\n * The converter joins consecutive assistant entries into one message. The\n * host appends a live turn while it is heard, and a delegated run keeps\n * appending its own steps after it, so a turn spoken during a run becomes a\n * text part inside the run's message, before the run's later tool calls.\n * Grouped views put the text before the last tool call in the details panel\n * as the run's narration; they use this to keep the spoken words visible.\n *\n * The joined message's `metadata.custom` merges every entry in it, so this\n * reads the part's own entry, which the converter binds to the part.\n */\nexport function isSpokenTurnTextPart(part: { readonly type: string }): boolean {\n if (part.type !== 'text') return false;\n const entries: readonly unknown[] = getExternalStoreMessages<unknown>(\n part as ThreadMessage['content'][number]\n );\n return entries.some((entry) => {\n if (!isRecord(entry) || entry.role !== 'assistant' || !isRecord(entry.metadata)) return false;\n const custom = entry.metadata.custom;\n return isRecord(custom) && custom.source === VOICE_SOURCE_SPOKEN;\n });\n}\n\nexport interface VoiceUpdateToolCall {\n /** `mode: \"speak\"` — read aloud to the user; otherwise a silent progress note. */\n spoken: boolean;\n /** The update's words. */\n text: string;\n /** Whether the live session took it; `null` while the call is still running. */\n delivered: boolean | null;\n}\n\n/**\n * What a `send_voice_update` tool call did, from its arguments and the tool's\n * teaching-text result (agora returns prose, not JSON, so the run can read it).\n */\nexport function describeVoiceUpdateToolCall({\n args,\n result,\n}: {\n args: Record<string, unknown> | undefined;\n result: unknown;\n}): VoiceUpdateToolCall {\n const text = typeof args?.text === 'string' ? args.text.trim() : '';\n const spoken = args?.mode === 'speak';\n if (typeof result !== 'string' || result.trim() === '') {\n return { spoken, text, delivered: null };\n }\n const outcome = result.toLowerCase();\n const failed =\n outcome.includes('could not be delivered') ||\n outcome.includes('was not delivered') ||\n outcome.includes('rejected the update') ||\n outcome.includes('has ended') ||\n outcome.includes('not configured') ||\n outcome.includes('no voice session');\n return { spoken, text, delivered: !failed };\n}\n\n/** The card's status line for a voice update in a given tool-call state. */\nexport function voiceUpdateStatusLabel(\n call: VoiceUpdateToolCall,\n status: 'running' | 'complete' | 'incomplete' | 'requires-action' | undefined\n): string {\n if (status === 'running' || (call.delivered === null && status !== 'incomplete')) {\n return call.spoken ? 'Saying to you…' : 'Noting progress…';\n }\n if (call.delivered === false || status === 'incomplete') {\n return call.spoken ? 'Could not say this to you' : 'Progress note not delivered';\n }\n return call.spoken ? 'Said aloud' : 'Progress noted silently';\n}\n"],"names":[],"mappings":";AAgBO,MAAM,sBAAsB;AAE5B,MAAM,qBAAqB;AAE3B,MAAM,0BAA0B;AAGhC,MAAM,6BAA6B;AAEnC,MAAM,iBAAiB;AAIvB,MAAM,qBAAqB;AAG3B,MAAM,yBAAyB;AAG/B,MAAM,wBAAwB;AAW9B,SAAS,yBAAyB,EAAE,WAAW,UAAuC;AAC3F,UACE,iCAAQ,YAAW,2BAClB,OAAO,cAAc,YAAY,UAAU,WAAW,0BAA0B;AAErF;AAuDA,MAAM,WAAW,CAAC,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAgB9D,SAAS,qBAAqB,MAA0C;AAC7E,MAAI,KAAK,SAAS,OAAQ,QAAO;AACjC,QAAM,UAA8B;AAAA,IAClC;AAAA,EAAA;AAEF,SAAO,QAAQ,KAAK,CAAC,UAAU;AAC7B,QAAI,CAAC,SAAS,KAAK,KAAK,MAAM,SAAS,eAAe,CAAC,SAAS,MAAM,QAAQ,EAAG,QAAO;AACxF,UAAM,SAAS,MAAM,SAAS;AAC9B,WAAO,SAAS,MAAM,KAAK,OAAO,WAAW;AAAA,EAC/C,CAAC;AACH;AAeO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AACF,GAGwB;AACtB,QAAM,OAAO,QAAO,6BAAM,UAAS,WAAW,KAAK,KAAK,SAAS;AACjE,QAAM,UAAS,6BAAM,UAAS;AAC9B,MAAI,OAAO,WAAW,YAAY,OAAO,KAAA,MAAW,IAAI;AACtD,WAAO,EAAE,QAAQ,MAAM,WAAW,KAAA;AAAA,EACpC;AACA,QAAM,UAAU,OAAO,YAAA;AACvB,QAAM,SACJ,QAAQ,SAAS,wBAAwB,KACzC,QAAQ,SAAS,mBAAmB,KACpC,QAAQ,SAAS,qBAAqB,KACtC,QAAQ,SAAS,WAAW,KAC5B,QAAQ,SAAS,gBAAgB,KACjC,QAAQ,SAAS,kBAAkB;AACrC,SAAO,EAAE,QAAQ,MAAM,WAAW,CAAC,OAAA;AACrC;AAGO,SAAS,uBACd,MACA,QACQ;AACR,MAAI,WAAW,aAAc,KAAK,cAAc,QAAQ,WAAW,cAAe;AAChF,WAAO,KAAK,SAAS,mBAAmB;AAAA,EAC1C;AACA,MAAI,KAAK,cAAc,SAAS,WAAW,cAAc;AACvD,WAAO,KAAK,SAAS,8BAA8B;AAAA,EACrD;AACA,SAAO,KAAK,SAAS,eAAe;AACtC;"}
|
|
1
|
+
{"version":3,"file":"voice-presentation.js","sources":["../../../../agent-runtime/src/voice-presentation.ts"],"sourcesContent":["/**\n * How voice-mode transcript entries present, shared by every chat surface\n * (Olympus, the Chrome side panel, the mobile app) so a spoken turn, a\n * delegation the live voice model handed off, and an interim spoken update\n * read the same everywhere. The wire contract is the deep-agent host's\n * `additional_kwargs.metadata` (exposed as `message.metadata.custom`):\n *\n * - `source: \"voice\"` — a spoken turn; `voice_live: true` while it is still\n * being heard or spoken (the ephemeral entry the durable row replaces);\n * - `source: \"text\"` (id prefix `voicetext_`) — a message typed into the live\n * conversation;\n * - `source: \"voice_delegation\"` (id prefix `voicetask_`) — the unit of work\n * the live voice model delegated to the thread (on GPT Live a typed request\n * is handed off by the host itself, `voicetask_typed_…`, with the typed\n * words as its text).\n *\n * A delegation's anchor is the delegated run's input, so it stays in the\n * transcript; what reads once is its presentation: right under the person's\n * own words it is only an echo of them, and every app shows just its header\n * ({@link voiceDelegationEchoesRequest}).\n */\nimport { getExternalStoreMessages, type ThreadMessage } from '@assistant-ui/core';\n\n/** `metadata.custom.source` of a spoken turn. */\nexport const VOICE_SOURCE_SPOKEN = 'voice';\n/** `metadata.custom.source` of a message typed into the live conversation. */\nexport const VOICE_SOURCE_TYPED = 'text';\n/** `metadata.custom.source` of a voice delegation's anchor message. */\nexport const VOICE_SOURCE_DELEGATION = 'voice_delegation';\n/** Id prefix of a voice delegation's anchor message (the working signal on\n * hosts that drop the command's metadata). */\nexport const VOICE_DELEGATION_ID_PREFIX = 'voicetask_';\n/** Id prefix of a message typed into a live call (`voice/user-text`). */\nexport const VOICE_TYPED_ID_PREFIX = 'voicetext_';\n/** `metadata.custom` flag on a spoken turn still in flight. */\nexport const VOICE_LIVE_KEY = 'voice_live';\n/** `metadata.custom` list of the wire ids a grouped spoken bubble stands for\n * (see `groupSpokenUserTurns`), so id-keyed lookups such as checkpoint markers\n * can resolve any member to the bubble. */\nexport const VOICE_TURN_IDS_KEY = 'voice_turn_ids';\n\n/** The delegated run's interim-update tool (agora `send_voice_update`). */\nexport const VOICE_UPDATE_TOOL_NAME = 'send_voice_update';\n\n/** Trailing hint on the live-session strip, worded once for every surface. */\nexport const VOICE_LIVE_TYPED_HINT = 'typed messages go to the voice conversation';\n\nexport interface VoiceMessageMarker {\n /** The thread message id; a host may not have assigned one yet. */\n messageId: string | undefined;\n /** The thread message's `metadata.custom`, when any. */\n custom: Record<string, unknown> | undefined;\n}\n\n/** Whether a thread message is a voice delegation's anchor (the ask the live\n * voice model handed to the thread), by source or by id prefix. */\nexport function isVoiceDelegationMessage({ messageId, custom }: VoiceMessageMarker): boolean {\n return (\n custom?.source === VOICE_SOURCE_DELEGATION ||\n (typeof messageId === 'string' && messageId.startsWith(VOICE_DELEGATION_ID_PREFIX))\n );\n}\n\n/** The fields of a transcript message {@link voiceDelegationEchoesRequest}\n * reads (structural, so any runtime's thread message fits). */\nexport interface VoiceTranscriptMessage {\n readonly id: string;\n readonly role: string;\n readonly metadata?: { readonly custom?: Record<string, unknown> } | undefined;\n}\n\n/** How far back {@link voiceDelegationEchoesRequest} looks for the person's\n * turn. Consecutive assistant entries are one message after conversion, so\n * the turn a delegation answers is a step or two away; past this it is not\n * \"right above\" any more. */\nconst DELEGATION_ECHO_LOOKBACK = 32;\n/** Id prefix of the task the host hands a typed request off as (GPT Live):\n * `voicetask_typed_<suffix>` for the turn `voicetext_<suffix>`. */\nconst TYPED_TASK_ID_PREFIX = `${VOICE_DELEGATION_ID_PREFIX}typed_`;\n\n/** Whether a message is the person's own turn in a call: spoken, or typed\n * into it (by source, or by the typed id on hosts that drop the metadata). */\nfunction isPersonCallTurn(message: VoiceTranscriptMessage): boolean {\n if (message.role !== 'user') return false;\n const custom = message.metadata?.custom;\n if (isVoiceDelegationMessage({ messageId: message.id, custom })) return false;\n const source = custom?.source;\n return (\n source === VOICE_SOURCE_SPOKEN ||\n source === VOICE_SOURCE_TYPED ||\n message.id.startsWith(VOICE_TYPED_ID_PREFIX)\n );\n}\n\n/**\n * Whether the voice delegation at `index` only repeats the person's request:\n * the nearest earlier user message is the person's own turn in the call\n * (spoken, or typed into it), which already shows the request. A typed request\n * on GPT Live is handed off with the very same words, and a spoken one in the\n * live model's paraphrase; either way the person reads it once, so the apps\n * draw such a delegation as its header alone, the task's wording one tap away.\n *\n * A typed request's task names its turn: `voicetask_typed_<suffix>` is the\n * hand-off of `voicetext_<suffix>`. Requests typed while a run works commit\n * together and their tasks run one after another, so the turns and tasks\n * interleave (`voicetext_A`, `voicetext_B`, `voicetask_typed_A`, A's run,\n * `voicetask_typed_B`); a typed task is an echo exactly when its own turn is\n * loaded within the look-back, never by whichever typed turn is nearest.\n *\n * Anything else keeps the full card: a spoken delegation whose nearest\n * earlier user message is another delegation (a follow-up spoken while a run\n * worked, whose spoken turn commits only after that run settles) or an\n * ordinary message, and a typed task whose turn is not loaded, are the only\n * place their ask shows. Assistant messages in between (a spoken\n * acknowledgement) are skipped. Pure and returns a boolean, so it is safe as\n * a selector.\n */\nexport function voiceDelegationEchoesRequest(\n messages: readonly VoiceTranscriptMessage[],\n index: number,\n): boolean {\n const message = messages[index];\n if (\n message === undefined ||\n message.role !== 'user' ||\n !isVoiceDelegationMessage({ messageId: message.id, custom: message.metadata?.custom })\n ) {\n return false;\n }\n const stop = Math.max(0, index - DELEGATION_ECHO_LOOKBACK);\n if (message.id.startsWith(TYPED_TASK_ID_PREFIX)) {\n const turnId = VOICE_TYPED_ID_PREFIX + message.id.slice(TYPED_TASK_ID_PREFIX.length);\n for (let position = index - 1; position >= stop; position--) {\n if (messages[position]?.id === turnId) return true;\n }\n return false;\n }\n for (let position = index - 1; position >= stop; position--) {\n const earlier = messages[position];\n if (earlier === undefined || earlier.role !== 'user') continue;\n return isPersonCallTurn(earlier);\n }\n return false;\n}\n\nexport interface VoiceProvenance {\n /** Chip copy: \"Listening\", \"Speaking\", \"Spoken\", or \"Delegated from voice\". */\n readonly label: string;\n /** True while the turn is still being heard or spoken. */\n readonly live: boolean;\n}\n\n// These values are read directly by useAuiState/useSyncExternalStore selectors.\n// Equal presentation must retain its identity, even when a transport replaces\n// the message metadata. Fresh objects here cause React's update-depth crash.\nconst SPOKEN_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Spoken',\n live: false,\n});\nconst LISTENING_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Listening',\n live: true,\n});\nconst SPEAKING_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Speaking',\n live: true,\n});\nconst DELEGATED_PROVENANCE: VoiceProvenance = Object.freeze({\n label: 'Delegated from voice',\n live: false,\n});\n\n/**\n * The provenance chip for a message, or `null` for ordinary chat messages so\n * the chip is free to mount on every row. A spoken turn still in flight reads\n * as what is happening (\"Listening\" for the user, \"Speaking\" for the\n * assistant) rather than as a finished \"Spoken\" turn; a typed-in-voice turn\n * is a normal message and gets no chip. Results are immutable and stable for\n * equal presentation, so all surfaces can use this function as a selector.\n */\nexport function voiceProvenanceLabel({\n messageId,\n custom,\n role,\n}: VoiceMessageMarker & { role: 'user' | 'assistant' | string }): VoiceProvenance | null {\n const source = custom?.source;\n if (source === VOICE_SOURCE_SPOKEN) {\n if (custom?.[VOICE_LIVE_KEY] === true) {\n return role === 'user' ? LISTENING_PROVENANCE : SPEAKING_PROVENANCE;\n }\n return SPOKEN_PROVENANCE;\n }\n if (isVoiceDelegationMessage({ messageId, custom })) {\n return DELEGATED_PROVENANCE;\n }\n return null;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Whether a part of an assistant message holds the words of a spoken\n * assistant turn.\n *\n * The converter joins consecutive assistant entries into one message. The\n * host appends a live turn while it is heard, and a delegated run keeps\n * appending its own steps after it, so a turn spoken during a run becomes a\n * text part inside the run's message, before the run's later tool calls.\n * Grouped views put the text before the last tool call in the details panel\n * as the run's narration; they use this to keep the spoken words visible.\n *\n * The joined message's `metadata.custom` merges every entry in it, so this\n * reads the part's own entry, which the converter binds to the part.\n */\nexport function isSpokenTurnTextPart(part: { readonly type: string }): boolean {\n if (part.type !== 'text') return false;\n const entries: readonly unknown[] = getExternalStoreMessages<unknown>(\n part as ThreadMessage['content'][number]\n );\n return entries.some((entry) => {\n if (!isRecord(entry) || entry.role !== 'assistant' || !isRecord(entry.metadata)) return false;\n const custom = entry.metadata.custom;\n return isRecord(custom) && custom.source === VOICE_SOURCE_SPOKEN;\n });\n}\n\nexport interface VoiceUpdateToolCall {\n /** `mode: \"speak\"` — read aloud to the user; otherwise a silent progress note. */\n spoken: boolean;\n /** The update's words. */\n text: string;\n /** Whether the live session took it; `null` while the call is still running. */\n delivered: boolean | null;\n}\n\n/**\n * What a `send_voice_update` tool call did, from its arguments and the tool's\n * teaching-text result (agora returns prose, not JSON, so the run can read it).\n */\nexport function describeVoiceUpdateToolCall({\n args,\n result,\n}: {\n args: Record<string, unknown> | undefined;\n result: unknown;\n}): VoiceUpdateToolCall {\n const text = typeof args?.text === 'string' ? args.text.trim() : '';\n const spoken = args?.mode === 'speak';\n if (typeof result !== 'string' || result.trim() === '') {\n return { spoken, text, delivered: null };\n }\n const outcome = result.toLowerCase();\n const failed =\n outcome.includes('could not be delivered') ||\n outcome.includes('was not delivered') ||\n outcome.includes('rejected the update') ||\n outcome.includes('has ended') ||\n outcome.includes('not configured') ||\n outcome.includes('no voice session');\n return { spoken, text, delivered: !failed };\n}\n\n/** The card's status line for a voice update in a given tool-call state. */\nexport function voiceUpdateStatusLabel(\n call: VoiceUpdateToolCall,\n status: 'running' | 'complete' | 'incomplete' | 'requires-action' | undefined\n): string {\n if (status === 'running' || (call.delivered === null && status !== 'incomplete')) {\n return call.spoken ? 'Saying to you…' : 'Noting progress…';\n }\n if (call.delivered === false || status === 'incomplete') {\n return call.spoken ? 'Could not say this to you' : 'Progress note not delivered';\n }\n return call.spoken ? 'Said aloud' : 'Progress noted silently';\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -26,6 +26,7 @@ const AthenaAssetEmbed = ({
|
|
|
26
26
|
citationActionBehavior,
|
|
27
27
|
onCitationAction,
|
|
28
28
|
embedUrl: hostEmbedUrl,
|
|
29
|
+
token: frameToken,
|
|
29
30
|
onReady,
|
|
30
31
|
onContentReady,
|
|
31
32
|
onLoad,
|
|
@@ -79,17 +80,18 @@ const AthenaAssetEmbed = ({
|
|
|
79
80
|
const sendConfig = require$$0.useCallback(() => {
|
|
80
81
|
var _a, _b;
|
|
81
82
|
if (!targetOrigin) return;
|
|
83
|
+
const viewerToken = frameToken ?? athenaConfig.token;
|
|
82
84
|
(_b = (_a = iframeRef.current) == null ? void 0 : _a.contentWindow) == null ? void 0 : _b.postMessage(
|
|
83
85
|
{
|
|
84
86
|
type: assetEmbedMessages.ATHENA_ASSET_EMBED_CONFIG_MESSAGE,
|
|
85
87
|
version: 1,
|
|
86
|
-
...
|
|
88
|
+
...viewerToken ? { token: viewerToken } : {},
|
|
87
89
|
citationBehavior: effectiveCitationBehavior,
|
|
88
90
|
citationActionBehavior: effectiveCitationActionBehavior
|
|
89
91
|
},
|
|
90
92
|
targetOrigin
|
|
91
93
|
);
|
|
92
|
-
}, [athenaConfig, effectiveCitationActionBehavior, effectiveCitationBehavior, targetOrigin]);
|
|
94
|
+
}, [frameToken, athenaConfig, effectiveCitationActionBehavior, effectiveCitationBehavior, targetOrigin]);
|
|
93
95
|
require$$0.useEffect(() => {
|
|
94
96
|
if (!embedUrl || !targetOrigin) return;
|
|
95
97
|
const handleMessage = (event) => {
|