@athenaintel/react 0.24.0 → 0.25.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 +29 -4
- package/dist/assets/AssetPanel.cjs +3 -2
- package/dist/assets/AssetPanel.cjs.map +1 -1
- package/dist/assets/AssetPanel.js +3 -2
- package/dist/assets/AssetPanel.js.map +1 -1
- package/dist/assets/AthenaAssetEmbed.cjs +1 -0
- package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
- package/dist/assets/AthenaAssetEmbed.js +1 -0
- package/dist/assets/AthenaAssetEmbed.js.map +1 -1
- package/dist/assets/use-asset-embed.cjs +22 -23
- package/dist/assets/use-asset-embed.cjs.map +1 -1
- package/dist/assets/use-asset-embed.d.ts +9 -0
- package/dist/assets/use-asset-embed.js +22 -23
- package/dist/assets/use-asset-embed.js.map +1 -1
- package/dist/auth/AthenaAuthProvider.cjs +32 -4
- package/dist/auth/AthenaAuthProvider.cjs.map +1 -1
- package/dist/auth/AthenaAuthProvider.d.ts +21 -0
- package/dist/auth/AthenaAuthProvider.js +33 -5
- package/dist/auth/AthenaAuthProvider.js.map +1 -1
- package/dist/auth/AthenaCredentialSourceContext.cjs +21 -0
- package/dist/auth/AthenaCredentialSourceContext.cjs.map +1 -0
- package/dist/auth/AthenaCredentialSourceContext.d.ts +14 -0
- package/dist/auth/AthenaCredentialSourceContext.js +21 -0
- package/dist/auth/AthenaCredentialSourceContext.js.map +1 -0
- package/dist/auth/AthenaSSOProvider.cjs +71 -47
- package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
- package/dist/auth/AthenaSSOProvider.js +71 -47
- package/dist/auth/AthenaSSOProvider.js.map +1 -1
- package/dist/auth/credential-source.cjs +120 -0
- package/dist/auth/credential-source.cjs.map +1 -0
- package/dist/auth/credential-source.d.ts +68 -0
- package/dist/auth/credential-source.js +120 -0
- package/dist/auth/credential-source.js.map +1 -0
- package/dist/auth/types.d.ts +9 -0
- package/dist/chat/AthenaChat.cjs +8 -0
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +8 -0
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/StatewireApprovalCard.cjs +31 -6
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.d.ts +34 -4
- package/dist/chat/StatewireApprovalCard.js +33 -8
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat-ui/index.d.ts +1 -1
- package/dist/chat-ui/mentions/suggestions/data/api-client.cjs +20 -11
- package/dist/chat-ui/mentions/suggestions/data/api-client.cjs.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/api-client.d.ts +11 -2
- package/dist/chat-ui/mentions/suggestions/data/api-client.js +20 -11
- package/dist/chat-ui/mentions/suggestions/data/api-client.js.map +1 -1
- package/dist/chat-ui/statewire.d.ts +1 -1
- package/dist/collab/mint.cjs +6 -15
- package/dist/collab/mint.cjs.map +1 -1
- package/dist/collab/mint.d.ts +8 -0
- package/dist/collab/mint.js +6 -15
- package/dist/collab/mint.js.map +1 -1
- package/dist/collab/presence-mint.cjs +20 -16
- package/dist/collab/presence-mint.cjs.map +1 -1
- package/dist/collab/presence-mint.js +21 -17
- package/dist/collab/presence-mint.js.map +1 -1
- package/dist/collab/presence-react.cjs +12 -5
- package/dist/collab/presence-react.cjs.map +1 -1
- package/dist/collab/presence-react.d.ts +3 -1
- package/dist/collab/presence-react.js +12 -5
- package/dist/collab/presence-react.js.map +1 -1
- package/dist/collab/react.cjs +5 -2
- package/dist/collab/react.cjs.map +1 -1
- package/dist/collab/react.js +5 -2
- package/dist/collab/react.js.map +1 -1
- package/dist/dictation/use-athena-dictation-engine.cjs +12 -11
- package/dist/dictation/use-athena-dictation-engine.cjs.map +1 -1
- package/dist/dictation/use-athena-dictation-engine.d.ts +4 -3
- package/dist/dictation/use-athena-dictation-engine.js +12 -11
- package/dist/dictation/use-athena-dictation-engine.js.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/packages/agent-runtime/src/conversation-list.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/conversation-list.js.map +1 -1
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs +1 -0
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.js +1 -0
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.cjs +22 -2
- package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.js +22 -2
- package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/packages/agent-runtime/src/messages.cjs +2 -1
- package/dist/packages/agent-runtime/src/messages.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/messages.js +2 -1
- package/dist/packages/agent-runtime/src/messages.js.map +1 -1
- package/dist/packages/agent-runtime/src/voice-presentation.cjs +6 -0
- package/dist/packages/agent-runtime/src/voice-presentation.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/voice-presentation.js +6 -0
- package/dist/packages/agent-runtime/src/voice-presentation.js.map +1 -1
- package/dist/packages/agent-runtime/src/voice-turn-grouping.cjs +68 -0
- package/dist/packages/agent-runtime/src/voice-turn-grouping.cjs.map +1 -0
- package/dist/packages/agent-runtime/src/voice-turn-grouping.js +68 -0
- package/dist/packages/agent-runtime/src/voice-turn-grouping.js.map +1 -0
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/dist/provider/AthenaContext.cjs.map +1 -1
- package/dist/provider/AthenaContext.d.ts +7 -1
- package/dist/provider/AthenaContext.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +24 -2
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.js +24 -2
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/provider/theme-style-context.cjs +12 -0
- package/dist/provider/theme-style-context.cjs.map +1 -0
- package/dist/provider/theme-style-context.d.ts +8 -0
- package/dist/provider/theme-style-context.js +12 -0
- package/dist/provider/theme-style-context.js.map +1 -0
- package/dist/runtime/auth-refusal-restart.cjs +66 -0
- package/dist/runtime/auth-refusal-restart.cjs.map +1 -0
- package/dist/runtime/auth-refusal-restart.d.ts +35 -0
- package/dist/runtime/auth-refusal-restart.js +66 -0
- package/dist/runtime/auth-refusal-restart.js.map +1 -0
- package/dist/runtime/auth.cjs +58 -21
- package/dist/runtime/auth.cjs.map +1 -1
- package/dist/runtime/auth.d.ts +11 -1
- package/dist/runtime/auth.js +59 -22
- package/dist/runtime/auth.js.map +1 -1
- package/dist/runtime/credential-renewal.cjs +29 -0
- package/dist/runtime/credential-renewal.cjs.map +1 -0
- package/dist/runtime/credential-renewal.d.ts +35 -0
- package/dist/runtime/credential-renewal.js +29 -0
- package/dist/runtime/credential-renewal.js.map +1 -0
- package/dist/search/useAthenaRepoSearch.cjs +3 -5
- package/dist/search/useAthenaRepoSearch.cjs.map +1 -1
- package/dist/search/useAthenaRepoSearch.d.ts +7 -7
- package/dist/search/useAthenaRepoSearch.js +3 -5
- package/dist/search/useAthenaRepoSearch.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/threads/SessionList.cjs +87 -16
- package/dist/threads/SessionList.cjs.map +1 -1
- package/dist/threads/SessionList.js +88 -17
- package/dist/threads/SessionList.js.map +1 -1
- package/dist/threads/api.cjs +5 -1
- package/dist/threads/api.cjs.map +1 -1
- package/dist/threads/api.js +5 -1
- package/dist/threads/api.js.map +1 -1
- package/dist/threads/conversation-list-fetcher.cjs +5 -5
- package/dist/threads/conversation-list-fetcher.cjs.map +1 -1
- package/dist/threads/conversation-list-fetcher.d.ts +6 -5
- package/dist/threads/conversation-list-fetcher.js +5 -5
- package/dist/threads/conversation-list-fetcher.js.map +1 -1
- package/dist/threads/session-list-model.cjs +13 -5
- package/dist/threads/session-list-model.cjs.map +1 -1
- package/dist/threads/session-list-model.d.ts +10 -3
- package/dist/threads/session-list-model.js +13 -5
- package/dist/threads/session-list-model.js.map +1 -1
- package/dist/threads/statewire-thread-list.cjs +13 -14
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.js +13 -14
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/dist/threads/use-rest-conversation-history.cjs +6 -2
- package/dist/threads/use-rest-conversation-history.cjs.map +1 -1
- package/dist/threads/use-rest-conversation-history.d.ts +3 -1
- package/dist/threads/use-rest-conversation-history.js +7 -3
- package/dist/threads/use-rest-conversation-history.js.map +1 -1
- package/dist/threads/use-statewire-conversation-history.cjs +9 -2
- package/dist/threads/use-statewire-conversation-history.cjs.map +1 -1
- package/dist/threads/use-statewire-conversation-history.d.ts +3 -1
- package/dist/threads/use-statewire-conversation-history.js +10 -3
- package/dist/threads/use-statewire-conversation-history.js.map +1 -1
- package/dist/threads/use-thread-archive.cjs +2 -1
- package/dist/threads/use-thread-archive.cjs.map +1 -1
- package/dist/threads/use-thread-archive.d.ts +3 -1
- package/dist/threads/use-thread-archive.js +2 -1
- package/dist/threads/use-thread-archive.js.map +1 -1
- package/dist/threads/use-thread-favorite.cjs.map +1 -1
- package/dist/threads/use-thread-favorite.d.ts +3 -1
- package/dist/threads/use-thread-favorite.js.map +1 -1
- package/dist/threads/use-thread-field-override.cjs +2 -1
- package/dist/threads/use-thread-field-override.cjs.map +1 -1
- package/dist/threads/use-thread-field-override.d.ts +3 -1
- package/dist/threads/use-thread-field-override.js +2 -1
- package/dist/threads/use-thread-field-override.js.map +1 -1
- package/dist/threads/use-thread-rename.cjs.map +1 -1
- package/dist/threads/use-thread-rename.d.ts +3 -1
- package/dist/threads/use-thread-rename.js.map +1 -1
- package/dist/tools/tool-uis.cjs +11 -15
- package/dist/tools/tool-uis.cjs.map +1 -1
- package/dist/tools/tool-uis.js +11 -15
- package/dist/tools/tool-uis.js.map +1 -1
- package/dist/ui/popover.cjs +46 -0
- package/dist/ui/popover.cjs.map +1 -0
- package/dist/ui/popover.d.ts +6 -0
- package/dist/ui/popover.js +46 -0
- package/dist/ui/popover.js.map +1 -0
- package/dist/voice/use-athena-voice-session.cjs +17 -5
- package/dist/voice/use-athena-voice-session.cjs.map +1 -1
- package/dist/voice/use-athena-voice-session.d.ts +2 -2
- package/dist/voice/use-athena-voice-session.js +17 -5
- package/dist/voice/use-athena-voice-session.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presence-react.js","sources":["../../src/collab/presence-react.ts"],"sourcesContent":["/**\n * React bindings for the workspace presence feed.\n *\n * `useWorkspacePresence(workspaceId)` opens (and owns) a\n * `connectWorkspacePresence` handle, resolving auth from the surrounding\n * `AthenaProvider` (per-viewer bearer when present, API key otherwise), and\n * re-renders on status/roster changes via `useSyncExternalStore`. Each person\n * carries a `state` on the intent ladder, recomputed against a clock that\n * ticks every `tickMs` so `active` decays to `idle` and `away` between\n * awareness events.\n *\n * `useWorkspacePresenceResolver(workspaceId)` returns the batch id→title\n * resolver bound to the same auth. It is deliberately thin: callers own\n * caching (TanStack Query is the expected home for it).\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type WorkspacePresenceHandle,\n type WorkspacePresenceStatus,\n connectWorkspacePresence,\n} from './presence-client';\nimport {\n type WorkspacePresenceResolveRequest,\n type WorkspacePresenceResolveResponse,\n resolveWorkspacePresence,\n} from './presence-mint';\nimport {\n type PresenceAgent,\n type PresenceHuman,\n type PresenceState,\n derivePresenceState,\n} from './presence-slots';\n\n/** Default cadence for re-deriving each person's intent state. */\nexport const PRESENCE_DEFAULT_TICK_MS = 10_000;\n\nexport interface PresenceHumanWithState extends PresenceHuman {\n /** Where the person sits on the intent ladder as of the last tick. */\n state: PresenceState;\n}\n\nexport interface UseWorkspacePresenceOptions {\n /** `false` closes the feed without unmounting the caller. Default `true`. */\n enabled?: boolean;\n /** How often `state` is re-derived from `lastIntentAt`. Default 10 s. */\n tickMs?: number;\n}\n\nexport interface UseWorkspacePresenceResult {\n /** `'closed'` while disabled or without a workspace; `'connecting'` until the first mint resolves. */\n status: WorkspacePresenceStatus;\n error: Error | null;\n accessType: 'r' | 'rw' | null;\n humans: PresenceHumanWithState[];\n agents: PresenceAgent[];\n /** Force an early token refresh; resolves immediately when no feed is open. */\n refreshNow: () => Promise<void>;\n}\n\nconst EMPTY_SNAPSHOT = 'init||0';\nconst NO_HUMANS: PresenceHuman[] = [];\nconst NO_AGENTS: PresenceAgent[] = [];\n\nexport function useWorkspacePresence(\n workspaceId: string | null,\n options?: UseWorkspacePresenceOptions\n): UseWorkspacePresenceResult {\n const config = useAthenaConfig();\n const enabled = options?.enabled !== false && workspaceId !== null;\n const tickMs = options?.tickMs ?? PRESENCE_DEFAULT_TICK_MS;\n const [entry, setEntry] = useState<{\n workspaceId: string;\n handle: WorkspacePresenceHandle;\n } | null>(null);\n const handleRef = useRef<WorkspacePresenceHandle | null>(null);\n\n useEffect(() => {\n if (!enabled || workspaceId === null) {\n setEntry(null);\n return;\n }\n const next = connectWorkspacePresence({\n workspaceId,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n },\n });\n handleRef.current = next;\n setEntry({ workspaceId, handle: next });\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or
|
|
1
|
+
{"version":3,"file":"presence-react.js","sources":["../../src/collab/presence-react.ts"],"sourcesContent":["/**\n * React bindings for the workspace presence feed.\n *\n * `useWorkspacePresence(workspaceId)` opens (and owns) a\n * `connectWorkspacePresence` handle, resolving auth from the surrounding\n * `AthenaProvider` (per-viewer bearer when present, API key otherwise), and\n * re-renders on status/roster changes via `useSyncExternalStore`. Each person\n * carries a `state` on the intent ladder, recomputed against a clock that\n * ticks every `tickMs` so `active` decays to `idle` and `away` between\n * awareness events.\n *\n * `useWorkspacePresenceResolver(workspaceId)` returns the batch id→title\n * resolver bound to the same auth. It is deliberately thin: callers own\n * caching (TanStack Query is the expected home for it).\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaCredentialIdentityEpoch } from '../auth/AthenaCredentialSourceContext';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type WorkspacePresenceHandle,\n type WorkspacePresenceStatus,\n connectWorkspacePresence,\n} from './presence-client';\nimport {\n type WorkspacePresenceResolveRequest,\n type WorkspacePresenceResolveResponse,\n resolveWorkspacePresence,\n} from './presence-mint';\nimport {\n type PresenceAgent,\n type PresenceHuman,\n type PresenceState,\n derivePresenceState,\n} from './presence-slots';\n\n/** Default cadence for re-deriving each person's intent state. */\nexport const PRESENCE_DEFAULT_TICK_MS = 10_000;\n\nexport interface PresenceHumanWithState extends PresenceHuman {\n /** Where the person sits on the intent ladder as of the last tick. */\n state: PresenceState;\n}\n\nexport interface UseWorkspacePresenceOptions {\n /** `false` closes the feed without unmounting the caller. Default `true`. */\n enabled?: boolean;\n /** How often `state` is re-derived from `lastIntentAt`. Default 10 s. */\n tickMs?: number;\n}\n\nexport interface UseWorkspacePresenceResult {\n /** `'closed'` while disabled or without a workspace; `'connecting'` until the first mint resolves. */\n status: WorkspacePresenceStatus;\n error: Error | null;\n accessType: 'r' | 'rw' | null;\n humans: PresenceHumanWithState[];\n agents: PresenceAgent[];\n /** Force an early token refresh; resolves immediately when no feed is open. */\n refreshNow: () => Promise<void>;\n}\n\nconst EMPTY_SNAPSHOT = 'init||0';\nconst NO_HUMANS: PresenceHuman[] = [];\nconst NO_AGENTS: PresenceAgent[] = [];\n\nexport function useWorkspacePresence(\n workspaceId: string | null,\n options?: UseWorkspacePresenceOptions\n): UseWorkspacePresenceResult {\n const config = useAthenaConfig();\n const identityEpoch = useAthenaCredentialIdentityEpoch();\n const enabled = options?.enabled !== false && workspaceId !== null;\n const tickMs = options?.tickMs ?? PRESENCE_DEFAULT_TICK_MS;\n const [entry, setEntry] = useState<{\n workspaceId: string;\n handle: WorkspacePresenceHandle;\n } | null>(null);\n const handleRef = useRef<WorkspacePresenceHandle | null>(null);\n\n useEffect(() => {\n if (!enabled || workspaceId === null) {\n setEntry(null);\n return;\n }\n const next = connectWorkspacePresence({\n workspaceId,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n getToken: config.getToken,\n },\n });\n handleRef.current = next;\n setEntry({ workspaceId, handle: next });\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or the signed-in identity changes (the\n // credential source's identity epoch). A renewed token for the same\n // identity reaches the next re-mint through `getToken`, so a renewal\n // does not tear the live connection down.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, workspaceId, config.backendUrl, config.apiKey, config.getToken, identityEpoch]);\n\n // React renders with the new arguments before the effect above runs, so a\n // handle opened for a previous workspace, or before the feed was disabled,\n // still sits in state for that one render. Expose it only while it is the\n // handle this render asked for; otherwise the closed / connecting empty\n // result wins, and a disabled feed never leaks a roster.\n const handle =\n enabled && entry !== null && entry.workspaceId === workspaceId ? entry.handle : null;\n\n const subscribe = useMemo(() => {\n return (onStoreChange: () => void) => {\n if (!handle) return () => {};\n return handle.onChange(onStoreChange);\n };\n }, [handle]);\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => {\n if (!handle) return EMPTY_SNAPSHOT;\n return `${handle.status}|${handle.accessType ?? ''}|${handle.version}`;\n },\n () => EMPTY_SNAPSHOT\n );\n void snapshot;\n\n // The ladder decays with wall-clock time, not with awareness events; a\n // person who stops typing must drift to idle even when nothing arrives.\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (!enabled) return;\n setNow(Date.now());\n const timer = setInterval(() => {\n setNow(Date.now());\n }, tickMs);\n return () => {\n clearInterval(timer);\n };\n }, [enabled, tickMs]);\n\n const rosterHumans = handle?.humans ?? NO_HUMANS;\n const humans = useMemo(\n () =>\n rosterHumans.map((human) => ({\n ...human,\n state: derivePresenceState({\n lastIntentAt: human.lastIntentAt,\n hidden: human.hidden,\n manualAway: human.manualAway,\n now,\n }),\n })),\n [rosterHumans, now]\n );\n\n const refreshNow = useCallback(() => handleRef.current?.refreshNow() ?? Promise.resolve(), []);\n\n return {\n status: handle ? handle.status : enabled ? 'connecting' : 'closed',\n error: handle?.error ?? null,\n accessType: handle?.accessType ?? null,\n humans,\n agents: handle?.agents ?? NO_AGENTS,\n refreshNow,\n };\n}\n\nexport type WorkspacePresenceResolver = (\n request: WorkspacePresenceResolveRequest,\n signal?: AbortSignal\n) => Promise<WorkspacePresenceResolveResponse>;\n\n/**\n * A stable resolver bound to the provider's auth and the given workspace.\n * Credentials are read on every call (the provider serves the current token\n * through getters), and `getToken` renews a refused one once. Rejects when\n * called without a workspace; no caching — callers own that.\n */\nexport function useWorkspacePresenceResolver(\n workspaceId: string | null\n): WorkspacePresenceResolver {\n const config = useAthenaConfig();\n return useCallback<WorkspacePresenceResolver>(\n (request, signal) => {\n if (workspaceId === null) {\n return Promise.reject(\n new Error('[AthenaSDK] useWorkspacePresenceResolver: no workspace selected')\n );\n }\n return resolveWorkspacePresence({\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n getToken: config.getToken,\n },\n workspaceId,\n request,\n signal,\n });\n },\n [workspaceId, config]\n );\n}\n"],"names":[],"mappings":"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"}
|
package/dist/collab/react.cjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const react = require("react");
|
|
4
|
+
const AthenaCredentialSourceContext = require("../auth/AthenaCredentialSourceContext.cjs");
|
|
4
5
|
const AthenaContext = require("../provider/AthenaContext.cjs");
|
|
5
6
|
const client = require("./client.cjs");
|
|
6
7
|
function useGenericDoc(assetId, options) {
|
|
7
8
|
const config = AthenaContext.useAthenaConfig();
|
|
9
|
+
const identityEpoch = AthenaCredentialSourceContext.useAthenaCredentialIdentityEpoch();
|
|
8
10
|
const access = (options == null ? void 0 : options.access) ?? "view";
|
|
9
11
|
const [handle, setHandle] = react.useState(null);
|
|
10
12
|
const handleRef = react.useRef(null);
|
|
@@ -15,7 +17,8 @@ function useGenericDoc(assetId, options) {
|
|
|
15
17
|
auth: {
|
|
16
18
|
backendUrl: config.backendUrl,
|
|
17
19
|
apiKey: config.apiKey,
|
|
18
|
-
token: config.token
|
|
20
|
+
token: config.token,
|
|
21
|
+
getToken: config.getToken
|
|
19
22
|
}
|
|
20
23
|
});
|
|
21
24
|
handleRef.current = next;
|
|
@@ -24,7 +27,7 @@ function useGenericDoc(assetId, options) {
|
|
|
24
27
|
handleRef.current = null;
|
|
25
28
|
next.destroy();
|
|
26
29
|
};
|
|
27
|
-
}, [assetId, access, config.backendUrl, config.apiKey, config.
|
|
30
|
+
}, [assetId, access, config.backendUrl, config.apiKey, config.getToken, identityEpoch]);
|
|
28
31
|
const subscribe = react.useMemo(() => {
|
|
29
32
|
return (onStoreChange) => {
|
|
30
33
|
if (!handle) return () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.cjs","sources":["../../src/collab/react.ts"],"sourcesContent":["/**\n * React bindings for Generic Doc live collaboration.\n *\n * `useGenericDoc(assetId)` opens (and owns) a `connectGenericDoc` handle,\n * resolving auth from the surrounding `AthenaProvider` (per-viewer bearer when\n * present, API key otherwise) and re-rendering on status/presence changes via\n * `useSyncExternalStore`.\n */\n\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type GenericDocHandle,\n type GenericDocStatus,\n type PresenceEntry,\n connectGenericDoc,\n} from './client';\nimport type * as Y from '@y/y';\n\nexport interface UseGenericDocResult {\n /** Null until the handle is created (first render effect). */\n doc: Y.Doc | null;\n status: GenericDocStatus;\n /**\n * Whether the initial server sync completed for the current connection —\n * `status === 'connected'` fires before the server state arrives, so gate\n * \"is this doc really empty?\" UI on this.\n */\n synced: boolean;\n accessType: 'r' | 'rw' | null;\n error: Error | null;\n presence: PresenceEntry[];\n setPresence: (state: Record<string, unknown> | null) => void;\n}\n\nexport function useGenericDoc(\n assetId: string,\n options?: { access?: 'view' | 'edit' }\n): UseGenericDocResult {\n const config = useAthenaConfig();\n const access = options?.access ?? 'view';\n const [handle, setHandle] = useState<GenericDocHandle | null>(null);\n const handleRef = useRef<GenericDocHandle | null>(null);\n\n useEffect(() => {\n const next = connectGenericDoc({\n assetId,\n access,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n },\n });\n handleRef.current = next;\n setHandle(next);\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or
|
|
1
|
+
{"version":3,"file":"react.cjs","sources":["../../src/collab/react.ts"],"sourcesContent":["/**\n * React bindings for Generic Doc live collaboration.\n *\n * `useGenericDoc(assetId)` opens (and owns) a `connectGenericDoc` handle,\n * resolving auth from the surrounding `AthenaProvider` (per-viewer bearer when\n * present, API key otherwise) and re-rendering on status/presence changes via\n * `useSyncExternalStore`.\n */\n\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaCredentialIdentityEpoch } from '../auth/AthenaCredentialSourceContext';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type GenericDocHandle,\n type GenericDocStatus,\n type PresenceEntry,\n connectGenericDoc,\n} from './client';\nimport type * as Y from '@y/y';\n\nexport interface UseGenericDocResult {\n /** Null until the handle is created (first render effect). */\n doc: Y.Doc | null;\n status: GenericDocStatus;\n /**\n * Whether the initial server sync completed for the current connection —\n * `status === 'connected'` fires before the server state arrives, so gate\n * \"is this doc really empty?\" UI on this.\n */\n synced: boolean;\n accessType: 'r' | 'rw' | null;\n error: Error | null;\n presence: PresenceEntry[];\n setPresence: (state: Record<string, unknown> | null) => void;\n}\n\nexport function useGenericDoc(\n assetId: string,\n options?: { access?: 'view' | 'edit' }\n): UseGenericDocResult {\n const config = useAthenaConfig();\n const identityEpoch = useAthenaCredentialIdentityEpoch();\n const access = options?.access ?? 'view';\n const [handle, setHandle] = useState<GenericDocHandle | null>(null);\n const handleRef = useRef<GenericDocHandle | null>(null);\n\n useEffect(() => {\n const next = connectGenericDoc({\n assetId,\n access,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n getToken: config.getToken,\n },\n });\n handleRef.current = next;\n setHandle(next);\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or the signed-in identity changes (the\n // credential source's identity epoch). A renewed token for the same\n // identity reaches the next re-mint through `getToken`, so a renewal\n // does not tear the live connection down.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [assetId, access, config.backendUrl, config.apiKey, config.getToken, identityEpoch]);\n\n const subscribe = useMemo(() => {\n return (onStoreChange: () => void) => {\n if (!handle) return () => {};\n return handle.onChange(onStoreChange);\n };\n }, [handle]);\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => {\n if (!handle) return EMPTY_SNAPSHOT;\n return `${handle.status}|${handle.synced}|${handle.accessType ?? ''}|${handle.version}`;\n },\n () => EMPTY_SNAPSHOT\n );\n void snapshot;\n\n return {\n doc: handle?.doc ?? null,\n status: handle?.status ?? 'connecting',\n synced: handle?.synced ?? false,\n accessType: handle?.accessType ?? null,\n error: handle?.error ?? null,\n presence: handle?.getPresence() ?? [],\n setPresence: (state) => handleRef.current?.setPresence(state),\n };\n}\n\nconst EMPTY_SNAPSHOT = 'init|false||0';\n"],"names":["useAthenaConfig","useAthenaCredentialIdentityEpoch","useState","useRef","useEffect","connectGenericDoc","useMemo","useSyncExternalStore"],"mappings":";;;;;;AAoCO,SAAS,cACd,SACA,SACqB;AACrB,QAAM,SAASA,cAAAA,gBAAA;AACf,QAAM,gBAAgBC,8BAAAA,iCAAA;AACtB,QAAM,UAAS,mCAAS,WAAU;AAClC,QAAM,CAAC,QAAQ,SAAS,IAAIC,MAAAA,SAAkC,IAAI;AAClE,QAAM,YAAYC,MAAAA,OAAgC,IAAI;AAEtDC,QAAAA,UAAU,MAAM;AACd,UAAM,OAAOC,OAAAA,kBAAkB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,MAAM;AAAA,QACJ,YAAY,OAAO;AAAA,QACnB,QAAQ,OAAO;AAAA,QACf,OAAO,OAAO;AAAA,QACd,UAAU,OAAO;AAAA,MAAA;AAAA,IACnB,CACD;AACD,cAAU,UAAU;AACpB,cAAU,IAAI;AACd,WAAO,MAAM;AACX,gBAAU,UAAU;AACpB,WAAK,QAAA;AAAA,IACP;AAAA,EAMF,GAAG,CAAC,SAAS,QAAQ,OAAO,YAAY,OAAO,QAAQ,OAAO,UAAU,aAAa,CAAC;AAEtF,QAAM,YAAYC,MAAAA,QAAQ,MAAM;AAC9B,WAAO,CAAC,kBAA8B;AACpC,UAAI,CAAC,OAAQ,QAAO,MAAM;AAAA,MAAC;AAC3B,aAAO,OAAO,SAAS,aAAa;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEMC,QAAAA;AAAAA,IACf;AAAA,IACA,MAAM;AACJ,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,GAAG,OAAO,MAAM,IAAI,OAAO,MAAM,IAAI,OAAO,cAAc,EAAE,IAAI,OAAO,OAAO;AAAA,IACvF;AAAA,IACA,MAAM;AAAA,EAAA;AAIR,SAAO;AAAA,IACL,MAAK,iCAAQ,QAAO;AAAA,IACpB,SAAQ,iCAAQ,WAAU;AAAA,IAC1B,SAAQ,iCAAQ,WAAU;AAAA,IAC1B,aAAY,iCAAQ,eAAc;AAAA,IAClC,QAAO,iCAAQ,UAAS;AAAA,IACxB,WAAU,iCAAQ,kBAAiB,CAAA;AAAA,IACnC,aAAa,CAAC,UAAA;;AAAU,6BAAU,YAAV,mBAAmB,YAAY;AAAA;AAAA,EAAK;AAEhE;AAEA,MAAM,iBAAiB;;"}
|
package/dist/collab/react.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
import { useAthenaCredentialIdentityEpoch } from "../auth/AthenaCredentialSourceContext.js";
|
|
2
3
|
import { useAthenaConfig } from "../provider/AthenaContext.js";
|
|
3
4
|
import { connectGenericDoc } from "./client.js";
|
|
4
5
|
function useGenericDoc(assetId, options) {
|
|
5
6
|
const config = useAthenaConfig();
|
|
7
|
+
const identityEpoch = useAthenaCredentialIdentityEpoch();
|
|
6
8
|
const access = (options == null ? void 0 : options.access) ?? "view";
|
|
7
9
|
const [handle, setHandle] = useState(null);
|
|
8
10
|
const handleRef = useRef(null);
|
|
@@ -13,7 +15,8 @@ function useGenericDoc(assetId, options) {
|
|
|
13
15
|
auth: {
|
|
14
16
|
backendUrl: config.backendUrl,
|
|
15
17
|
apiKey: config.apiKey,
|
|
16
|
-
token: config.token
|
|
18
|
+
token: config.token,
|
|
19
|
+
getToken: config.getToken
|
|
17
20
|
}
|
|
18
21
|
});
|
|
19
22
|
handleRef.current = next;
|
|
@@ -22,7 +25,7 @@ function useGenericDoc(assetId, options) {
|
|
|
22
25
|
handleRef.current = null;
|
|
23
26
|
next.destroy();
|
|
24
27
|
};
|
|
25
|
-
}, [assetId, access, config.backendUrl, config.apiKey, config.
|
|
28
|
+
}, [assetId, access, config.backendUrl, config.apiKey, config.getToken, identityEpoch]);
|
|
26
29
|
const subscribe = useMemo(() => {
|
|
27
30
|
return (onStoreChange) => {
|
|
28
31
|
if (!handle) return () => {
|
package/dist/collab/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sources":["../../src/collab/react.ts"],"sourcesContent":["/**\n * React bindings for Generic Doc live collaboration.\n *\n * `useGenericDoc(assetId)` opens (and owns) a `connectGenericDoc` handle,\n * resolving auth from the surrounding `AthenaProvider` (per-viewer bearer when\n * present, API key otherwise) and re-rendering on status/presence changes via\n * `useSyncExternalStore`.\n */\n\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type GenericDocHandle,\n type GenericDocStatus,\n type PresenceEntry,\n connectGenericDoc,\n} from './client';\nimport type * as Y from '@y/y';\n\nexport interface UseGenericDocResult {\n /** Null until the handle is created (first render effect). */\n doc: Y.Doc | null;\n status: GenericDocStatus;\n /**\n * Whether the initial server sync completed for the current connection —\n * `status === 'connected'` fires before the server state arrives, so gate\n * \"is this doc really empty?\" UI on this.\n */\n synced: boolean;\n accessType: 'r' | 'rw' | null;\n error: Error | null;\n presence: PresenceEntry[];\n setPresence: (state: Record<string, unknown> | null) => void;\n}\n\nexport function useGenericDoc(\n assetId: string,\n options?: { access?: 'view' | 'edit' }\n): UseGenericDocResult {\n const config = useAthenaConfig();\n const access = options?.access ?? 'view';\n const [handle, setHandle] = useState<GenericDocHandle | null>(null);\n const handleRef = useRef<GenericDocHandle | null>(null);\n\n useEffect(() => {\n const next = connectGenericDoc({\n assetId,\n access,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n },\n });\n handleRef.current = next;\n setHandle(next);\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or
|
|
1
|
+
{"version":3,"file":"react.js","sources":["../../src/collab/react.ts"],"sourcesContent":["/**\n * React bindings for Generic Doc live collaboration.\n *\n * `useGenericDoc(assetId)` opens (and owns) a `connectGenericDoc` handle,\n * resolving auth from the surrounding `AthenaProvider` (per-viewer bearer when\n * present, API key otherwise) and re-rendering on status/presence changes via\n * `useSyncExternalStore`.\n */\n\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaCredentialIdentityEpoch } from '../auth/AthenaCredentialSourceContext';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type GenericDocHandle,\n type GenericDocStatus,\n type PresenceEntry,\n connectGenericDoc,\n} from './client';\nimport type * as Y from '@y/y';\n\nexport interface UseGenericDocResult {\n /** Null until the handle is created (first render effect). */\n doc: Y.Doc | null;\n status: GenericDocStatus;\n /**\n * Whether the initial server sync completed for the current connection —\n * `status === 'connected'` fires before the server state arrives, so gate\n * \"is this doc really empty?\" UI on this.\n */\n synced: boolean;\n accessType: 'r' | 'rw' | null;\n error: Error | null;\n presence: PresenceEntry[];\n setPresence: (state: Record<string, unknown> | null) => void;\n}\n\nexport function useGenericDoc(\n assetId: string,\n options?: { access?: 'view' | 'edit' }\n): UseGenericDocResult {\n const config = useAthenaConfig();\n const identityEpoch = useAthenaCredentialIdentityEpoch();\n const access = options?.access ?? 'view';\n const [handle, setHandle] = useState<GenericDocHandle | null>(null);\n const handleRef = useRef<GenericDocHandle | null>(null);\n\n useEffect(() => {\n const next = connectGenericDoc({\n assetId,\n access,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n getToken: config.getToken,\n },\n });\n handleRef.current = next;\n setHandle(next);\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or the signed-in identity changes (the\n // credential source's identity epoch). A renewed token for the same\n // identity reaches the next re-mint through `getToken`, so a renewal\n // does not tear the live connection down.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [assetId, access, config.backendUrl, config.apiKey, config.getToken, identityEpoch]);\n\n const subscribe = useMemo(() => {\n return (onStoreChange: () => void) => {\n if (!handle) return () => {};\n return handle.onChange(onStoreChange);\n };\n }, [handle]);\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => {\n if (!handle) return EMPTY_SNAPSHOT;\n return `${handle.status}|${handle.synced}|${handle.accessType ?? ''}|${handle.version}`;\n },\n () => EMPTY_SNAPSHOT\n );\n void snapshot;\n\n return {\n doc: handle?.doc ?? null,\n status: handle?.status ?? 'connecting',\n synced: handle?.synced ?? false,\n accessType: handle?.accessType ?? null,\n error: handle?.error ?? null,\n presence: handle?.getPresence() ?? [],\n setPresence: (state) => handleRef.current?.setPresence(state),\n };\n}\n\nconst EMPTY_SNAPSHOT = 'init|false||0';\n"],"names":[],"mappings":";;;;AAoCO,SAAS,cACd,SACA,SACqB;AACrB,QAAM,SAAS,gBAAA;AACf,QAAM,gBAAgB,iCAAA;AACtB,QAAM,UAAS,mCAAS,WAAU;AAClC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAkC,IAAI;AAClE,QAAM,YAAY,OAAgC,IAAI;AAEtD,YAAU,MAAM;AACd,UAAM,OAAO,kBAAkB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,MAAM;AAAA,QACJ,YAAY,OAAO;AAAA,QACnB,QAAQ,OAAO;AAAA,QACf,OAAO,OAAO;AAAA,QACd,UAAU,OAAO;AAAA,MAAA;AAAA,IACnB,CACD;AACD,cAAU,UAAU;AACpB,cAAU,IAAI;AACd,WAAO,MAAM;AACX,gBAAU,UAAU;AACpB,WAAK,QAAA;AAAA,IACP;AAAA,EAMF,GAAG,CAAC,SAAS,QAAQ,OAAO,YAAY,OAAO,QAAQ,OAAO,UAAU,aAAa,CAAC;AAEtF,QAAM,YAAY,QAAQ,MAAM;AAC9B,WAAO,CAAC,kBAA8B;AACpC,UAAI,CAAC,OAAQ,QAAO,MAAM;AAAA,MAAC;AAC3B,aAAO,OAAO,SAAS,aAAa;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEM;AAAA,IACf;AAAA,IACA,MAAM;AACJ,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,GAAG,OAAO,MAAM,IAAI,OAAO,MAAM,IAAI,OAAO,cAAc,EAAE,IAAI,OAAO,OAAO;AAAA,IACvF;AAAA,IACA,MAAM;AAAA,EAAA;AAIR,SAAO;AAAA,IACL,MAAK,iCAAQ,QAAO;AAAA,IACpB,SAAQ,iCAAQ,WAAU;AAAA,IAC1B,SAAQ,iCAAQ,WAAU;AAAA,IAC1B,aAAY,iCAAQ,eAAc;AAAA,IAClC,QAAO,iCAAQ,UAAS;AAAA,IACxB,WAAU,iCAAQ,kBAAiB,CAAA;AAAA,IACnC,aAAa,CAAC,UAAA;;AAAU,6BAAU,YAAV,mBAAmB,YAAY;AAAA;AAAA,EAAK;AAEhE;AAEA,MAAM,iBAAiB;"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const react = require("react");
|
|
4
4
|
const AthenaContext = require("../provider/AthenaContext.cjs");
|
|
5
|
-
const
|
|
5
|
+
const credentialRenewal = require("../runtime/credential-renewal.cjs");
|
|
6
6
|
const dictationEngine = require("./dictation-engine.cjs");
|
|
7
7
|
const recordedDictationEngine = require("./recorded-dictation-engine.cjs");
|
|
8
8
|
function transcriptionUploadUrl(backendUrl) {
|
|
@@ -28,17 +28,18 @@ function transcriptionUploadErrorMessage(status) {
|
|
|
28
28
|
function createAthenaUploadTranscriber(getConfig) {
|
|
29
29
|
return async (clip, { lang, signal }) => {
|
|
30
30
|
const config = getConfig();
|
|
31
|
-
const headers = await statewireAuthHeaders.resolveStatewireAuthHeaders(config, { staleAuth: false });
|
|
32
|
-
const form = new FormData();
|
|
33
31
|
const extension = clip.type.includes("mp4") ? "mp4" : clip.type.includes("ogg") ? "ogg" : "webm";
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
const response = await credentialRenewal.sendWithCredentialRenewal(config, (headers) => {
|
|
33
|
+
const form = new FormData();
|
|
34
|
+
form.append("file", clip, `dictation.${extension}`);
|
|
35
|
+
if (lang) form.append("language", lang);
|
|
36
|
+
return fetch(transcriptionUploadUrl(config.backendUrl), {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers,
|
|
39
|
+
body: form,
|
|
40
|
+
credentials: "include",
|
|
41
|
+
signal
|
|
42
|
+
});
|
|
42
43
|
});
|
|
43
44
|
if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));
|
|
44
45
|
const payload = await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-athena-dictation-engine.cjs","sources":["../../src/dictation/use-athena-dictation-engine.ts"],"sourcesContent":["import { useMemo, useRef } from 'react';\nimport { type AthenaConfig, useAthenaConfig } from '../provider/AthenaContext';\nimport {
|
|
1
|
+
{"version":3,"file":"use-athena-dictation-engine.cjs","sources":["../../src/dictation/use-athena-dictation-engine.ts"],"sourcesContent":["import { useMemo, useRef } from 'react';\nimport { type AthenaConfig, useAthenaConfig } from '../provider/AthenaContext';\nimport { sendWithCredentialRenewal } from '../runtime/credential-renewal';\nimport { createWebSpeechDictationEngine, type DictationEngine } from './dictation-engine';\nimport { createRecordedDictationEngine, type DictationTranscriber } from './recorded-dictation-engine';\n\ninterface UploadTranscriptionResponse {\n transcript: string;\n}\n\n/** Agora's REST base from the configured backend URL (which may point at a nested endpoint). */\nexport function transcriptionUploadUrl(backendUrl: string): string {\n return `${backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '').replace(/\\/$/, '')}/api/transcription/upload`;\n}\n\n/**\n * One line for the composer per way `/transcription/upload` refuses a clip.\n * The copy lives here, apart from the fetch, so it can be pinned by a test.\n */\nexport function transcriptionUploadErrorMessage(status: number): string {\n switch (status) {\n case 401:\n case 403:\n return 'Dictation is not available for this sign-in. Reload and try again.';\n case 413:\n return 'That recording is too long to transcribe.';\n case 422:\n return 'The dictation language was rejected. Use a BCP-47 tag such as en-US.';\n case 429:\n return 'Too many dictations in a row. Wait a moment and try again.';\n case 503:\n return 'Dictation is not set up on this server.';\n default:\n return `Transcription failed (${status}).`;\n }\n}\n\n/**\n * Post a finished clip to agora's `/transcription/upload` route, which runs\n * it through Deepgram and returns the text. Credentials are read at call\n * time so a rotated token is picked up, and a `401` gets one forced renewal\n * (`getToken({ force: true })`) and a resend before it fails. The session\n * language (host `lang` or the document's) is forwarded so non-English speech\n * is recognised as such; the request is dropped when the engine aborts.\n */\nexport function createAthenaUploadTranscriber(\n getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>,\n): DictationTranscriber {\n return async (clip, { lang, signal }) => {\n const config = getConfig();\n const extension = clip.type.includes('mp4') ? 'mp4' : clip.type.includes('ogg') ? 'ogg' : 'webm';\n const response = await sendWithCredentialRenewal(config, (headers) => {\n // A request body is single-use: every attempt builds its own form.\n const form = new FormData();\n form.append('file', clip, `dictation.${extension}`);\n if (lang) form.append('language', lang);\n return fetch(transcriptionUploadUrl(config.backendUrl), {\n method: 'POST',\n headers,\n body: form,\n credentials: 'include',\n signal,\n });\n });\n if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));\n const payload = (await response.json()) as UploadTranscriptionResponse;\n return typeof payload.transcript === 'string' ? payload.transcript : '';\n };\n}\n\n/**\n * The composer's default speech engine: record with `MediaRecorder`, then\n * transcribe the clip through agora (Deepgram). Falls back to the browser's\n * Web Speech API only where `MediaRecorder` is missing. A host-provided\n * `engine` always wins.\n */\nexport function useAthenaDictationEngine(override?: DictationEngine): DictationEngine {\n const config = useAthenaConfig();\n const configRef = useRef(config);\n configRef.current = config;\n return useMemo(() => {\n if (override) return override;\n const recorded = createRecordedDictationEngine({\n transcribe: createAthenaUploadTranscriber(() => configRef.current),\n });\n return recorded.isSupported() ? recorded : createWebSpeechDictationEngine();\n }, [override]);\n}\n"],"names":["sendWithCredentialRenewal","useAthenaConfig","useRef","useMemo","createRecordedDictationEngine","createWebSpeechDictationEngine"],"mappings":";;;;;;;AAWO,SAAS,uBAAuB,YAA4B;AACjE,SAAO,GAAG,WAAW,QAAQ,2BAA2B,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC;AAChF;AAMO,SAAS,gCAAgC,QAAwB;AACtE,UAAQ,QAAA;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO,yBAAyB,MAAM;AAAA,EAAA;AAE5C;AAUO,SAAS,8BACd,WACsB;AACtB,SAAO,OAAO,MAAM,EAAE,MAAM,aAAa;AACvC,UAAM,SAAS,UAAA;AACf,UAAM,YAAY,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ;AAC1F,UAAM,WAAW,MAAMA,kBAAAA,0BAA0B,QAAQ,CAAC,YAAY;AAEpE,YAAM,OAAO,IAAI,SAAA;AACjB,WAAK,OAAO,QAAQ,MAAM,aAAa,SAAS,EAAE;AAClD,UAAI,KAAM,MAAK,OAAO,YAAY,IAAI;AACtC,aAAO,MAAM,uBAAuB,OAAO,UAAU,GAAG;AAAA,QACtD,QAAQ;AAAA,QACR;AAAA,QACA,MAAM;AAAA,QACN,aAAa;AAAA,QACb;AAAA,MAAA,CACD;AAAA,IACH,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,gCAAgC,SAAS,MAAM,CAAC;AAClF,UAAM,UAAW,MAAM,SAAS,KAAA;AAChC,WAAO,OAAO,QAAQ,eAAe,WAAW,QAAQ,aAAa;AAAA,EACvE;AACF;AAQO,SAAS,yBAAyB,UAA6C;AACpF,QAAM,SAASC,cAAAA,gBAAA;AACf,QAAM,YAAYC,MAAAA,OAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,SAAOC,MAAAA,QAAQ,MAAM;AACnB,QAAI,SAAU,QAAO;AACrB,UAAM,WAAWC,wBAAAA,8BAA8B;AAAA,MAC7C,YAAY,8BAA8B,MAAM,UAAU,OAAO;AAAA,IAAA,CAClE;AACD,WAAO,SAAS,gBAAgB,WAAWC,gBAAAA,+BAAA;AAAA,EAC7C,GAAG,CAAC,QAAQ,CAAC;AACf;;;;;"}
|
|
@@ -11,9 +11,10 @@ export declare function transcriptionUploadErrorMessage(status: number): string;
|
|
|
11
11
|
/**
|
|
12
12
|
* Post a finished clip to agora's `/transcription/upload` route, which runs
|
|
13
13
|
* it through Deepgram and returns the text. Credentials are read at call
|
|
14
|
-
* time so a rotated token is picked up
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* time so a rotated token is picked up, and a `401` gets one forced renewal
|
|
15
|
+
* (`getToken({ force: true })`) and a resend before it fails. The session
|
|
16
|
+
* language (host `lang` or the document's) is forwarded so non-English speech
|
|
17
|
+
* is recognised as such; the request is dropped when the engine aborts.
|
|
17
18
|
*/
|
|
18
19
|
export declare function createAthenaUploadTranscriber(getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>): DictationTranscriber;
|
|
19
20
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRef, useMemo } from "react";
|
|
2
2
|
import { useAthenaConfig } from "../provider/AthenaContext.js";
|
|
3
|
-
import {
|
|
3
|
+
import { sendWithCredentialRenewal } from "../runtime/credential-renewal.js";
|
|
4
4
|
import { createWebSpeechDictationEngine } from "./dictation-engine.js";
|
|
5
5
|
import { createRecordedDictationEngine } from "./recorded-dictation-engine.js";
|
|
6
6
|
function transcriptionUploadUrl(backendUrl) {
|
|
@@ -26,17 +26,18 @@ function transcriptionUploadErrorMessage(status) {
|
|
|
26
26
|
function createAthenaUploadTranscriber(getConfig) {
|
|
27
27
|
return async (clip, { lang, signal }) => {
|
|
28
28
|
const config = getConfig();
|
|
29
|
-
const headers = await resolveStatewireAuthHeaders(config, { staleAuth: false });
|
|
30
|
-
const form = new FormData();
|
|
31
29
|
const extension = clip.type.includes("mp4") ? "mp4" : clip.type.includes("ogg") ? "ogg" : "webm";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
const response = await sendWithCredentialRenewal(config, (headers) => {
|
|
31
|
+
const form = new FormData();
|
|
32
|
+
form.append("file", clip, `dictation.${extension}`);
|
|
33
|
+
if (lang) form.append("language", lang);
|
|
34
|
+
return fetch(transcriptionUploadUrl(config.backendUrl), {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers,
|
|
37
|
+
body: form,
|
|
38
|
+
credentials: "include",
|
|
39
|
+
signal
|
|
40
|
+
});
|
|
40
41
|
});
|
|
41
42
|
if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));
|
|
42
43
|
const payload = await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-athena-dictation-engine.js","sources":["../../src/dictation/use-athena-dictation-engine.ts"],"sourcesContent":["import { useMemo, useRef } from 'react';\nimport { type AthenaConfig, useAthenaConfig } from '../provider/AthenaContext';\nimport {
|
|
1
|
+
{"version":3,"file":"use-athena-dictation-engine.js","sources":["../../src/dictation/use-athena-dictation-engine.ts"],"sourcesContent":["import { useMemo, useRef } from 'react';\nimport { type AthenaConfig, useAthenaConfig } from '../provider/AthenaContext';\nimport { sendWithCredentialRenewal } from '../runtime/credential-renewal';\nimport { createWebSpeechDictationEngine, type DictationEngine } from './dictation-engine';\nimport { createRecordedDictationEngine, type DictationTranscriber } from './recorded-dictation-engine';\n\ninterface UploadTranscriptionResponse {\n transcript: string;\n}\n\n/** Agora's REST base from the configured backend URL (which may point at a nested endpoint). */\nexport function transcriptionUploadUrl(backendUrl: string): string {\n return `${backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '').replace(/\\/$/, '')}/api/transcription/upload`;\n}\n\n/**\n * One line for the composer per way `/transcription/upload` refuses a clip.\n * The copy lives here, apart from the fetch, so it can be pinned by a test.\n */\nexport function transcriptionUploadErrorMessage(status: number): string {\n switch (status) {\n case 401:\n case 403:\n return 'Dictation is not available for this sign-in. Reload and try again.';\n case 413:\n return 'That recording is too long to transcribe.';\n case 422:\n return 'The dictation language was rejected. Use a BCP-47 tag such as en-US.';\n case 429:\n return 'Too many dictations in a row. Wait a moment and try again.';\n case 503:\n return 'Dictation is not set up on this server.';\n default:\n return `Transcription failed (${status}).`;\n }\n}\n\n/**\n * Post a finished clip to agora's `/transcription/upload` route, which runs\n * it through Deepgram and returns the text. Credentials are read at call\n * time so a rotated token is picked up, and a `401` gets one forced renewal\n * (`getToken({ force: true })`) and a resend before it fails. The session\n * language (host `lang` or the document's) is forwarded so non-English speech\n * is recognised as such; the request is dropped when the engine aborts.\n */\nexport function createAthenaUploadTranscriber(\n getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>,\n): DictationTranscriber {\n return async (clip, { lang, signal }) => {\n const config = getConfig();\n const extension = clip.type.includes('mp4') ? 'mp4' : clip.type.includes('ogg') ? 'ogg' : 'webm';\n const response = await sendWithCredentialRenewal(config, (headers) => {\n // A request body is single-use: every attempt builds its own form.\n const form = new FormData();\n form.append('file', clip, `dictation.${extension}`);\n if (lang) form.append('language', lang);\n return fetch(transcriptionUploadUrl(config.backendUrl), {\n method: 'POST',\n headers,\n body: form,\n credentials: 'include',\n signal,\n });\n });\n if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));\n const payload = (await response.json()) as UploadTranscriptionResponse;\n return typeof payload.transcript === 'string' ? payload.transcript : '';\n };\n}\n\n/**\n * The composer's default speech engine: record with `MediaRecorder`, then\n * transcribe the clip through agora (Deepgram). Falls back to the browser's\n * Web Speech API only where `MediaRecorder` is missing. A host-provided\n * `engine` always wins.\n */\nexport function useAthenaDictationEngine(override?: DictationEngine): DictationEngine {\n const config = useAthenaConfig();\n const configRef = useRef(config);\n configRef.current = config;\n return useMemo(() => {\n if (override) return override;\n const recorded = createRecordedDictationEngine({\n transcribe: createAthenaUploadTranscriber(() => configRef.current),\n });\n return recorded.isSupported() ? recorded : createWebSpeechDictationEngine();\n }, [override]);\n}\n"],"names":[],"mappings":";;;;;AAWO,SAAS,uBAAuB,YAA4B;AACjE,SAAO,GAAG,WAAW,QAAQ,2BAA2B,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC;AAChF;AAMO,SAAS,gCAAgC,QAAwB;AACtE,UAAQ,QAAA;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO,yBAAyB,MAAM;AAAA,EAAA;AAE5C;AAUO,SAAS,8BACd,WACsB;AACtB,SAAO,OAAO,MAAM,EAAE,MAAM,aAAa;AACvC,UAAM,SAAS,UAAA;AACf,UAAM,YAAY,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ;AAC1F,UAAM,WAAW,MAAM,0BAA0B,QAAQ,CAAC,YAAY;AAEpE,YAAM,OAAO,IAAI,SAAA;AACjB,WAAK,OAAO,QAAQ,MAAM,aAAa,SAAS,EAAE;AAClD,UAAI,KAAM,MAAK,OAAO,YAAY,IAAI;AACtC,aAAO,MAAM,uBAAuB,OAAO,UAAU,GAAG;AAAA,QACtD,QAAQ;AAAA,QACR;AAAA,QACA,MAAM;AAAA,QACN,aAAa;AAAA,QACb;AAAA,MAAA,CACD;AAAA,IACH,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,gCAAgC,SAAS,MAAM,CAAC;AAClF,UAAM,UAAW,MAAM,SAAS,KAAA;AAChC,WAAO,OAAO,QAAQ,eAAe,WAAW,QAAQ,aAAa;AAAA,EACvE;AACF;AAQO,SAAS,yBAAyB,UAA6C;AACpF,QAAM,SAAS,gBAAA;AACf,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,SAAO,QAAQ,MAAM;AACnB,QAAI,SAAU,QAAO;AACrB,UAAM,WAAW,8BAA8B;AAAA,MAC7C,YAAY,8BAA8B,MAAM,UAAU,OAAO;AAAA,IAAA,CAClE;AACD,WAAO,SAAS,gBAAgB,WAAW,+BAAA;AAAA,EAC7C,GAAG,CAAC,QAAQ,CAAC;AACf;"}
|
package/dist/index.cjs
CHANGED
|
@@ -247,6 +247,7 @@ exports.ThreadList = ThreadList.ThreadList;
|
|
|
247
247
|
exports.SessionList = SessionList.SessionList;
|
|
248
248
|
exports.useAthenaSessionAttention = useAthenaSessionAttention.useAthenaSessionAttention;
|
|
249
249
|
exports.groupSessions = sessionListModel.groupSessions;
|
|
250
|
+
exports.orderedPullRequests = sessionListModel.orderedPullRequests;
|
|
250
251
|
exports.pullRequestChip = sessionListModel.pullRequestChip;
|
|
251
252
|
exports.pullRequestChipState = sessionListModel.pullRequestChipState;
|
|
252
253
|
exports.sessionIndicator = sessionListModel.sessionIndicator;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type { WelcomeSuggestion } from './chat/athena-chat-welcome';
|
|
|
22
22
|
export { AthenaChatErrorBoundary } from './chat/AthenaChatErrorBoundary';
|
|
23
23
|
export { type ActivityItem, type ActivityPhase, type ActivityTimelineOptions, type ActivityToolState, buildActivityTimeline, CollapsibleGroup, type CollapsibleGroupProps, countActivityTools, createToolBasedGroupingFunction, DEFAULT_TOOLKIT_THEME, defaultGetToolGroupKey, excludeFinalTextFromGroups, type FinalTextPart, getActivityItemLabel, getActivitySummary, getActivityToolState, getFinalText, type GetToolGroupKey, type GroupablePart, type GroupingFunction, type MessagePartGroup, resetRememberedExpansionsForTests, resolveActivityPhase, SuperGroupingCard, type SuperGroupingCardProps, SuperGroupingFinalText, SuperGroupingProvider, TOOLKIT_THEMES, type ToolGroupKeyOptions, type ToolkitTheme, useActivityExpansion, } from './chat/super-grouping';
|
|
24
24
|
export { StatewireApprovalCard } from './chat/StatewireApprovalCard';
|
|
25
|
-
export type { StatewireApprovalCardProps, StatewireApprovalRequestContext, } from './chat/StatewireApprovalCard';
|
|
25
|
+
export type { StatewireApprovalCardProps, StatewireApprovalEditorContext, StatewireApprovalRequestContext, } from './chat/StatewireApprovalCard';
|
|
26
26
|
export { useStatewireApprovalRequest } from './chat/use-statewire-approval-request';
|
|
27
27
|
export type { StatewireApprovalRequest, StatewireInterruptPredicate, UseStatewireApprovalRequestOptions, } from './chat/use-statewire-approval-request';
|
|
28
28
|
export { useAthenaRunLive } from './chat/use-athena-run-live';
|
|
@@ -99,7 +99,7 @@ export { SessionList } from './threads/SessionList';
|
|
|
99
99
|
export type { SessionListProps } from './threads/SessionList';
|
|
100
100
|
export { useAthenaSessionAttention } from './threads/use-athena-session-attention';
|
|
101
101
|
export type { AthenaSessionAttention } from './threads/use-athena-session-attention';
|
|
102
|
-
export { groupSessions, pullRequestChip, pullRequestChipState, sessionIndicator, sessionListGroupOf, sessionOriginLabel, sessionSecondaryLine, } from './threads/session-list-model';
|
|
102
|
+
export { groupSessions, orderedPullRequests, pullRequestChip, pullRequestChipState, sessionIndicator, sessionListGroupOf, sessionOriginLabel, sessionSecondaryLine, } from './threads/session-list-model';
|
|
103
103
|
export type { PullRequestChip, PullRequestChipState, SessionIndicator, SessionLineTone, SessionListGroup, SessionListSection, SessionSecondaryLine, } from './threads/session-list-model';
|
|
104
104
|
export { deriveConversationAttention, githubWakeEventLabel, githubWakeMarkerText, isGithubWakeMessage, readGithubWakeEvent, } from '@athenaintel/agent-runtime';
|
|
105
105
|
export type { ConversationAttention, ConversationAttentionSource, ConversationPullRequest, ConversationSummary, ConversationWake, GithubWakeEvent, } from '@athenaintel/agent-runtime';
|
|
@@ -148,7 +148,7 @@ export type { AthenaStatewireRuntimeConfig } from './runtime/useAthenaStatewireR
|
|
|
148
148
|
export { buildStatewireRunConfig } from './runtime/statewire-run-config';
|
|
149
149
|
export type { StatewireRunConfigOptions } from './runtime/statewire-run-config';
|
|
150
150
|
export { useParentAuth, useParentBridge } from './runtime/auth';
|
|
151
|
-
export type { ParentBridgeState } from './runtime/auth';
|
|
151
|
+
export type { ParentBridge, ParentBridgeState } from './runtime/auth';
|
|
152
152
|
export { createAthenaSpacesUrl, DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl, } from './lib/athena-urls';
|
|
153
153
|
export type { AthenaEnvironment } from './lib/athena-urls';
|
|
154
154
|
export { Button, buttonVariants } from './ui/button';
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ import { useAthenaConfig } from "./provider/AthenaContext.js";
|
|
|
58
58
|
import { ThreadList } from "./threads/ThreadList.js";
|
|
59
59
|
import { SessionList } from "./threads/SessionList.js";
|
|
60
60
|
import { useAthenaSessionAttention } from "./threads/use-athena-session-attention.js";
|
|
61
|
-
import { groupSessions, pullRequestChip, pullRequestChipState, sessionIndicator, sessionListGroupOf, sessionOriginLabel, sessionSecondaryLine } from "./threads/session-list-model.js";
|
|
61
|
+
import { groupSessions, orderedPullRequests, pullRequestChip, pullRequestChipState, sessionIndicator, sessionListGroupOf, sessionOriginLabel, sessionSecondaryLine } from "./threads/session-list-model.js";
|
|
62
62
|
import { AthenaThreadIdContext, useAthenaStatewireThreadId, useAthenaThreadId } from "./threads/thread-id-context.js";
|
|
63
63
|
import { THREAD_TITLE_MAX_LENGTH, archiveThread, createThread, getThreadState, isThreadArchiveUnavailableError, isThreadFavoriteUnavailableError, isThreadRenameUnavailableError, listThreads, renameThread, setThreadFavorite, setThreadReadState } from "./threads/api.js";
|
|
64
64
|
import { THREAD_PREFETCH_HOVER_DELAY_MS, THREAD_PREFETCH_MAX_CONCURRENT } from "./threads/thread-prefetcher.js";
|
|
@@ -264,6 +264,7 @@ export {
|
|
|
264
264
|
isToolResultSuccess,
|
|
265
265
|
listThreads,
|
|
266
266
|
normalizeResult,
|
|
267
|
+
orderedPullRequests,
|
|
267
268
|
parseAthenaCitationLink,
|
|
268
269
|
parseCollabAgentRef,
|
|
269
270
|
parseTodoPlan,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-list.cjs","sources":["../../../../../agent-runtime/src/conversation-list.ts"],"sourcesContent":["import type { InfiniteData, QueryClient } from '@tanstack/react-query';\nimport type { ConversationIndexEntry } from './conversation-index';\n\n/**\n * What a session asks of the viewer, derived server-side from the run, the\n * parked interrupt, the pull requests it opened, its termination and its last\n * wake (the Devin-style session list). `needs_you` is the only state that\n * demands the viewer act; `working` is live; `ready` finished with something\n * to look at; `watching` waits on an external party (a review, a check);\n * `done` and `error` are terminal. A backend newer than this client may report\n * a value outside the union — render it as \"no attention\" rather than failing.\n */\nexport type ConversationAttention =\n | 'needs_you'\n | 'working'\n | 'ready'\n | 'watching'\n | 'done'\n | 'error';\n\nexport const CONVERSATION_ATTENTIONS: readonly ConversationAttention[] = [\n 'needs_you',\n 'working',\n 'ready',\n 'watching',\n 'done',\n 'error',\n];\n\n/** Which signal produced the attention state (and the `nextAction` copy). */\nexport type ConversationAttentionSource =\n | 'interrupt'\n | 'run'\n | 'pull_request'\n | 'termination'\n | 'wake'\n | 'handoff';\n\n/** The last event that woke a resting session (a GitHub webhook, a schedule, a handoff). */\nexport interface ConversationWake {\n source: string | null;\n reason: string | null;\n /** ISO 8601 timestamp of the wake. */\n at: string | null;\n}\n\n/**\n * Live GitHub state of a pull request the session opened, read by the server\n * when the list renders. Never pushed by the conversation index — it comes\n * from the list fetch only, so a fold keeps the fetched value.\n */\nexport interface ConversationPullRequest {\n /** `owner/repo` the pull request lives in. */\n repoFullName: string;\n number: number;\n url: string;\n title: string;\n state: 'OPEN' | 'CLOSED' | 'MERGED';\n isDraft: boolean;\n /** GitHub's review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED) or null. */\n reviewDecision: string | null;\n /** Combined state of the checks on the head commit. */\n checksState: 'SUCCESS' | 'FAILURE' | 'PENDING' | 'NONE';\n /** ISO 8601 timestamp of the pull request's last update on GitHub. */\n updatedAt: string;\n}\n\n/**\n * Shared conversation/thread LIST contract (chat-stack unification —\n * thread-list-contract-convergence).\n *\n * Every chat surface lists \"my conversations\" against the same underlying\n * session assets, but historically each declared its own row shape, paging\n * model, and filter set (Olympus GraphQL `listConversations`, the SDK/Chrome\n * REST `/api/conversations/threads/list`, mobile's `/api/v0/sessions`). This\n * module is the ONE contract those surfaces converge on:\n *\n * - **Row shape**: {@link ConversationSummary} — the canonical camelCase\n * superset. Fields a transport cannot supply are `null`, never invented.\n * - **Ordering**: newest first by `updatedAt`, always. Ordering is part of the\n * contract, not a parameter — no surface sorts any other way.\n * - **Paging**: offset/limit with a LIVE newest page and explicitly-fetched\n * frozen history pages (see `useConversationList`). `hasMore` continues the\n * scan; `totalCount` is advisory and may be `null` for transports that do\n * not count.\n * - **Filtering**: {@link ConversationListFilter}. `appId` follows\n * `docs/SESSION_APP_ID_GUIDE.md`: exact match when set (legacy `null`\n * sessions excluded), no filter when unset. A fetcher that cannot express a\n * requested filter field MUST throw rather than silently ignore it — a\n * dropped filter returns wrong rows under a cache key that claims otherwise.\n * - **Freshness**: the per-user conversation-index statewire\n * (`conversation-index.ts`) is the push signal; these queries are the data\n * source. Push handlers call {@link invalidateConversationListCaches}.\n */\nexport interface ConversationSummary {\n /** LangGraph thread id — the cross-surface conversation identity. */\n threadId: string;\n title: string;\n /** ISO 8601 timestamp of last update (the contract's sort key). */\n updatedAt: string;\n /** ISO 8601 creation timestamp, when the transport reports it. */\n createdAt: string | null;\n /** Session lifecycle state from athena_metadata (e.g. running, completed). */\n state: string | null;\n /** Agent runtime marker (e.g. 'athena_deep_agent'), when reported. */\n runtime: string | null;\n /** Channel the session was started from (e.g. agent_email, web). */\n sourceChannel: string | null;\n /** Asset type (session, video_session, desktop_session, mobile_session). */\n assetType: string | null;\n /** Session asset id (`asset_<uuid>`), when the transport reports it. */\n assetId: string | null;\n /** Client app that created the session (SESSION_APP_ID_GUIDE). */\n appId: string | null;\n /** Computer asset whose sandbox started the session (`sourceChannel` 'computer'); omitted by surfaces that don't carry it. */\n computerAssetId?: string | null;\n /** Trigger marker (`manual` remains user-initiated; other values are automated). */\n triggerType: string | null;\n /** Last-message preview used when a legacy session has no useful title. */\n preview: string | null;\n messageCount: number | null;\n /** Per-user unread flag derived server-side from the read receipt. */\n isUnread: boolean;\n /**\n * Canonical session status (status_v2: idle | active | needs_input | error),\n * or null when the transport does not report it.\n */\n sessionStatus: string | null;\n /**\n * Canonical status of the session's current run (status_v2: scheduled |\n * queued | running | needs_input | completed | failed | canceled). Null when\n * the transport does not report it — {@link isConversationRunning} then\n * falls back to the legacy `state`. Running badges on every surface derive\n * from this one helper so they cannot disagree.\n */\n runStatus: string | null;\n /**\n * Server-derived attention state. `undefined` when the transport (or an\n * older server) does not report it — {@link deriveConversationAttention}\n * then falls back to the status planes; `null` when the server reports none.\n */\n attention?: ConversationAttention | null;\n /** One line (≤ 80 chars) of what the viewer should do next, when the server derives one. */\n nextAction?: string | null;\n /** Which signal produced `attention` / `nextAction`. */\n attentionSource?: ConversationAttentionSource | null;\n /** Whether the viewer pinned (favorited) the session; `undefined` when not reported. */\n isFavorited?: boolean;\n /** The last wake of a resting session; `undefined` when not reported. */\n lastWake?: ConversationWake | null;\n /**\n * Pull requests the session opened with their live GitHub state. `[]` when\n * it opened none, `null` when the state could not be read right now,\n * `undefined` when the transport did not ask for (or report) it.\n */\n pullRequests?: ConversationPullRequest[] | null;\n}\n\n/**\n * The canonical filter set. Semantics are defined here once; each fetcher\n * translates the fields its transport supports and throws on the rest.\n */\nexport interface ConversationListFilter {\n /** Exact-match client app filter; unset = all apps (SESSION_APP_ID_GUIDE). */\n appId?: string;\n /** Asset types to include (e.g. ['session']); unset = all session types. */\n assetTypes?: readonly string[];\n /** Case-insensitive title search. */\n search?: string;\n /** Hide triggered/background sessions (REST thread list only today). */\n excludeTriggered?: boolean;\n /** Start-channel filter (v0 sessions / REST thread list only today). */\n sourceChannels?: readonly string[];\n /** Start channels to hide (e.g. ['api']); GraphQL `listConversations` only today. */\n excludeSourceChannels?: readonly string[];\n /** Exact computer asset whose sandbox started the session; GraphQL `listConversations` only today. */\n computerAssetId?: string;\n /**\n * Lifecycle-state filter (e.g. ['running'], ['completed']). Transports match\n * it with the shared status_v2 rule (running carries a 12-hour recency cutoff).\n */\n states?: readonly string[];\n /** Restrict to sessions run by any of these agents (athena_metadata.agent). */\n agentIds?: readonly string[];\n /**\n * Include branched sub-sessions and orchestration children, which every list\n * hides by default because they belong under their parent thread.\n */\n includeSubSessions?: boolean;\n}\n\nexport interface ConversationListPage {\n /** Rows in contract order (updatedAt desc). */\n items: ConversationSummary[];\n /** Whether another `offset + limit` scan would return more rows. */\n hasMore: boolean;\n /** Total matching rows, or null when the transport does not count. */\n totalCount: number | null;\n}\n\nexport interface ConversationListFetchParams {\n limit: number;\n offset: number;\n filter: ConversationListFilter;\n signal?: AbortSignal;\n}\n\n/**\n * One page fetch against some transport (GraphQL `listConversations`, the\n * REST thread list, …). Must reject on filter fields it cannot express.\n */\nexport type ConversationListFetcher = (\n params: ConversationListFetchParams\n) => Promise<ConversationListPage>;\n\n/** Root of every conversation-list query key (all surfaces, all filters). */\nexport const CONVERSATION_LIST_QUERY_KEY_ROOT = 'athena-conversation-list';\n\n/**\n * Scope used by `useConversationList` when the caller passes no `scopeKey`.\n * Exported so cache seeders (e.g. Olympus's spacesBootstrap adoption) can\n * address the exact keys default-scoped hooks read from.\n */\nexport const CONVERSATION_LIST_DEFAULT_SCOPE_KEY = 'default';\n\n/**\n * Normalize a filter to its semantic content: unset/empty fields are dropped\n * (so `{}` and `{ appId: undefined }` are the same filter) and array order is\n * canonicalized. Fetchers receive the normalized form, so what a cache entry\n * claims to hold is exactly what was fetched.\n */\nexport function normalizeConversationListFilter(\n filter: ConversationListFilter\n): ConversationListFilter {\n const normalized: ConversationListFilter = {};\n if (filter.appId) normalized.appId = filter.appId;\n if (filter.assetTypes && filter.assetTypes.length > 0) {\n normalized.assetTypes = [...filter.assetTypes].sort();\n }\n if (filter.search) normalized.search = filter.search;\n if (filter.excludeTriggered !== undefined) normalized.excludeTriggered = filter.excludeTriggered;\n if (filter.sourceChannels && filter.sourceChannels.length > 0) {\n normalized.sourceChannels = [...filter.sourceChannels].sort();\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n normalized.excludeSourceChannels = [...filter.excludeSourceChannels].sort();\n }\n if (filter.computerAssetId) normalized.computerAssetId = filter.computerAssetId;\n if (filter.states && filter.states.length > 0) {\n normalized.states = [...filter.states].sort();\n }\n if (filter.agentIds && filter.agentIds.length > 0) {\n normalized.agentIds = [...filter.agentIds].sort();\n }\n if (filter.includeSubSessions) normalized.includeSubSessions = true;\n return normalized;\n}\n\n/** status_v2 run statuses during which a session is actively working. */\nconst RUNNING_RUN_STATUSES: ReadonlySet<string> = new Set(['queued', 'running']);\n\n/**\n * THE running rule for every conversation-list surface (sidebar pane, recent\n * dropdown, workspace section, pushed index rows): the canonical status_v2 run\n * status when the transport reports one, else the legacy `state`. A session\n * paused on a human gate (`needs_input`) is not running.\n */\nexport function isConversationRunning(summary: {\n runStatus?: string | null;\n state?: string | null;\n}): boolean {\n if (summary.runStatus !== null && summary.runStatus !== undefined) {\n return RUNNING_RUN_STATUSES.has(summary.runStatus);\n }\n return summary.state === 'running';\n}\n\nconst KNOWN_ATTENTIONS: ReadonlySet<string> = new Set(CONVERSATION_ATTENTIONS);\n\n/**\n * THE attention rule for every session-list surface: the server-derived\n * `attention` when the transport reports one it knows, else a client fallback\n * from the status planes so an older server still yields the two states a\n * list cannot do without — a human gate reads as `needs_you`, a live run as\n * `working`, a failed run as `error`. `null` when nothing applies.\n */\nexport function deriveConversationAttention(\n summary: Pick<ConversationSummary, 'attention' | 'runStatus' | 'sessionStatus' | 'state'>\n): ConversationAttention | null {\n if (typeof summary.attention === 'string' && KNOWN_ATTENTIONS.has(summary.attention)) {\n return summary.attention;\n }\n if (summary.runStatus === 'needs_input' || summary.sessionStatus === 'needs_input') {\n return 'needs_you';\n }\n if (isConversationRunning(summary)) return 'working';\n if (summary.runStatus === 'failed' || summary.sessionStatus === 'error') return 'error';\n return null;\n}\n\n/**\n * Stable cache identity for a filter: the normalized fields serialized in\n * sorted key order, so semantically equal filters share one cache entry.\n */\nexport function conversationListFilterKey(filter: ConversationListFilter): string {\n const normalized = normalizeConversationListFilter(filter) as Record<string, unknown>;\n return JSON.stringify(\n Object.keys(normalized)\n .sort()\n .map((key) => [key, normalized[key]])\n );\n}\n\n/**\n * Query keys for the two-tier pager. `scopeKey` isolates caches beyond the\n * filter — surfaces MUST include everything that changes whose list this is\n * (principal/user id, backend base URL) so a credential swap can never read\n * another identity's rows out of the cache.\n *\n * `pageSize` is part of the key because it is baked into every cached page's\n * offset boundaries: two hooks sharing a scope and filter but paging by\n * different sizes would otherwise reuse each other's incompatible boundaries\n * and skip or duplicate rows.\n */\nexport const conversationListQueryKeys = {\n root: [CONVERSATION_LIST_QUERY_KEY_ROOT] as const,\n scope: (scopeKey: string) => [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey] as const,\n firstPage: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'first-page'] as const,\n history: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'history'] as const,\n};\n\n/**\n * Flatten pages into one newest-first list, deduplicating by `threadId`.\n * The live first page wins over retained history copies of the same thread.\n */\nexport function mergeConversationPages(\n pages: readonly ConversationListPage[]\n): ConversationSummary[] {\n const seen = new Set<string>();\n const items: ConversationSummary[] = [];\n for (const page of pages) {\n for (const item of page.items) {\n if (seen.has(item.threadId)) continue;\n seen.add(item.threadId);\n items.push(item);\n }\n }\n return items;\n}\n\n/**\n * Prepend a conversation to a page unless its thread is already present\n * (optimistic \"new thread appears at the top\" write). Identity-preserving\n * no-op when present, so equal writes never notify subscribers.\n */\nexport function prependConversationToPage(\n page: ConversationListPage,\n item: ConversationSummary\n): ConversationListPage {\n if (page.items.some((existing) => existing.threadId === item.threadId)) return page;\n return {\n ...page,\n items: [item, ...page.items],\n totalCount: page.totalCount === null ? null : page.totalCount + 1,\n };\n}\n\n/** Remove a thread's row from a page; identity-preserving when absent. */\nexport function removeConversationFromPage(\n page: ConversationListPage,\n threadId: string\n): ConversationListPage {\n if (!page.items.some((item) => item.threadId === threadId)) return page;\n return { ...page, items: page.items.filter((item) => item.threadId !== threadId) };\n}\n\n/**\n * Patch every row matching `match`; identity-preserving when nothing matches\n * so cache writes cannot cause spurious re-renders.\n */\nexport function patchConversationsInPage(\n page: ConversationListPage,\n match: (item: ConversationSummary) => boolean,\n patch: Partial<ConversationSummary>\n): ConversationListPage {\n let changed = false;\n const items = page.items.map((item) => {\n if (!match(item)) return item;\n changed = true;\n return { ...item, ...patch };\n });\n return changed ? { ...page, items } : page;\n}\n\ntype CachedConversationList =\n | ConversationListPage\n | InfiniteData<ConversationListPage, number>\n | undefined;\n\nfunction isInfinitePages(\n data: ConversationListPage | InfiniteData<ConversationListPage, number>\n): data is InfiniteData<ConversationListPage, number> {\n return 'pages' in data;\n}\n\nfunction mapCachedPages(\n data: CachedConversationList,\n transform: (page: ConversationListPage) => ConversationListPage\n): CachedConversationList {\n if (!data) return data;\n if (isInfinitePages(data)) {\n let changed = false;\n const pages = data.pages.map((page) => {\n const next = transform(page);\n if (next !== page) changed = true;\n return next;\n });\n return changed ? { ...data, pages } : data;\n }\n return transform(data);\n}\n\ninterface ConversationListCacheScope {\n /** Restrict to one scope; omitted = every conversation-list cache. */\n scopeKey?: string;\n}\n\nfunction scopedKey(options?: ConversationListCacheScope): readonly string[] {\n return options?.scopeKey !== undefined\n ? conversationListQueryKeys.scope(options.scopeKey)\n : conversationListQueryKeys.root;\n}\n\n/**\n * Invalidate conversation-list caches (all filters, first page + history).\n * This is the hook for push signals — the conversation-index statewire, a\n * finished run, an archive/restore — anything that changed the server list.\n */\nexport function invalidateConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.invalidateQueries({ queryKey: scopedKey(options) });\n}\n\n/** Cancel in-flight list fetches so an optimistic write cannot be overwritten. */\nexport function cancelConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.cancelQueries({ queryKey: scopedKey(options) });\n}\n\n/**\n * Patch matching rows in every cached conversation list (first pages, history\n * pages, searches) so per-row state (read receipts, titles) flips in the same\n * frame as the interaction instead of after the next refetch.\n */\nexport function patchConversationListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & {\n match: (item: ConversationSummary) => boolean;\n patch: Partial<ConversationSummary>;\n }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => patchConversationsInPage(page, options.match, options.patch))\n );\n}\n\n/** Remove a thread's row from every cached conversation list in scope. */\nexport function removeConversationFromListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & { threadId: string }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => removeConversationFromPage(page, options.threadId))\n );\n}\n\nfunction timestampMs(value: string): number {\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nfunction sameSummary(a: ConversationSummary, b: ConversationSummary): boolean {\n return (\n a.threadId === b.threadId &&\n a.title === b.title &&\n a.updatedAt === b.updatedAt &&\n a.createdAt === b.createdAt &&\n a.state === b.state &&\n a.runtime === b.runtime &&\n a.sourceChannel === b.sourceChannel &&\n a.assetType === b.assetType &&\n a.assetId === b.assetId &&\n a.appId === b.appId &&\n a.triggerType === b.triggerType &&\n a.preview === b.preview &&\n a.messageCount === b.messageCount &&\n a.isUnread === b.isUnread &&\n a.sessionStatus === b.sessionStatus &&\n a.runStatus === b.runStatus &&\n a.attention === b.attention &&\n a.nextAction === b.nextAction &&\n a.attentionSource === b.attentionSource &&\n a.isFavorited === b.isFavorited &&\n // Never pushed by the index, so a fold carries the previous references\n // through unchanged; identity is the equality that matters here.\n a.lastWake === b.lastWake &&\n a.pullRequests === b.pullRequests\n );\n}\n\n/**\n * Project one replicated conversation-index row onto the shared summary shape.\n * Fields the index never carries (`createdAt`) and viewer-specific fields a\n * lifecycle publish cannot know (`is_unread: null`) keep the row's previous\n * value. Canonical status is taken from the entry when the publishing pod\n * reports it; a snapshot from a pod that predates the fields falls back to the\n * legacy `state` (null status) rather than keeping a stale run status.\n *\n * Attention fields (`attention`, `next_action`, `attention_source`,\n * `is_favorited`) are taken from the entry when the publishing pod reports\n * them, so a live push flips a row to \"needs you\" the way it renames it; a\n * pod that predates them leaves the row's previous values. Pull-request state\n * and the last wake are never pushed and always keep the fetched value.\n */\nexport function summaryFromConversationIndexEntry(\n entry: ConversationIndexEntry,\n previous?: ConversationSummary\n): ConversationSummary {\n const carriesStatus = entry.run_status !== undefined || entry.session_status !== undefined;\n return {\n threadId: entry.thread_id,\n title: entry.title,\n updatedAt: entry.updated_at,\n createdAt: previous?.createdAt ?? null,\n state: entry.state,\n runtime: entry.runtime,\n sourceChannel: entry.start_channel,\n assetType: entry.asset_type,\n assetId: entry.asset_id ?? previous?.assetId ?? null,\n computerAssetId: previous?.computerAssetId ?? null,\n appId: entry.app_id,\n triggerType: entry.trigger_type,\n preview: entry.last_message_preview,\n messageCount: entry.message_count,\n isUnread: entry.is_unread ?? previous?.isUnread ?? false,\n sessionStatus: carriesStatus ? (entry.session_status ?? null) : null,\n runStatus: carriesStatus ? (entry.run_status ?? null) : null,\n attention: entry.attention !== undefined ? entry.attention : previous?.attention,\n nextAction: entry.next_action !== undefined ? entry.next_action : previous?.nextAction,\n attentionSource:\n entry.attention_source !== undefined ? entry.attention_source : previous?.attentionSource,\n isFavorited: entry.is_favorited ?? previous?.isFavorited,\n lastWake: previous?.lastWake,\n pullRequests: previous?.pullRequests,\n };\n}\n\n/**\n * Fold the user's replicated conversation index (the newest-page window the\n * per-user statewire pushes) into a cached live first page, so a thread\n * created, renamed, finished, or read anywhere updates every list surface in\n * the same frame — without refetching the page.\n *\n * - Rows in the window are upserted (new threads appear, existing rows take\n * the pushed fields, see {@link summaryFromConversationIndexEntry}).\n * - Rows NOT in the window but at least as new as the window's oldest row\n * should have been in it, so they left the list (archived, hidden, shared\n * away) and are dropped. Older rows are beyond the window and are kept.\n * - When the index reports no further rows (`indexHasMore === false`) the\n * window IS the whole list, so every row outside it is dropped.\n *\n * Newest-first ordering is restored and the page identity is preserved when\n * nothing changed, so an equal push never notifies subscribers.\n */\nexport function mergeConversationIndexIntoPage(\n page: ConversationListPage,\n entries: readonly ConversationIndexEntry[],\n options: { indexHasMore: boolean }\n): ConversationListPage {\n const previousByThread = new Map(page.items.map((item) => [item.threadId, item]));\n const inWindow = new Set(entries.map((entry) => entry.thread_id));\n const windowTailMs =\n entries.length > 0 ? Math.min(...entries.map((entry) => timestampMs(entry.updated_at))) : null;\n\n const merged: ConversationSummary[] = entries.map((entry) => {\n const previous = previousByThread.get(entry.thread_id);\n const next = summaryFromConversationIndexEntry(entry, previous);\n return previous && sameSummary(previous, next) ? previous : next;\n });\n\n for (const item of page.items) {\n if (inWindow.has(item.threadId)) continue;\n if (!options.indexHasMore) continue;\n if (windowTailMs !== null && timestampMs(item.updatedAt) >= windowTailMs) continue;\n merged.push(item);\n }\n\n merged.sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt));\n\n const unchanged =\n merged.length === page.items.length &&\n merged.every((item, index) => item === page.items[index]);\n if (unchanged) return page;\n\n return { ...page, items: merged };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"conversation-list.cjs","sources":["../../../../../agent-runtime/src/conversation-list.ts"],"sourcesContent":["import type { InfiniteData, QueryClient } from '@tanstack/react-query';\nimport type { ConversationIndexEntry } from './conversation-index';\n\n/**\n * What a session asks of the viewer, derived server-side from the run, the\n * parked interrupt, the pull requests it opened, its termination and its last\n * wake (the Devin-style session list). `needs_you` is the only state that\n * demands the viewer act; `working` is live; `ready` finished with something\n * to look at; `watching` waits on an external party (a review, a check);\n * `done` and `error` are terminal. A backend newer than this client may report\n * a value outside the union — render it as \"no attention\" rather than failing.\n */\nexport type ConversationAttention =\n | 'needs_you'\n | 'working'\n | 'ready'\n | 'watching'\n | 'done'\n | 'error';\n\nexport const CONVERSATION_ATTENTIONS: readonly ConversationAttention[] = [\n 'needs_you',\n 'working',\n 'ready',\n 'watching',\n 'done',\n 'error',\n];\n\n/** Which signal produced the attention state (and the `nextAction` copy). */\nexport type ConversationAttentionSource =\n | 'interrupt'\n | 'run'\n | 'pull_request'\n | 'termination'\n | 'wake'\n | 'handoff'\n | 'follow_up';\n\n/** The last event that woke a resting session (a GitHub webhook, a schedule, a handoff). */\nexport interface ConversationWake {\n source: string | null;\n reason: string | null;\n /** ISO 8601 timestamp of the wake. */\n at: string | null;\n}\n\n/**\n * Live GitHub state of a pull request the session opened, read by the server\n * when the list renders. Never pushed by the conversation index — it comes\n * from the list fetch only, so a fold keeps the fetched value.\n */\nexport interface ConversationPullRequest {\n /** `owner/repo` the pull request lives in. */\n repoFullName: string;\n number: number;\n url: string;\n title: string;\n state: 'OPEN' | 'CLOSED' | 'MERGED';\n isDraft: boolean;\n /** GitHub's review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED) or null. */\n reviewDecision: string | null;\n /** Combined state of the checks on the head commit. */\n checksState: 'SUCCESS' | 'FAILURE' | 'PENDING' | 'NONE';\n /** ISO 8601 timestamp of the pull request's last update on GitHub. */\n updatedAt: string;\n}\n\n/**\n * Shared conversation/thread LIST contract (chat-stack unification —\n * thread-list-contract-convergence).\n *\n * Every chat surface lists \"my conversations\" against the same underlying\n * session assets, but historically each declared its own row shape, paging\n * model, and filter set (Olympus GraphQL `listConversations`, the SDK/Chrome\n * REST `/api/conversations/threads/list`, mobile's `/api/v0/sessions`). This\n * module is the ONE contract those surfaces converge on:\n *\n * - **Row shape**: {@link ConversationSummary} — the canonical camelCase\n * superset. Fields a transport cannot supply are `null`, never invented.\n * - **Ordering**: newest first by `updatedAt`, always. Ordering is part of the\n * contract, not a parameter — no surface sorts any other way.\n * - **Paging**: offset/limit with a LIVE newest page and explicitly-fetched\n * frozen history pages (see `useConversationList`). `hasMore` continues the\n * scan; `totalCount` is advisory and may be `null` for transports that do\n * not count.\n * - **Filtering**: {@link ConversationListFilter}. `appId` follows\n * `docs/SESSION_APP_ID_GUIDE.md`: exact match when set (legacy `null`\n * sessions excluded), no filter when unset. A fetcher that cannot express a\n * requested filter field MUST throw rather than silently ignore it — a\n * dropped filter returns wrong rows under a cache key that claims otherwise.\n * - **Freshness**: the per-user conversation-index statewire\n * (`conversation-index.ts`) is the push signal; these queries are the data\n * source. Push handlers call {@link invalidateConversationListCaches}.\n */\nexport interface ConversationSummary {\n /** LangGraph thread id — the cross-surface conversation identity. */\n threadId: string;\n title: string;\n /** ISO 8601 timestamp of last update (the contract's sort key). */\n updatedAt: string;\n /** ISO 8601 creation timestamp, when the transport reports it. */\n createdAt: string | null;\n /** Session lifecycle state from athena_metadata (e.g. running, completed). */\n state: string | null;\n /** Agent runtime marker (e.g. 'athena_deep_agent'), when reported. */\n runtime: string | null;\n /** Channel the session was started from (e.g. agent_email, web). */\n sourceChannel: string | null;\n /** Asset type (session, video_session, desktop_session, mobile_session). */\n assetType: string | null;\n /** Session asset id (`asset_<uuid>`), when the transport reports it. */\n assetId: string | null;\n /** Client app that created the session (SESSION_APP_ID_GUIDE). */\n appId: string | null;\n /** Computer asset whose sandbox started the session (`sourceChannel` 'computer'); omitted by surfaces that don't carry it. */\n computerAssetId?: string | null;\n /** Trigger marker (`manual` remains user-initiated; other values are automated). */\n triggerType: string | null;\n /** Last-message preview used when a legacy session has no useful title. */\n preview: string | null;\n messageCount: number | null;\n /** Per-user unread flag derived server-side from the read receipt. */\n isUnread: boolean;\n /**\n * Canonical session status (status_v2: idle | active | needs_input | error),\n * or null when the transport does not report it.\n */\n sessionStatus: string | null;\n /**\n * Canonical status of the session's current run (status_v2: scheduled |\n * queued | running | needs_input | completed | failed | canceled). Null when\n * the transport does not report it — {@link isConversationRunning} then\n * falls back to the legacy `state`. Running badges on every surface derive\n * from this one helper so they cannot disagree.\n */\n runStatus: string | null;\n /**\n * Server-derived attention state. `undefined` when the transport (or an\n * older server) does not report it — {@link deriveConversationAttention}\n * then falls back to the status planes; `null` when the server reports none.\n */\n attention?: ConversationAttention | null;\n /** One line (≤ 80 chars) of what the viewer should do next, when the server derives one. */\n nextAction?: string | null;\n /** Which signal produced `attention` / `nextAction`. */\n attentionSource?: ConversationAttentionSource | null;\n /** Whether the viewer pinned (favorited) the session; `undefined` when not reported. */\n isFavorited?: boolean;\n /** The last wake of a resting session; `undefined` when not reported. */\n lastWake?: ConversationWake | null;\n /**\n * Pull requests the session opened with their live GitHub state. `[]` when\n * it opened none, `null` when the state could not be read right now,\n * `undefined` when the transport did not ask for (or report) it.\n */\n pullRequests?: ConversationPullRequest[] | null;\n}\n\n/**\n * The canonical filter set. Semantics are defined here once; each fetcher\n * translates the fields its transport supports and throws on the rest.\n */\nexport interface ConversationListFilter {\n /** Exact-match client app filter; unset = all apps (SESSION_APP_ID_GUIDE). */\n appId?: string;\n /** Asset types to include (e.g. ['session']); unset = all session types. */\n assetTypes?: readonly string[];\n /** Case-insensitive title search. */\n search?: string;\n /** Hide triggered/background sessions (REST thread list only today). */\n excludeTriggered?: boolean;\n /** Start-channel filter (v0 sessions / REST thread list only today). */\n sourceChannels?: readonly string[];\n /** Start channels to hide (e.g. ['api']); GraphQL `listConversations` only today. */\n excludeSourceChannels?: readonly string[];\n /** Exact computer asset whose sandbox started the session; GraphQL `listConversations` only today. */\n computerAssetId?: string;\n /**\n * Lifecycle-state filter (e.g. ['running'], ['completed']). Transports match\n * it with the shared status_v2 rule (running carries a 12-hour recency cutoff).\n */\n states?: readonly string[];\n /** Restrict to sessions run by any of these agents (athena_metadata.agent). */\n agentIds?: readonly string[];\n /**\n * Include branched sub-sessions and orchestration children, which every list\n * hides by default because they belong under their parent thread.\n */\n includeSubSessions?: boolean;\n}\n\nexport interface ConversationListPage {\n /** Rows in contract order (updatedAt desc). */\n items: ConversationSummary[];\n /** Whether another `offset + limit` scan would return more rows. */\n hasMore: boolean;\n /** Total matching rows, or null when the transport does not count. */\n totalCount: number | null;\n}\n\nexport interface ConversationListFetchParams {\n limit: number;\n offset: number;\n filter: ConversationListFilter;\n signal?: AbortSignal;\n}\n\n/**\n * One page fetch against some transport (GraphQL `listConversations`, the\n * REST thread list, …). Must reject on filter fields it cannot express.\n */\nexport type ConversationListFetcher = (\n params: ConversationListFetchParams\n) => Promise<ConversationListPage>;\n\n/** Root of every conversation-list query key (all surfaces, all filters). */\nexport const CONVERSATION_LIST_QUERY_KEY_ROOT = 'athena-conversation-list';\n\n/**\n * Scope used by `useConversationList` when the caller passes no `scopeKey`.\n * Exported so cache seeders (e.g. Olympus's spacesBootstrap adoption) can\n * address the exact keys default-scoped hooks read from.\n */\nexport const CONVERSATION_LIST_DEFAULT_SCOPE_KEY = 'default';\n\n/**\n * Normalize a filter to its semantic content: unset/empty fields are dropped\n * (so `{}` and `{ appId: undefined }` are the same filter) and array order is\n * canonicalized. Fetchers receive the normalized form, so what a cache entry\n * claims to hold is exactly what was fetched.\n */\nexport function normalizeConversationListFilter(\n filter: ConversationListFilter\n): ConversationListFilter {\n const normalized: ConversationListFilter = {};\n if (filter.appId) normalized.appId = filter.appId;\n if (filter.assetTypes && filter.assetTypes.length > 0) {\n normalized.assetTypes = [...filter.assetTypes].sort();\n }\n if (filter.search) normalized.search = filter.search;\n if (filter.excludeTriggered !== undefined) normalized.excludeTriggered = filter.excludeTriggered;\n if (filter.sourceChannels && filter.sourceChannels.length > 0) {\n normalized.sourceChannels = [...filter.sourceChannels].sort();\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n normalized.excludeSourceChannels = [...filter.excludeSourceChannels].sort();\n }\n if (filter.computerAssetId) normalized.computerAssetId = filter.computerAssetId;\n if (filter.states && filter.states.length > 0) {\n normalized.states = [...filter.states].sort();\n }\n if (filter.agentIds && filter.agentIds.length > 0) {\n normalized.agentIds = [...filter.agentIds].sort();\n }\n if (filter.includeSubSessions) normalized.includeSubSessions = true;\n return normalized;\n}\n\n/** status_v2 run statuses during which a session is actively working. */\nconst RUNNING_RUN_STATUSES: ReadonlySet<string> = new Set(['queued', 'running']);\n\n/**\n * THE running rule for every conversation-list surface (sidebar pane, recent\n * dropdown, workspace section, pushed index rows): the canonical status_v2 run\n * status when the transport reports one, else the legacy `state`. A session\n * paused on a human gate (`needs_input`) is not running.\n */\nexport function isConversationRunning(summary: {\n runStatus?: string | null;\n state?: string | null;\n}): boolean {\n if (summary.runStatus !== null && summary.runStatus !== undefined) {\n return RUNNING_RUN_STATUSES.has(summary.runStatus);\n }\n return summary.state === 'running';\n}\n\nconst KNOWN_ATTENTIONS: ReadonlySet<string> = new Set(CONVERSATION_ATTENTIONS);\n\n/**\n * THE attention rule for every session-list surface: the server-derived\n * `attention` when the transport reports one it knows, else a client fallback\n * from the status planes so an older server still yields the two states a\n * list cannot do without — a human gate reads as `needs_you`, a live run as\n * `working`, a failed run as `error`. `null` when nothing applies.\n */\nexport function deriveConversationAttention(\n summary: Pick<ConversationSummary, 'attention' | 'runStatus' | 'sessionStatus' | 'state'>\n): ConversationAttention | null {\n if (typeof summary.attention === 'string' && KNOWN_ATTENTIONS.has(summary.attention)) {\n return summary.attention;\n }\n if (summary.runStatus === 'needs_input' || summary.sessionStatus === 'needs_input') {\n return 'needs_you';\n }\n if (isConversationRunning(summary)) return 'working';\n if (summary.runStatus === 'failed' || summary.sessionStatus === 'error') return 'error';\n return null;\n}\n\n/**\n * Stable cache identity for a filter: the normalized fields serialized in\n * sorted key order, so semantically equal filters share one cache entry.\n */\nexport function conversationListFilterKey(filter: ConversationListFilter): string {\n const normalized = normalizeConversationListFilter(filter) as Record<string, unknown>;\n return JSON.stringify(\n Object.keys(normalized)\n .sort()\n .map((key) => [key, normalized[key]])\n );\n}\n\n/**\n * Query keys for the two-tier pager. `scopeKey` isolates caches beyond the\n * filter — surfaces MUST include everything that changes whose list this is\n * (principal/user id, backend base URL) so a credential swap can never read\n * another identity's rows out of the cache.\n *\n * `pageSize` is part of the key because it is baked into every cached page's\n * offset boundaries: two hooks sharing a scope and filter but paging by\n * different sizes would otherwise reuse each other's incompatible boundaries\n * and skip or duplicate rows.\n */\nexport const conversationListQueryKeys = {\n root: [CONVERSATION_LIST_QUERY_KEY_ROOT] as const,\n scope: (scopeKey: string) => [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey] as const,\n firstPage: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'first-page'] as const,\n history: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'history'] as const,\n};\n\n/**\n * Flatten pages into one newest-first list, deduplicating by `threadId`.\n * The live first page wins over retained history copies of the same thread.\n */\nexport function mergeConversationPages(\n pages: readonly ConversationListPage[]\n): ConversationSummary[] {\n const seen = new Set<string>();\n const items: ConversationSummary[] = [];\n for (const page of pages) {\n for (const item of page.items) {\n if (seen.has(item.threadId)) continue;\n seen.add(item.threadId);\n items.push(item);\n }\n }\n return items;\n}\n\n/**\n * Prepend a conversation to a page unless its thread is already present\n * (optimistic \"new thread appears at the top\" write). Identity-preserving\n * no-op when present, so equal writes never notify subscribers.\n */\nexport function prependConversationToPage(\n page: ConversationListPage,\n item: ConversationSummary\n): ConversationListPage {\n if (page.items.some((existing) => existing.threadId === item.threadId)) return page;\n return {\n ...page,\n items: [item, ...page.items],\n totalCount: page.totalCount === null ? null : page.totalCount + 1,\n };\n}\n\n/** Remove a thread's row from a page; identity-preserving when absent. */\nexport function removeConversationFromPage(\n page: ConversationListPage,\n threadId: string\n): ConversationListPage {\n if (!page.items.some((item) => item.threadId === threadId)) return page;\n return { ...page, items: page.items.filter((item) => item.threadId !== threadId) };\n}\n\n/**\n * Patch every row matching `match`; identity-preserving when nothing matches\n * so cache writes cannot cause spurious re-renders.\n */\nexport function patchConversationsInPage(\n page: ConversationListPage,\n match: (item: ConversationSummary) => boolean,\n patch: Partial<ConversationSummary>\n): ConversationListPage {\n let changed = false;\n const items = page.items.map((item) => {\n if (!match(item)) return item;\n changed = true;\n return { ...item, ...patch };\n });\n return changed ? { ...page, items } : page;\n}\n\ntype CachedConversationList =\n | ConversationListPage\n | InfiniteData<ConversationListPage, number>\n | undefined;\n\nfunction isInfinitePages(\n data: ConversationListPage | InfiniteData<ConversationListPage, number>\n): data is InfiniteData<ConversationListPage, number> {\n return 'pages' in data;\n}\n\nfunction mapCachedPages(\n data: CachedConversationList,\n transform: (page: ConversationListPage) => ConversationListPage\n): CachedConversationList {\n if (!data) return data;\n if (isInfinitePages(data)) {\n let changed = false;\n const pages = data.pages.map((page) => {\n const next = transform(page);\n if (next !== page) changed = true;\n return next;\n });\n return changed ? { ...data, pages } : data;\n }\n return transform(data);\n}\n\ninterface ConversationListCacheScope {\n /** Restrict to one scope; omitted = every conversation-list cache. */\n scopeKey?: string;\n}\n\nfunction scopedKey(options?: ConversationListCacheScope): readonly string[] {\n return options?.scopeKey !== undefined\n ? conversationListQueryKeys.scope(options.scopeKey)\n : conversationListQueryKeys.root;\n}\n\n/**\n * Invalidate conversation-list caches (all filters, first page + history).\n * This is the hook for push signals — the conversation-index statewire, a\n * finished run, an archive/restore — anything that changed the server list.\n */\nexport function invalidateConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.invalidateQueries({ queryKey: scopedKey(options) });\n}\n\n/** Cancel in-flight list fetches so an optimistic write cannot be overwritten. */\nexport function cancelConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.cancelQueries({ queryKey: scopedKey(options) });\n}\n\n/**\n * Patch matching rows in every cached conversation list (first pages, history\n * pages, searches) so per-row state (read receipts, titles) flips in the same\n * frame as the interaction instead of after the next refetch.\n */\nexport function patchConversationListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & {\n match: (item: ConversationSummary) => boolean;\n patch: Partial<ConversationSummary>;\n }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => patchConversationsInPage(page, options.match, options.patch))\n );\n}\n\n/** Remove a thread's row from every cached conversation list in scope. */\nexport function removeConversationFromListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & { threadId: string }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => removeConversationFromPage(page, options.threadId))\n );\n}\n\nfunction timestampMs(value: string): number {\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nfunction sameSummary(a: ConversationSummary, b: ConversationSummary): boolean {\n return (\n a.threadId === b.threadId &&\n a.title === b.title &&\n a.updatedAt === b.updatedAt &&\n a.createdAt === b.createdAt &&\n a.state === b.state &&\n a.runtime === b.runtime &&\n a.sourceChannel === b.sourceChannel &&\n a.assetType === b.assetType &&\n a.assetId === b.assetId &&\n a.appId === b.appId &&\n a.triggerType === b.triggerType &&\n a.preview === b.preview &&\n a.messageCount === b.messageCount &&\n a.isUnread === b.isUnread &&\n a.sessionStatus === b.sessionStatus &&\n a.runStatus === b.runStatus &&\n a.attention === b.attention &&\n a.nextAction === b.nextAction &&\n a.attentionSource === b.attentionSource &&\n a.isFavorited === b.isFavorited &&\n // Never pushed by the index, so a fold carries the previous references\n // through unchanged; identity is the equality that matters here.\n a.lastWake === b.lastWake &&\n a.pullRequests === b.pullRequests\n );\n}\n\n/**\n * Project one replicated conversation-index row onto the shared summary shape.\n * Fields the index never carries (`createdAt`) and viewer-specific fields a\n * lifecycle publish cannot know (`is_unread: null`) keep the row's previous\n * value. Canonical status is taken from the entry when the publishing pod\n * reports it; a snapshot from a pod that predates the fields falls back to the\n * legacy `state` (null status) rather than keeping a stale run status.\n *\n * Attention fields (`attention`, `next_action`, `attention_source`,\n * `is_favorited`) are taken from the entry when the publishing pod reports\n * them, so a live push flips a row to \"needs you\" the way it renames it; a\n * pod that predates them leaves the row's previous values. Pull-request state\n * and the last wake are never pushed and always keep the fetched value.\n */\nexport function summaryFromConversationIndexEntry(\n entry: ConversationIndexEntry,\n previous?: ConversationSummary\n): ConversationSummary {\n const carriesStatus = entry.run_status !== undefined || entry.session_status !== undefined;\n return {\n threadId: entry.thread_id,\n title: entry.title,\n updatedAt: entry.updated_at,\n createdAt: previous?.createdAt ?? null,\n state: entry.state,\n runtime: entry.runtime,\n sourceChannel: entry.start_channel,\n assetType: entry.asset_type,\n assetId: entry.asset_id ?? previous?.assetId ?? null,\n computerAssetId: previous?.computerAssetId ?? null,\n appId: entry.app_id,\n triggerType: entry.trigger_type,\n preview: entry.last_message_preview,\n messageCount: entry.message_count,\n isUnread: entry.is_unread ?? previous?.isUnread ?? false,\n sessionStatus: carriesStatus ? (entry.session_status ?? null) : null,\n runStatus: carriesStatus ? (entry.run_status ?? null) : null,\n attention: entry.attention !== undefined ? entry.attention : previous?.attention,\n nextAction: entry.next_action !== undefined ? entry.next_action : previous?.nextAction,\n attentionSource:\n entry.attention_source !== undefined ? entry.attention_source : previous?.attentionSource,\n isFavorited: entry.is_favorited ?? previous?.isFavorited,\n lastWake: previous?.lastWake,\n pullRequests: previous?.pullRequests,\n };\n}\n\n/**\n * Fold the user's replicated conversation index (the newest-page window the\n * per-user statewire pushes) into a cached live first page, so a thread\n * created, renamed, finished, or read anywhere updates every list surface in\n * the same frame — without refetching the page.\n *\n * - Rows in the window are upserted (new threads appear, existing rows take\n * the pushed fields, see {@link summaryFromConversationIndexEntry}).\n * - Rows NOT in the window but at least as new as the window's oldest row\n * should have been in it, so they left the list (archived, hidden, shared\n * away) and are dropped. Older rows are beyond the window and are kept.\n * - When the index reports no further rows (`indexHasMore === false`) the\n * window IS the whole list, so every row outside it is dropped.\n *\n * Newest-first ordering is restored and the page identity is preserved when\n * nothing changed, so an equal push never notifies subscribers.\n */\nexport function mergeConversationIndexIntoPage(\n page: ConversationListPage,\n entries: readonly ConversationIndexEntry[],\n options: { indexHasMore: boolean }\n): ConversationListPage {\n const previousByThread = new Map(page.items.map((item) => [item.threadId, item]));\n const inWindow = new Set(entries.map((entry) => entry.thread_id));\n const windowTailMs =\n entries.length > 0 ? Math.min(...entries.map((entry) => timestampMs(entry.updated_at))) : null;\n\n const merged: ConversationSummary[] = entries.map((entry) => {\n const previous = previousByThread.get(entry.thread_id);\n const next = summaryFromConversationIndexEntry(entry, previous);\n return previous && sameSummary(previous, next) ? previous : next;\n });\n\n for (const item of page.items) {\n if (inWindow.has(item.threadId)) continue;\n if (!options.indexHasMore) continue;\n if (windowTailMs !== null && timestampMs(item.updatedAt) >= windowTailMs) continue;\n merged.push(item);\n }\n\n merged.sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt));\n\n const unchanged =\n merged.length === page.items.length &&\n merged.every((item, index) => item === page.items[index]);\n if (unchanged) return page;\n\n return { ...page, items: merged };\n}\n"],"names":[],"mappings":"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"}
|