@athenaintel/react 0.50.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/agent-runtime/src/desktop-handoff.d.ts +10 -0
- package/dist/agent-runtime/src/edit-command.cjs +21 -0
- package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
- package/dist/agent-runtime/src/edit-command.d.ts +8 -0
- package/dist/agent-runtime/src/edit-command.js +21 -0
- package/dist/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.cjs +19 -0
- package/dist/agent-runtime/src/handoff-arrival.cjs.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.d.ts +18 -3
- package/dist/agent-runtime/src/handoff-arrival.js +20 -1
- package/dist/agent-runtime/src/handoff-arrival.js.map +1 -1
- package/dist/agent-runtime/src/handoff-cursor-id.cjs +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor-id.d.ts +12 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs +88 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.d.ts +120 -0
- package/dist/agent-runtime/src/handoff-cursor.js +88 -0
- package/dist/agent-runtime/src/handoff-cursor.js.map +1 -0
- package/dist/agent-runtime/src/index.d.ts +4 -3
- package/dist/agent-runtime/src/thread-handoff-flow-machine.d.ts +6 -0
- package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +114 -4
- package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +8 -3
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +7 -0
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +9 -4
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
- package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +5 -0
- package/dist/chat/AthenaChat.cjs +27 -12
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.d.ts +13 -0
- package/dist/chat/AthenaChat.js +27 -12
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/AthenaPlanStrip.cjs +97 -0
- package/dist/chat/AthenaPlanStrip.cjs.map +1 -0
- package/dist/chat/AthenaPlanStrip.d.ts +15 -0
- package/dist/chat/AthenaPlanStrip.js +97 -0
- package/dist/chat/AthenaPlanStrip.js.map +1 -0
- package/dist/chat/super-grouping/SuperGroupingCard.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +6 -4
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.d.ts +9 -1
- package/dist/chat/super-grouping/activity-timeline.js +6 -4
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/chat/super-grouping/index.d.ts +1 -1
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs.map +1 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.d.ts +8 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js.map +1 -0
- package/dist/chat-ui/index.cjs +8 -0
- package/dist/chat-ui/index.cjs.map +1 -1
- package/dist/chat-ui/index.d.ts +2 -1
- package/dist/chat-ui/index.js +9 -1
- package/dist/chat-ui/index.js.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.cjs +1 -12
- package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.js +1 -12
- package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
- package/dist/chat-ui/mentions/use-mention-asset.cjs +18 -3
- package/dist/chat-ui/mentions/use-mention-asset.cjs.map +1 -1
- package/dist/chat-ui/mentions/use-mention-asset.js +19 -4
- package/dist/chat-ui/mentions/use-mention-asset.js.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.cjs +2 -0
- package/dist/chat-ui/thread/chat-thread-shell.cjs.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.d.ts +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.js +2 -0
- package/dist/chat-ui/thread/chat-thread-shell.js.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.cjs +216 -0
- package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -0
- package/dist/chat-ui/thread/handoff-cursor.d.ts +57 -0
- package/dist/chat-ui/thread/handoff-cursor.js +216 -0
- package/dist/chat-ui/thread/handoff-cursor.js.map +1 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.d.ts +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.js +3 -1
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +20 -3
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +11 -2
- package/dist/keryx-http/src/unconfirmed-update-attributions.js +21 -4
- package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/tools/write-todos-tool-ui.cjs +8 -0
- package/dist/tools/write-todos-tool-ui.cjs.map +1 -1
- package/dist/tools/write-todos-tool-ui.d.ts +2 -0
- package/dist/tools/write-todos-tool-ui.js +8 -0
- package/dist/tools/write-todos-tool-ui.js.map +1 -1
- package/dist/workbench/session-workbench-panes.cjs +1 -8
- package/dist/workbench/session-workbench-panes.cjs.map +1 -1
- package/dist/workbench/session-workbench-panes.js +2 -9
- package/dist/workbench/session-workbench-panes.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { QueryClientContext, useQuery, QueryClient } from "@tanstack/react-query";
|
|
2
2
|
import { useContext } from "react";
|
|
3
|
-
import { MentionAuthCtx, fetchGraphQL } from "./suggestions/data/api-client.js";
|
|
3
|
+
import { MentionAuthCtx, fetchGraphQL, AthenaGraphQLError } from "./suggestions/data/api-client.js";
|
|
4
4
|
const MENTION_ASSET_BATCH_SIZE = 50;
|
|
5
5
|
const MENTION_ASSET_BATCH_WINDOW_MS = 16;
|
|
6
6
|
const MENTION_ASSET_STALE_TIME_MS = 6e4;
|
|
7
7
|
const MENTION_ASSETS_QUERY = `
|
|
8
8
|
query MentionChipAssets($assetIds: [String!]!) {
|
|
9
|
-
|
|
9
|
+
getAssetChipDetails(assetIds: $assetIds) {
|
|
10
10
|
id
|
|
11
11
|
title
|
|
12
12
|
athenaOriginalType
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
`;
|
|
16
|
-
const LOOKUP_ID = /^asset_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
|
+
const LOOKUP_ID = /^asset_(?:thread_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
17
|
+
const backendsWithoutChipDetails = /* @__PURE__ */ new Set();
|
|
18
|
+
function isMissingChipDetails(error) {
|
|
19
|
+
return error instanceof AthenaGraphQLError && (error.status === 400 || error.message.includes("getAssetChipDetails"));
|
|
20
|
+
}
|
|
17
21
|
function mentionAssetLookupId(assetId) {
|
|
18
22
|
const baseId = (assetId == null ? void 0 : assetId.trim().split("@")[0]) ?? "";
|
|
19
23
|
return LOOKUP_ID.test(baseId) ? baseId : null;
|
|
@@ -57,6 +61,10 @@ function sendBatch(batchKey) {
|
|
|
57
61
|
if (!batch) return;
|
|
58
62
|
pendingBatches.delete(batchKey);
|
|
59
63
|
const ids = [...batch.waiters.keys()];
|
|
64
|
+
if (backendsWithoutChipDetails.has(batch.auth.backendUrl)) {
|
|
65
|
+
for (const id of ids) settle(batch.waiters.get(id), null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
60
68
|
for (let start = 0; start < ids.length; start += MENTION_ASSET_BATCH_SIZE) {
|
|
61
69
|
const chunk = ids.slice(start, start + MENTION_ASSET_BATCH_SIZE);
|
|
62
70
|
fetchGraphQL(batch.auth, MENTION_ASSETS_QUERY, {
|
|
@@ -64,7 +72,9 @@ function sendBatch(batchKey) {
|
|
|
64
72
|
}).then(
|
|
65
73
|
(data) => {
|
|
66
74
|
var _a, _b;
|
|
67
|
-
const found = new Map(
|
|
75
|
+
const found = new Map(
|
|
76
|
+
(data.getAssetChipDetails ?? []).map((asset) => [asset.id, asset])
|
|
77
|
+
);
|
|
68
78
|
for (const id of chunk) {
|
|
69
79
|
const asset = found.get(id);
|
|
70
80
|
settle(
|
|
@@ -78,6 +88,11 @@ function sendBatch(batchKey) {
|
|
|
78
88
|
}
|
|
79
89
|
},
|
|
80
90
|
(error) => {
|
|
91
|
+
if (isMissingChipDetails(error)) {
|
|
92
|
+
backendsWithoutChipDetails.add(batch.auth.backendUrl);
|
|
93
|
+
for (const id of chunk) settle(batch.waiters.get(id), null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
81
96
|
for (const id of chunk) fail(batch.waiters.get(id), error);
|
|
82
97
|
}
|
|
83
98
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-mention-asset.js","sources":["../../../src/chat-ui/mentions/use-mention-asset.ts"],"sourcesContent":["/**\n * The asset an asset chip names, read the way Olympus's chips read it: its\n * title and type from agora's `getAssetsByIds`, which returns only the assets\n * the caller may read (an id they cannot read is left out of the answer, as is\n * another workspace's). A chip then shows the asset's own name and icon\n * instead of whatever label and `type=` its link carried.\n *\n * Chips that mount together ask together: one GraphQL request per batch of up\n * to {@link MENTION_ASSET_BATCH_SIZE} ids, gathered over\n * {@link MENTION_ASSET_BATCH_WINDOW_MS}. Each answer is a TanStack query keyed\n * by backend, the person asking and the id, fresh for a minute, so a\n * transcript full of citations to one repository asks for it once.\n */\nimport { QueryClient, QueryClientContext, useQuery } from \"@tanstack/react-query\";\nimport { useContext } from \"react\";\nimport {\n fetchGraphQL,\n type MentionAuthContext,\n MentionAuthCtx,\n} from \"./suggestions/data/api-client\";\n\n/** What a chip draws from the asset: its current title and its type. */\nexport interface MentionAsset {\n id: string;\n title: string;\n assetType: string;\n}\n\nexport const MENTION_ASSET_BATCH_SIZE = 50;\nexport const MENTION_ASSET_BATCH_WINDOW_MS = 16;\nconst MENTION_ASSET_STALE_TIME_MS = 60_000;\n\n/** No metadata: a session's carries its token ledgers, and a chip needs a name and a type. */\nconst MENTION_ASSETS_QUERY = `\n query MentionChipAssets($assetIds: [String!]!) {\n getAssetsByIds(assetIds: $assetIds) {\n id\n title\n athenaOriginalType\n }\n }\n`;\n\ninterface MentionAssetsResponse {\n getAssetsByIds?: Array<{\n id: string;\n title: string | null;\n athenaOriginalType: string | null;\n }> | null;\n}\n\n/**\n * The ids a lookup asks about: a stored asset. A versioned id (`asset_x@3`)\n * asks about its asset. A live provider file (`asset_<catalog>_<file>`) is not\n * a row `getAssetsByIds` holds, and a session (`asset_thread_<uuid>`) is never\n * asked about: agora reads the whole row, and a session's metadata carries\n * its transcript's ledgers (megabytes on a long session) — too much for a name.\n */\nconst LOOKUP_ID = /^asset_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport function mentionAssetLookupId(assetId: string | undefined): string | null {\n const baseId = assetId?.trim().split(\"@\")[0] ?? \"\";\n return LOOKUP_ID.test(baseId) ? baseId : null;\n}\n\n/** Whether a request can carry a credential (`mentionRequestInit` refuses one that cannot). */\nfunction canAuthenticate(auth: MentionAuthContext): boolean {\n return Boolean(auth.accessToken || auth.apiKey || auth.credentials || auth.getAccessToken);\n}\n\n/** A short, stable stand-in for a credential in a cache key; never the credential itself. */\nfunction fingerprint(credential: string): string {\n let hash = 0x811c9dc5;\n for (let index = 0; index < credential.length; index += 1) {\n hash ^= credential.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193) >>> 0;\n }\n return hash.toString(16).padStart(8, \"0\");\n}\n\n/** The scope each credential with no stable identity of its own gets: its own auth object. */\nconst authScopes = new WeakMap<MentionAuthContext, string>();\nlet authScopeCount = 0;\n\n/**\n * Who is asking, as narrowly as the credential says: what `getAssetsByIds`\n * answers differs per person and per API key, and an answer (or a batch's\n * request) is only ever shared within one scope. A person's id and an API key\n * scope the cache across token rotations. A credential that names neither (a\n * session cookie, a token callback) shares nothing beyond its own auth object\n * and its current token, so a token swapped on the same object asks again.\n */\nfunction principalOf(auth: MentionAuthContext): string {\n const scope: string[] = [];\n if (auth.userId) scope.push(`user:${auth.userId}`);\n if (auth.apiKey) scope.push(`key:${fingerprint(auth.apiKey)}`);\n if (scope.length) return scope.join(\"|\");\n let own = authScopes.get(auth);\n if (!own) {\n authScopeCount += 1;\n own = `auth:${authScopeCount}`;\n authScopes.set(auth, own);\n }\n const token = auth.accessToken;\n return token ? `${own}|token:${fingerprint(token)}` : own;\n}\n\ninterface Waiter {\n resolve: (asset: MentionAsset | null) => void;\n reject: (error: unknown) => void;\n}\n\ninterface PendingBatch {\n auth: MentionAuthContext;\n waiters: Map<string, Waiter[]>;\n}\n\n/** The batch each backend and principal is gathering, until its window closes. */\nconst pendingBatches = new Map<string, PendingBatch>();\n\nfunction settle(waiters: Waiter[] | undefined, asset: MentionAsset | null): void {\n for (const waiter of waiters ?? []) waiter.resolve(asset);\n}\n\nfunction fail(waiters: Waiter[] | undefined, error: unknown): void {\n for (const waiter of waiters ?? []) waiter.reject(error);\n}\n\nfunction sendBatch(batchKey: string): void {\n const batch = pendingBatches.get(batchKey);\n if (!batch) return;\n pendingBatches.delete(batchKey);\n const ids = [...batch.waiters.keys()];\n for (let start = 0; start < ids.length; start += MENTION_ASSET_BATCH_SIZE) {\n const chunk = ids.slice(start, start + MENTION_ASSET_BATCH_SIZE);\n fetchGraphQL<MentionAssetsResponse>(batch.auth, MENTION_ASSETS_QUERY, {\n assetIds: chunk,\n }).then(\n (data) => {\n const found = new Map((data.getAssetsByIds ?? []).map((asset) => [asset.id, asset]));\n for (const id of chunk) {\n const asset = found.get(id);\n settle(\n batch.waiters.get(id),\n asset\n ? {\n id,\n title: asset.title?.trim() ?? \"\",\n assetType: asset.athenaOriginalType?.trim() ?? \"\",\n }\n : null,\n );\n }\n },\n (error: unknown) => {\n for (const id of chunk) fail(batch.waiters.get(id), error);\n },\n );\n }\n}\n\nfunction loadMentionAsset({\n auth,\n batchKey,\n assetId,\n}: {\n auth: MentionAuthContext;\n batchKey: string;\n assetId: string;\n}): Promise<MentionAsset | null> {\n return new Promise((resolve, reject) => {\n let batch = pendingBatches.get(batchKey);\n if (!batch) {\n batch = { auth, waiters: new Map() };\n pendingBatches.set(batchKey, batch);\n setTimeout(() => sendBatch(batchKey), MENTION_ASSET_BATCH_WINDOW_MS);\n }\n const waiters = batch.waiters.get(assetId) ?? [];\n waiters.push({ resolve, reject });\n batch.waiters.set(assetId, waiters);\n });\n}\n\n/** Chips rendered outside any query client (a host's own tray) still share one cache. */\nconst chipQueryClient = new QueryClient();\n\n/**\n * The asset an asset chip names: `undefined` while unknown (no lookup, still\n * loading, or the lookup failed), `null` when agora returned no asset the\n * viewer may read under that id, else its title and type.\n */\nexport function useMentionAsset(assetId: string | undefined): MentionAsset | null | undefined {\n const auth = useContext(MentionAuthCtx);\n const hostQueryClient = useContext(QueryClientContext);\n const lookupId = mentionAssetLookupId(assetId);\n const enabled = Boolean(auth && lookupId && canAuthenticate(auth));\n const backendUrl = auth?.backendUrl ?? \"\";\n const principal = auth ? principalOf(auth) : \"\";\n const { data } = useQuery(\n {\n queryKey: [\"athena-sdk\", \"mention-asset\", backendUrl, principal, lookupId ?? \"\"],\n queryFn: () => {\n if (!auth || !lookupId) return null;\n return loadMentionAsset({\n auth,\n batchKey: `${backendUrl}|${principal}`,\n assetId: lookupId,\n });\n },\n enabled,\n staleTime: MENTION_ASSET_STALE_TIME_MS,\n // Kept no longer than it is fresh: a title is shown again only after\n // agora answers again, never from a cache a minute past its check.\n gcTime: MENTION_ASSET_STALE_TIME_MS,\n retry: false,\n refetchOnWindowFocus: false,\n },\n hostQueryClient ?? chipQueryClient,\n );\n return enabled ? data : undefined;\n}\n\nexport function resetMentionAssetsForTests(): void {\n pendingBatches.clear();\n chipQueryClient.clear();\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-mention-asset.js","sources":["../../../src/chat-ui/mentions/use-mention-asset.ts"],"sourcesContent":["/**\n * The asset an asset chip names, read the way Olympus's chips read it: its\n * title and type from agora's `getAssetChipDetails`, which reads only what a\n * chip draws (never an asset's whole row) and returns only the assets the\n * caller may read (an id they cannot read is left out of the answer, as is\n * another workspace's). A chip then shows the asset's own name and icon\n * instead of whatever label and `type=` its link carried. A backend that\n * predates the field is asked once: its chips keep their links' labels, and\n * nothing falls back to a whole-row read.\n *\n * Chips that mount together ask together: one GraphQL request per batch of up\n * to {@link MENTION_ASSET_BATCH_SIZE} ids, gathered over\n * {@link MENTION_ASSET_BATCH_WINDOW_MS}. Each answer is a TanStack query keyed\n * by backend, the person asking and the id, fresh for a minute, so a\n * transcript full of citations to one repository asks for it once.\n */\nimport { QueryClient, QueryClientContext, useQuery } from \"@tanstack/react-query\";\nimport { useContext } from \"react\";\nimport {\n AthenaGraphQLError,\n fetchGraphQL,\n type MentionAuthContext,\n MentionAuthCtx,\n} from \"./suggestions/data/api-client\";\n\n/** What a chip draws from the asset: its current title and its type. */\nexport interface MentionAsset {\n id: string;\n title: string;\n assetType: string;\n}\n\nexport const MENTION_ASSET_BATCH_SIZE = 50;\nexport const MENTION_ASSET_BATCH_WINDOW_MS = 16;\nconst MENTION_ASSET_STALE_TIME_MS = 60_000;\n\nconst MENTION_ASSETS_QUERY = `\n query MentionChipAssets($assetIds: [String!]!) {\n getAssetChipDetails(assetIds: $assetIds) {\n id\n title\n athenaOriginalType\n }\n }\n`;\n\ninterface MentionAssetsResponse {\n getAssetChipDetails?: Array<{\n id: string;\n title: string | null;\n athenaOriginalType: string | null;\n }> | null;\n}\n\n/**\n * The ids a lookup asks about: a stored asset, a session's\n * (`asset_thread_<uuid>`) included. A versioned id (`asset_x@3`) asks about its\n * asset. A live provider file (`asset_<catalog>_<file>`) is not asked about:\n * agora answers it from the provider, too slow for a name.\n */\nconst LOOKUP_ID =\n /^asset_(?:thread_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Backends whose agora predates `getAssetChipDetails`, learned from their\n * answer: their chips are never asked about again.\n */\nconst backendsWithoutChipDetails = new Set<string>();\n\nfunction isMissingChipDetails(error: unknown): boolean {\n return (\n error instanceof AthenaGraphQLError &&\n (error.status === 400 || error.message.includes(\"getAssetChipDetails\"))\n );\n}\n\nexport function mentionAssetLookupId(assetId: string | undefined): string | null {\n const baseId = assetId?.trim().split(\"@\")[0] ?? \"\";\n return LOOKUP_ID.test(baseId) ? baseId : null;\n}\n\n/** Whether a request can carry a credential (`mentionRequestInit` refuses one that cannot). */\nfunction canAuthenticate(auth: MentionAuthContext): boolean {\n return Boolean(auth.accessToken || auth.apiKey || auth.credentials || auth.getAccessToken);\n}\n\n/** A short, stable stand-in for a credential in a cache key; never the credential itself. */\nfunction fingerprint(credential: string): string {\n let hash = 0x811c9dc5;\n for (let index = 0; index < credential.length; index += 1) {\n hash ^= credential.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193) >>> 0;\n }\n return hash.toString(16).padStart(8, \"0\");\n}\n\n/** The scope each credential with no stable identity of its own gets: its own auth object. */\nconst authScopes = new WeakMap<MentionAuthContext, string>();\nlet authScopeCount = 0;\n\n/**\n * Who is asking, as narrowly as the credential says: what `getAssetsByIds`\n * answers differs per person and per API key, and an answer (or a batch's\n * request) is only ever shared within one scope. A person's id and an API key\n * scope the cache across token rotations. A credential that names neither (a\n * session cookie, a token callback) shares nothing beyond its own auth object\n * and its current token, so a token swapped on the same object asks again.\n */\nfunction principalOf(auth: MentionAuthContext): string {\n const scope: string[] = [];\n if (auth.userId) scope.push(`user:${auth.userId}`);\n if (auth.apiKey) scope.push(`key:${fingerprint(auth.apiKey)}`);\n if (scope.length) return scope.join(\"|\");\n let own = authScopes.get(auth);\n if (!own) {\n authScopeCount += 1;\n own = `auth:${authScopeCount}`;\n authScopes.set(auth, own);\n }\n const token = auth.accessToken;\n return token ? `${own}|token:${fingerprint(token)}` : own;\n}\n\ninterface Waiter {\n resolve: (asset: MentionAsset | null) => void;\n reject: (error: unknown) => void;\n}\n\ninterface PendingBatch {\n auth: MentionAuthContext;\n waiters: Map<string, Waiter[]>;\n}\n\n/** The batch each backend and principal is gathering, until its window closes. */\nconst pendingBatches = new Map<string, PendingBatch>();\n\nfunction settle(waiters: Waiter[] | undefined, asset: MentionAsset | null): void {\n for (const waiter of waiters ?? []) waiter.resolve(asset);\n}\n\nfunction fail(waiters: Waiter[] | undefined, error: unknown): void {\n for (const waiter of waiters ?? []) waiter.reject(error);\n}\n\nfunction sendBatch(batchKey: string): void {\n const batch = pendingBatches.get(batchKey);\n if (!batch) return;\n pendingBatches.delete(batchKey);\n const ids = [...batch.waiters.keys()];\n if (backendsWithoutChipDetails.has(batch.auth.backendUrl)) {\n for (const id of ids) settle(batch.waiters.get(id), null);\n return;\n }\n for (let start = 0; start < ids.length; start += MENTION_ASSET_BATCH_SIZE) {\n const chunk = ids.slice(start, start + MENTION_ASSET_BATCH_SIZE);\n fetchGraphQL<MentionAssetsResponse>(batch.auth, MENTION_ASSETS_QUERY, {\n assetIds: chunk,\n }).then(\n (data) => {\n const found = new Map(\n (data.getAssetChipDetails ?? []).map((asset) => [asset.id, asset]),\n );\n for (const id of chunk) {\n const asset = found.get(id);\n settle(\n batch.waiters.get(id),\n asset\n ? {\n id,\n title: asset.title?.trim() ?? \"\",\n assetType: asset.athenaOriginalType?.trim() ?? \"\",\n }\n : null,\n );\n }\n },\n (error: unknown) => {\n if (isMissingChipDetails(error)) {\n backendsWithoutChipDetails.add(batch.auth.backendUrl);\n for (const id of chunk) settle(batch.waiters.get(id), null);\n return;\n }\n for (const id of chunk) fail(batch.waiters.get(id), error);\n },\n );\n }\n}\n\nfunction loadMentionAsset({\n auth,\n batchKey,\n assetId,\n}: {\n auth: MentionAuthContext;\n batchKey: string;\n assetId: string;\n}): Promise<MentionAsset | null> {\n return new Promise((resolve, reject) => {\n let batch = pendingBatches.get(batchKey);\n if (!batch) {\n batch = { auth, waiters: new Map() };\n pendingBatches.set(batchKey, batch);\n setTimeout(() => sendBatch(batchKey), MENTION_ASSET_BATCH_WINDOW_MS);\n }\n const waiters = batch.waiters.get(assetId) ?? [];\n waiters.push({ resolve, reject });\n batch.waiters.set(assetId, waiters);\n });\n}\n\n/** Chips rendered outside any query client (a host's own tray) still share one cache. */\nconst chipQueryClient = new QueryClient();\n\n/**\n * The asset an asset chip names: `undefined` while unknown (no lookup, still\n * loading, or the lookup failed), `null` when agora returned no asset the\n * viewer may read under that id, else its title and type.\n */\nexport function useMentionAsset(assetId: string | undefined): MentionAsset | null | undefined {\n const auth = useContext(MentionAuthCtx);\n const hostQueryClient = useContext(QueryClientContext);\n const lookupId = mentionAssetLookupId(assetId);\n const enabled = Boolean(auth && lookupId && canAuthenticate(auth));\n const backendUrl = auth?.backendUrl ?? \"\";\n const principal = auth ? principalOf(auth) : \"\";\n const { data } = useQuery(\n {\n queryKey: [\"athena-sdk\", \"mention-asset\", backendUrl, principal, lookupId ?? \"\"],\n queryFn: () => {\n if (!auth || !lookupId) return null;\n return loadMentionAsset({\n auth,\n batchKey: `${backendUrl}|${principal}`,\n assetId: lookupId,\n });\n },\n enabled,\n staleTime: MENTION_ASSET_STALE_TIME_MS,\n // Kept no longer than it is fresh: a title is shown again only after\n // agora answers again, never from a cache a minute past its check.\n gcTime: MENTION_ASSET_STALE_TIME_MS,\n retry: false,\n refetchOnWindowFocus: false,\n },\n hostQueryClient ?? chipQueryClient,\n );\n return enabled ? data : undefined;\n}\n\nexport function resetMentionAssetsForTests(): void {\n pendingBatches.clear();\n backendsWithoutChipDetails.clear();\n chipQueryClient.clear();\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,7 @@ const userEditComposer = require("../messages/user-edit-composer.cjs");
|
|
|
11
11
|
const runningIndicator = require("../messages/running-indicator.cjs");
|
|
12
12
|
const handoffArrivalDivider = require("../messages/handoff-arrival-divider.cjs");
|
|
13
13
|
const fromTheCallCard = require("../messages/from-the-call-card.cjs");
|
|
14
|
+
const statewireHistoryEdge = require("./statewire-history-edge.cjs");
|
|
14
15
|
const threadChrome = require("../messages/thread-chrome.cjs");
|
|
15
16
|
const messages = require("../../agent-runtime/src/messages.cjs");
|
|
16
17
|
const withArrivalDivider = /* @__PURE__ */ (() => {
|
|
@@ -115,6 +116,7 @@ function ChatThreadShell({
|
|
|
115
116
|
react.ThreadPrimitive.ViewportFooter,
|
|
116
117
|
{
|
|
117
118
|
className: viewportFooterClassName ?? DEFAULT_VIEWPORT_FOOTER_CLASS,
|
|
119
|
+
...{ [statewireHistoryEdge.THREAD_VIEWPORT_FOOTER_ATTR]: "" },
|
|
118
120
|
children: [
|
|
119
121
|
scrollToBottom,
|
|
120
122
|
viewportFooter
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-thread-shell.cjs","sources":["../../../src/chat-ui/thread/chat-thread-shell.tsx"],"sourcesContent":["import { AuiIf, ThreadPrimitive, useAuiState } from \"@assistant-ui/react\";\nimport { deepAgentRowKey } from \"@athenaintel/agent-runtime\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, FC, ReactNode } from \"react\";\nimport { useMemo } from \"react\";\nimport {\n THREAD_PANEL_CLASS,\n THREAD_SCROLL_TO_BOTTOM_CLASS,\n} from \"../messages/message-components\";\nimport { AnswerLabelScope } from \"../messages/answer-label\";\nimport { MessageActionsReadOnlyProvider } from \"../messages/message-actions-config\";\nimport { UserEditComposer } from \"../messages/user-edit-composer\";\nimport { ThreadRunningIndicator } from \"../messages/running-indicator\";\nimport {\n HandoffArrivalAfterMessage,\n HandoffArrivalScope,\n} from \"../messages/handoff-arrival-divider\";\nimport { FromTheCallAfterMessage } from \"../messages/from-the-call-card\";\n\nexport interface ChatThreadMessageComponents {\n UserMessage: FC;\n AssistantMessage: FC;\n /**\n * Rendered in place of a user message while it is being edited (the action\n * bar's Edit control). Defaults to the shared UserEditComposer.\n */\n UserEditComposer?: FC;\n}\n\n// One wrapper per message renderer, so a renderer keeps its identity across\n// shells and renders: a row renders its message through the component it is\n// handed, and a new component identity would remount every row.\nconst withArrivalDivider = (() => {\n const wrapped = new WeakMap<FC, FC>();\n return (Message: FC): FC => {\n const existing = wrapped.get(Message);\n if (existing) return existing;\n // After the message: the \"From the call\" card where a call's stretch\n // ends, then the arrival divider (the arrival came after the call).\n const WithArrivalDivider: FC = () => (\n <>\n <Message />\n <FromTheCallAfterMessage />\n <HandoffArrivalAfterMessage />\n </>\n );\n wrapped.set(Message, WithArrivalDivider);\n return WithArrivalDivider;\n };\n})();\n\ntype ThreadRowComponents = ComponentProps<typeof ThreadPrimitive.MessageByIndex>[\"components\"];\n\nconst ROW_KEY_SEPARATOR = \"\\u0000\";\n\n/**\n * The thread's messages, one row each under its row key (`deepAgentRowKey`).\n *\n * `ThreadPrimitive.Messages` keys a row by its message id. A page of older\n * history that folds into the first loaded turn (a long run's earlier steps)\n * gives that turn a new id on every page, so keyed by id the turn's row\n * remounted once per page: it flashed through its entrance animation, closed\n * what the person had opened in it and took the browser's scroll anchor with\n * it. The row key stays with the row the turn continues, so the page lands in\n * the mounted row. A message from another runtime keys by its id, as before.\n */\nconst ThreadRows: FC<{ components: ThreadRowComponents }> = ({ components }) => {\n const joinedKeys = useAuiState((s) =>\n s.thread.messages.map((message) => deepAgentRowKey(message)).join(ROW_KEY_SEPARATOR),\n );\n const rowKeys = useMemo(\n () => (joinedKeys === \"\" ? [] : joinedKeys.split(ROW_KEY_SEPARATOR)),\n [joinedKeys],\n );\n return (\n <>\n {rowKeys.map((key, index) => (\n <ThreadPrimitive.MessageByIndex key={key} index={index} components={components} />\n ))}\n </>\n );\n};\n\nconst DEFAULT_VIEWPORT_CLASS =\n \"relative flex flex-1 flex-col overflow-y-auto scroll-smooth\";\nconst DEFAULT_CONTENT_CLASS = \"w-full flex-1 px-3 pt-3\";\nconst DEFAULT_VIEWPORT_FOOTER_CLASS = \"sticky bottom-0\";\n\nconst DefaultScrollToBottom: FC = () => (\n <AuiIf condition={(state) => !state.thread.isEmpty}>\n <div className=\"flex justify-center pb-0.5\">\n <ThreadPrimitive.ScrollToBottom\n aria-label=\"Scroll to bottom\"\n title=\"Scroll to bottom\"\n className={THREAD_SCROLL_TO_BOTTOM_CLASS}\n >\n <ChevronDownIcon className=\"size-3.5\" />\n </ThreadPrimitive.ScrollToBottom>\n </div>\n </AuiIf>\n);\n\nexport interface ChatThreadShellProps {\n /**\n * Rendered as the first child of the panel — for absolutely-positioned\n * scrims that must cover the whole panel (AthenaChat's thread-loading\n * overlay). The shell does not force a positioning context; hosts include\n * `relative` in `panelClassName` when the scrim anchors to the panel.\n */\n overlay?: ReactNode;\n /**\n * Rendered above the viewport, inside the panel: header rows, slide-over\n * panels, and anything else the host docks at the top.\n */\n header?: ReactNode;\n /** Status/approval banners rendered between the header and the viewport. */\n banners?: ReactNode;\n /**\n * Nodes rendered inside the scroll viewport before the message column —\n * absolutely/sticky-positioned overlays (thread-switch skeletons,\n * scroll-to-top controls) and invisible observers (read receipts).\n */\n viewportOverlay?: ReactNode;\n /** Empty-thread state, rendered via ThreadPrimitive.Empty when provided. */\n welcome?: ReactNode;\n /**\n * Rendered inside the message column right above the messages — the\n * windowed wire's history edge (`StatewireHistoryEdge`): the \"N earlier\n * messages\" row that pages older history in and fetches cut tool outputs.\n */\n beforeMessages?: ReactNode;\n /** Message renderers for the thread's rows. */\n components: ChatThreadMessageComponents;\n /**\n * Wraps the messages element — for hosts that provide message-scoped\n * context (e.g. activity-grouping providers). Must render its argument.\n */\n renderMessages?: (messages: ReactNode) => ReactNode;\n /**\n * Rendered between the viewport and the composer: connection banners,\n * queued-message trays.\n */\n belowViewport?: ReactNode;\n /** The composer dock (or a read-only notice replacing it). */\n composer?: ReactNode;\n /**\n * Replaces the docked-panel card chrome — for hosts that render a\n * full-page column (AthenaChat's `aui-*` surface) instead of the\n * side-panel/task-pane card.\n */\n panelClassName?: string;\n /**\n * Value of the `--thread-max-width` variable read by width-capped\n * message/welcome/footer columns. Panels fill their width (the default,\n * \"100%\"); full-page hosts cap the column (AthenaChat defaults to 44rem).\n */\n maxWidth?: string;\n /**\n * Anchors the latest turn to the top of the viewport instead of pinning\n * scroll to the bottom (AthenaChat). Omitted = the primitive's default.\n */\n turnAnchor?: \"top\" | \"bottom\";\n /** Replaces the scroll-viewport chrome (padding, overflow tuning). */\n viewportClassName?: string;\n /**\n * Replaces the message-column chrome. Hosts whose welcome screen centers\n * vertically make this a flex column so the welcome node can grow.\n */\n contentClassName?: string;\n /**\n * Thread-level \"Thinking...\" indicator rendered after the messages. Hosts\n * whose message components render their own per-message indicators pass\n * null so the two never double up.\n */\n runningIndicator?: ReactNode;\n /**\n * Replaces the default centered scroll-to-bottom pill inside the sticky\n * viewport footer (AthenaChat floats a circular button above its composer).\n */\n scrollToBottom?: ReactNode;\n /**\n * Content docked inside the sticky viewport footer, after the\n * scroll-to-bottom control — for hosts whose banners + composer float over\n * the scrolling messages (AthenaChat) instead of sitting below the\n * viewport via `belowViewport`/`composer`.\n */\n viewportFooter?: ReactNode;\n /** Replaces the sticky viewport-footer chrome. */\n viewportFooterClassName?: string;\n /**\n * Marks the transcript read-only: mutating message actions (Edit,\n * Regenerate) hide while Copy stays available. For threads whose server\n * rejects dispatch (e.g. legacy pre-cutover conversations) — the runtime's\n * static capability flags cannot express a per-thread fence.\n */\n readOnly?: boolean;\n /**\n * The backend id of the thread on screen. When this surface picked the\n * conversation up from another one (`recordHandoffArrival`), the transcript\n * shows \"Continued from …\" after the last message that was already there.\n */\n arrivalThreadId?: string | null;\n /**\n * The signed-in viewer's Athena user id (`user_…`), for the answer labels\n * (`AnswerLabel`): their own answers read \"for you\", and an answer for\n * another person is labelled even in a 1:1 chat. Omitted, answers name the\n * person by name and are labelled only once the thread holds several agents\n * or several people.\n */\n viewerUserId?: string | null;\n}\n\n/**\n * The shared thread layout: panel chrome, scroll viewport with welcome +\n * messages + running indicator, the sticky scroll-to-bottom control, and the\n * host-supplied composer dock.\n *\n * Hosts express their genuine divergences through the slots above; the\n * layout, scroll behaviour, and empty/running states stay identical across\n * surfaces so fixes land once.\n */\nexport function ChatThreadShell({\n overlay,\n header,\n banners,\n viewportOverlay,\n welcome,\n beforeMessages,\n components,\n renderMessages,\n belowViewport,\n composer,\n panelClassName,\n maxWidth = \"100%\",\n turnAnchor,\n viewportClassName,\n contentClassName,\n runningIndicator = <ThreadRunningIndicator />,\n scrollToBottom = <DefaultScrollToBottom />,\n viewportFooter,\n viewportFooterClassName,\n readOnly = false,\n arrivalThreadId = null,\n viewerUserId = null,\n}: ChatThreadShellProps) {\n const {\n UserMessage,\n AssistantMessage,\n UserEditComposer: UserEditComposerComponent = UserEditComposer,\n } = components;\n // A row resolves its renderer by component identity, so keep the merged map\n // referentially stable across renders.\n const messageComponents = useMemo(\n () => ({\n UserMessage: withArrivalDivider(UserMessage),\n AssistantMessage: withArrivalDivider(AssistantMessage),\n UserEditComposer: UserEditComposerComponent,\n }),\n [UserMessage, AssistantMessage, UserEditComposerComponent],\n );\n const messages = (\n <HandoffArrivalScope threadId={arrivalThreadId}>\n <AnswerLabelScope viewerUserId={viewerUserId}>\n <ThreadRows components={messageComponents} />\n </AnswerLabelScope>\n </HandoffArrivalScope>\n );\n return (\n <MessageActionsReadOnlyProvider readOnly={readOnly}>\n <ThreadPrimitive.Root\n className={panelClassName ?? THREAD_PANEL_CLASS}\n style={{ \"--thread-max-width\": maxWidth } as CSSProperties}\n >\n {overlay}\n {header}\n {banners}\n <ThreadPrimitive.Viewport\n turnAnchor={turnAnchor}\n className={viewportClassName ?? DEFAULT_VIEWPORT_CLASS}\n data-aui-thread-viewport=\"\"\n >\n {viewportOverlay}\n {/* No min-h-0 here: the column's automatic minimum height is what\n lets it extend past one screen, keeping the sticky footer's flow\n position after the full message list. Capping it detaches the\n footer (scroll pill, docked composers) as soon as the thread\n scrolls — proven in a live-browser probe of both structures. */}\n <div className=\"flex flex-1 flex-col items-center\">\n <div className={contentClassName ?? DEFAULT_CONTENT_CLASS}>\n {welcome != null ? (\n <ThreadPrimitive.Empty>{welcome}</ThreadPrimitive.Empty>\n ) : null}\n {beforeMessages}\n {renderMessages ? renderMessages(messages) : messages}\n {runningIndicator}\n </div>\n </div>\n <ThreadPrimitive.ViewportFooter\n className={viewportFooterClassName ?? DEFAULT_VIEWPORT_FOOTER_CLASS}\n >\n {scrollToBottom}\n {viewportFooter}\n </ThreadPrimitive.ViewportFooter>\n </ThreadPrimitive.Viewport>\n {belowViewport}\n {composer}\n </ThreadPrimitive.Root>\n </MessageActionsReadOnlyProvider>\n );\n}\n"],"names":["jsxs","Fragment","jsx","FromTheCallAfterMessage","HandoffArrivalAfterMessage","useAuiState","deepAgentRowKey","useMemo","ThreadPrimitive","AuiIf","THREAD_SCROLL_TO_BOTTOM_CLASS","ChevronDownIcon","runningIndicator","ThreadRunningIndicator","UserEditComposer","messageComponents","messages","HandoffArrivalScope","AnswerLabelScope","MessageActionsReadOnlyProvider","THREAD_PANEL_CLASS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-thread-shell.cjs","sources":["../../../src/chat-ui/thread/chat-thread-shell.tsx"],"sourcesContent":["import { AuiIf, ThreadPrimitive, useAuiState } from \"@assistant-ui/react\";\nimport { deepAgentRowKey } from \"@athenaintel/agent-runtime\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, FC, ReactNode } from \"react\";\nimport { useMemo } from \"react\";\nimport {\n THREAD_PANEL_CLASS,\n THREAD_SCROLL_TO_BOTTOM_CLASS,\n} from \"../messages/message-components\";\nimport { AnswerLabelScope } from \"../messages/answer-label\";\nimport { MessageActionsReadOnlyProvider } from \"../messages/message-actions-config\";\nimport { UserEditComposer } from \"../messages/user-edit-composer\";\nimport { ThreadRunningIndicator } from \"../messages/running-indicator\";\nimport {\n HandoffArrivalAfterMessage,\n HandoffArrivalScope,\n} from \"../messages/handoff-arrival-divider\";\nimport { FromTheCallAfterMessage } from \"../messages/from-the-call-card\";\nimport { THREAD_VIEWPORT_FOOTER_ATTR } from \"./statewire-history-edge\";\n\nexport interface ChatThreadMessageComponents {\n UserMessage: FC;\n AssistantMessage: FC;\n /**\n * Rendered in place of a user message while it is being edited (the action\n * bar's Edit control). Defaults to the shared UserEditComposer.\n */\n UserEditComposer?: FC;\n}\n\n// One wrapper per message renderer, so a renderer keeps its identity across\n// shells and renders: a row renders its message through the component it is\n// handed, and a new component identity would remount every row.\nconst withArrivalDivider = (() => {\n const wrapped = new WeakMap<FC, FC>();\n return (Message: FC): FC => {\n const existing = wrapped.get(Message);\n if (existing) return existing;\n // After the message: the \"From the call\" card where a call's stretch\n // ends, then the arrival divider (the arrival came after the call).\n const WithArrivalDivider: FC = () => (\n <>\n <Message />\n <FromTheCallAfterMessage />\n <HandoffArrivalAfterMessage />\n </>\n );\n wrapped.set(Message, WithArrivalDivider);\n return WithArrivalDivider;\n };\n})();\n\ntype ThreadRowComponents = ComponentProps<typeof ThreadPrimitive.MessageByIndex>[\"components\"];\n\nconst ROW_KEY_SEPARATOR = \"\\u0000\";\n\n/**\n * The thread's messages, one row each under its row key (`deepAgentRowKey`).\n *\n * `ThreadPrimitive.Messages` keys a row by its message id. A page of older\n * history that folds into the first loaded turn (a long run's earlier steps)\n * gives that turn a new id on every page, so keyed by id the turn's row\n * remounted once per page: it flashed through its entrance animation, closed\n * what the person had opened in it and took the browser's scroll anchor with\n * it. The row key stays with the row the turn continues, so the page lands in\n * the mounted row. A message from another runtime keys by its id, as before.\n */\nconst ThreadRows: FC<{ components: ThreadRowComponents }> = ({ components }) => {\n const joinedKeys = useAuiState((s) =>\n s.thread.messages.map((message) => deepAgentRowKey(message)).join(ROW_KEY_SEPARATOR),\n );\n const rowKeys = useMemo(\n () => (joinedKeys === \"\" ? [] : joinedKeys.split(ROW_KEY_SEPARATOR)),\n [joinedKeys],\n );\n return (\n <>\n {rowKeys.map((key, index) => (\n <ThreadPrimitive.MessageByIndex key={key} index={index} components={components} />\n ))}\n </>\n );\n};\n\nconst DEFAULT_VIEWPORT_CLASS =\n \"relative flex flex-1 flex-col overflow-y-auto scroll-smooth\";\nconst DEFAULT_CONTENT_CLASS = \"w-full flex-1 px-3 pt-3\";\nconst DEFAULT_VIEWPORT_FOOTER_CLASS = \"sticky bottom-0\";\n\nconst DefaultScrollToBottom: FC = () => (\n <AuiIf condition={(state) => !state.thread.isEmpty}>\n <div className=\"flex justify-center pb-0.5\">\n <ThreadPrimitive.ScrollToBottom\n aria-label=\"Scroll to bottom\"\n title=\"Scroll to bottom\"\n className={THREAD_SCROLL_TO_BOTTOM_CLASS}\n >\n <ChevronDownIcon className=\"size-3.5\" />\n </ThreadPrimitive.ScrollToBottom>\n </div>\n </AuiIf>\n);\n\nexport interface ChatThreadShellProps {\n /**\n * Rendered as the first child of the panel — for absolutely-positioned\n * scrims that must cover the whole panel (AthenaChat's thread-loading\n * overlay). The shell does not force a positioning context; hosts include\n * `relative` in `panelClassName` when the scrim anchors to the panel.\n */\n overlay?: ReactNode;\n /**\n * Rendered above the viewport, inside the panel: header rows, slide-over\n * panels, and anything else the host docks at the top.\n */\n header?: ReactNode;\n /** Status/approval banners rendered between the header and the viewport. */\n banners?: ReactNode;\n /**\n * Nodes rendered inside the scroll viewport before the message column —\n * absolutely/sticky-positioned overlays (thread-switch skeletons,\n * scroll-to-top controls) and invisible observers (read receipts).\n */\n viewportOverlay?: ReactNode;\n /** Empty-thread state, rendered via ThreadPrimitive.Empty when provided. */\n welcome?: ReactNode;\n /**\n * Rendered inside the message column right above the messages — the\n * windowed wire's history edge (`StatewireHistoryEdge`): the \"N earlier\n * messages\" row that pages older history in and fetches cut tool outputs.\n */\n beforeMessages?: ReactNode;\n /** Message renderers for the thread's rows. */\n components: ChatThreadMessageComponents;\n /**\n * Wraps the messages element — for hosts that provide message-scoped\n * context (e.g. activity-grouping providers). Must render its argument.\n */\n renderMessages?: (messages: ReactNode) => ReactNode;\n /**\n * Rendered between the viewport and the composer: connection banners,\n * queued-message trays.\n */\n belowViewport?: ReactNode;\n /** The composer dock (or a read-only notice replacing it). */\n composer?: ReactNode;\n /**\n * Replaces the docked-panel card chrome — for hosts that render a\n * full-page column (AthenaChat's `aui-*` surface) instead of the\n * side-panel/task-pane card.\n */\n panelClassName?: string;\n /**\n * Value of the `--thread-max-width` variable read by width-capped\n * message/welcome/footer columns. Panels fill their width (the default,\n * \"100%\"); full-page hosts cap the column (AthenaChat defaults to 44rem).\n */\n maxWidth?: string;\n /**\n * Anchors the latest turn to the top of the viewport instead of pinning\n * scroll to the bottom (AthenaChat). Omitted = the primitive's default.\n */\n turnAnchor?: \"top\" | \"bottom\";\n /** Replaces the scroll-viewport chrome (padding, overflow tuning). */\n viewportClassName?: string;\n /**\n * Replaces the message-column chrome. Hosts whose welcome screen centers\n * vertically make this a flex column so the welcome node can grow.\n */\n contentClassName?: string;\n /**\n * Thread-level \"Thinking...\" indicator rendered after the messages. Hosts\n * whose message components render their own per-message indicators pass\n * null so the two never double up.\n */\n runningIndicator?: ReactNode;\n /**\n * Replaces the default centered scroll-to-bottom pill inside the sticky\n * viewport footer (AthenaChat floats a circular button above its composer).\n */\n scrollToBottom?: ReactNode;\n /**\n * Content docked inside the sticky viewport footer, after the\n * scroll-to-bottom control — for hosts whose banners + composer float over\n * the scrolling messages (AthenaChat) instead of sitting below the\n * viewport via `belowViewport`/`composer`.\n */\n viewportFooter?: ReactNode;\n /** Replaces the sticky viewport-footer chrome. */\n viewportFooterClassName?: string;\n /**\n * Marks the transcript read-only: mutating message actions (Edit,\n * Regenerate) hide while Copy stays available. For threads whose server\n * rejects dispatch (e.g. legacy pre-cutover conversations) — the runtime's\n * static capability flags cannot express a per-thread fence.\n */\n readOnly?: boolean;\n /**\n * The backend id of the thread on screen. When this surface picked the\n * conversation up from another one (`recordHandoffArrival`), the transcript\n * shows \"Continued from …\" after the last message that was already there.\n * A host that is a handover destination also mounts `HandoffCursorReveal`\n * for it in `viewportOverlay`, so the transcript opens at the message the\n * other surface was reading (hosts that never receive one, the Office pane,\n * don't carry it).\n */\n arrivalThreadId?: string | null;\n /**\n * The signed-in viewer's Athena user id (`user_…`), for the answer labels\n * (`AnswerLabel`): their own answers read \"for you\", and an answer for\n * another person is labelled even in a 1:1 chat. Omitted, answers name the\n * person by name and are labelled only once the thread holds several agents\n * or several people.\n */\n viewerUserId?: string | null;\n}\n\n/**\n * The shared thread layout: panel chrome, scroll viewport with welcome +\n * messages + running indicator, the sticky scroll-to-bottom control, and the\n * host-supplied composer dock.\n *\n * Hosts express their genuine divergences through the slots above; the\n * layout, scroll behaviour, and empty/running states stay identical across\n * surfaces so fixes land once.\n */\nexport function ChatThreadShell({\n overlay,\n header,\n banners,\n viewportOverlay,\n welcome,\n beforeMessages,\n components,\n renderMessages,\n belowViewport,\n composer,\n panelClassName,\n maxWidth = \"100%\",\n turnAnchor,\n viewportClassName,\n contentClassName,\n runningIndicator = <ThreadRunningIndicator />,\n scrollToBottom = <DefaultScrollToBottom />,\n viewportFooter,\n viewportFooterClassName,\n readOnly = false,\n arrivalThreadId = null,\n viewerUserId = null,\n}: ChatThreadShellProps) {\n const {\n UserMessage,\n AssistantMessage,\n UserEditComposer: UserEditComposerComponent = UserEditComposer,\n } = components;\n // A row resolves its renderer by component identity, so keep the merged map\n // referentially stable across renders.\n const messageComponents = useMemo(\n () => ({\n UserMessage: withArrivalDivider(UserMessage),\n AssistantMessage: withArrivalDivider(AssistantMessage),\n UserEditComposer: UserEditComposerComponent,\n }),\n [UserMessage, AssistantMessage, UserEditComposerComponent],\n );\n const messages = (\n <HandoffArrivalScope threadId={arrivalThreadId}>\n <AnswerLabelScope viewerUserId={viewerUserId}>\n <ThreadRows components={messageComponents} />\n </AnswerLabelScope>\n </HandoffArrivalScope>\n );\n return (\n <MessageActionsReadOnlyProvider readOnly={readOnly}>\n <ThreadPrimitive.Root\n className={panelClassName ?? THREAD_PANEL_CLASS}\n style={{ \"--thread-max-width\": maxWidth } as CSSProperties}\n >\n {overlay}\n {header}\n {banners}\n <ThreadPrimitive.Viewport\n turnAnchor={turnAnchor}\n className={viewportClassName ?? DEFAULT_VIEWPORT_CLASS}\n data-aui-thread-viewport=\"\"\n >\n {viewportOverlay}\n {/* No min-h-0 here: the column's automatic minimum height is what\n lets it extend past one screen, keeping the sticky footer's flow\n position after the full message list. Capping it detaches the\n footer (scroll pill, docked composers) as soon as the thread\n scrolls — proven in a live-browser probe of both structures. */}\n <div className=\"flex flex-1 flex-col items-center\">\n <div className={contentClassName ?? DEFAULT_CONTENT_CLASS}>\n {welcome != null ? (\n <ThreadPrimitive.Empty>{welcome}</ThreadPrimitive.Empty>\n ) : null}\n {beforeMessages}\n {renderMessages ? renderMessages(messages) : messages}\n {runningIndicator}\n </div>\n </div>\n <ThreadPrimitive.ViewportFooter\n className={viewportFooterClassName ?? DEFAULT_VIEWPORT_FOOTER_CLASS}\n {...{ [THREAD_VIEWPORT_FOOTER_ATTR]: \"\" }}\n >\n {scrollToBottom}\n {viewportFooter}\n </ThreadPrimitive.ViewportFooter>\n </ThreadPrimitive.Viewport>\n {belowViewport}\n {composer}\n </ThreadPrimitive.Root>\n </MessageActionsReadOnlyProvider>\n );\n}\n"],"names":["jsxs","Fragment","jsx","FromTheCallAfterMessage","HandoffArrivalAfterMessage","useAuiState","deepAgentRowKey","useMemo","ThreadPrimitive","AuiIf","THREAD_SCROLL_TO_BOTTOM_CLASS","ChevronDownIcon","runningIndicator","ThreadRunningIndicator","UserEditComposer","messageComponents","messages","HandoffArrivalScope","AnswerLabelScope","MessageActionsReadOnlyProvider","THREAD_PANEL_CLASS","THREAD_VIEWPORT_FOOTER_ATTR"],"mappings":"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"}
|
|
@@ -106,6 +106,10 @@ export interface ChatThreadShellProps {
|
|
|
106
106
|
* The backend id of the thread on screen. When this surface picked the
|
|
107
107
|
* conversation up from another one (`recordHandoffArrival`), the transcript
|
|
108
108
|
* shows "Continued from …" after the last message that was already there.
|
|
109
|
+
* A host that is a handover destination also mounts `HandoffCursorReveal`
|
|
110
|
+
* for it in `viewportOverlay`, so the transcript opens at the message the
|
|
111
|
+
* other surface was reading (hosts that never receive one, the Office pane,
|
|
112
|
+
* don't carry it).
|
|
109
113
|
*/
|
|
110
114
|
arrivalThreadId?: string | null;
|
|
111
115
|
/**
|
|
@@ -9,6 +9,7 @@ import { UserEditComposer } from "../messages/user-edit-composer.js";
|
|
|
9
9
|
import { ThreadRunningIndicator } from "../messages/running-indicator.js";
|
|
10
10
|
import { HandoffArrivalScope, HandoffArrivalAfterMessage } from "../messages/handoff-arrival-divider.js";
|
|
11
11
|
import { FromTheCallAfterMessage } from "../messages/from-the-call-card.js";
|
|
12
|
+
import { THREAD_VIEWPORT_FOOTER_ATTR } from "./statewire-history-edge.js";
|
|
12
13
|
import { THREAD_PANEL_CLASS } from "../messages/thread-chrome.js";
|
|
13
14
|
import { deepAgentRowKey } from "../../agent-runtime/src/messages.js";
|
|
14
15
|
const withArrivalDivider = /* @__PURE__ */ (() => {
|
|
@@ -113,6 +114,7 @@ function ChatThreadShell({
|
|
|
113
114
|
ThreadPrimitive.ViewportFooter,
|
|
114
115
|
{
|
|
115
116
|
className: viewportFooterClassName ?? DEFAULT_VIEWPORT_FOOTER_CLASS,
|
|
117
|
+
...{ [THREAD_VIEWPORT_FOOTER_ATTR]: "" },
|
|
116
118
|
children: [
|
|
117
119
|
scrollToBottom,
|
|
118
120
|
viewportFooter
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-thread-shell.js","sources":["../../../src/chat-ui/thread/chat-thread-shell.tsx"],"sourcesContent":["import { AuiIf, ThreadPrimitive, useAuiState } from \"@assistant-ui/react\";\nimport { deepAgentRowKey } from \"@athenaintel/agent-runtime\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, FC, ReactNode } from \"react\";\nimport { useMemo } from \"react\";\nimport {\n THREAD_PANEL_CLASS,\n THREAD_SCROLL_TO_BOTTOM_CLASS,\n} from \"../messages/message-components\";\nimport { AnswerLabelScope } from \"../messages/answer-label\";\nimport { MessageActionsReadOnlyProvider } from \"../messages/message-actions-config\";\nimport { UserEditComposer } from \"../messages/user-edit-composer\";\nimport { ThreadRunningIndicator } from \"../messages/running-indicator\";\nimport {\n HandoffArrivalAfterMessage,\n HandoffArrivalScope,\n} from \"../messages/handoff-arrival-divider\";\nimport { FromTheCallAfterMessage } from \"../messages/from-the-call-card\";\n\nexport interface ChatThreadMessageComponents {\n UserMessage: FC;\n AssistantMessage: FC;\n /**\n * Rendered in place of a user message while it is being edited (the action\n * bar's Edit control). Defaults to the shared UserEditComposer.\n */\n UserEditComposer?: FC;\n}\n\n// One wrapper per message renderer, so a renderer keeps its identity across\n// shells and renders: a row renders its message through the component it is\n// handed, and a new component identity would remount every row.\nconst withArrivalDivider = (() => {\n const wrapped = new WeakMap<FC, FC>();\n return (Message: FC): FC => {\n const existing = wrapped.get(Message);\n if (existing) return existing;\n // After the message: the \"From the call\" card where a call's stretch\n // ends, then the arrival divider (the arrival came after the call).\n const WithArrivalDivider: FC = () => (\n <>\n <Message />\n <FromTheCallAfterMessage />\n <HandoffArrivalAfterMessage />\n </>\n );\n wrapped.set(Message, WithArrivalDivider);\n return WithArrivalDivider;\n };\n})();\n\ntype ThreadRowComponents = ComponentProps<typeof ThreadPrimitive.MessageByIndex>[\"components\"];\n\nconst ROW_KEY_SEPARATOR = \"\\u0000\";\n\n/**\n * The thread's messages, one row each under its row key (`deepAgentRowKey`).\n *\n * `ThreadPrimitive.Messages` keys a row by its message id. A page of older\n * history that folds into the first loaded turn (a long run's earlier steps)\n * gives that turn a new id on every page, so keyed by id the turn's row\n * remounted once per page: it flashed through its entrance animation, closed\n * what the person had opened in it and took the browser's scroll anchor with\n * it. The row key stays with the row the turn continues, so the page lands in\n * the mounted row. A message from another runtime keys by its id, as before.\n */\nconst ThreadRows: FC<{ components: ThreadRowComponents }> = ({ components }) => {\n const joinedKeys = useAuiState((s) =>\n s.thread.messages.map((message) => deepAgentRowKey(message)).join(ROW_KEY_SEPARATOR),\n );\n const rowKeys = useMemo(\n () => (joinedKeys === \"\" ? [] : joinedKeys.split(ROW_KEY_SEPARATOR)),\n [joinedKeys],\n );\n return (\n <>\n {rowKeys.map((key, index) => (\n <ThreadPrimitive.MessageByIndex key={key} index={index} components={components} />\n ))}\n </>\n );\n};\n\nconst DEFAULT_VIEWPORT_CLASS =\n \"relative flex flex-1 flex-col overflow-y-auto scroll-smooth\";\nconst DEFAULT_CONTENT_CLASS = \"w-full flex-1 px-3 pt-3\";\nconst DEFAULT_VIEWPORT_FOOTER_CLASS = \"sticky bottom-0\";\n\nconst DefaultScrollToBottom: FC = () => (\n <AuiIf condition={(state) => !state.thread.isEmpty}>\n <div className=\"flex justify-center pb-0.5\">\n <ThreadPrimitive.ScrollToBottom\n aria-label=\"Scroll to bottom\"\n title=\"Scroll to bottom\"\n className={THREAD_SCROLL_TO_BOTTOM_CLASS}\n >\n <ChevronDownIcon className=\"size-3.5\" />\n </ThreadPrimitive.ScrollToBottom>\n </div>\n </AuiIf>\n);\n\nexport interface ChatThreadShellProps {\n /**\n * Rendered as the first child of the panel — for absolutely-positioned\n * scrims that must cover the whole panel (AthenaChat's thread-loading\n * overlay). The shell does not force a positioning context; hosts include\n * `relative` in `panelClassName` when the scrim anchors to the panel.\n */\n overlay?: ReactNode;\n /**\n * Rendered above the viewport, inside the panel: header rows, slide-over\n * panels, and anything else the host docks at the top.\n */\n header?: ReactNode;\n /** Status/approval banners rendered between the header and the viewport. */\n banners?: ReactNode;\n /**\n * Nodes rendered inside the scroll viewport before the message column —\n * absolutely/sticky-positioned overlays (thread-switch skeletons,\n * scroll-to-top controls) and invisible observers (read receipts).\n */\n viewportOverlay?: ReactNode;\n /** Empty-thread state, rendered via ThreadPrimitive.Empty when provided. */\n welcome?: ReactNode;\n /**\n * Rendered inside the message column right above the messages — the\n * windowed wire's history edge (`StatewireHistoryEdge`): the \"N earlier\n * messages\" row that pages older history in and fetches cut tool outputs.\n */\n beforeMessages?: ReactNode;\n /** Message renderers for the thread's rows. */\n components: ChatThreadMessageComponents;\n /**\n * Wraps the messages element — for hosts that provide message-scoped\n * context (e.g. activity-grouping providers). Must render its argument.\n */\n renderMessages?: (messages: ReactNode) => ReactNode;\n /**\n * Rendered between the viewport and the composer: connection banners,\n * queued-message trays.\n */\n belowViewport?: ReactNode;\n /** The composer dock (or a read-only notice replacing it). */\n composer?: ReactNode;\n /**\n * Replaces the docked-panel card chrome — for hosts that render a\n * full-page column (AthenaChat's `aui-*` surface) instead of the\n * side-panel/task-pane card.\n */\n panelClassName?: string;\n /**\n * Value of the `--thread-max-width` variable read by width-capped\n * message/welcome/footer columns. Panels fill their width (the default,\n * \"100%\"); full-page hosts cap the column (AthenaChat defaults to 44rem).\n */\n maxWidth?: string;\n /**\n * Anchors the latest turn to the top of the viewport instead of pinning\n * scroll to the bottom (AthenaChat). Omitted = the primitive's default.\n */\n turnAnchor?: \"top\" | \"bottom\";\n /** Replaces the scroll-viewport chrome (padding, overflow tuning). */\n viewportClassName?: string;\n /**\n * Replaces the message-column chrome. Hosts whose welcome screen centers\n * vertically make this a flex column so the welcome node can grow.\n */\n contentClassName?: string;\n /**\n * Thread-level \"Thinking...\" indicator rendered after the messages. Hosts\n * whose message components render their own per-message indicators pass\n * null so the two never double up.\n */\n runningIndicator?: ReactNode;\n /**\n * Replaces the default centered scroll-to-bottom pill inside the sticky\n * viewport footer (AthenaChat floats a circular button above its composer).\n */\n scrollToBottom?: ReactNode;\n /**\n * Content docked inside the sticky viewport footer, after the\n * scroll-to-bottom control — for hosts whose banners + composer float over\n * the scrolling messages (AthenaChat) instead of sitting below the\n * viewport via `belowViewport`/`composer`.\n */\n viewportFooter?: ReactNode;\n /** Replaces the sticky viewport-footer chrome. */\n viewportFooterClassName?: string;\n /**\n * Marks the transcript read-only: mutating message actions (Edit,\n * Regenerate) hide while Copy stays available. For threads whose server\n * rejects dispatch (e.g. legacy pre-cutover conversations) — the runtime's\n * static capability flags cannot express a per-thread fence.\n */\n readOnly?: boolean;\n /**\n * The backend id of the thread on screen. When this surface picked the\n * conversation up from another one (`recordHandoffArrival`), the transcript\n * shows \"Continued from …\" after the last message that was already there.\n */\n arrivalThreadId?: string | null;\n /**\n * The signed-in viewer's Athena user id (`user_…`), for the answer labels\n * (`AnswerLabel`): their own answers read \"for you\", and an answer for\n * another person is labelled even in a 1:1 chat. Omitted, answers name the\n * person by name and are labelled only once the thread holds several agents\n * or several people.\n */\n viewerUserId?: string | null;\n}\n\n/**\n * The shared thread layout: panel chrome, scroll viewport with welcome +\n * messages + running indicator, the sticky scroll-to-bottom control, and the\n * host-supplied composer dock.\n *\n * Hosts express their genuine divergences through the slots above; the\n * layout, scroll behaviour, and empty/running states stay identical across\n * surfaces so fixes land once.\n */\nexport function ChatThreadShell({\n overlay,\n header,\n banners,\n viewportOverlay,\n welcome,\n beforeMessages,\n components,\n renderMessages,\n belowViewport,\n composer,\n panelClassName,\n maxWidth = \"100%\",\n turnAnchor,\n viewportClassName,\n contentClassName,\n runningIndicator = <ThreadRunningIndicator />,\n scrollToBottom = <DefaultScrollToBottom />,\n viewportFooter,\n viewportFooterClassName,\n readOnly = false,\n arrivalThreadId = null,\n viewerUserId = null,\n}: ChatThreadShellProps) {\n const {\n UserMessage,\n AssistantMessage,\n UserEditComposer: UserEditComposerComponent = UserEditComposer,\n } = components;\n // A row resolves its renderer by component identity, so keep the merged map\n // referentially stable across renders.\n const messageComponents = useMemo(\n () => ({\n UserMessage: withArrivalDivider(UserMessage),\n AssistantMessage: withArrivalDivider(AssistantMessage),\n UserEditComposer: UserEditComposerComponent,\n }),\n [UserMessage, AssistantMessage, UserEditComposerComponent],\n );\n const messages = (\n <HandoffArrivalScope threadId={arrivalThreadId}>\n <AnswerLabelScope viewerUserId={viewerUserId}>\n <ThreadRows components={messageComponents} />\n </AnswerLabelScope>\n </HandoffArrivalScope>\n );\n return (\n <MessageActionsReadOnlyProvider readOnly={readOnly}>\n <ThreadPrimitive.Root\n className={panelClassName ?? THREAD_PANEL_CLASS}\n style={{ \"--thread-max-width\": maxWidth } as CSSProperties}\n >\n {overlay}\n {header}\n {banners}\n <ThreadPrimitive.Viewport\n turnAnchor={turnAnchor}\n className={viewportClassName ?? DEFAULT_VIEWPORT_CLASS}\n data-aui-thread-viewport=\"\"\n >\n {viewportOverlay}\n {/* No min-h-0 here: the column's automatic minimum height is what\n lets it extend past one screen, keeping the sticky footer's flow\n position after the full message list. Capping it detaches the\n footer (scroll pill, docked composers) as soon as the thread\n scrolls — proven in a live-browser probe of both structures. */}\n <div className=\"flex flex-1 flex-col items-center\">\n <div className={contentClassName ?? DEFAULT_CONTENT_CLASS}>\n {welcome != null ? (\n <ThreadPrimitive.Empty>{welcome}</ThreadPrimitive.Empty>\n ) : null}\n {beforeMessages}\n {renderMessages ? renderMessages(messages) : messages}\n {runningIndicator}\n </div>\n </div>\n <ThreadPrimitive.ViewportFooter\n className={viewportFooterClassName ?? DEFAULT_VIEWPORT_FOOTER_CLASS}\n >\n {scrollToBottom}\n {viewportFooter}\n </ThreadPrimitive.ViewportFooter>\n </ThreadPrimitive.Viewport>\n {belowViewport}\n {composer}\n </ThreadPrimitive.Root>\n </MessageActionsReadOnlyProvider>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-thread-shell.js","sources":["../../../src/chat-ui/thread/chat-thread-shell.tsx"],"sourcesContent":["import { AuiIf, ThreadPrimitive, useAuiState } from \"@assistant-ui/react\";\nimport { deepAgentRowKey } from \"@athenaintel/agent-runtime\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, FC, ReactNode } from \"react\";\nimport { useMemo } from \"react\";\nimport {\n THREAD_PANEL_CLASS,\n THREAD_SCROLL_TO_BOTTOM_CLASS,\n} from \"../messages/message-components\";\nimport { AnswerLabelScope } from \"../messages/answer-label\";\nimport { MessageActionsReadOnlyProvider } from \"../messages/message-actions-config\";\nimport { UserEditComposer } from \"../messages/user-edit-composer\";\nimport { ThreadRunningIndicator } from \"../messages/running-indicator\";\nimport {\n HandoffArrivalAfterMessage,\n HandoffArrivalScope,\n} from \"../messages/handoff-arrival-divider\";\nimport { FromTheCallAfterMessage } from \"../messages/from-the-call-card\";\nimport { THREAD_VIEWPORT_FOOTER_ATTR } from \"./statewire-history-edge\";\n\nexport interface ChatThreadMessageComponents {\n UserMessage: FC;\n AssistantMessage: FC;\n /**\n * Rendered in place of a user message while it is being edited (the action\n * bar's Edit control). Defaults to the shared UserEditComposer.\n */\n UserEditComposer?: FC;\n}\n\n// One wrapper per message renderer, so a renderer keeps its identity across\n// shells and renders: a row renders its message through the component it is\n// handed, and a new component identity would remount every row.\nconst withArrivalDivider = (() => {\n const wrapped = new WeakMap<FC, FC>();\n return (Message: FC): FC => {\n const existing = wrapped.get(Message);\n if (existing) return existing;\n // After the message: the \"From the call\" card where a call's stretch\n // ends, then the arrival divider (the arrival came after the call).\n const WithArrivalDivider: FC = () => (\n <>\n <Message />\n <FromTheCallAfterMessage />\n <HandoffArrivalAfterMessage />\n </>\n );\n wrapped.set(Message, WithArrivalDivider);\n return WithArrivalDivider;\n };\n})();\n\ntype ThreadRowComponents = ComponentProps<typeof ThreadPrimitive.MessageByIndex>[\"components\"];\n\nconst ROW_KEY_SEPARATOR = \"\\u0000\";\n\n/**\n * The thread's messages, one row each under its row key (`deepAgentRowKey`).\n *\n * `ThreadPrimitive.Messages` keys a row by its message id. A page of older\n * history that folds into the first loaded turn (a long run's earlier steps)\n * gives that turn a new id on every page, so keyed by id the turn's row\n * remounted once per page: it flashed through its entrance animation, closed\n * what the person had opened in it and took the browser's scroll anchor with\n * it. The row key stays with the row the turn continues, so the page lands in\n * the mounted row. A message from another runtime keys by its id, as before.\n */\nconst ThreadRows: FC<{ components: ThreadRowComponents }> = ({ components }) => {\n const joinedKeys = useAuiState((s) =>\n s.thread.messages.map((message) => deepAgentRowKey(message)).join(ROW_KEY_SEPARATOR),\n );\n const rowKeys = useMemo(\n () => (joinedKeys === \"\" ? [] : joinedKeys.split(ROW_KEY_SEPARATOR)),\n [joinedKeys],\n );\n return (\n <>\n {rowKeys.map((key, index) => (\n <ThreadPrimitive.MessageByIndex key={key} index={index} components={components} />\n ))}\n </>\n );\n};\n\nconst DEFAULT_VIEWPORT_CLASS =\n \"relative flex flex-1 flex-col overflow-y-auto scroll-smooth\";\nconst DEFAULT_CONTENT_CLASS = \"w-full flex-1 px-3 pt-3\";\nconst DEFAULT_VIEWPORT_FOOTER_CLASS = \"sticky bottom-0\";\n\nconst DefaultScrollToBottom: FC = () => (\n <AuiIf condition={(state) => !state.thread.isEmpty}>\n <div className=\"flex justify-center pb-0.5\">\n <ThreadPrimitive.ScrollToBottom\n aria-label=\"Scroll to bottom\"\n title=\"Scroll to bottom\"\n className={THREAD_SCROLL_TO_BOTTOM_CLASS}\n >\n <ChevronDownIcon className=\"size-3.5\" />\n </ThreadPrimitive.ScrollToBottom>\n </div>\n </AuiIf>\n);\n\nexport interface ChatThreadShellProps {\n /**\n * Rendered as the first child of the panel — for absolutely-positioned\n * scrims that must cover the whole panel (AthenaChat's thread-loading\n * overlay). The shell does not force a positioning context; hosts include\n * `relative` in `panelClassName` when the scrim anchors to the panel.\n */\n overlay?: ReactNode;\n /**\n * Rendered above the viewport, inside the panel: header rows, slide-over\n * panels, and anything else the host docks at the top.\n */\n header?: ReactNode;\n /** Status/approval banners rendered between the header and the viewport. */\n banners?: ReactNode;\n /**\n * Nodes rendered inside the scroll viewport before the message column —\n * absolutely/sticky-positioned overlays (thread-switch skeletons,\n * scroll-to-top controls) and invisible observers (read receipts).\n */\n viewportOverlay?: ReactNode;\n /** Empty-thread state, rendered via ThreadPrimitive.Empty when provided. */\n welcome?: ReactNode;\n /**\n * Rendered inside the message column right above the messages — the\n * windowed wire's history edge (`StatewireHistoryEdge`): the \"N earlier\n * messages\" row that pages older history in and fetches cut tool outputs.\n */\n beforeMessages?: ReactNode;\n /** Message renderers for the thread's rows. */\n components: ChatThreadMessageComponents;\n /**\n * Wraps the messages element — for hosts that provide message-scoped\n * context (e.g. activity-grouping providers). Must render its argument.\n */\n renderMessages?: (messages: ReactNode) => ReactNode;\n /**\n * Rendered between the viewport and the composer: connection banners,\n * queued-message trays.\n */\n belowViewport?: ReactNode;\n /** The composer dock (or a read-only notice replacing it). */\n composer?: ReactNode;\n /**\n * Replaces the docked-panel card chrome — for hosts that render a\n * full-page column (AthenaChat's `aui-*` surface) instead of the\n * side-panel/task-pane card.\n */\n panelClassName?: string;\n /**\n * Value of the `--thread-max-width` variable read by width-capped\n * message/welcome/footer columns. Panels fill their width (the default,\n * \"100%\"); full-page hosts cap the column (AthenaChat defaults to 44rem).\n */\n maxWidth?: string;\n /**\n * Anchors the latest turn to the top of the viewport instead of pinning\n * scroll to the bottom (AthenaChat). Omitted = the primitive's default.\n */\n turnAnchor?: \"top\" | \"bottom\";\n /** Replaces the scroll-viewport chrome (padding, overflow tuning). */\n viewportClassName?: string;\n /**\n * Replaces the message-column chrome. Hosts whose welcome screen centers\n * vertically make this a flex column so the welcome node can grow.\n */\n contentClassName?: string;\n /**\n * Thread-level \"Thinking...\" indicator rendered after the messages. Hosts\n * whose message components render their own per-message indicators pass\n * null so the two never double up.\n */\n runningIndicator?: ReactNode;\n /**\n * Replaces the default centered scroll-to-bottom pill inside the sticky\n * viewport footer (AthenaChat floats a circular button above its composer).\n */\n scrollToBottom?: ReactNode;\n /**\n * Content docked inside the sticky viewport footer, after the\n * scroll-to-bottom control — for hosts whose banners + composer float over\n * the scrolling messages (AthenaChat) instead of sitting below the\n * viewport via `belowViewport`/`composer`.\n */\n viewportFooter?: ReactNode;\n /** Replaces the sticky viewport-footer chrome. */\n viewportFooterClassName?: string;\n /**\n * Marks the transcript read-only: mutating message actions (Edit,\n * Regenerate) hide while Copy stays available. For threads whose server\n * rejects dispatch (e.g. legacy pre-cutover conversations) — the runtime's\n * static capability flags cannot express a per-thread fence.\n */\n readOnly?: boolean;\n /**\n * The backend id of the thread on screen. When this surface picked the\n * conversation up from another one (`recordHandoffArrival`), the transcript\n * shows \"Continued from …\" after the last message that was already there.\n * A host that is a handover destination also mounts `HandoffCursorReveal`\n * for it in `viewportOverlay`, so the transcript opens at the message the\n * other surface was reading (hosts that never receive one, the Office pane,\n * don't carry it).\n */\n arrivalThreadId?: string | null;\n /**\n * The signed-in viewer's Athena user id (`user_…`), for the answer labels\n * (`AnswerLabel`): their own answers read \"for you\", and an answer for\n * another person is labelled even in a 1:1 chat. Omitted, answers name the\n * person by name and are labelled only once the thread holds several agents\n * or several people.\n */\n viewerUserId?: string | null;\n}\n\n/**\n * The shared thread layout: panel chrome, scroll viewport with welcome +\n * messages + running indicator, the sticky scroll-to-bottom control, and the\n * host-supplied composer dock.\n *\n * Hosts express their genuine divergences through the slots above; the\n * layout, scroll behaviour, and empty/running states stay identical across\n * surfaces so fixes land once.\n */\nexport function ChatThreadShell({\n overlay,\n header,\n banners,\n viewportOverlay,\n welcome,\n beforeMessages,\n components,\n renderMessages,\n belowViewport,\n composer,\n panelClassName,\n maxWidth = \"100%\",\n turnAnchor,\n viewportClassName,\n contentClassName,\n runningIndicator = <ThreadRunningIndicator />,\n scrollToBottom = <DefaultScrollToBottom />,\n viewportFooter,\n viewportFooterClassName,\n readOnly = false,\n arrivalThreadId = null,\n viewerUserId = null,\n}: ChatThreadShellProps) {\n const {\n UserMessage,\n AssistantMessage,\n UserEditComposer: UserEditComposerComponent = UserEditComposer,\n } = components;\n // A row resolves its renderer by component identity, so keep the merged map\n // referentially stable across renders.\n const messageComponents = useMemo(\n () => ({\n UserMessage: withArrivalDivider(UserMessage),\n AssistantMessage: withArrivalDivider(AssistantMessage),\n UserEditComposer: UserEditComposerComponent,\n }),\n [UserMessage, AssistantMessage, UserEditComposerComponent],\n );\n const messages = (\n <HandoffArrivalScope threadId={arrivalThreadId}>\n <AnswerLabelScope viewerUserId={viewerUserId}>\n <ThreadRows components={messageComponents} />\n </AnswerLabelScope>\n </HandoffArrivalScope>\n );\n return (\n <MessageActionsReadOnlyProvider readOnly={readOnly}>\n <ThreadPrimitive.Root\n className={panelClassName ?? THREAD_PANEL_CLASS}\n style={{ \"--thread-max-width\": maxWidth } as CSSProperties}\n >\n {overlay}\n {header}\n {banners}\n <ThreadPrimitive.Viewport\n turnAnchor={turnAnchor}\n className={viewportClassName ?? DEFAULT_VIEWPORT_CLASS}\n data-aui-thread-viewport=\"\"\n >\n {viewportOverlay}\n {/* No min-h-0 here: the column's automatic minimum height is what\n lets it extend past one screen, keeping the sticky footer's flow\n position after the full message list. Capping it detaches the\n footer (scroll pill, docked composers) as soon as the thread\n scrolls — proven in a live-browser probe of both structures. */}\n <div className=\"flex flex-1 flex-col items-center\">\n <div className={contentClassName ?? DEFAULT_CONTENT_CLASS}>\n {welcome != null ? (\n <ThreadPrimitive.Empty>{welcome}</ThreadPrimitive.Empty>\n ) : null}\n {beforeMessages}\n {renderMessages ? renderMessages(messages) : messages}\n {runningIndicator}\n </div>\n </div>\n <ThreadPrimitive.ViewportFooter\n className={viewportFooterClassName ?? DEFAULT_VIEWPORT_FOOTER_CLASS}\n {...{ [THREAD_VIEWPORT_FOOTER_ATTR]: \"\" }}\n >\n {scrollToBottom}\n {viewportFooter}\n </ThreadPrimitive.ViewportFooter>\n </ThreadPrimitive.Viewport>\n {belowViewport}\n {composer}\n </ThreadPrimitive.Root>\n </MessageActionsReadOnlyProvider>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAiCA,MAAM,qBAAsB,uBAAM;AAChC,QAAM,8BAAc,QAAA;AACpB,SAAO,CAAC,YAAoB;AAC1B,UAAM,WAAW,QAAQ,IAAI,OAAO;AACpC,QAAI,SAAU,QAAO;AAGrB,UAAM,qBAAyB,MAC7B,qBAAA,UAAA,EACE,UAAA;AAAA,MAAA,oBAAC,SAAA,EAAQ;AAAA,0BACR,yBAAA,EAAwB;AAAA,0BACxB,4BAAA,CAAA,CAA2B;AAAA,IAAA,GAC9B;AAEF,YAAQ,IAAI,SAAS,kBAAkB;AACvC,WAAO;AAAA,EACT;AACF,GAAA;AAIA,MAAM,oBAAoB;AAa1B,MAAM,aAAsD,CAAC,EAAE,iBAAiB;AAC9E,QAAM,aAAa;AAAA,IAAY,CAAC,MAC9B,EAAE,OAAO,SAAS,IAAI,CAAC,YAAY,gBAAgB,OAAO,CAAC,EAAE,KAAK,iBAAiB;AAAA,EAAA;AAErF,QAAM,UAAU;AAAA,IACd,MAAO,eAAe,KAAK,CAAA,IAAK,WAAW,MAAM,iBAAiB;AAAA,IAClE,CAAC,UAAU;AAAA,EAAA;AAEb,SACE,oBAAA,UAAA,EACG,UAAA,QAAQ,IAAI,CAAC,KAAK,UACjB,oBAAC,gBAAgB,gBAAhB,EAAyC,OAAc,cAAnB,GAA2C,CACjF,GACH;AAEJ;AAEA,MAAM,yBACJ;AACF,MAAM,wBAAwB;AAC9B,MAAM,gCAAgC;AAEtC,MAAM,wBAA4B,MAChC,oBAAC,OAAA,EAAM,WAAW,CAAC,UAAU,CAAC,MAAM,OAAO,SACzC,UAAA,oBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC,cAAW;AAAA,IACX,OAAM;AAAA,IACN,WAAW;AAAA,IAEX,UAAA,oBAAC,iBAAA,EAAgB,WAAU,WAAA,CAAW;AAAA,EAAA;AACxC,EAAA,CACF,EAAA,CACF;AA8HK,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,uCAAoB,wBAAA,EAAuB;AAAA,EAC3C,qCAAkB,uBAAA,EAAsB;AAAA,EACxC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,kBAAkB;AAAA,EAClB,eAAe;AACjB,GAAyB;AACvB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,kBAAkB,4BAA4B;AAAA,EAAA,IAC5C;AAGJ,QAAM,oBAAoB;AAAA,IACxB,OAAO;AAAA,MACL,aAAa,mBAAmB,WAAW;AAAA,MAC3C,kBAAkB,mBAAmB,gBAAgB;AAAA,MACrD,kBAAkB;AAAA,IAAA;AAAA,IAEpB,CAAC,aAAa,kBAAkB,yBAAyB;AAAA,EAAA;AAE3D,QAAM,WACJ,oBAAC,qBAAA,EAAoB,UAAU,iBAC7B,UAAA,oBAAC,kBAAA,EAAiB,cAChB,UAAA,oBAAC,YAAA,EAAW,YAAY,kBAAA,CAAmB,GAC7C,GACF;AAEF,SACE,oBAAC,kCAA+B,UAC9B,UAAA;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,WAAW,kBAAkB;AAAA,MAC7B,OAAO,EAAE,sBAAsB,SAAA;AAAA,MAE9B,UAAA;AAAA,QAAA;AAAA,QACA;AAAA,QACA;AAAA,QACD;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC;AAAA,YACA,WAAW,qBAAqB;AAAA,YAChC,4BAAyB;AAAA,YAExB,UAAA;AAAA,cAAA;AAAA,cAMD,oBAAC,SAAI,WAAU,qCACb,+BAAC,OAAA,EAAI,WAAW,oBAAoB,uBACjC,UAAA;AAAA,gBAAA,WAAW,OACV,oBAAC,gBAAgB,OAAhB,EAAuB,mBAAQ,IAC9B;AAAA,gBACH;AAAA,gBACA,iBAAiB,eAAe,QAAQ,IAAI;AAAA,gBAC5C;AAAA,cAAA,EAAA,CACH,EAAA,CACF;AAAA,cACA;AAAA,gBAAC,gBAAgB;AAAA,gBAAhB;AAAA,kBACC,WAAW,2BAA2B;AAAA,kBACrC,GAAG,EAAE,CAAC,2BAA2B,GAAG,GAAA;AAAA,kBAEpC,UAAA;AAAA,oBAAA;AAAA,oBACA;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA;AAAA,YACH;AAAA,UAAA;AAAA,QAAA;AAAA,QAED;AAAA,QACA;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAEL;AAEJ;"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const react = require("@assistant-ui/react");
|
|
4
|
+
const require$$0 = require("react");
|
|
5
|
+
const statewireHistoryEdge = require("./statewire-history-edge.cjs");
|
|
6
|
+
const handoffArrival = require("../../agent-runtime/src/handoff-arrival.cjs");
|
|
7
|
+
const handoffCursor = require("../../agent-runtime/src/handoff-cursor.cjs");
|
|
8
|
+
const editCommand = require("../../agent-runtime/src/edit-command.cjs");
|
|
9
|
+
const THREAD_ROOT_SELECTOR = "[data-aui-thread-root]";
|
|
10
|
+
const HANDOFF_CURSOR_REVEALED_ATTR = "data-handoff-cursor-revealed";
|
|
11
|
+
const HANDOFF_CURSOR_POLL_MS = 150;
|
|
12
|
+
const escapeAttributeValue = (value) => typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
|
|
13
|
+
function findThreadViewport(anchor) {
|
|
14
|
+
var _a;
|
|
15
|
+
if (!anchor) return null;
|
|
16
|
+
const inside = anchor.closest(`[${statewireHistoryEdge.THREAD_VIEWPORT_ATTR}]`);
|
|
17
|
+
if (inside) return inside;
|
|
18
|
+
return ((_a = anchor.closest(THREAD_ROOT_SELECTOR)) == null ? void 0 : _a.querySelector(`[${statewireHistoryEdge.THREAD_VIEWPORT_ATTR}]`)) ?? null;
|
|
19
|
+
}
|
|
20
|
+
function coveredBottomOf(viewport) {
|
|
21
|
+
const footer = viewport.querySelector(`[${statewireHistoryEdge.THREAD_VIEWPORT_FOOTER_ATTR}]`);
|
|
22
|
+
if (!footer) return 0;
|
|
23
|
+
return Math.max(0, viewport.getBoundingClientRect().bottom - footer.getBoundingClientRect().top);
|
|
24
|
+
}
|
|
25
|
+
function readThreadReadingCursor(viewport) {
|
|
26
|
+
if (!viewport) return null;
|
|
27
|
+
return handoffCursor.readingCursorOf({ viewport, coveredBottomPx: coveredBottomOf(viewport) });
|
|
28
|
+
}
|
|
29
|
+
function useReadThreadReadingCursor() {
|
|
30
|
+
const store = react.useThreadViewportStore({ optional: true });
|
|
31
|
+
return require$$0.useCallback(
|
|
32
|
+
(anchor) => readThreadReadingCursor((store == null ? void 0 : store.getState().element.viewport) ?? findThreadViewport(anchor)),
|
|
33
|
+
[store]
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
function scrollMessageIntoView({
|
|
37
|
+
viewport,
|
|
38
|
+
element
|
|
39
|
+
}) {
|
|
40
|
+
const box = viewport.getBoundingClientRect();
|
|
41
|
+
const rect = element.getBoundingClientRect();
|
|
42
|
+
const visible = Math.max(1, box.height - coveredBottomOf(viewport));
|
|
43
|
+
const offsetTop = rect.top - box.top + viewport.scrollTop;
|
|
44
|
+
const top = rect.height >= visible ? offsetTop - 8 : offsetTop - (visible - rect.height) / 2;
|
|
45
|
+
viewport.scrollTo({ top: Math.max(0, top), behavior: "instant" });
|
|
46
|
+
}
|
|
47
|
+
function isInView({ viewport, element }) {
|
|
48
|
+
const box = viewport.getBoundingClientRect();
|
|
49
|
+
const rect = element.getBoundingClientRect();
|
|
50
|
+
return rect.bottom > box.top && rect.top < box.bottom - coveredBottomOf(viewport);
|
|
51
|
+
}
|
|
52
|
+
function highlightMessage(element) {
|
|
53
|
+
element.setAttribute(HANDOFF_CURSOR_REVEALED_ATTR, "");
|
|
54
|
+
const animation = typeof element.animate === "function" ? element.animate(
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
backgroundColor: "rgba(37, 99, 235, 0.12)",
|
|
58
|
+
boxShadow: "0 0 0 2px rgba(37, 99, 235, 0.35)",
|
|
59
|
+
borderRadius: "12px"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
backgroundColor: "rgba(37, 99, 235, 0.12)",
|
|
63
|
+
boxShadow: "0 0 0 2px rgba(37, 99, 235, 0.35)",
|
|
64
|
+
borderRadius: "12px",
|
|
65
|
+
offset: 0.5
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
backgroundColor: "rgba(37, 99, 235, 0)",
|
|
69
|
+
boxShadow: "0 0 0 2px rgba(37, 99, 235, 0)",
|
|
70
|
+
borderRadius: "12px"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
{ duration: handoffCursor.HANDOFF_CURSOR_HIGHLIGHT_MS, easing: "ease-out" }
|
|
74
|
+
) : null;
|
|
75
|
+
const timer = setTimeout(
|
|
76
|
+
() => element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR),
|
|
77
|
+
handoffCursor.HANDOFF_CURSOR_HIGHLIGHT_MS
|
|
78
|
+
);
|
|
79
|
+
return () => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
animation == null ? void 0 : animation.cancel();
|
|
82
|
+
element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function startHandoffCursorReveal({
|
|
86
|
+
cursor,
|
|
87
|
+
readThread,
|
|
88
|
+
readViewport,
|
|
89
|
+
onSettled,
|
|
90
|
+
now = () => Date.now()
|
|
91
|
+
}) {
|
|
92
|
+
let waitingSince = now();
|
|
93
|
+
let pagesLoaded = 0;
|
|
94
|
+
let paging = false;
|
|
95
|
+
let pageLandsAt = 0;
|
|
96
|
+
let settled = false;
|
|
97
|
+
let revealed = null;
|
|
98
|
+
let personScrolled = false;
|
|
99
|
+
let unhighlight = null;
|
|
100
|
+
let detach = null;
|
|
101
|
+
const settle = () => {
|
|
102
|
+
if (settled) return;
|
|
103
|
+
settled = true;
|
|
104
|
+
clearInterval(timer);
|
|
105
|
+
detach == null ? void 0 : detach();
|
|
106
|
+
onSettled();
|
|
107
|
+
};
|
|
108
|
+
const pageOlder = () => {
|
|
109
|
+
paging = true;
|
|
110
|
+
editCommand.loadOlderDeepAgentHistoryPage({
|
|
111
|
+
readExtras: () => editCommand.readDeepAgentStatewireExtras(readThread().extras)
|
|
112
|
+
}).then((loaded) => {
|
|
113
|
+
if (!loaded) {
|
|
114
|
+
pagesLoaded = Number.POSITIVE_INFINITY;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
pagesLoaded += 1;
|
|
118
|
+
waitingSince = now();
|
|
119
|
+
}).catch(() => {
|
|
120
|
+
pagesLoaded = Number.POSITIVE_INFINITY;
|
|
121
|
+
}).finally(() => {
|
|
122
|
+
paging = false;
|
|
123
|
+
pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
const tick = () => {
|
|
127
|
+
if (settled) return;
|
|
128
|
+
if (revealed) {
|
|
129
|
+
if (personScrolled || now() - revealed.at >= handoffCursor.HANDOFF_CURSOR_SETTLE_MS) {
|
|
130
|
+
settle();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (revealed.element.isConnected && !isInView(revealed)) scrollMessageIntoView(revealed);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const viewport = readViewport();
|
|
137
|
+
const { messages, isLoading, extras } = readThread();
|
|
138
|
+
const targetId = handoffCursor.handoffCursorMessageId({ messages, cursor });
|
|
139
|
+
const element = targetId !== null && viewport ? viewport.querySelector(
|
|
140
|
+
`[${handoffCursor.MESSAGE_ROW_ID_ATTRIBUTE}="${escapeAttributeValue(targetId)}"]`
|
|
141
|
+
) : null;
|
|
142
|
+
const step = handoffCursor.handoffCursorRevealStep({
|
|
143
|
+
found: element !== null,
|
|
144
|
+
// Still attaching, a page in flight, or the row not rendered yet.
|
|
145
|
+
loading: isLoading || paging || now() < pageLandsAt || messages.length === 0 || targetId !== null && !element,
|
|
146
|
+
hasOlder: editCommand.deepAgentHistoryHasOlder(extras),
|
|
147
|
+
pagesLoaded,
|
|
148
|
+
elapsedMs: now() - waitingSince
|
|
149
|
+
});
|
|
150
|
+
switch (step) {
|
|
151
|
+
case "reveal": {
|
|
152
|
+
if (!viewport || !element) return;
|
|
153
|
+
scrollMessageIntoView({ viewport, element });
|
|
154
|
+
unhighlight = highlightMessage(element);
|
|
155
|
+
revealed = { element, viewport, at: now() };
|
|
156
|
+
const stop = () => {
|
|
157
|
+
personScrolled = true;
|
|
158
|
+
};
|
|
159
|
+
const events = ["wheel", "touchstart", "pointerdown", "keydown"];
|
|
160
|
+
for (const event of events) viewport.addEventListener(event, stop, { passive: true });
|
|
161
|
+
detach = () => {
|
|
162
|
+
for (const event of events) viewport.removeEventListener(event, stop);
|
|
163
|
+
};
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
case "page":
|
|
167
|
+
pageOlder();
|
|
168
|
+
return;
|
|
169
|
+
case "wait":
|
|
170
|
+
return;
|
|
171
|
+
case "bottom":
|
|
172
|
+
settle();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const timer = setInterval(tick, HANDOFF_CURSOR_POLL_MS);
|
|
177
|
+
tick();
|
|
178
|
+
return () => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
settled = true;
|
|
181
|
+
clearInterval(timer);
|
|
182
|
+
detach == null ? void 0 : detach();
|
|
183
|
+
unhighlight == null ? void 0 : unhighlight();
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const HandoffCursorReveal = ({ threadId }) => {
|
|
187
|
+
const cursor = handoffArrival.useHandoffArrivalCursor(threadId);
|
|
188
|
+
const aui = react.useAui();
|
|
189
|
+
const viewport = react.useThreadViewport({
|
|
190
|
+
optional: true,
|
|
191
|
+
selector: (state) => state.element.viewport
|
|
192
|
+
});
|
|
193
|
+
const viewportRef = require$$0.useRef(viewport);
|
|
194
|
+
viewportRef.current = viewport;
|
|
195
|
+
require$$0.useEffect(() => {
|
|
196
|
+
if (!threadId || !cursor) return void 0;
|
|
197
|
+
return startHandoffCursorReveal({
|
|
198
|
+
cursor,
|
|
199
|
+
readThread: () => {
|
|
200
|
+
const state = aui.thread().getState();
|
|
201
|
+
return { messages: state.messages, isLoading: state.isLoading, extras: state.extras };
|
|
202
|
+
},
|
|
203
|
+
readViewport: () => viewportRef.current ?? null,
|
|
204
|
+
onSettled: () => handoffArrival.settleHandoffArrivalCursor(threadId)
|
|
205
|
+
});
|
|
206
|
+
}, [aui, cursor, threadId]);
|
|
207
|
+
return null;
|
|
208
|
+
};
|
|
209
|
+
exports.HANDOFF_CURSOR_POLL_MS = HANDOFF_CURSOR_POLL_MS;
|
|
210
|
+
exports.HANDOFF_CURSOR_REVEALED_ATTR = HANDOFF_CURSOR_REVEALED_ATTR;
|
|
211
|
+
exports.HandoffCursorReveal = HandoffCursorReveal;
|
|
212
|
+
exports.findThreadViewport = findThreadViewport;
|
|
213
|
+
exports.readThreadReadingCursor = readThreadReadingCursor;
|
|
214
|
+
exports.startHandoffCursorReveal = startHandoffCursorReveal;
|
|
215
|
+
exports.useReadThreadReadingCursor = useReadThreadReadingCursor;
|
|
216
|
+
//# sourceMappingURL=handoff-cursor.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-cursor.cjs","sources":["../../../src/chat-ui/thread/handoff-cursor.tsx"],"sourcesContent":["/**\n * Handovers open where the person was reading (`@athenaintel/agent-runtime`'s\n * `handoff-cursor`), for every host of `ChatThreadShell` (Athena in Chrome,\n * the desktop app, `AthenaChat`).\n *\n * As a source, `useReadThreadReadingCursor` reads the shell's viewport at the\n * moment of the click: `null` at the bottom, else the id of the last message\n * fully in view. As a destination, `HandoffCursorReveal` (mounted by the shell\n * for its `arrivalThreadId`) scrolls to the message the arrival names once the\n * transcript has it, paging older history in when it is further back, and\n * highlights it briefly; a cursor it can't find leaves the view at the bottom.\n */\nimport {\n type ThreadMessage,\n type ThreadViewportState,\n useAui,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n deepAgentHistoryHasOlder,\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n HANDOFF_CURSOR_SETTLE_MS,\n handoffCursorMessageId,\n handoffCursorRevealStep,\n loadOlderDeepAgentHistoryPage,\n MESSAGE_ROW_ID_ATTRIBUTE,\n readDeepAgentStatewireExtras,\n readingCursorOf,\n settleHandoffArrivalCursor,\n useHandoffArrivalCursor,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { THREAD_VIEWPORT_ATTR, THREAD_VIEWPORT_FOOTER_ATTR } from \"./statewire-history-edge\";\n/** What assistant-ui's `ThreadPrimitive.Root` stamps on the thread panel. */\nconst THREAD_ROOT_SELECTOR = \"[data-aui-thread-root]\";\n/** Set on the revealed message while it is highlighted. */\nexport const HANDOFF_CURSOR_REVEALED_ATTR = \"data-handoff-cursor-revealed\";\n/** How often a landing looks for its message while the transcript loads. */\nexport const HANDOFF_CURSOR_POLL_MS = 150;\n\nconst escapeAttributeValue = (value: string): string =>\n typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\"\n ? CSS.escape(value)\n : value.replace(/[\"\\\\]/g, \"\\\\$&\");\n\n/** The thread viewport `anchor` sits in, or the one of the thread panel it sits in. */\nexport function findThreadViewport(anchor: Element | null | undefined): HTMLElement | null {\n if (!anchor) return null;\n const inside = anchor.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`);\n if (inside) return inside;\n return (\n anchor.closest(THREAD_ROOT_SELECTOR)?.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ??\n null\n );\n}\n\n/** How much of `viewport` its sticky footer (the composer dock) covers at the bottom. */\nfunction coveredBottomOf(viewport: HTMLElement): number {\n const footer = viewport.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_FOOTER_ATTR}]`);\n if (!footer) return 0;\n return Math.max(0, viewport.getBoundingClientRect().bottom - footer.getBoundingClientRect().top);\n}\n\n/** Where `viewport` is being read: `null` at the bottom, else a message id. */\nexport function readThreadReadingCursor(viewport: HTMLElement | null): string | null {\n if (!viewport) return null;\n return readingCursorOf({ viewport, coveredBottomPx: coveredBottomOf(viewport) });\n}\n\n/**\n * Reads where the thread is being read, at the moment of a click: inside the\n * viewport (a composer toolbar, even from a portaled dialog) through the\n * thread's viewport store; elsewhere in the panel (a header menu) from the\n * DOM around `anchor`. `null` at the bottom or with no viewport found.\n */\nexport function useReadThreadReadingCursor(): (anchor?: Element | null) => string | null {\n const store = useThreadViewportStore({ optional: true });\n return useCallback(\n (anchor?: Element | null) =>\n readThreadReadingCursor(store?.getState().element.viewport ?? findThreadViewport(anchor)),\n [store],\n );\n}\n\n/** Scrolls `element` into `viewport`'s visible part: centred, or its top when taller. */\nfunction scrollMessageIntoView({\n viewport,\n element,\n}: {\n viewport: HTMLElement;\n element: HTMLElement;\n}): void {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n const visible = Math.max(1, box.height - coveredBottomOf(viewport));\n const offsetTop = rect.top - box.top + viewport.scrollTop;\n const top =\n rect.height >= visible ? offsetTop - 8 : offsetTop - (visible - rect.height) / 2;\n // `instant`: the shell's viewport scrolls smoothly by default.\n viewport.scrollTo({ top: Math.max(0, top), behavior: \"instant\" });\n}\n\nfunction isInView({ viewport, element }: { viewport: HTMLElement; element: HTMLElement }): boolean {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n return rect.bottom > box.top && rect.top < box.bottom - coveredBottomOf(viewport);\n}\n\n/**\n * A brief tint on the revealed message, through the Web Animations API so it\n * needs nothing from the host's stylesheet. Returns its undo.\n */\nfunction highlightMessage(element: HTMLElement): () => void {\n element.setAttribute(HANDOFF_CURSOR_REVEALED_ATTR, \"\");\n const animation =\n typeof element.animate === \"function\"\n ? element.animate(\n [\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n offset: 0.5,\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0)\",\n borderRadius: \"12px\",\n },\n ],\n { duration: HANDOFF_CURSOR_HIGHLIGHT_MS, easing: \"ease-out\" },\n )\n : null;\n const timer = setTimeout(\n () => element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR),\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n );\n return () => {\n clearTimeout(timer);\n animation?.cancel();\n element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR);\n };\n}\n\ninterface ThreadSnapshot {\n messages: readonly ThreadMessage[];\n isLoading: boolean;\n extras: unknown;\n}\n\n/**\n * Opens the transcript at `cursor`: looks for the message every\n * {@link HANDOFF_CURSOR_POLL_MS}, pages older history in while it is further\n * back, scrolls to it, holds it in view while the transcript settles (until\n * the person scrolls), and highlights it. Calls `onSettled` once it revealed\n * the message or gave up (the bottom). Returns its cancel.\n */\nexport function startHandoffCursorReveal({\n cursor,\n readThread,\n readViewport,\n onSettled,\n now = () => Date.now(),\n}: {\n cursor: string;\n readThread: () => ThreadSnapshot;\n readViewport: () => HTMLElement | null;\n onSettled: () => void;\n now?: () => number;\n}): () => void {\n // Progress restarts the wait: the reveal started, or a page of older history landed.\n let waitingSince = now();\n let pagesLoaded = 0;\n let paging = false;\n // A page that landed renders on a later commit: look again before paging on.\n let pageLandsAt = 0;\n let settled = false;\n let revealed: { element: HTMLElement; viewport: HTMLElement; at: number } | null = null;\n let personScrolled = false;\n let unhighlight: (() => void) | null = null;\n let detach: (() => void) | null = null;\n\n const settle = () => {\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n onSettled();\n };\n const pageOlder = () => {\n paging = true;\n loadOlderDeepAgentHistoryPage({\n readExtras: () => readDeepAgentStatewireExtras(readThread().extras),\n })\n .then((loaded) => {\n if (!loaded) {\n // Nothing older could be stated: the history is exhausted.\n pagesLoaded = Number.POSITIVE_INFINITY;\n return;\n }\n pagesLoaded += 1;\n waitingSince = now();\n })\n .catch(() => {\n pagesLoaded = Number.POSITIVE_INFINITY;\n })\n .finally(() => {\n paging = false;\n pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;\n });\n };\n const tick = () => {\n if (settled) return;\n if (revealed) {\n if (personScrolled || now() - revealed.at >= HANDOFF_CURSOR_SETTLE_MS) {\n settle();\n return;\n }\n if (revealed.element.isConnected && !isInView(revealed)) scrollMessageIntoView(revealed);\n return;\n }\n const viewport = readViewport();\n const { messages, isLoading, extras } = readThread();\n const targetId = handoffCursorMessageId({ messages, cursor });\n const element =\n targetId !== null && viewport\n ? viewport.querySelector<HTMLElement>(\n `[${MESSAGE_ROW_ID_ATTRIBUTE}=\"${escapeAttributeValue(targetId)}\"]`,\n )\n : null;\n const step = handoffCursorRevealStep({\n found: element !== null,\n // Still attaching, a page in flight, or the row not rendered yet.\n loading:\n isLoading ||\n paging ||\n now() < pageLandsAt ||\n messages.length === 0 ||\n (targetId !== null && !element),\n hasOlder: deepAgentHistoryHasOlder(extras),\n pagesLoaded,\n elapsedMs: now() - waitingSince,\n });\n switch (step) {\n case \"reveal\": {\n if (!viewport || !element) return;\n scrollMessageIntoView({ viewport, element });\n unhighlight = highlightMessage(element);\n revealed = { element, viewport, at: now() };\n const stop = () => {\n personScrolled = true;\n };\n const events = [\"wheel\", \"touchstart\", \"pointerdown\", \"keydown\"] as const;\n for (const event of events) viewport.addEventListener(event, stop, { passive: true });\n detach = () => {\n for (const event of events) viewport.removeEventListener(event, stop);\n };\n return;\n }\n case \"page\":\n pageOlder();\n return;\n case \"wait\":\n return;\n case \"bottom\":\n settle();\n return;\n }\n };\n const timer = setInterval(tick, HANDOFF_CURSOR_POLL_MS);\n tick();\n return () => {\n // Once settled, the highlight fades out on its own timer.\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n unhighlight?.();\n };\n}\n\n/**\n * Opens `threadId`'s transcript at the message the source was reading, once,\n * when this surface picked the conversation up with a cursor\n * (`recordHandoffArrival({ cursor })`). Renders nothing.\n */\nexport const HandoffCursorReveal: FC<{ threadId: string | null | undefined }> = ({ threadId }) => {\n const cursor = useHandoffArrivalCursor(threadId);\n const aui = useAui();\n const viewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n useEffect(() => {\n if (!threadId || !cursor) return undefined;\n return startHandoffCursorReveal({\n cursor,\n readThread: () => {\n const state = aui.thread().getState();\n return { messages: state.messages, isLoading: state.isLoading, extras: state.extras };\n },\n readViewport: () => viewportRef.current ?? null,\n onSettled: () => settleHandoffArrivalCursor(threadId),\n });\n }, [aui, cursor, threadId]);\n return null;\n};\n"],"names":["THREAD_VIEWPORT_ATTR","THREAD_VIEWPORT_FOOTER_ATTR","readingCursorOf","useThreadViewportStore","useCallback","HANDOFF_CURSOR_HIGHLIGHT_MS","loadOlderDeepAgentHistoryPage","readDeepAgentStatewireExtras","HANDOFF_CURSOR_SETTLE_MS","handoffCursorMessageId","MESSAGE_ROW_ID_ATTRIBUTE","handoffCursorRevealStep","deepAgentHistoryHasOlder","useHandoffArrivalCursor","useAui","useThreadViewport","useRef","useEffect","settleHandoffArrivalCursor"],"mappings":"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"}
|