@athenaintel/react 0.36.0 → 0.37.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 +12 -2
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +24 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +24 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
- package/dist/assets/AthenaLayout.cjs +9 -2
- package/dist/assets/AthenaLayout.cjs.map +1 -1
- package/dist/assets/AthenaLayout.d.ts +6 -1
- package/dist/assets/AthenaLayout.js +9 -2
- package/dist/assets/AthenaLayout.js.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.cjs +149 -26
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.d.ts +10 -3
- package/dist/chat-ui/thread/statewire-history-edge.js +152 -29
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/workbench/SessionWorkbench.cjs +13 -3
- package/dist/workbench/SessionWorkbench.cjs.map +1 -1
- package/dist/workbench/SessionWorkbench.d.ts +5 -3
- package/dist/workbench/SessionWorkbench.js +14 -4
- package/dist/workbench/SessionWorkbench.js.map +1 -1
- package/dist/workbench/workbench-layout-context.cjs.map +1 -1
- package/dist/workbench/workbench-layout-context.d.ts +2 -0
- package/dist/workbench/workbench-layout-context.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -623,8 +623,11 @@ workbench the layout opens no asset column of its own: assets the agent or the
|
|
|
623
623
|
person opens show as the workbench's **Open assets** tab (closing the tab
|
|
624
624
|
closes the asset panel). Maximize is offered only inside `AthenaLayout`, which
|
|
625
625
|
owns the column's width; collapsing a maximized workspace brings the chat back
|
|
626
|
-
beside the rail.
|
|
627
|
-
|
|
626
|
+
beside the rail. Each session keeps its own open or collapsed workspace while
|
|
627
|
+
the layout stays mounted, so `defaultWorkbenchCollapsed` is how a session the
|
|
628
|
+
person has not opened it in starts. `SessionWorkbench` also works on its own
|
|
629
|
+
anywhere under `<AthenaProvider>`; it then keeps its collapsed state itself,
|
|
630
|
+
per session too.
|
|
628
631
|
|
|
629
632
|
Built-in panes and what each reads:
|
|
630
633
|
|
|
@@ -774,6 +777,13 @@ Both use the same bearer / API key as session creation. The full recipe
|
|
|
774
777
|
template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
|
|
775
778
|
exported if you hold the bridge state yourself.
|
|
776
779
|
|
|
780
|
+
## What's new in 0.37.0
|
|
781
|
+
|
|
782
|
+
**Each session keeps its own open or collapsed workspace.** `SessionWorkbench` already kept each session's tabs; it now keeps whether its workspace is open too. A session the person has not opened or collapsed it in, a new one or one switched to, shows `AthenaLayout`'s `defaultWorkbenchCollapsed`. With `defaultWorkbenchCollapsed`, every session starts with the workspace on its rail, and opening it in one session no longer opens it in the next. An asset the agent or the person opens still brings it out, for that session. Without the prop nothing changes for a session that has not collapsed it, but collapsing one no longer collapses the others.
|
|
783
|
+
## What's new in 0.36.1
|
|
784
|
+
|
|
785
|
+
**Opening a long session no longer jumps the transcript around.** As older history pages in above what you are reading, the reading position now stays put, instantly. Before, the chat added its own scroll correction on top of the browser's scroll anchoring (a jump by a page's height), let it play through the viewport's smooth scrolling (a fast scroll through the transcript), and missed a page that folds into the first loaded turn, which moved the text under the reader. Cut tool outputs are now read only for messages near the viewport, at most two at a time, instead of every loaded step at once: opening a long agent run sent dozens of reads, each re-rendering the transcript and all queued ahead of your next message. Nothing to change in your app.
|
|
786
|
+
|
|
777
787
|
## What's new in 0.36.0
|
|
778
788
|
|
|
779
789
|
**An agent's modes in your chat.** Pass `modes` (mode ids, `base` for the agent as configured) beside `agent` and `channel`, and `AthenaChat` shows a mode menu in its composer with the ones the agent's published definition allows on that channel: its start mode, the channel itself, and the modes its author lists for it. A pick applies to the open session's next send, or starts a new session in it. It is sent as `collab_channel_id` with the channel as `collab_entry_channel_id`, so the session keeps the channel's switch rules and still shows in a thread list filtered by channel (Athena's server reads that key from this release on and ignores it before). A channel whose author locks it lets people pick a session's mode before its first message only. `useAthenaChatMode()` and `ChatModePicker` build a menu of your own. See [Switching an agent's modes](#switching-an-agents-modes).
|
|
@@ -52,6 +52,7 @@ function useDeepAgentWindowSync({
|
|
|
52
52
|
nearTop = false,
|
|
53
53
|
nearTopEpoch = 0,
|
|
54
54
|
isMessageMounted = everyMessageMounted,
|
|
55
|
+
maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,
|
|
55
56
|
maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,
|
|
56
57
|
maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,
|
|
57
58
|
onHistoryLoad,
|
|
@@ -71,6 +72,9 @@ function useDeepAgentWindowSync({
|
|
|
71
72
|
const statedRef = react.useRef(null);
|
|
72
73
|
const restatedConnectionRef = react.useRef(null);
|
|
73
74
|
const answeredRef = react.useRef(/* @__PURE__ */ new Map());
|
|
75
|
+
const toolOutputsInFlightRef = react.useRef(0);
|
|
76
|
+
const toolOutputsWaitingRef = react.useRef(false);
|
|
77
|
+
const [toolOutputSettles, setToolOutputSettles] = react.useState(0);
|
|
74
78
|
const onHistoryLoadRef = react.useRef(onHistoryLoad);
|
|
75
79
|
onHistoryLoadRef.current = onHistoryLoad;
|
|
76
80
|
const onErrorRef = react.useRef(onError);
|
|
@@ -164,6 +168,7 @@ function useDeepAgentWindowSync({
|
|
|
164
168
|
react.useEffect(() => {
|
|
165
169
|
var _a;
|
|
166
170
|
if (window === null || toolOutputs === void 0) return;
|
|
171
|
+
toolOutputsWaitingRef.current = false;
|
|
167
172
|
for (const message of messages) {
|
|
168
173
|
if (!isMessageMounted(message.id)) continue;
|
|
169
174
|
for (const part of message.content) {
|
|
@@ -172,7 +177,12 @@ function useDeepAgentWindowSync({
|
|
|
172
177
|
if (marker === null || toolOutputs.isPending(marker.messageId)) continue;
|
|
173
178
|
if (((_a = toolOutputs.entry(marker.messageId)) == null ? void 0 : _a.chars) === marker.chars) continue;
|
|
174
179
|
if (answeredRef.current.get(marker.messageId) === marker.chars) continue;
|
|
180
|
+
if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {
|
|
181
|
+
toolOutputsWaitingRef.current = true;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
175
184
|
const { messageId, chars } = marker;
|
|
185
|
+
toolOutputsInFlightRef.current += 1;
|
|
176
186
|
toolOutputs.markPending(messageId);
|
|
177
187
|
Promise.resolve(sendCommand(messageWindow.buildToolOutputCommand({ messageId }))).then((payload) => {
|
|
178
188
|
answeredRef.current.set(messageId, chars);
|
|
@@ -187,10 +197,23 @@ function useDeepAgentWindowSync({
|
|
|
187
197
|
answeredRef.current.set(messageId, chars);
|
|
188
198
|
toolOutputs.clearPending(messageId);
|
|
189
199
|
(_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, error, "history/tool_output");
|
|
200
|
+
}).finally(() => {
|
|
201
|
+
toolOutputsInFlightRef.current -= 1;
|
|
202
|
+
if (toolOutputsWaitingRef.current && aliveRef.current) {
|
|
203
|
+
setToolOutputSettles((count) => count + 1);
|
|
204
|
+
}
|
|
190
205
|
});
|
|
191
206
|
}
|
|
192
207
|
}
|
|
193
|
-
}, [
|
|
208
|
+
}, [
|
|
209
|
+
messages,
|
|
210
|
+
isMessageMounted,
|
|
211
|
+
window,
|
|
212
|
+
sendCommand,
|
|
213
|
+
toolOutputs,
|
|
214
|
+
maxConcurrentToolOutputs,
|
|
215
|
+
toolOutputSettles
|
|
216
|
+
]);
|
|
194
217
|
const loadOlder = react.useCallback(() => {
|
|
195
218
|
setRequests((count) => count + 1);
|
|
196
219
|
}, []);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-deep-agent-window-sync.cjs","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 } 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 */\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 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; the converter puts\n * it back (`toolOutputs`), so the card re-renders complete. An output the\n * host 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/**\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 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 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 pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\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 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 const { messageId, chars } = marker;\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 }\n }\n }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);\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":["useAuiState","hasStatewireThreadExtras","useSyncExternalStore","useMemo","readDeepAgentMessageWindow","useState","useRef","useEffect","interestsCovered","DEEP_AGENT_INTEREST_PROTOCOL","pageInterestRequests","_a","readTruncatedToolOutputArtifact","buildToolOutputCommand","readToolOutputResult","useCallback"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-deep-agent-window-sync.cjs","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 } 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 */\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/**\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 pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\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":["useAuiState","hasStatewireThreadExtras","useSyncExternalStore","useMemo","readDeepAgentMessageWindow","useState","useRef","useEffect","interestsCovered","DEEP_AGENT_INTEREST_PROTOCOL","pageInterestRequests","_a","readTruncatedToolOutputArtifact","buildToolOutputCommand","readToolOutputResult","useCallback"],"mappings":"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"}
|
|
@@ -50,6 +50,7 @@ function useDeepAgentWindowSync({
|
|
|
50
50
|
nearTop = false,
|
|
51
51
|
nearTopEpoch = 0,
|
|
52
52
|
isMessageMounted = everyMessageMounted,
|
|
53
|
+
maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,
|
|
53
54
|
maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,
|
|
54
55
|
maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,
|
|
55
56
|
onHistoryLoad,
|
|
@@ -69,6 +70,9 @@ function useDeepAgentWindowSync({
|
|
|
69
70
|
const statedRef = useRef(null);
|
|
70
71
|
const restatedConnectionRef = useRef(null);
|
|
71
72
|
const answeredRef = useRef(/* @__PURE__ */ new Map());
|
|
73
|
+
const toolOutputsInFlightRef = useRef(0);
|
|
74
|
+
const toolOutputsWaitingRef = useRef(false);
|
|
75
|
+
const [toolOutputSettles, setToolOutputSettles] = useState(0);
|
|
72
76
|
const onHistoryLoadRef = useRef(onHistoryLoad);
|
|
73
77
|
onHistoryLoadRef.current = onHistoryLoad;
|
|
74
78
|
const onErrorRef = useRef(onError);
|
|
@@ -162,6 +166,7 @@ function useDeepAgentWindowSync({
|
|
|
162
166
|
useEffect(() => {
|
|
163
167
|
var _a;
|
|
164
168
|
if (window === null || toolOutputs === void 0) return;
|
|
169
|
+
toolOutputsWaitingRef.current = false;
|
|
165
170
|
for (const message of messages) {
|
|
166
171
|
if (!isMessageMounted(message.id)) continue;
|
|
167
172
|
for (const part of message.content) {
|
|
@@ -170,7 +175,12 @@ function useDeepAgentWindowSync({
|
|
|
170
175
|
if (marker === null || toolOutputs.isPending(marker.messageId)) continue;
|
|
171
176
|
if (((_a = toolOutputs.entry(marker.messageId)) == null ? void 0 : _a.chars) === marker.chars) continue;
|
|
172
177
|
if (answeredRef.current.get(marker.messageId) === marker.chars) continue;
|
|
178
|
+
if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {
|
|
179
|
+
toolOutputsWaitingRef.current = true;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
173
182
|
const { messageId, chars } = marker;
|
|
183
|
+
toolOutputsInFlightRef.current += 1;
|
|
174
184
|
toolOutputs.markPending(messageId);
|
|
175
185
|
Promise.resolve(sendCommand(buildToolOutputCommand({ messageId }))).then((payload) => {
|
|
176
186
|
answeredRef.current.set(messageId, chars);
|
|
@@ -185,10 +195,23 @@ function useDeepAgentWindowSync({
|
|
|
185
195
|
answeredRef.current.set(messageId, chars);
|
|
186
196
|
toolOutputs.clearPending(messageId);
|
|
187
197
|
(_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, error, "history/tool_output");
|
|
198
|
+
}).finally(() => {
|
|
199
|
+
toolOutputsInFlightRef.current -= 1;
|
|
200
|
+
if (toolOutputsWaitingRef.current && aliveRef.current) {
|
|
201
|
+
setToolOutputSettles((count) => count + 1);
|
|
202
|
+
}
|
|
188
203
|
});
|
|
189
204
|
}
|
|
190
205
|
}
|
|
191
|
-
}, [
|
|
206
|
+
}, [
|
|
207
|
+
messages,
|
|
208
|
+
isMessageMounted,
|
|
209
|
+
window,
|
|
210
|
+
sendCommand,
|
|
211
|
+
toolOutputs,
|
|
212
|
+
maxConcurrentToolOutputs,
|
|
213
|
+
toolOutputSettles
|
|
214
|
+
]);
|
|
192
215
|
const loadOlder = useCallback(() => {
|
|
193
216
|
setRequests((count) => count + 1);
|
|
194
217
|
}, []);
|
|
@@ -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 } 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 */\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 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; the converter puts\n * it back (`toolOutputs`), so the card re-renders complete. An output the\n * host 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/**\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 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 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 pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\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 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 const { messageId, chars } = marker;\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 }\n }\n }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);\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 { DEEP_AGENT_INTEREST_PROTOCOL } 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 */\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/**\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 pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\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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"}
|
|
@@ -96,8 +96,15 @@ const WorkbenchSplitLayout = ({
|
|
|
96
96
|
)
|
|
97
97
|
});
|
|
98
98
|
const layoutState = react.useMemo(
|
|
99
|
-
() => ({
|
|
100
|
-
|
|
99
|
+
() => ({
|
|
100
|
+
collapsed,
|
|
101
|
+
setCollapsed,
|
|
102
|
+
defaultCollapsed: defaultWorkbenchCollapsed,
|
|
103
|
+
maximized,
|
|
104
|
+
setMaximized,
|
|
105
|
+
hostsAssetPanel: true
|
|
106
|
+
}),
|
|
107
|
+
[collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized]
|
|
101
108
|
);
|
|
102
109
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "flex h-full w-full", children: [
|
|
103
110
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AthenaLayout.cjs","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /** Start with the workbench collapsed to its rail. Defaults to `false`. */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({ collapsed, setCollapsed, maximized, setMaximized, hostsAssetPanel: true }),\n [collapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":["useRef","useCallback","useAssetPanelStore","useEffect","jsx","useState","jsxs","AssetPanel","useMemo","cn","WorkbenchLayoutContext"],"mappings":";;;;;;;;AAwCA,SAAS,aAAa;AAAA,EACpB;AACF,GAGG;AACD,QAAM,eAAeA,MAAAA,OAAuB,IAAI;AAChD,QAAM,WAAWA,MAAAA,OAAO,KAAK;AAC7B,QAAM,gBAAgBC,kBAAY,CAAC,MAA0B;AAC3D,MAAE,eAAA;AACF,aAAS,UAAU;AAClB,MAAE,OAAuB,kBAAkB,EAAE,SAAS;AAAA,EACzD,GAAG,CAAA,CAAE;AACL,QAAM,gBAAgBA,MAAAA;AAAAA,IACpB,CAAC,MAA0B;AACzB,UAAI,CAAC,SAAS,WAAW,CAAC,aAAa,QAAS;AAChD,YAAM,OAAO,aAAa,QAAQ,sBAAA;AAClC,iBAAY,EAAE,UAAU,KAAK,QAAQ,KAAK,QAAS,GAAG;AAAA,IACxD;AAAA,IACA,CAAC,SAAS;AAAA,EAAA;AAEZ,QAAM,cAAcA,MAAAA,YAAY,MAAM;AACpC,aAAS,UAAU;AAAA,EACrB,GAAG,CAAA,CAAE;AACL,SAAO,EAAE,cAAc,UAAU,EAAE,eAAe,eAAe,cAAY;AAC/E;AAEA,MAAM,sBACJ;AAOK,MAAM,eAAsC,CAAC,EAAE,WAAW,GAAG,YAAY;AAC9E,QAAM,yBAAyBC,gBAAAA,mBAAmB,CAAC,MAAM,EAAE,sBAAsB;AACjF,QAAM,2BAA2BA,gBAAAA,mBAAmB,CAAC,MAAM,EAAE,wBAAwB;AAErFC,QAAAA,UAAU,MAAM;AACd,2BAAA;AACA,WAAO,MAAM;AACX,+BAAA;AAAA,IACF;AAAA,EACF,GAAG,CAAC,wBAAwB,wBAAwB,CAAC;AAErD,SAAO,cAAc,UAAa,cAAc,OAC9CC,2BAAAA,IAAC,kBAAA,EAAkB,GAAG,MAAA,CAAO,IAE7BA,2BAAAA,IAAC,sBAAA,EAAsB,GAAG,OAAO,WAAsB;AAE3D;AAEA,MAAM,mBAA6D,CAAC;AAAA,EAClE;AAAA,EACA,qBAAqB;AAAA,EACrB,aAAa;AACf,MAAM;AACJ,QAAM,SAASF,gBAAAA,mBAAmB,CAAC,MAAM,EAAE,MAAM;AACjD,QAAM,CAAC,aAAa,cAAc,IAAIG,MAAAA,SAAS,kBAAkB;AACjE,QAAM,EAAE,cAAc,SAAA,IAAa,aAAa;AAAA,IAC9C,WAAWJ,MAAAA;AAAAA,MACT,CAAC,QAAgB,eAAe,KAAK,IAAI,YAAY,KAAK,IAAI,MAAM,YAAY,GAAG,CAAC,CAAC;AAAA,MACrF,CAAC,UAAU;AAAA,IAAA;AAAA,EACb,CACD;AAED,MAAI,CAAC,QAAQ;AACX,WAAOG,2BAAAA,IAAC,OAAA,EAAI,WAAU,+BAA+B,SAAA,CAAS;AAAA,EAChE;AAEA,SACEE,2BAAAA,KAAC,OAAA,EAAI,KAAK,cAAc,WAAU,sBAChC,UAAA;AAAA,IAAAF,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO,EAAE,OAAO,GAAG,WAAW,IAAA;AAAA,QAE7B;AAAA,MAAA;AAAA,IAAA;AAAA,IAEHA,2BAAAA,IAAC,OAAA,EAAI,WAAW,qBAAsB,GAAG,SAAA,CAAU;AAAA,IACnDA,2BAAAA,IAAC,SAAI,WAAU,iEACb,yCAACG,uBAAA,EAAW,kCAAkC,OAAO,EAAA,CACvD;AAAA,EAAA,GACF;AAEJ;AAEA,MAAM,uBAEF,CAAC;AAAA,EACH;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,0BAA0B;AAAA,EAC1B,4BAA4B;AAC9B,MAAM;AACJ,QAAM,CAAC,kBAAkB,mBAAmB,IAAIF,MAAAA,SAAS,uBAAuB;AAChF,QAAM,CAAC,WAAW,iBAAiB,IAAIA,MAAAA,SAAS,yBAAyB;AACzE,QAAM,CAAC,gBAAgB,iBAAiB,IAAIA,MAAAA,SAAS,KAAK;AAE1D,QAAM,YAAY,kBAAkB,CAAC;AACrC,QAAM,eAAeJ,kBAAY,CAAC,SAAkB;AAClD,sBAAkB,IAAI;AACtB,QAAI,wBAAwB,KAAK;AAAA,EACnC,GAAG,CAAA,CAAE;AACL,QAAM,eAAeA,kBAAY,CAAC,SAAkB;AAClD,sBAAkB,IAAI;AACtB,QAAI,wBAAwB,KAAK;AAAA,EACnC,GAAG,CAAA,CAAE;AACL,QAAM,EAAE,cAAc,SAAA,IAAa,aAAa;AAAA,IAC9C,WAAWA,MAAAA;AAAAA,MACT,CAAC,QACC,oBAAoB,KAAK,IAAI,YAAY,KAAK,IAAI,MAAM,YAAY,MAAM,GAAG,CAAC,CAAC;AAAA,MACjF,CAAC,UAAU;AAAA,IAAA;AAAA,EACb,CACD;AACD,QAAM,cAAcO,MAAAA;AAAAA,IAClB,OAAO,EAAE,WAAW,cAAc,WAAW,cAAc,iBAAiB;IAC5E,CAAC,WAAW,WAAW,cAAc,YAAY;AAAA,EAAA;AAGnD,SACEF,2BAAAA,KAAC,OAAA,EAAI,KAAK,cAAc,WAAU,sBAEhC,UAAA;AAAA,IAAAF,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAWK,OAAAA,GAAG,uDAAuD,aAAa,QAAQ;AAAA,QAEzF;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF,aAAa,YAAY,OACxBL,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,oBAAiB;AAAA,QACjB,cAAW;AAAA,QACX,WAAW;AAAA,QACV,GAAG;AAAA,MAAA;AAAA,IAAA;AAAA,IAGRA,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAWK,OAAAA;AAAAA,UACT;AAAA,UACA,aAAa;AAAA,UACb,aAAa;AAAA,QAAA;AAAA,QAGf,OAAO,aAAa,YAAY,SAAY,EAAE,OAAO,GAAG,gBAAgB,IAAA;AAAA,QAExE,yCAACC,uBAAAA,uBAAuB,UAAvB,EAAgC,OAAO,aACrC,UAAA,UAAA,CACH;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;;"}
|
|
1
|
+
{"version":3,"file":"AthenaLayout.cjs","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /**\n * Whether a session's workbench starts collapsed to its rail. Each session\n * keeps its own open or collapsed workbench while the layout stays mounted,\n * as it keeps its own tabs, so this is how every session the person has not\n * opened it in shows it — a new one, or one switched to. Defaults to `false`.\n */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({\n collapsed,\n setCollapsed,\n defaultCollapsed: defaultWorkbenchCollapsed,\n maximized,\n setMaximized,\n hostsAssetPanel: true,\n }),\n [collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":["useRef","useCallback","useAssetPanelStore","useEffect","jsx","useState","jsxs","AssetPanel","useMemo","cn","WorkbenchLayoutContext"],"mappings":"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"}
|
|
@@ -15,7 +15,12 @@ export interface AthenaLayoutProps {
|
|
|
15
15
|
workbench?: ReactNode;
|
|
16
16
|
/** Default width (%) of the workbench column. Defaults to 40. */
|
|
17
17
|
defaultWorkbenchPercent?: number;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Whether a session's workbench starts collapsed to its rail. Each session
|
|
20
|
+
* keeps its own open or collapsed workbench while the layout stays mounted,
|
|
21
|
+
* as it keeps its own tabs, so this is how every session the person has not
|
|
22
|
+
* opened it in shows it — a new one, or one switched to. Defaults to `false`.
|
|
23
|
+
*/
|
|
19
24
|
defaultWorkbenchCollapsed?: boolean;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
@@ -94,8 +94,15 @@ const WorkbenchSplitLayout = ({
|
|
|
94
94
|
)
|
|
95
95
|
});
|
|
96
96
|
const layoutState = useMemo(
|
|
97
|
-
() => ({
|
|
98
|
-
|
|
97
|
+
() => ({
|
|
98
|
+
collapsed,
|
|
99
|
+
setCollapsed,
|
|
100
|
+
defaultCollapsed: defaultWorkbenchCollapsed,
|
|
101
|
+
maximized,
|
|
102
|
+
setMaximized,
|
|
103
|
+
hostsAssetPanel: true
|
|
104
|
+
}),
|
|
105
|
+
[collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized]
|
|
99
106
|
);
|
|
100
107
|
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "flex h-full w-full", children: [
|
|
101
108
|
/* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AthenaLayout.js","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /** Start with the workbench collapsed to its rail. Defaults to `false`. */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({ collapsed, setCollapsed, maximized, setMaximized, hostsAssetPanel: true }),\n [collapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AthenaLayout.js","sources":["../../src/assets/AthenaLayout.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport {\n WorkbenchLayoutContext,\n type WorkbenchLayoutState,\n} from \"../workbench/workbench-layout-context\";\nimport { useAssetPanelStore } from \"./asset-panel-store\";\nimport { AssetPanel } from \"./AssetPanel\";\n\nexport interface AthenaLayoutProps {\n children: ReactNode;\n /** Default width (%) of the chat panel when assets are open. Defaults to 50. */\n defaultChatPercent?: number;\n /** Minimum width (%) of each panel. Defaults to 25. */\n minPercent?: number;\n /**\n * A right-hand workspace column beside the chat — typically\n * `<SessionWorkbench />`. It is always shown (collapsible to a rail,\n * maximizable over the chat, and resizable by its edge), and the assets the agent or the person opens\n * render inside it as its \"Open assets\" tab instead of in a column of their\n * own.\n */\n workbench?: ReactNode;\n /** Default width (%) of the workbench column. Defaults to 40. */\n defaultWorkbenchPercent?: number;\n /**\n * Whether a session's workbench starts collapsed to its rail. Each session\n * keeps its own open or collapsed workbench while the layout stays mounted,\n * as it keeps its own tabs, so this is how every session the person has not\n * opened it in shows it — a new one, or one switched to. Defaults to `false`.\n */\n defaultWorkbenchCollapsed?: boolean;\n}\n\n/** Drag-to-resize for a split: the pointer's x as a share of the container. */\nfunction useSplitDrag({\n onPercent,\n}: {\n /** Called with the pointer's x position as a percentage of the container width. */\n onPercent: (percentFromLeft: number) => void;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dragging = useRef(false);\n const onPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault();\n dragging.current = true;\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!dragging.current || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n onPercent(((e.clientX - rect.left) / rect.width) * 100);\n },\n [onPercent],\n );\n const onPointerUp = useCallback(() => {\n dragging.current = false;\n }, []);\n return { containerRef, handlers: { onPointerDown, onPointerMove, onPointerUp } };\n}\n\nconst RESIZE_HANDLE_CLASS =\n \"athena-sdk-chrome flex h-full w-1 shrink-0 cursor-col-resize items-center bg-border/40 transition-colors hover:bg-border active:bg-primary/30\";\n\n/**\n * Layout that places the chat on the left and the asset panel on the right.\n * When no assets are open, the chat takes full width. Includes a draggable resize handle.\n * With `workbench`, the right-hand column is the workbench instead (see the prop).\n */\nexport const AthenaLayout: FC<AthenaLayoutProps> = ({ workbench, ...props }) => {\n const registerAssetPanelHost = useAssetPanelStore((s) => s.registerAssetPanelHost);\n const unregisterAssetPanelHost = useAssetPanelStore((s) => s.unregisterAssetPanelHost);\n\n useEffect(() => {\n registerAssetPanelHost();\n return () => {\n unregisterAssetPanelHost();\n };\n }, [registerAssetPanelHost, unregisterAssetPanelHost]);\n\n return workbench === undefined || workbench === null ? (\n <AssetSplitLayout {...props} />\n ) : (\n <WorkbenchSplitLayout {...props} workbench={workbench} />\n );\n};\n\nconst AssetSplitLayout: FC<Omit<AthenaLayoutProps, \"workbench\">> = ({\n children,\n defaultChatPercent = 50,\n minPercent = 25,\n}) => {\n const isOpen = useAssetPanelStore((s) => s.isOpen);\n const [chatPercent, setChatPercent] = useState(defaultChatPercent);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) => setChatPercent(Math.max(minPercent, Math.min(100 - minPercent, pct))),\n [minPercent],\n ),\n });\n\n if (!isOpen) {\n return <div className=\"flex h-full w-full flex-col\">{children}</div>;\n }\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n <div\n className=\"flex h-full flex-col overflow-hidden\"\n style={{ width: `${chatPercent}%` }}\n >\n {children}\n </div>\n <div className={RESIZE_HANDLE_CLASS} {...handlers} />\n <div className=\"athena-sdk-chrome flex h-full flex-1 flex-col overflow-hidden\">\n <AssetPanel manageAssetPanelHostRegistration={false} />\n </div>\n </div>\n );\n};\n\nconst WorkbenchSplitLayout: FC<\n Omit<AthenaLayoutProps, \"workbench\" | \"defaultChatPercent\"> & { workbench: ReactNode }\n> = ({\n children,\n workbench,\n minPercent = 25,\n defaultWorkbenchPercent = 40,\n defaultWorkbenchCollapsed = false,\n}) => {\n const [workbenchPercent, setWorkbenchPercent] = useState(defaultWorkbenchPercent);\n const [collapsed, setCollapsedState] = useState(defaultWorkbenchCollapsed);\n const [maximizedState, setMaximizedState] = useState(false);\n // A rail is never maximized: collapsing restores the chat beside it.\n const maximized = maximizedState && !collapsed;\n const setCollapsed = useCallback((next: boolean) => {\n setCollapsedState(next);\n if (next) setMaximizedState(false);\n }, []);\n const setMaximized = useCallback((next: boolean) => {\n setMaximizedState(next);\n if (next) setCollapsedState(false);\n }, []);\n const { containerRef, handlers } = useSplitDrag({\n onPercent: useCallback(\n (pct: number) =>\n setWorkbenchPercent(Math.max(minPercent, Math.min(100 - minPercent, 100 - pct))),\n [minPercent],\n ),\n });\n const layoutState = useMemo<WorkbenchLayoutState>(\n () => ({\n collapsed,\n setCollapsed,\n defaultCollapsed: defaultWorkbenchCollapsed,\n maximized,\n setMaximized,\n hostsAssetPanel: true,\n }),\n [collapsed, defaultWorkbenchCollapsed, maximized, setCollapsed, setMaximized],\n );\n\n return (\n <div ref={containerRef} className=\"flex h-full w-full\">\n {/* Hidden, not unmounted, while the workbench is maximized: the chat keeps its thread and draft. */}\n <div\n className={cn(\"flex h-full min-w-0 flex-1 flex-col overflow-hidden\", maximized && \"hidden\")}\n >\n {children}\n </div>\n {collapsed || maximized ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize workspace\"\n className={RESIZE_HANDLE_CLASS}\n {...handlers}\n />\n )}\n <div\n className={cn(\n \"flex h-full shrink-0 flex-col overflow-hidden\",\n collapsed && \"w-10\",\n maximized && \"min-w-0 flex-1\",\n )}\n // The dragged width is a runtime percentage — no static utility expresses it.\n style={collapsed || maximized ? undefined : { width: `${workbenchPercent}%` }}\n >\n <WorkbenchLayoutContext.Provider value={layoutState}>\n {workbench}\n </WorkbenchLayoutContext.Provider>\n </div>\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
|