@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.
Files changed (126) hide show
  1. package/README.md +39 -3
  2. package/dist/agent-runtime/src/errors.cjs +8 -0
  3. package/dist/agent-runtime/src/errors.cjs.map +1 -1
  4. package/dist/agent-runtime/src/errors.js +8 -0
  5. package/dist/agent-runtime/src/errors.js.map +1 -1
  6. package/dist/agent-runtime/src/messages.cjs +76 -16
  7. package/dist/agent-runtime/src/messages.cjs.map +1 -1
  8. package/dist/agent-runtime/src/messages.js +76 -16
  9. package/dist/agent-runtime/src/messages.js.map +1 -1
  10. package/dist/agent-runtime/src/tool-metadata.cjs +15 -0
  11. package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
  12. package/dist/agent-runtime/src/tool-metadata.js +15 -0
  13. package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
  14. package/dist/assets/use-asset-content.cjs +127 -0
  15. package/dist/assets/use-asset-content.cjs.map +1 -0
  16. package/dist/assets/use-asset-content.d.ts +60 -0
  17. package/dist/assets/use-asset-content.js +127 -0
  18. package/dist/assets/use-asset-content.js.map +1 -0
  19. package/dist/chat/AthenaChat.cjs +24 -10
  20. package/dist/chat/AthenaChat.cjs.map +1 -1
  21. package/dist/chat/AthenaChat.js +25 -11
  22. package/dist/chat/AthenaChat.js.map +1 -1
  23. package/dist/chat/run-activity.cjs +29 -10
  24. package/dist/chat/run-activity.cjs.map +1 -1
  25. package/dist/chat/run-activity.d.ts +30 -4
  26. package/dist/chat/run-activity.js +29 -10
  27. package/dist/chat/run-activity.js.map +1 -1
  28. package/dist/chat/super-grouping/CollapsibleGroup.cjs +16 -8
  29. package/dist/chat/super-grouping/CollapsibleGroup.cjs.map +1 -1
  30. package/dist/chat/super-grouping/CollapsibleGroup.d.ts +26 -1
  31. package/dist/chat/super-grouping/CollapsibleGroup.js +16 -8
  32. package/dist/chat/super-grouping/CollapsibleGroup.js.map +1 -1
  33. package/dist/chat/super-grouping/GroupedMessageParts.cjs +59 -0
  34. package/dist/chat/super-grouping/GroupedMessageParts.cjs.map +1 -0
  35. package/dist/chat/super-grouping/GroupedMessageParts.d.ts +25 -0
  36. package/dist/chat/super-grouping/GroupedMessageParts.js +59 -0
  37. package/dist/chat/super-grouping/GroupedMessageParts.js.map +1 -0
  38. package/dist/chat/super-grouping/SuperGroupingCard.cjs +9 -5
  39. package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
  40. package/dist/chat/super-grouping/SuperGroupingCard.d.ts +2 -1
  41. package/dist/chat/super-grouping/SuperGroupingCard.js +13 -9
  42. package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
  43. package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +10 -6
  44. package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
  45. package/dist/chat/super-grouping/SuperGroupingTranscript.js +14 -10
  46. package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
  47. package/dist/chat/super-grouping/grouped-part-keys.cjs +104 -0
  48. package/dist/chat/super-grouping/grouped-part-keys.cjs.map +1 -0
  49. package/dist/chat/super-grouping/grouped-part-keys.d.ts +37 -0
  50. package/dist/chat/super-grouping/grouped-part-keys.js +104 -0
  51. package/dist/chat/super-grouping/grouped-part-keys.js.map +1 -0
  52. package/dist/chat/super-grouping/index.cjs +2 -0
  53. package/dist/chat/super-grouping/index.cjs.map +1 -1
  54. package/dist/chat/super-grouping/index.d.ts +3 -1
  55. package/dist/chat/super-grouping/index.js +2 -0
  56. package/dist/chat/super-grouping/index.js.map +1 -1
  57. package/dist/chat-ui/composer/tiptap-composer.cjs +25 -19
  58. package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
  59. package/dist/chat-ui/composer/tiptap-composer.d.ts +5 -0
  60. package/dist/chat-ui/composer/tiptap-composer.js +26 -20
  61. package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
  62. package/dist/composer/clipboard.cjs +16 -0
  63. package/dist/composer/clipboard.cjs.map +1 -1
  64. package/dist/composer/clipboard.d.ts +9 -0
  65. package/dist/composer/clipboard.js +17 -1
  66. package/dist/composer/clipboard.js.map +1 -1
  67. package/dist/diagnostics/errors.cjs +6 -0
  68. package/dist/diagnostics/errors.cjs.map +1 -1
  69. package/dist/diagnostics/errors.d.ts +6 -0
  70. package/dist/diagnostics/errors.js +6 -0
  71. package/dist/diagnostics/errors.js.map +1 -1
  72. package/dist/index.cjs +23 -0
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.d.ts +8 -1
  75. package/dist/index.js +23 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/lib/use-document-active.cjs +40 -0
  78. package/dist/lib/use-document-active.cjs.map +1 -0
  79. package/dist/lib/use-document-active.d.ts +9 -0
  80. package/dist/lib/use-document-active.js +40 -0
  81. package/dist/lib/use-document-active.js.map +1 -0
  82. package/dist/react/package.json.cjs +1 -1
  83. package/dist/react/package.json.js +1 -1
  84. package/dist/runtime/statewire-lifecycle.cjs +8 -0
  85. package/dist/runtime/statewire-lifecycle.cjs.map +1 -1
  86. package/dist/runtime/statewire-lifecycle.js +9 -1
  87. package/dist/runtime/statewire-lifecycle.js.map +1 -1
  88. package/dist/styles.css +1 -1
  89. package/dist/threads/Inbox.cjs +6 -2
  90. package/dist/threads/Inbox.cjs.map +1 -1
  91. package/dist/threads/Inbox.js +6 -2
  92. package/dist/threads/Inbox.js.map +1 -1
  93. package/dist/threads/mark-viewed-thread-read.cjs +97 -0
  94. package/dist/threads/mark-viewed-thread-read.cjs.map +1 -0
  95. package/dist/threads/mark-viewed-thread-read.d.ts +7 -0
  96. package/dist/threads/mark-viewed-thread-read.js +97 -0
  97. package/dist/threads/mark-viewed-thread-read.js.map +1 -0
  98. package/dist/tools/index.cjs +7 -0
  99. package/dist/tools/index.cjs.map +1 -1
  100. package/dist/tools/index.d.ts +2 -0
  101. package/dist/tools/index.js +7 -0
  102. package/dist/tools/index.js.map +1 -1
  103. package/dist/tools/inline-tools.cjs +10 -0
  104. package/dist/tools/inline-tools.cjs.map +1 -0
  105. package/dist/tools/inline-tools.d.ts +2 -0
  106. package/dist/tools/inline-tools.js +10 -0
  107. package/dist/tools/inline-tools.js.map +1 -0
  108. package/dist/tools/test-recording-report.cjs +175 -0
  109. package/dist/tools/test-recording-report.cjs.map +1 -0
  110. package/dist/tools/test-recording-report.d.ts +82 -0
  111. package/dist/tools/test-recording-report.js +175 -0
  112. package/dist/tools/test-recording-report.js.map +1 -0
  113. package/dist/tools/test-recording-tool-uis.cjs +498 -0
  114. package/dist/tools/test-recording-tool-uis.cjs.map +1 -0
  115. package/dist/tools/test-recording-tool-uis.d.ts +32 -0
  116. package/dist/tools/test-recording-tool-uis.js +498 -0
  117. package/dist/tools/test-recording-tool-uis.js.map +1 -0
  118. package/dist/tools/tool-summary.cjs +49 -1
  119. package/dist/tools/tool-summary.cjs.map +1 -1
  120. package/dist/tools/tool-summary.js +49 -1
  121. package/dist/tools/tool-summary.js.map +1 -1
  122. package/dist/tools/tool-uis.cjs +6 -1
  123. package/dist/tools/tool-uis.cjs.map +1 -1
  124. package/dist/tools/tool-uis.js +6 -1
  125. package/dist/tools/tool-uis.js.map +1 -1
  126. 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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"}
@@ -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.jsx(threadLoadingOverlay.ThreadLoadingOverlay, {}),
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 >= runActivity.RUN_ACTIVITY_STALL_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(RunStatusLine, { verb: verb ?? "Working", resetKey: activityKey });
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 seconds = useSecondsSince(activityKey ?? "thinking");
677
- const stalled = seconds >= runActivity.RUN_ACTIVITY_STALL_SECONDS;
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",