@athenaintel/react 0.39.0 → 0.40.1
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 +39 -3
- package/dist/agent-runtime/src/errors.cjs +8 -0
- package/dist/agent-runtime/src/errors.cjs.map +1 -1
- package/dist/agent-runtime/src/errors.js +8 -0
- package/dist/agent-runtime/src/errors.js.map +1 -1
- package/dist/agent-runtime/src/messages.cjs +76 -16
- package/dist/agent-runtime/src/messages.cjs.map +1 -1
- package/dist/agent-runtime/src/messages.js +76 -16
- package/dist/agent-runtime/src/messages.js.map +1 -1
- package/dist/agent-runtime/src/tool-metadata.cjs +15 -0
- package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/agent-runtime/src/tool-metadata.js +15 -0
- package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/assets/use-asset-content.cjs +127 -0
- package/dist/assets/use-asset-content.cjs.map +1 -0
- package/dist/assets/use-asset-content.d.ts +60 -0
- package/dist/assets/use-asset-content.js +127 -0
- package/dist/assets/use-asset-content.js.map +1 -0
- package/dist/chat/AthenaChat.cjs +24 -10
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +25 -11
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/run-activity.cjs +29 -10
- package/dist/chat/run-activity.cjs.map +1 -1
- package/dist/chat/run-activity.d.ts +30 -4
- package/dist/chat/run-activity.js +29 -10
- package/dist/chat/run-activity.js.map +1 -1
- package/dist/chat/super-grouping/CollapsibleGroup.cjs +16 -8
- package/dist/chat/super-grouping/CollapsibleGroup.cjs.map +1 -1
- package/dist/chat/super-grouping/CollapsibleGroup.d.ts +26 -1
- package/dist/chat/super-grouping/CollapsibleGroup.js +16 -8
- package/dist/chat/super-grouping/CollapsibleGroup.js.map +1 -1
- package/dist/chat/super-grouping/GroupedMessageParts.cjs +59 -0
- package/dist/chat/super-grouping/GroupedMessageParts.cjs.map +1 -0
- package/dist/chat/super-grouping/GroupedMessageParts.d.ts +25 -0
- package/dist/chat/super-grouping/GroupedMessageParts.js +59 -0
- package/dist/chat/super-grouping/GroupedMessageParts.js.map +1 -0
- package/dist/chat/super-grouping/SuperGroupingCard.cjs +9 -5
- package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.d.ts +2 -1
- package/dist/chat/super-grouping/SuperGroupingCard.js +13 -9
- package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +10 -6
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.js +14 -10
- package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
- package/dist/chat/super-grouping/grouped-part-keys.cjs +104 -0
- package/dist/chat/super-grouping/grouped-part-keys.cjs.map +1 -0
- package/dist/chat/super-grouping/grouped-part-keys.d.ts +37 -0
- package/dist/chat/super-grouping/grouped-part-keys.js +104 -0
- package/dist/chat/super-grouping/grouped-part-keys.js.map +1 -0
- package/dist/chat/super-grouping/index.cjs +2 -0
- package/dist/chat/super-grouping/index.cjs.map +1 -1
- package/dist/chat/super-grouping/index.d.ts +3 -1
- package/dist/chat/super-grouping/index.js +2 -0
- package/dist/chat/super-grouping/index.js.map +1 -1
- package/dist/chat-ui/composer/tiptap-composer.cjs +25 -19
- package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
- package/dist/chat-ui/composer/tiptap-composer.d.ts +5 -0
- package/dist/chat-ui/composer/tiptap-composer.js +26 -20
- package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
- package/dist/composer/clipboard.cjs +16 -0
- package/dist/composer/clipboard.cjs.map +1 -1
- package/dist/composer/clipboard.d.ts +9 -0
- package/dist/composer/clipboard.js +17 -1
- package/dist/composer/clipboard.js.map +1 -1
- package/dist/diagnostics/errors.cjs +6 -0
- package/dist/diagnostics/errors.cjs.map +1 -1
- package/dist/diagnostics/errors.d.ts +6 -0
- package/dist/diagnostics/errors.js +6 -0
- package/dist/diagnostics/errors.js.map +1 -1
- package/dist/index.cjs +23 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/use-document-active.cjs +40 -0
- package/dist/lib/use-document-active.cjs.map +1 -0
- package/dist/lib/use-document-active.d.ts +9 -0
- package/dist/lib/use-document-active.js +40 -0
- package/dist/lib/use-document-active.js.map +1 -0
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/runtime/statewire-lifecycle.cjs +8 -0
- package/dist/runtime/statewire-lifecycle.cjs.map +1 -1
- package/dist/runtime/statewire-lifecycle.js +9 -1
- package/dist/runtime/statewire-lifecycle.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/threads/Inbox.cjs +6 -2
- package/dist/threads/Inbox.cjs.map +1 -1
- package/dist/threads/Inbox.js +6 -2
- package/dist/threads/Inbox.js.map +1 -1
- package/dist/threads/mark-viewed-thread-read.cjs +97 -0
- package/dist/threads/mark-viewed-thread-read.cjs.map +1 -0
- package/dist/threads/mark-viewed-thread-read.d.ts +7 -0
- package/dist/threads/mark-viewed-thread-read.js +97 -0
- package/dist/threads/mark-viewed-thread-read.js.map +1 -0
- package/dist/tools/index.cjs +7 -0
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inline-tools.cjs +10 -0
- package/dist/tools/inline-tools.cjs.map +1 -0
- package/dist/tools/inline-tools.d.ts +2 -0
- package/dist/tools/inline-tools.js +10 -0
- package/dist/tools/inline-tools.js.map +1 -0
- package/dist/tools/test-recording-report.cjs +175 -0
- package/dist/tools/test-recording-report.cjs.map +1 -0
- package/dist/tools/test-recording-report.d.ts +82 -0
- package/dist/tools/test-recording-report.js +175 -0
- package/dist/tools/test-recording-report.js.map +1 -0
- package/dist/tools/test-recording-tool-uis.cjs +498 -0
- package/dist/tools/test-recording-tool-uis.cjs.map +1 -0
- package/dist/tools/test-recording-tool-uis.d.ts +32 -0
- package/dist/tools/test-recording-tool-uis.js +498 -0
- package/dist/tools/test-recording-tool-uis.js.map +1 -0
- package/dist/tools/tool-summary.cjs +49 -1
- package/dist/tools/tool-summary.cjs.map +1 -1
- package/dist/tools/tool-summary.js +49 -1
- package/dist/tools/tool-summary.js.map +1 -1
- package/dist/tools/tool-uis.cjs +6 -1
- package/dist/tools/tool-uis.cjs.map +1 -1
- package/dist/tools/tool-uis.js +6 -1
- package/dist/tools/tool-uis.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const reactQuery = require("@tanstack/react-query");
|
|
4
|
+
const react = require("react");
|
|
5
|
+
const mint = require("../collab/mint.cjs");
|
|
6
|
+
const errors = require("../diagnostics/errors.cjs");
|
|
7
|
+
const useAthenaRestQuery = require("../lib/use-athena-rest-query.cjs");
|
|
8
|
+
const AthenaContext = require("../provider/AthenaContext.cjs");
|
|
9
|
+
const credentialRenewal = require("../runtime/credential-renewal.cjs");
|
|
10
|
+
function athenaAssetContentPath(assetId) {
|
|
11
|
+
return `/api/asset/content/${encodeURIComponent(assetId)}`;
|
|
12
|
+
}
|
|
13
|
+
function isAbortError(error) {
|
|
14
|
+
return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
|
|
15
|
+
}
|
|
16
|
+
function assetContentError({
|
|
17
|
+
error,
|
|
18
|
+
assetId,
|
|
19
|
+
message,
|
|
20
|
+
hint
|
|
21
|
+
}) {
|
|
22
|
+
if (isAbortError(error) || errors.isAthenaSdkError(error)) return error;
|
|
23
|
+
return new errors.AthenaSdkError({
|
|
24
|
+
code: "asset_content_failed",
|
|
25
|
+
message,
|
|
26
|
+
cause: error,
|
|
27
|
+
hint,
|
|
28
|
+
context: { assetId }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function fetchAthenaAssetContent({
|
|
32
|
+
backendUrl,
|
|
33
|
+
auth,
|
|
34
|
+
assetId,
|
|
35
|
+
signal
|
|
36
|
+
}) {
|
|
37
|
+
const url = `${mint.getAthenaApiBaseUrl(backendUrl)}${athenaAssetContentPath(assetId)}`;
|
|
38
|
+
let response;
|
|
39
|
+
try {
|
|
40
|
+
response = await credentialRenewal.sendWithCredentialRenewal(
|
|
41
|
+
auth,
|
|
42
|
+
(headers) => fetch(url, { method: "GET", headers, signal: signal ?? null })
|
|
43
|
+
);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw assetContentError({
|
|
46
|
+
error,
|
|
47
|
+
assetId,
|
|
48
|
+
message: "Reading the asset could not reach the Agora API, or its credential could not be resolved.",
|
|
49
|
+
hint: "Check the network, that backendUrl points at the Agora API base, and that getToken resolves."
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const refused = response.status === 401 || response.status === 403;
|
|
54
|
+
throw new errors.AthenaSdkError({
|
|
55
|
+
code: refused ? "auth_rejected" : "asset_content_failed",
|
|
56
|
+
message: `Reading the asset failed: ${response.status}`,
|
|
57
|
+
status: response.status,
|
|
58
|
+
detail: await response.json().catch(() => void 0),
|
|
59
|
+
hint: refused ? "The signed-in user cannot read this asset." : response.status === 404 ? "The asset has no stored file, or it no longer exists." : "Check that backendUrl points at the Agora API base.",
|
|
60
|
+
context: { assetId }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return await response.blob();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw assetContentError({
|
|
67
|
+
error,
|
|
68
|
+
assetId,
|
|
69
|
+
message: "Reading the asset failed while its file downloaded.",
|
|
70
|
+
hint: "Check the network and try again."
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const NO_RETRY_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 409]);
|
|
75
|
+
function retryTransient(failureCount, error) {
|
|
76
|
+
const status = error.status;
|
|
77
|
+
if (typeof status === "number" && NO_RETRY_STATUSES.has(status)) return false;
|
|
78
|
+
return failureCount < 1;
|
|
79
|
+
}
|
|
80
|
+
function useAthenaAssetContent({
|
|
81
|
+
assetId,
|
|
82
|
+
enabled = true
|
|
83
|
+
}) {
|
|
84
|
+
const config = AthenaContext.useAthenaConfig();
|
|
85
|
+
const { backendUrl, apiKey, getToken } = config;
|
|
86
|
+
const identity = useAthenaRestQuery.useAthenaQueryIdentity({ apiKey, token: config.token, getToken });
|
|
87
|
+
const queryClient = useAthenaRestQuery.useAthenaQueryClient();
|
|
88
|
+
const reader = react.useId();
|
|
89
|
+
return reactQuery.useQuery(
|
|
90
|
+
{
|
|
91
|
+
queryKey: ["athena-sdk", "asset-content", backendUrl, ...identity, assetId, reader],
|
|
92
|
+
// `config.token` is read at fetch time: the provider serves the latest
|
|
93
|
+
// token through a getter, so rotations reach the request.
|
|
94
|
+
queryFn: ({ signal }) => fetchAthenaAssetContent({
|
|
95
|
+
backendUrl,
|
|
96
|
+
auth: { apiKey, token: config.token, getToken },
|
|
97
|
+
assetId: assetId ?? "",
|
|
98
|
+
signal
|
|
99
|
+
}),
|
|
100
|
+
enabled: enabled && assetId !== null && assetId.length > 0 && backendUrl.length > 0,
|
|
101
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
102
|
+
gcTime: 0,
|
|
103
|
+
refetchOnWindowFocus: false,
|
|
104
|
+
refetchOnReconnect: false,
|
|
105
|
+
retry: retryTransient
|
|
106
|
+
},
|
|
107
|
+
queryClient
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function useObjectUrl(blob) {
|
|
111
|
+
const [url, setUrl] = react.useState(null);
|
|
112
|
+
react.useEffect(() => {
|
|
113
|
+
if (!blob) {
|
|
114
|
+
setUrl(null);
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
118
|
+
setUrl(objectUrl);
|
|
119
|
+
return () => URL.revokeObjectURL(objectUrl);
|
|
120
|
+
}, [blob]);
|
|
121
|
+
return url;
|
|
122
|
+
}
|
|
123
|
+
exports.athenaAssetContentPath = athenaAssetContentPath;
|
|
124
|
+
exports.fetchAthenaAssetContent = fetchAthenaAssetContent;
|
|
125
|
+
exports.useAthenaAssetContent = useAthenaAssetContent;
|
|
126
|
+
exports.useObjectUrl = useObjectUrl;
|
|
127
|
+
//# sourceMappingURL=use-asset-content.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-asset-content.cjs","sources":["../../src/assets/use-asset-content.ts"],"sourcesContent":["/**\n * An asset's stored file, read with the signed-in user's own credential:\n * `GET /api/asset/content/{asset_id}` (agora `web/api/asset/views.py`\n * `serve_asset_content`), which passes the asset's permission check\n * (`get_asset_with_permission_check`) and streams the bytes through Agora.\n * No link is minted — no presigned storage URL, no embed token — so nothing\n * that could be opened without the credential ever exists: the bytes land in\n * a `Blob` that only this page holds, and a media element plays it through an\n * object URL (`useObjectUrl`).\n *\n * This is how the SDK plays an asset's video: a `<video src>` cannot carry\n * the `Authorization` / `X-API-KEY` header the SDK authenticates with, and the\n * alternatives (`/api/asset/fetch-presigned-url`, an `AthenaAssetEmbed` link)\n * each hand out a URL that works for whoever holds it. The whole file is read\n * before playback starts, so read it when the person asks to play, not on\n * mount.\n */\nimport { useQuery, type UseQueryResult } from '@tanstack/react-query';\nimport { useEffect, useId, useState } from 'react';\nimport { getAthenaApiBaseUrl } from '../collab/mint';\nimport { AthenaSdkError, isAthenaSdkError } from '../diagnostics/errors';\nimport { useAthenaQueryClient, useAthenaQueryIdentity } from '../lib/use-athena-rest-query';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport { sendWithCredentialRenewal } from '../runtime/credential-renewal';\nimport type { StatewireAuthSource } from '../runtime/statewire-auth-headers';\n\n/** `/api/asset/content/{asset_id}`: the asset's stored file, permission-checked. */\nexport function athenaAssetContentPath(assetId: string): string {\n return `/api/asset/content/${encodeURIComponent(assetId)}`;\n}\n\n/** A `fetch` the caller cancelled (unmount, a newer query): lifecycle, not failure. */\nfunction isAbortError(error: unknown): boolean {\n return (error instanceof DOMException || error instanceof Error) && error.name === 'AbortError';\n}\n\n/** `error` as an `asset_content_failed` SDK error; an abort or an SDK error passes through. */\nfunction assetContentError({\n error,\n assetId,\n message,\n hint,\n}: {\n error: unknown;\n assetId: string;\n message: string;\n hint: string;\n}): unknown {\n if (isAbortError(error) || isAthenaSdkError(error)) return error;\n return new AthenaSdkError({\n code: 'asset_content_failed',\n message,\n cause: error,\n hint,\n context: { assetId },\n });\n}\n\n/**\n * Read an asset's stored file as a `Blob`. The credential rides\n * `Authorization: Bearer` (resolved live through `getToken` when the host gave\n * one) or `X-API-KEY`; a refused bearer (`401`) is renewed once\n * (`sendWithCredentialRenewal`). Every failure is an `AthenaSdkError` —\n * `auth_rejected` for a 401 / 403, else `asset_content_failed` (with the HTTP\n * `status` when the server answered), a credential that could not be\n * resolved and a download cut off mid-body included; only an abort keeps its\n * own identity.\n */\nexport async function fetchAthenaAssetContent({\n backendUrl,\n auth,\n assetId,\n signal,\n}: {\n backendUrl: string;\n auth: StatewireAuthSource;\n assetId: string;\n signal?: AbortSignal;\n}): Promise<Blob> {\n const url = `${getAthenaApiBaseUrl(backendUrl)}${athenaAssetContentPath(assetId)}`;\n let response: Response;\n try {\n response = await sendWithCredentialRenewal(auth, (headers) =>\n fetch(url, { method: 'GET', headers, signal: signal ?? null }),\n );\n } catch (error) {\n throw assetContentError({\n error,\n assetId,\n message: 'Reading the asset could not reach the Agora API, or its credential could not be resolved.',\n hint: 'Check the network, that backendUrl points at the Agora API base, and that getToken resolves.',\n });\n }\n if (!response.ok) {\n const refused = response.status === 401 || response.status === 403;\n throw new AthenaSdkError({\n code: refused ? 'auth_rejected' : 'asset_content_failed',\n message: `Reading the asset failed: ${response.status}`,\n status: response.status,\n detail: await response.json().catch(() => undefined),\n hint: refused\n ? 'The signed-in user cannot read this asset.'\n : response.status === 404\n ? 'The asset has no stored file, or it no longer exists.'\n : 'Check that backendUrl points at the Agora API base.',\n context: { assetId },\n });\n }\n try {\n return await response.blob();\n } catch (error) {\n throw assetContentError({\n error,\n assetId,\n message: 'Reading the asset failed while its file downloaded.',\n hint: 'Check the network and try again.',\n });\n }\n}\n\nconst NO_RETRY_STATUSES: ReadonlySet<number> = new Set([400, 401, 403, 404, 409]);\n\n/** Retry a transient failure once; a refusal or a missing file answers the same way again. */\nfunction retryTransient(failureCount: number, error: Error): boolean {\n const status = (error as { status?: unknown }).status;\n if (typeof status === 'number' && NO_RETRY_STATUSES.has(status)) return false;\n return failureCount < 1;\n}\n\nexport interface UseAthenaAssetContentOptions {\n /** The asset to read; `null` holds the query idle. */\n assetId: string | null;\n /** `false` holds the query idle (read on demand: the whole file is downloaded). Default `true`. */\n enabled?: boolean;\n}\n\n/**\n * An asset's stored file through TanStack Query, with the provider's\n * `backendUrl` and credentials. Each mounted hook reads its own copy: the key\n * carries the hook instance as well as the asset, the signed-in principal and\n * a fingerprint of the credential (`useAthenaQueryIdentity`, never the\n * credential itself), and the bytes are dropped as soon as it unmounts. So no\n * reader is ever handed bytes another mount read — not even behind a host\n * `getToken`, whose account the key cannot see — and every new reader passes\n * the permission check again. While mounted the file is read once (a stored\n * file does not change under its asset id); `refetch()` reads it again.\n */\nexport function useAthenaAssetContent({\n assetId,\n enabled = true,\n}: UseAthenaAssetContentOptions): UseQueryResult<Blob, Error> {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const identity = useAthenaQueryIdentity({ apiKey, token: config.token, getToken });\n const queryClient = useAthenaQueryClient();\n const reader = useId();\n return useQuery(\n {\n queryKey: ['athena-sdk', 'asset-content', backendUrl, ...identity, assetId, reader],\n // `config.token` is read at fetch time: the provider serves the latest\n // token through a getter, so rotations reach the request.\n queryFn: ({ signal }) =>\n fetchAthenaAssetContent({\n backendUrl,\n auth: { apiKey, token: config.token, getToken },\n assetId: assetId ?? '',\n signal,\n }),\n enabled: enabled && assetId !== null && assetId.length > 0 && backendUrl.length > 0,\n staleTime: Number.POSITIVE_INFINITY,\n gcTime: 0,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n retry: retryTransient,\n },\n queryClient,\n );\n}\n\n/**\n * An object URL for `blob` while it is shown, revoked when the blob changes\n * or the component unmounts; `null` without a blob.\n */\nexport function useObjectUrl(blob: Blob | null | undefined): string | null {\n const [url, setUrl] = useState<string | null>(null);\n useEffect(() => {\n if (!blob) {\n setUrl(null);\n return undefined;\n }\n const objectUrl = URL.createObjectURL(blob);\n setUrl(objectUrl);\n return () => URL.revokeObjectURL(objectUrl);\n }, [blob]);\n return url;\n}\n"],"names":["isAthenaSdkError","AthenaSdkError","getAthenaApiBaseUrl","sendWithCredentialRenewal","useAthenaConfig","useAthenaQueryIdentity","useAthenaQueryClient","useId","useQuery","useState","useEffect"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An asset's stored file, read with the signed-in user's own credential:
|
|
3
|
+
* `GET /api/asset/content/{asset_id}` (agora `web/api/asset/views.py`
|
|
4
|
+
* `serve_asset_content`), which passes the asset's permission check
|
|
5
|
+
* (`get_asset_with_permission_check`) and streams the bytes through Agora.
|
|
6
|
+
* No link is minted — no presigned storage URL, no embed token — so nothing
|
|
7
|
+
* that could be opened without the credential ever exists: the bytes land in
|
|
8
|
+
* a `Blob` that only this page holds, and a media element plays it through an
|
|
9
|
+
* object URL (`useObjectUrl`).
|
|
10
|
+
*
|
|
11
|
+
* This is how the SDK plays an asset's video: a `<video src>` cannot carry
|
|
12
|
+
* the `Authorization` / `X-API-KEY` header the SDK authenticates with, and the
|
|
13
|
+
* alternatives (`/api/asset/fetch-presigned-url`, an `AthenaAssetEmbed` link)
|
|
14
|
+
* each hand out a URL that works for whoever holds it. The whole file is read
|
|
15
|
+
* before playback starts, so read it when the person asks to play, not on
|
|
16
|
+
* mount.
|
|
17
|
+
*/
|
|
18
|
+
import { type UseQueryResult } from '@tanstack/react-query';
|
|
19
|
+
import type { StatewireAuthSource } from '../runtime/statewire-auth-headers';
|
|
20
|
+
/** `/api/asset/content/{asset_id}`: the asset's stored file, permission-checked. */
|
|
21
|
+
export declare function athenaAssetContentPath(assetId: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Read an asset's stored file as a `Blob`. The credential rides
|
|
24
|
+
* `Authorization: Bearer` (resolved live through `getToken` when the host gave
|
|
25
|
+
* one) or `X-API-KEY`; a refused bearer (`401`) is renewed once
|
|
26
|
+
* (`sendWithCredentialRenewal`). Every failure is an `AthenaSdkError` —
|
|
27
|
+
* `auth_rejected` for a 401 / 403, else `asset_content_failed` (with the HTTP
|
|
28
|
+
* `status` when the server answered), a credential that could not be
|
|
29
|
+
* resolved and a download cut off mid-body included; only an abort keeps its
|
|
30
|
+
* own identity.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fetchAthenaAssetContent({ backendUrl, auth, assetId, signal, }: {
|
|
33
|
+
backendUrl: string;
|
|
34
|
+
auth: StatewireAuthSource;
|
|
35
|
+
assetId: string;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}): Promise<Blob>;
|
|
38
|
+
export interface UseAthenaAssetContentOptions {
|
|
39
|
+
/** The asset to read; `null` holds the query idle. */
|
|
40
|
+
assetId: string | null;
|
|
41
|
+
/** `false` holds the query idle (read on demand: the whole file is downloaded). Default `true`. */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* An asset's stored file through TanStack Query, with the provider's
|
|
46
|
+
* `backendUrl` and credentials. Each mounted hook reads its own copy: the key
|
|
47
|
+
* carries the hook instance as well as the asset, the signed-in principal and
|
|
48
|
+
* a fingerprint of the credential (`useAthenaQueryIdentity`, never the
|
|
49
|
+
* credential itself), and the bytes are dropped as soon as it unmounts. So no
|
|
50
|
+
* reader is ever handed bytes another mount read — not even behind a host
|
|
51
|
+
* `getToken`, whose account the key cannot see — and every new reader passes
|
|
52
|
+
* the permission check again. While mounted the file is read once (a stored
|
|
53
|
+
* file does not change under its asset id); `refetch()` reads it again.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useAthenaAssetContent({ assetId, enabled, }: UseAthenaAssetContentOptions): UseQueryResult<Blob, Error>;
|
|
56
|
+
/**
|
|
57
|
+
* An object URL for `blob` while it is shown, revoked when the blob changes
|
|
58
|
+
* or the component unmounts; `null` without a blob.
|
|
59
|
+
*/
|
|
60
|
+
export declare function useObjectUrl(blob: Blob | null | undefined): string | null;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { useId, useState, useEffect } from "react";
|
|
3
|
+
import { getAthenaApiBaseUrl } from "../collab/mint.js";
|
|
4
|
+
import { AthenaSdkError, isAthenaSdkError } from "../diagnostics/errors.js";
|
|
5
|
+
import { useAthenaQueryIdentity, useAthenaQueryClient } from "../lib/use-athena-rest-query.js";
|
|
6
|
+
import { useAthenaConfig } from "../provider/AthenaContext.js";
|
|
7
|
+
import { sendWithCredentialRenewal } from "../runtime/credential-renewal.js";
|
|
8
|
+
function athenaAssetContentPath(assetId) {
|
|
9
|
+
return `/api/asset/content/${encodeURIComponent(assetId)}`;
|
|
10
|
+
}
|
|
11
|
+
function isAbortError(error) {
|
|
12
|
+
return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
|
|
13
|
+
}
|
|
14
|
+
function assetContentError({
|
|
15
|
+
error,
|
|
16
|
+
assetId,
|
|
17
|
+
message,
|
|
18
|
+
hint
|
|
19
|
+
}) {
|
|
20
|
+
if (isAbortError(error) || isAthenaSdkError(error)) return error;
|
|
21
|
+
return new AthenaSdkError({
|
|
22
|
+
code: "asset_content_failed",
|
|
23
|
+
message,
|
|
24
|
+
cause: error,
|
|
25
|
+
hint,
|
|
26
|
+
context: { assetId }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function fetchAthenaAssetContent({
|
|
30
|
+
backendUrl,
|
|
31
|
+
auth,
|
|
32
|
+
assetId,
|
|
33
|
+
signal
|
|
34
|
+
}) {
|
|
35
|
+
const url = `${getAthenaApiBaseUrl(backendUrl)}${athenaAssetContentPath(assetId)}`;
|
|
36
|
+
let response;
|
|
37
|
+
try {
|
|
38
|
+
response = await sendWithCredentialRenewal(
|
|
39
|
+
auth,
|
|
40
|
+
(headers) => fetch(url, { method: "GET", headers, signal: signal ?? null })
|
|
41
|
+
);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw assetContentError({
|
|
44
|
+
error,
|
|
45
|
+
assetId,
|
|
46
|
+
message: "Reading the asset could not reach the Agora API, or its credential could not be resolved.",
|
|
47
|
+
hint: "Check the network, that backendUrl points at the Agora API base, and that getToken resolves."
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const refused = response.status === 401 || response.status === 403;
|
|
52
|
+
throw new AthenaSdkError({
|
|
53
|
+
code: refused ? "auth_rejected" : "asset_content_failed",
|
|
54
|
+
message: `Reading the asset failed: ${response.status}`,
|
|
55
|
+
status: response.status,
|
|
56
|
+
detail: await response.json().catch(() => void 0),
|
|
57
|
+
hint: refused ? "The signed-in user cannot read this asset." : response.status === 404 ? "The asset has no stored file, or it no longer exists." : "Check that backendUrl points at the Agora API base.",
|
|
58
|
+
context: { assetId }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return await response.blob();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw assetContentError({
|
|
65
|
+
error,
|
|
66
|
+
assetId,
|
|
67
|
+
message: "Reading the asset failed while its file downloaded.",
|
|
68
|
+
hint: "Check the network and try again."
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const NO_RETRY_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 409]);
|
|
73
|
+
function retryTransient(failureCount, error) {
|
|
74
|
+
const status = error.status;
|
|
75
|
+
if (typeof status === "number" && NO_RETRY_STATUSES.has(status)) return false;
|
|
76
|
+
return failureCount < 1;
|
|
77
|
+
}
|
|
78
|
+
function useAthenaAssetContent({
|
|
79
|
+
assetId,
|
|
80
|
+
enabled = true
|
|
81
|
+
}) {
|
|
82
|
+
const config = useAthenaConfig();
|
|
83
|
+
const { backendUrl, apiKey, getToken } = config;
|
|
84
|
+
const identity = useAthenaQueryIdentity({ apiKey, token: config.token, getToken });
|
|
85
|
+
const queryClient = useAthenaQueryClient();
|
|
86
|
+
const reader = useId();
|
|
87
|
+
return useQuery(
|
|
88
|
+
{
|
|
89
|
+
queryKey: ["athena-sdk", "asset-content", backendUrl, ...identity, assetId, reader],
|
|
90
|
+
// `config.token` is read at fetch time: the provider serves the latest
|
|
91
|
+
// token through a getter, so rotations reach the request.
|
|
92
|
+
queryFn: ({ signal }) => fetchAthenaAssetContent({
|
|
93
|
+
backendUrl,
|
|
94
|
+
auth: { apiKey, token: config.token, getToken },
|
|
95
|
+
assetId: assetId ?? "",
|
|
96
|
+
signal
|
|
97
|
+
}),
|
|
98
|
+
enabled: enabled && assetId !== null && assetId.length > 0 && backendUrl.length > 0,
|
|
99
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
100
|
+
gcTime: 0,
|
|
101
|
+
refetchOnWindowFocus: false,
|
|
102
|
+
refetchOnReconnect: false,
|
|
103
|
+
retry: retryTransient
|
|
104
|
+
},
|
|
105
|
+
queryClient
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function useObjectUrl(blob) {
|
|
109
|
+
const [url, setUrl] = useState(null);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!blob) {
|
|
112
|
+
setUrl(null);
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
116
|
+
setUrl(objectUrl);
|
|
117
|
+
return () => URL.revokeObjectURL(objectUrl);
|
|
118
|
+
}, [blob]);
|
|
119
|
+
return url;
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
athenaAssetContentPath,
|
|
123
|
+
fetchAthenaAssetContent,
|
|
124
|
+
useAthenaAssetContent,
|
|
125
|
+
useObjectUrl
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=use-asset-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-asset-content.js","sources":["../../src/assets/use-asset-content.ts"],"sourcesContent":["/**\n * An asset's stored file, read with the signed-in user's own credential:\n * `GET /api/asset/content/{asset_id}` (agora `web/api/asset/views.py`\n * `serve_asset_content`), which passes the asset's permission check\n * (`get_asset_with_permission_check`) and streams the bytes through Agora.\n * No link is minted — no presigned storage URL, no embed token — so nothing\n * that could be opened without the credential ever exists: the bytes land in\n * a `Blob` that only this page holds, and a media element plays it through an\n * object URL (`useObjectUrl`).\n *\n * This is how the SDK plays an asset's video: a `<video src>` cannot carry\n * the `Authorization` / `X-API-KEY` header the SDK authenticates with, and the\n * alternatives (`/api/asset/fetch-presigned-url`, an `AthenaAssetEmbed` link)\n * each hand out a URL that works for whoever holds it. The whole file is read\n * before playback starts, so read it when the person asks to play, not on\n * mount.\n */\nimport { useQuery, type UseQueryResult } from '@tanstack/react-query';\nimport { useEffect, useId, useState } from 'react';\nimport { getAthenaApiBaseUrl } from '../collab/mint';\nimport { AthenaSdkError, isAthenaSdkError } from '../diagnostics/errors';\nimport { useAthenaQueryClient, useAthenaQueryIdentity } from '../lib/use-athena-rest-query';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport { sendWithCredentialRenewal } from '../runtime/credential-renewal';\nimport type { StatewireAuthSource } from '../runtime/statewire-auth-headers';\n\n/** `/api/asset/content/{asset_id}`: the asset's stored file, permission-checked. */\nexport function athenaAssetContentPath(assetId: string): string {\n return `/api/asset/content/${encodeURIComponent(assetId)}`;\n}\n\n/** A `fetch` the caller cancelled (unmount, a newer query): lifecycle, not failure. */\nfunction isAbortError(error: unknown): boolean {\n return (error instanceof DOMException || error instanceof Error) && error.name === 'AbortError';\n}\n\n/** `error` as an `asset_content_failed` SDK error; an abort or an SDK error passes through. */\nfunction assetContentError({\n error,\n assetId,\n message,\n hint,\n}: {\n error: unknown;\n assetId: string;\n message: string;\n hint: string;\n}): unknown {\n if (isAbortError(error) || isAthenaSdkError(error)) return error;\n return new AthenaSdkError({\n code: 'asset_content_failed',\n message,\n cause: error,\n hint,\n context: { assetId },\n });\n}\n\n/**\n * Read an asset's stored file as a `Blob`. The credential rides\n * `Authorization: Bearer` (resolved live through `getToken` when the host gave\n * one) or `X-API-KEY`; a refused bearer (`401`) is renewed once\n * (`sendWithCredentialRenewal`). Every failure is an `AthenaSdkError` —\n * `auth_rejected` for a 401 / 403, else `asset_content_failed` (with the HTTP\n * `status` when the server answered), a credential that could not be\n * resolved and a download cut off mid-body included; only an abort keeps its\n * own identity.\n */\nexport async function fetchAthenaAssetContent({\n backendUrl,\n auth,\n assetId,\n signal,\n}: {\n backendUrl: string;\n auth: StatewireAuthSource;\n assetId: string;\n signal?: AbortSignal;\n}): Promise<Blob> {\n const url = `${getAthenaApiBaseUrl(backendUrl)}${athenaAssetContentPath(assetId)}`;\n let response: Response;\n try {\n response = await sendWithCredentialRenewal(auth, (headers) =>\n fetch(url, { method: 'GET', headers, signal: signal ?? null }),\n );\n } catch (error) {\n throw assetContentError({\n error,\n assetId,\n message: 'Reading the asset could not reach the Agora API, or its credential could not be resolved.',\n hint: 'Check the network, that backendUrl points at the Agora API base, and that getToken resolves.',\n });\n }\n if (!response.ok) {\n const refused = response.status === 401 || response.status === 403;\n throw new AthenaSdkError({\n code: refused ? 'auth_rejected' : 'asset_content_failed',\n message: `Reading the asset failed: ${response.status}`,\n status: response.status,\n detail: await response.json().catch(() => undefined),\n hint: refused\n ? 'The signed-in user cannot read this asset.'\n : response.status === 404\n ? 'The asset has no stored file, or it no longer exists.'\n : 'Check that backendUrl points at the Agora API base.',\n context: { assetId },\n });\n }\n try {\n return await response.blob();\n } catch (error) {\n throw assetContentError({\n error,\n assetId,\n message: 'Reading the asset failed while its file downloaded.',\n hint: 'Check the network and try again.',\n });\n }\n}\n\nconst NO_RETRY_STATUSES: ReadonlySet<number> = new Set([400, 401, 403, 404, 409]);\n\n/** Retry a transient failure once; a refusal or a missing file answers the same way again. */\nfunction retryTransient(failureCount: number, error: Error): boolean {\n const status = (error as { status?: unknown }).status;\n if (typeof status === 'number' && NO_RETRY_STATUSES.has(status)) return false;\n return failureCount < 1;\n}\n\nexport interface UseAthenaAssetContentOptions {\n /** The asset to read; `null` holds the query idle. */\n assetId: string | null;\n /** `false` holds the query idle (read on demand: the whole file is downloaded). Default `true`. */\n enabled?: boolean;\n}\n\n/**\n * An asset's stored file through TanStack Query, with the provider's\n * `backendUrl` and credentials. Each mounted hook reads its own copy: the key\n * carries the hook instance as well as the asset, the signed-in principal and\n * a fingerprint of the credential (`useAthenaQueryIdentity`, never the\n * credential itself), and the bytes are dropped as soon as it unmounts. So no\n * reader is ever handed bytes another mount read — not even behind a host\n * `getToken`, whose account the key cannot see — and every new reader passes\n * the permission check again. While mounted the file is read once (a stored\n * file does not change under its asset id); `refetch()` reads it again.\n */\nexport function useAthenaAssetContent({\n assetId,\n enabled = true,\n}: UseAthenaAssetContentOptions): UseQueryResult<Blob, Error> {\n const config = useAthenaConfig();\n const { backendUrl, apiKey, getToken } = config;\n const identity = useAthenaQueryIdentity({ apiKey, token: config.token, getToken });\n const queryClient = useAthenaQueryClient();\n const reader = useId();\n return useQuery(\n {\n queryKey: ['athena-sdk', 'asset-content', backendUrl, ...identity, assetId, reader],\n // `config.token` is read at fetch time: the provider serves the latest\n // token through a getter, so rotations reach the request.\n queryFn: ({ signal }) =>\n fetchAthenaAssetContent({\n backendUrl,\n auth: { apiKey, token: config.token, getToken },\n assetId: assetId ?? '',\n signal,\n }),\n enabled: enabled && assetId !== null && assetId.length > 0 && backendUrl.length > 0,\n staleTime: Number.POSITIVE_INFINITY,\n gcTime: 0,\n refetchOnWindowFocus: false,\n refetchOnReconnect: false,\n retry: retryTransient,\n },\n queryClient,\n );\n}\n\n/**\n * An object URL for `blob` while it is shown, revoked when the blob changes\n * or the component unmounts; `null` without a blob.\n */\nexport function useObjectUrl(blob: Blob | null | undefined): string | null {\n const [url, setUrl] = useState<string | null>(null);\n useEffect(() => {\n if (!blob) {\n setUrl(null);\n return undefined;\n }\n const objectUrl = URL.createObjectURL(blob);\n setUrl(objectUrl);\n return () => URL.revokeObjectURL(objectUrl);\n }, [blob]);\n return url;\n}\n"],"names":[],"mappings":"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"}
|
package/dist/chat/AthenaChat.cjs
CHANGED
|
@@ -49,6 +49,7 @@ const lucideReact = require("lucide-react");
|
|
|
49
49
|
const react$1 = require("react");
|
|
50
50
|
const threadIdContext = require("../threads/thread-id-context.cjs");
|
|
51
51
|
const statewireThreadList = require("../threads/statewire-thread-list.cjs");
|
|
52
|
+
const markViewedThreadRead = require("../threads/mark-viewed-thread-read.cjs");
|
|
52
53
|
const FileUploadButton = require("../composer/FileUploadButton.cjs");
|
|
53
54
|
const ComposerQuotePreview = require("../composer/ComposerQuotePreview.cjs");
|
|
54
55
|
const ComposerAttachmentPreview = require("../composer/ComposerAttachmentPreview.cjs");
|
|
@@ -252,7 +253,10 @@ const AthenaChat = ({
|
|
|
252
253
|
return /* @__PURE__ */ jsxRuntime.jsx(AthenaChatDefaultComponentsContext.Provider, { value: defaultComponentsContextValue, children: /* @__PURE__ */ jsxRuntime.jsx(AthenaChatErrorBoundary.AthenaChatErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(toolFallback.ToolFallbackAppearanceProvider, { appearance: "thread", children: /* @__PURE__ */ jsxRuntime.jsx(apiClient.MentionAuthCtx.Provider, { value: mentionAuth, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
253
254
|
chatThreadShell.ChatThreadShell,
|
|
254
255
|
{
|
|
255
|
-
overlay: /* @__PURE__ */ jsxRuntime.
|
|
256
|
+
overlay: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
257
|
+
/* @__PURE__ */ jsxRuntime.jsx(threadLoadingOverlay.ThreadLoadingOverlay, {}),
|
|
258
|
+
/* @__PURE__ */ jsxRuntime.jsx(markViewedThreadRead.MarkViewedThreadRead, {})
|
|
259
|
+
] }),
|
|
256
260
|
panelClassName: cnfast.cn(
|
|
257
261
|
"aui-root aui-thread-root @container relative flex h-full flex-col bg-background",
|
|
258
262
|
className
|
|
@@ -598,21 +602,22 @@ const MessageError = () => /* @__PURE__ */ jsxRuntime.jsx(react.MessagePrimitive
|
|
|
598
602
|
}
|
|
599
603
|
) })
|
|
600
604
|
] }) });
|
|
601
|
-
function useSecondsSince(resetKey) {
|
|
605
|
+
function useSecondsSince({ resetKey, ticking = true }) {
|
|
602
606
|
const [seconds, setSeconds] = react$1.useState(0);
|
|
603
607
|
react$1.useEffect(() => {
|
|
604
608
|
setSeconds(0);
|
|
609
|
+
if (!ticking) return;
|
|
605
610
|
const startedAt = Date.now();
|
|
606
611
|
const timer = setInterval(() => {
|
|
607
612
|
setSeconds(Math.floor((Date.now() - startedAt) / 1e3));
|
|
608
613
|
}, 1e3);
|
|
609
614
|
return () => clearInterval(timer);
|
|
610
|
-
}, [resetKey]);
|
|
615
|
+
}, [resetKey, ticking]);
|
|
611
616
|
return seconds;
|
|
612
617
|
}
|
|
613
|
-
const RunStatusLine = ({ verb, resetKey, className }) => {
|
|
614
|
-
const seconds = useSecondsSince(resetKey);
|
|
615
|
-
const stalled = seconds
|
|
618
|
+
const RunStatusLine = ({ verb, resetKey, toolCallInFlight = false, className }) => {
|
|
619
|
+
const seconds = useSecondsSince({ resetKey });
|
|
620
|
+
const stalled = runActivity.isRunActivityStalled({ seconds, toolCallInFlight });
|
|
616
621
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
617
622
|
"div",
|
|
618
623
|
{
|
|
@@ -638,7 +643,7 @@ const RunStatusLine = ({ verb, resetKey, className }) => {
|
|
|
638
643
|
{
|
|
639
644
|
"aria-hidden": "true",
|
|
640
645
|
className: cnfast.cn("tabular-nums", stalled && "text-amber-600 dark:text-amber-400"),
|
|
641
|
-
children: runActivity.runActivityLabel(seconds, verb)
|
|
646
|
+
children: runActivity.runActivityLabel({ seconds, verb, stalled })
|
|
642
647
|
}
|
|
643
648
|
),
|
|
644
649
|
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-live": "polite", className: "sr-only", children: stalled ? "Assistant is still working; nothing new for over thirty seconds." : `${verb}…` })
|
|
@@ -668,13 +673,22 @@ const AthenaMessageActivityIndicator = () => {
|
|
|
668
673
|
});
|
|
669
674
|
const activityKey = react.useAuiState((s) => runActivity.runActivityKey(s.message.parts));
|
|
670
675
|
const verb = react.useAuiState((s) => runActivity.runActivityVerb(s.message.parts));
|
|
676
|
+
const toolCallInFlight = react.useAuiState((s) => runActivity.isToolCallInFlight(s.message.parts));
|
|
671
677
|
if (!isRunning || activityKey === null) return null;
|
|
672
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
678
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
679
|
+
RunStatusLine,
|
|
680
|
+
{
|
|
681
|
+
verb: verb ?? "Working",
|
|
682
|
+
resetKey: activityKey,
|
|
683
|
+
toolCallInFlight
|
|
684
|
+
}
|
|
685
|
+
);
|
|
673
686
|
};
|
|
674
687
|
const AthenaGroupedRunMeta = () => {
|
|
675
688
|
const activityKey = react.useAuiState((s) => runActivity.runActivityKey(s.message.parts));
|
|
676
|
-
const
|
|
677
|
-
const
|
|
689
|
+
const toolCallInFlight = react.useAuiState((s) => runActivity.isToolCallInFlight(s.message.parts));
|
|
690
|
+
const seconds = useSecondsSince({ resetKey: activityKey ?? "thinking", ticking: !toolCallInFlight });
|
|
691
|
+
const stalled = runActivity.isRunActivityStalled({ seconds, toolCallInFlight });
|
|
678
692
|
if (!stalled) return null;
|
|
679
693
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
680
694
|
"span",
|