@athenaintel/react 0.41.1 → 0.41.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +12 -0
  2. package/dist/agent-runtime/src/durable-thread-snapshot-cache.cjs +158 -0
  3. package/dist/agent-runtime/src/durable-thread-snapshot-cache.cjs.map +1 -0
  4. package/dist/agent-runtime/src/durable-thread-snapshot-cache.js +158 -0
  5. package/dist/agent-runtime/src/durable-thread-snapshot-cache.js.map +1 -0
  6. package/dist/agent-runtime/src/protocol.cjs +15 -0
  7. package/dist/agent-runtime/src/protocol.cjs.map +1 -1
  8. package/dist/agent-runtime/src/protocol.js +16 -1
  9. package/dist/agent-runtime/src/protocol.js.map +1 -1
  10. package/dist/agent-runtime/src/thread-snapshot-cache.cjs +2 -1
  11. package/dist/agent-runtime/src/thread-snapshot-cache.cjs.map +1 -1
  12. package/dist/agent-runtime/src/thread-snapshot-cache.js +2 -1
  13. package/dist/agent-runtime/src/thread-snapshot-cache.js.map +1 -1
  14. package/dist/agent-runtime/src/thread-snapshot-mirror.cjs +400 -0
  15. package/dist/agent-runtime/src/thread-snapshot-mirror.cjs.map +1 -0
  16. package/dist/agent-runtime/src/thread-snapshot-mirror.js +400 -0
  17. package/dist/agent-runtime/src/thread-snapshot-mirror.js.map +1 -0
  18. package/dist/chat/AthenaChat.cjs +5 -1
  19. package/dist/chat/AthenaChat.cjs.map +1 -1
  20. package/dist/chat/AthenaChat.js +5 -1
  21. package/dist/chat/AthenaChat.js.map +1 -1
  22. package/dist/chat/super-grouping/activity-timeline.cjs +4 -3
  23. package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
  24. package/dist/chat/super-grouping/activity-timeline.d.ts +7 -1
  25. package/dist/chat/super-grouping/activity-timeline.js +4 -3
  26. package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
  27. package/dist/index.cjs +5 -0
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +5 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/offline/offline-history.cjs +77 -0
  33. package/dist/offline/offline-history.cjs.map +1 -0
  34. package/dist/offline/offline-history.d.ts +60 -0
  35. package/dist/offline/offline-history.js +77 -0
  36. package/dist/offline/offline-history.js.map +1 -0
  37. package/dist/provider/AthenaProvider.cjs +46 -3
  38. package/dist/provider/AthenaProvider.cjs.map +1 -1
  39. package/dist/provider/AthenaProvider.d.ts +8 -1
  40. package/dist/provider/AthenaProvider.js +46 -3
  41. package/dist/provider/AthenaProvider.js.map +1 -1
  42. package/dist/react/package.json.cjs +1 -1
  43. package/dist/react/package.json.js +1 -1
  44. package/dist/runtime/useAthenaStatewireRuntime.cjs +4 -14
  45. package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
  46. package/dist/runtime/useAthenaStatewireRuntime.d.ts +5 -4
  47. package/dist/runtime/useAthenaStatewireRuntime.js +4 -14
  48. package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
  49. package/dist/threads/offline-session-list.cjs +75 -0
  50. package/dist/threads/offline-session-list.cjs.map +1 -0
  51. package/dist/threads/offline-session-list.d.ts +27 -0
  52. package/dist/threads/offline-session-list.js +75 -0
  53. package/dist/threads/offline-session-list.js.map +1 -0
  54. package/dist/threads/statewire-thread-list.cjs +63 -4
  55. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  56. package/dist/threads/statewire-thread-list.d.ts +21 -2
  57. package/dist/threads/statewire-thread-list.js +64 -5
  58. package/dist/threads/statewire-thread-list.js.map +1 -1
  59. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"useAthenaStatewireRuntime.js","sources":["../../src/runtime/useAthenaStatewireRuntime.ts"],"sourcesContent":["import type { AssistantClient, Toolkit } from '@assistant-ui/react';\nimport { Tools, useAui } from '@assistant-ui/react';\nimport {\n DEEP_AGENT_DEFAULT_MESSAGE_WINDOW,\n type DeepAgentMessageWindowDeclaration,\n type DeepAgentStreamValues,\n type DeepAgentThreadElement,\n type DeepAgentThreadSnapshotCache,\n type DeepAgentToolOutputStore,\n isDeepAgentStateHydrated,\n isStatewireChannelTeardown,\n logUnhandledStatewireError,\n type RegisteredClientTool,\n useDeepAgentRuntime,\n type UseDeepAgentThreadOptions,\n withStagedComposerMarkdown,\n} from '@athenaintel/agent-runtime';\nimport { useContext, useMemo, useRef } from 'react';\nimport { AthenaAuthContext } from '../auth/AthenaAuthContext';\nimport {\n collectStatewireClientTools,\n statewireClientToolWireEntries,\n} from '../chat/statewire-client-tools';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport {\n type AthenaStatewireTokenGetter,\n resolveStatewireAuthHeaders,\n} from './statewire-auth-headers';\nimport { statewireSessionKey } from './statewire-session-key';\n\ntype AthenaDiagnosticsSpanStarter = typeof athenaDiagnostics.span;\nimport {\n buildStatewireRunConfig,\n DEFAULT_STATEWIRE_MODEL,\n resolveAgentProp,\n} from './statewire-run-config';\n\nexport { DEFAULT_STATEWIRE_MODEL };\n\n/**\n * The head interest every SDK attach states (`athena/interest`): the host\n * mounts the transcript's tail as message documents instead of every message\n * — megabytes on long threads, over the 1 MiB frame limit of stock websocket\n * clients. Older pages load from the `StatewireHistoryEdge` row the chat UI\n * mounts above the messages, and cut tool outputs are fetched back for the\n * cards that show them.\n */\nexport const ATHENA_SDK_MESSAGE_WINDOW: DeepAgentMessageWindowDeclaration = {\n size: DEEP_AGENT_DEFAULT_MESSAGE_WINDOW,\n};\n\nexport interface AthenaStatewireRuntimeConfig {\n /** Base URL of the statewire sync host (the `…/v2/threads` mount). */\n syncUrl: string;\n /** Thread ID the statewire session attaches to. */\n threadId: string;\n /** True only for a never-started local chat. Existing (or pre-assigned)\n * threads load until their server snapshot arrives. */\n isNewChat?: boolean;\n /** API key for authentication. Used when no auth token is provided. */\n apiKey?: string;\n /** Auth token. Takes priority over apiKey. */\n token?: string | null;\n /** Resolve the latest token immediately before a request (for expiring\n * clients). Called with `{ force: true }` right after the host refused the\n * previous credential (the transport's `staleAuth`), so a caching getter\n * mints a fresh token instead of returning the refused one. */\n getToken?: AthenaStatewireTokenGetter;\n /**\n * Per-provider LRU of each thread's last wire state. A switch back to a\n * remembered thread mounts with that snapshot and projects ready on the\n * first frame instead of a loading treatment; the attach snapshot then\n * replaces it. Owned above the per-thread mount so it survives switches.\n */\n snapshotCache?: DeepAgentThreadSnapshotCache;\n /** The head interest stated on the attach (see {@link ATHENA_SDK_MESSAGE_WINDOW}, the default). */\n messageWindow?: DeepAgentMessageWindowDeclaration;\n /**\n * Full tool outputs fetched back for a windowed wire's cut tool messages;\n * the thread re-converts when one lands. `AthenaProvider` owns one per\n * provider and hands the same store to `StatewireHistoryEdge`.\n */\n toolOutputs?: DeepAgentToolOutputStore;\n /** LLM model identifier. Defaults to the deep-agent default model. */\n model?: string;\n /** Agent selector: only `collab_agent:<asset_id>` refs are accepted (the run\n * executes AS that published collab agent). Any other non-empty value\n * throws — see `resolveAgentProp`. */\n agent?: string;\n /** Collab-agent channel id selecting that channel's overrides. */\n channel?: string;\n /** The agent's mode picked for this thread (`AthenaProvider` `modes`). */\n mode?: string;\n /** Backend toolkit IDs to enable. */\n tools?: string[];\n /** Frontend tool IDs to include in enabled_tools. */\n frontendToolIds?: string[];\n /** Frontend tools registered on the assistant client. Tools with a local\n * `execute` are also declared as `client_tools` and executed through the\n * statewire client-tool bridge. */\n frontendToolkit?: Toolkit;\n /** Asset IDs to include in the workbench. */\n workbench?: string[];\n /** Knowledge base IDs for RAG context. */\n knowledgeBase?: string[];\n /** System prompt override. */\n systemPrompt?: string;\n /** Client app identifier for session filtering. Sent as app_id in runConfig.custom. */\n appId?: string;\n /** Additional fields merged into runConfig.custom. */\n extraRunConfig?: Record<string, unknown>;\n /** Called on statewire transport errors (send rejections, stream failures). */\n onError?: (error: Error) => void;\n /** Projected connection status changes (deduped; null on unmount). */\n onConnectionChange?: UseDeepAgentThreadOptions['onConnectionChange'];\n /** Raw connection observer — carries the transport's reconnect handle. */\n onRawConnectionChange?: UseDeepAgentThreadOptions['onRawConnectionChange'];\n /** Notified when a run starts/stops. */\n onRunningChange?: UseDeepAgentThreadOptions['onRunningChange'];\n /**\n * Observes the host's own state: called once the attach's hello has landed\n * (with the snapshot it carried) and then on every later frame — never with\n * the placeholder or remembered snapshot a thread renders before its hello.\n * Called per streamed token, so keep it cheap.\n */\n onServerState?: (state: DeepAgentStreamValues) => void;\n}\n\nconst EMPTY_TOOLKIT: Toolkit = {};\nconst EMPTY_TOOL_IDS: string[] = [];\n\n/** The client tools derived from the runtime's frontend toolkit — exposed so\n * the provider can mount the client-tool bridge with the same set. */\nexport function useStatewireClientTools(\n frontendToolkit: Toolkit | undefined,\n): RegisteredClientTool[] {\n return useMemo(\n () => collectStatewireClientTools(frontendToolkit ?? EMPTY_TOOLKIT),\n [frontendToolkit],\n );\n}\n\n/**\n * Statewire runtime for the Athena SDK — a thin shim over the shared\n * `@athenaintel/agent-runtime` `useDeepAgentRuntime`: it attaches to the\n * agora-hosted deep-agent thread at `${syncUrl}/${threadId}` and returns the\n * `AssistantClient` to mount via `AuiProvider` — the same integration shape as\n * the Olympus, Chrome extension, Office, and mobile statewire hosts.\n *\n * Auth: a token (resolved live through `getToken` when provided) rides\n * `Authorization: Bearer`, and an API key falls back to `X-API-KEY` — both\n * accepted by the statewire host.\n */\nexport function useAthenaStatewireRuntime(\n config: AthenaStatewireRuntimeConfig,\n): AssistantClient {\n const {\n syncUrl,\n threadId,\n isNewChat = false,\n apiKey,\n token,\n getToken,\n model,\n agent,\n channel,\n mode,\n tools = EMPTY_TOOL_IDS,\n frontendToolIds = EMPTY_TOOL_IDS,\n frontendToolkit = EMPTY_TOOLKIT,\n workbench,\n knowledgeBase,\n systemPrompt,\n appId,\n extraRunConfig,\n snapshotCache,\n messageWindow = ATHENA_SDK_MESSAGE_WINDOW,\n toolOutputs,\n onError,\n onConnectionChange,\n onRawConnectionChange,\n onRunningChange,\n onServerState,\n } = config;\n\n // Fail loud at mount, not at first send: the statewire host always runs the\n // Athena deep agent, so an `agent` outside the collab_agent namespace can\n // never apply. Throwing here (render time) is what makes a misconfigured\n // integration visible instead of quietly answering as the wrong agent.\n resolveAgentProp({ agent });\n\n const clientTools = useStatewireClientTools(frontendToolkit);\n\n // The persisted command lane is keyed by WHO is signed in as well as by host\n // and thread (see `statewireSessionKey`).\n const authContext = useContext(AthenaAuthContext);\n const principal = authContext?.user?.userId ?? '';\n\n // Attach → first server snapshot span, keyed per thread attach.\n const attachSpanRef = useRef<ReturnType<AthenaDiagnosticsSpanStarter> | null>(null);\n const attachThreadRef = useRef<string | null>(null);\n if (attachThreadRef.current !== threadId) {\n attachThreadRef.current = threadId;\n attachSpanRef.current = athenaDiagnostics.span(\n `statewire.attach:${threadId}`,\n 'statewire.attach.start',\n { thread_id: threadId, is_new_chat: isNewChat },\n );\n }\n\n // Built every render so its JSON signature can key the model-context\n // registration below: consumers commonly pass fresh array literals per\n // render, which must not thrash registration, while a changed value (a new\n // `model`, channel, tool list, system prompt, …) must re-sync the context\n // before the next send. The core pulls this value only on model-context\n // notifications, which a key change triggers.\n const runConfigValue = buildStatewireRunConfig({\n model,\n agent,\n channel,\n mode,\n tools,\n frontendToolIds,\n workbench,\n knowledgeBase,\n systemPrompt,\n appId,\n extraRunConfig,\n clientTools: statewireClientToolWireEntries(clientTools),\n });\n const runConfigKey = JSON.stringify(runConfigValue);\n\n // The host's state, told apart from what the thread renders before its\n // hello: statewire publishes the initial (placeholder or remembered) state\n // through `onStateChange` too, and reports `connected` in the same flush\n // right after publishing the state the hello carried. So the latest state\n // seen when the connection first turns `connected` is the hello's, and every\n // state after it is the host's. Refs are per attach: the runtime is mounted\n // per thread.\n const lastStateRef = useRef<DeepAgentStreamValues | null>(null);\n const helloLandedRef = useRef(false);\n\n const auiTools = useMemo(() => Tools({ toolkit: frontendToolkit }), [frontendToolkit]);\n // Web client for the shared runtime core (a `use*` name so the\n // rules-of-hooks lint sees the `useAui` call inside a custom hook; the core\n // invokes it unconditionally every render).\n const useSdkClient = (thread: DeepAgentThreadElement) => useAui({ thread, tools: auiTools });\n\n return useDeepAgentRuntime({\n surface: 'AthenaSDK',\n threadId,\n baseUrl: syncUrl,\n // Option closures are re-read per render by the core, so credential\n // rotation (new `token`/`getToken`/`apiKey` props) reaches the next\n // request without remounting the thread. The transport's `staleAuth`\n // (true right after a refused credential) is forwarded as `force`.\n headers: (ctx) => resolveStatewireAuthHeaders({ apiKey, token, getToken }, ctx),\n isNewChat,\n // Queued entries render in the SDK's queue panel, so only the steer lane\n // projects into the message tail.\n laneProjection: 'steer-only',\n // Durable session: a reload resumes the lane and resends whatever the\n // server has not marked durable. Scoped by host AND principal — a thread\n // id is only meaningful against the backend that minted it, and a stored\n // lane must never resend against another signed-in user. (Consumers\n // server-render this SDK; the core's availability probe keeps those\n // sessions process-local, as before the seam existed.)\n session: {\n kind: 'local-storage',\n key: (id) => statewireSessionKey({ syncUrl, principal, threadId: id }),\n },\n snapshotCache,\n // Head interest (see `ATHENA_SDK_MESSAGE_WINDOW`): the tail of the\n // transcript instead of every message; the chat UI's history edge pages\n // older ranges in and fetches cut tool outputs into `toolOutputs`.\n messageWindow,\n toolOutputs,\n capabilities: { edit: true, reload: true, continue: true },\n // The composer stages the draft each send came from right before it\n // sends; the message carries it as its composer markdown, so its bubble\n // shows the text exactly as typed (`UserMessageText`).\n getMessageMetadata: withStagedComposerMarkdown({ threadId }),\n useClient: useSdkClient,\n onStateChange: (state) => {\n // First snapshot for this attach ends the attach span (time-to-history).\n const endSpan = attachSpanRef.current;\n if (endSpan) {\n attachSpanRef.current = null;\n endSpan('statewire.snapshot', { data: { thread_id: threadId } });\n }\n // A replay boundary republishes the pre-stream default: not host state.\n if (!isDeepAgentStateHydrated(state)) return;\n lastStateRef.current = state;\n if (helloLandedRef.current) onServerState?.(state);\n },\n onConnectionChange: (connection) => {\n athenaDiagnostics.gauge('statewire.connection', connection?.status ?? null);\n if (connection?.status === 'stopped') {\n athenaDiagnostics.emit('statewire.connection', {\n level: 'warn',\n data: {\n status: connection.status,\n reason: connection.reason,\n message: connection.message,\n },\n });\n }\n onConnectionChange?.(connection);\n },\n onRawConnectionChange: (connection) => {\n if (connection.status === 'connected' && !helloLandedRef.current) {\n helloLandedRef.current = true;\n const helloState = lastStateRef.current;\n if (helloState) onServerState?.(helloState);\n }\n onRawConnectionChange?.(connection);\n },\n onRunningChange,\n // Each model-context notification pulls the latest getter through the\n // core's per-render ref; the value-stable key re-registers (and so\n // notifies) only when the built runConfig actually changed. On a thread\n // with a server config lane, agora keeps only the per-send keys of what\n // this carries; `StatewireThreadConfigLaneSync` writes a changed model,\n // system prompt, tool list, workbench or knowledge base to the lane itself.\n runConfig: {\n get: () => runConfigValue,\n key: runConfigKey,\n },\n onError: (error) => {\n if (!isStatewireChannelTeardown(error)) {\n athenaDiagnostics.error(\n 'statewire.error',\n error,\n {\n code: 'statewire_rejected',\n message: 'The statewire host rejected a command',\n hint: 'Inspect error.detail — agora rejections carry a structured {code, message}.',\n },\n { thread_id: threadId },\n );\n }\n if (onError) {\n onError(error);\n return;\n }\n logUnhandledStatewireError('AthenaSDK', error);\n },\n });\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useAthenaStatewireRuntime.js","sources":["../../src/runtime/useAthenaStatewireRuntime.ts"],"sourcesContent":["import type { AssistantClient, Toolkit } from '@assistant-ui/react';\nimport { Tools, useAui } from '@assistant-ui/react';\nimport {\n DEEP_AGENT_DEFAULT_MESSAGE_WINDOW,\n type DeepAgentMessageWindowDeclaration,\n type DeepAgentStreamValues,\n type DeepAgentThreadElement,\n type DeepAgentThreadSnapshotCache,\n type DeepAgentToolOutputStore,\n isDeepAgentHostState,\n isStatewireChannelTeardown,\n logUnhandledStatewireError,\n type RegisteredClientTool,\n useDeepAgentRuntime,\n type UseDeepAgentThreadOptions,\n withStagedComposerMarkdown,\n} from '@athenaintel/agent-runtime';\nimport { useContext, useMemo, useRef } from 'react';\nimport { AthenaAuthContext } from '../auth/AthenaAuthContext';\nimport {\n collectStatewireClientTools,\n statewireClientToolWireEntries,\n} from '../chat/statewire-client-tools';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport {\n type AthenaStatewireTokenGetter,\n resolveStatewireAuthHeaders,\n} from './statewire-auth-headers';\nimport { statewireSessionKey } from './statewire-session-key';\n\ntype AthenaDiagnosticsSpanStarter = typeof athenaDiagnostics.span;\nimport {\n buildStatewireRunConfig,\n DEFAULT_STATEWIRE_MODEL,\n resolveAgentProp,\n} from './statewire-run-config';\n\nexport { DEFAULT_STATEWIRE_MODEL };\n\n/**\n * The head interest every SDK attach states (`athena/interest`): the host\n * mounts the transcript's tail as message documents instead of every message\n * — megabytes on long threads, over the 1 MiB frame limit of stock websocket\n * clients. Older pages load from the `StatewireHistoryEdge` row the chat UI\n * mounts above the messages, and cut tool outputs are fetched back for the\n * cards that show them.\n */\nexport const ATHENA_SDK_MESSAGE_WINDOW: DeepAgentMessageWindowDeclaration = {\n size: DEEP_AGENT_DEFAULT_MESSAGE_WINDOW,\n};\n\nexport interface AthenaStatewireRuntimeConfig {\n /** Base URL of the statewire sync host (the `…/v2/threads` mount). */\n syncUrl: string;\n /** Thread ID the statewire session attaches to. */\n threadId: string;\n /** True only for a never-started local chat. Existing (or pre-assigned)\n * threads load until their server snapshot arrives. */\n isNewChat?: boolean;\n /** API key for authentication. Used when no auth token is provided. */\n apiKey?: string;\n /** Auth token. Takes priority over apiKey. */\n token?: string | null;\n /** Resolve the latest token immediately before a request (for expiring\n * clients). Called with `{ force: true }` right after the host refused the\n * previous credential (the transport's `staleAuth`), so a caching getter\n * mints a fresh token instead of returning the refused one. */\n getToken?: AthenaStatewireTokenGetter;\n /**\n * Per-provider LRU of each thread's last wire state. A switch back to a\n * remembered thread mounts with that snapshot and projects ready on the\n * first frame instead of a loading treatment; the attach snapshot then\n * replaces it. Owned above the per-thread mount so it survives switches.\n */\n snapshotCache?: DeepAgentThreadSnapshotCache;\n /** The head interest stated on the attach (see {@link ATHENA_SDK_MESSAGE_WINDOW}, the default). */\n messageWindow?: DeepAgentMessageWindowDeclaration;\n /**\n * Full tool outputs fetched back for a windowed wire's cut tool messages;\n * the thread re-converts when one lands. `AthenaProvider` owns one per\n * provider and hands the same store to `StatewireHistoryEdge`.\n */\n toolOutputs?: DeepAgentToolOutputStore;\n /** LLM model identifier. Defaults to the deep-agent default model. */\n model?: string;\n /** Agent selector: only `collab_agent:<asset_id>` refs are accepted (the run\n * executes AS that published collab agent). Any other non-empty value\n * throws — see `resolveAgentProp`. */\n agent?: string;\n /** Collab-agent channel id selecting that channel's overrides. */\n channel?: string;\n /** The agent's mode picked for this thread (`AthenaProvider` `modes`). */\n mode?: string;\n /** Backend toolkit IDs to enable. */\n tools?: string[];\n /** Frontend tool IDs to include in enabled_tools. */\n frontendToolIds?: string[];\n /** Frontend tools registered on the assistant client. Tools with a local\n * `execute` are also declared as `client_tools` and executed through the\n * statewire client-tool bridge. */\n frontendToolkit?: Toolkit;\n /** Asset IDs to include in the workbench. */\n workbench?: string[];\n /** Knowledge base IDs for RAG context. */\n knowledgeBase?: string[];\n /** System prompt override. */\n systemPrompt?: string;\n /** Client app identifier for session filtering. Sent as app_id in runConfig.custom. */\n appId?: string;\n /** Additional fields merged into runConfig.custom. */\n extraRunConfig?: Record<string, unknown>;\n /** Called on statewire transport errors (send rejections, stream failures). */\n onError?: (error: Error) => void;\n /** Projected connection status changes (deduped; null on unmount). */\n onConnectionChange?: UseDeepAgentThreadOptions['onConnectionChange'];\n /** Raw connection observer — carries the transport's reconnect handle. */\n onRawConnectionChange?: UseDeepAgentThreadOptions['onRawConnectionChange'];\n /** Notified when a run starts/stops. */\n onRunningChange?: UseDeepAgentThreadOptions['onRunningChange'];\n /**\n * Observes the host's own state: called with the attach snapshot and then on\n * every later frame (`isDeepAgentHostState`) — never with the placeholder or\n * remembered snapshot a thread renders before its snapshot applies, which\n * can be after the connection reports `connected`. Called per streamed\n * token, so keep it cheap.\n */\n onServerState?: (state: DeepAgentStreamValues) => void;\n}\n\nconst EMPTY_TOOLKIT: Toolkit = {};\nconst EMPTY_TOOL_IDS: string[] = [];\n\n/** The client tools derived from the runtime's frontend toolkit — exposed so\n * the provider can mount the client-tool bridge with the same set. */\nexport function useStatewireClientTools(\n frontendToolkit: Toolkit | undefined,\n): RegisteredClientTool[] {\n return useMemo(\n () => collectStatewireClientTools(frontendToolkit ?? EMPTY_TOOLKIT),\n [frontendToolkit],\n );\n}\n\n/**\n * Statewire runtime for the Athena SDK — a thin shim over the shared\n * `@athenaintel/agent-runtime` `useDeepAgentRuntime`: it attaches to the\n * agora-hosted deep-agent thread at `${syncUrl}/${threadId}` and returns the\n * `AssistantClient` to mount via `AuiProvider` — the same integration shape as\n * the Olympus, Chrome extension, Office, and mobile statewire hosts.\n *\n * Auth: a token (resolved live through `getToken` when provided) rides\n * `Authorization: Bearer`, and an API key falls back to `X-API-KEY` — both\n * accepted by the statewire host.\n */\nexport function useAthenaStatewireRuntime(\n config: AthenaStatewireRuntimeConfig,\n): AssistantClient {\n const {\n syncUrl,\n threadId,\n isNewChat = false,\n apiKey,\n token,\n getToken,\n model,\n agent,\n channel,\n mode,\n tools = EMPTY_TOOL_IDS,\n frontendToolIds = EMPTY_TOOL_IDS,\n frontendToolkit = EMPTY_TOOLKIT,\n workbench,\n knowledgeBase,\n systemPrompt,\n appId,\n extraRunConfig,\n snapshotCache,\n messageWindow = ATHENA_SDK_MESSAGE_WINDOW,\n toolOutputs,\n onError,\n onConnectionChange,\n onRawConnectionChange,\n onRunningChange,\n onServerState,\n } = config;\n\n // Fail loud at mount, not at first send: the statewire host always runs the\n // Athena deep agent, so an `agent` outside the collab_agent namespace can\n // never apply. Throwing here (render time) is what makes a misconfigured\n // integration visible instead of quietly answering as the wrong agent.\n resolveAgentProp({ agent });\n\n const clientTools = useStatewireClientTools(frontendToolkit);\n\n // The persisted command lane is keyed by WHO is signed in as well as by host\n // and thread (see `statewireSessionKey`).\n const authContext = useContext(AthenaAuthContext);\n const principal = authContext?.user?.userId ?? '';\n\n // Attach → first server snapshot span, keyed per thread attach.\n const attachSpanRef = useRef<ReturnType<AthenaDiagnosticsSpanStarter> | null>(null);\n const attachThreadRef = useRef<string | null>(null);\n if (attachThreadRef.current !== threadId) {\n attachThreadRef.current = threadId;\n attachSpanRef.current = athenaDiagnostics.span(\n `statewire.attach:${threadId}`,\n 'statewire.attach.start',\n { thread_id: threadId, is_new_chat: isNewChat },\n );\n }\n\n // Built every render so its JSON signature can key the model-context\n // registration below: consumers commonly pass fresh array literals per\n // render, which must not thrash registration, while a changed value (a new\n // `model`, channel, tool list, system prompt, …) must re-sync the context\n // before the next send. The core pulls this value only on model-context\n // notifications, which a key change triggers.\n const runConfigValue = buildStatewireRunConfig({\n model,\n agent,\n channel,\n mode,\n tools,\n frontendToolIds,\n workbench,\n knowledgeBase,\n systemPrompt,\n appId,\n extraRunConfig,\n clientTools: statewireClientToolWireEntries(clientTools),\n });\n const runConfigKey = JSON.stringify(runConfigValue);\n\n const auiTools = useMemo(() => Tools({ toolkit: frontendToolkit }), [frontendToolkit]);\n // Web client for the shared runtime core (a `use*` name so the\n // rules-of-hooks lint sees the `useAui` call inside a custom hook; the core\n // invokes it unconditionally every render).\n const useSdkClient = (thread: DeepAgentThreadElement) => useAui({ thread, tools: auiTools });\n\n return useDeepAgentRuntime({\n surface: 'AthenaSDK',\n threadId,\n baseUrl: syncUrl,\n // Option closures are re-read per render by the core, so credential\n // rotation (new `token`/`getToken`/`apiKey` props) reaches the next\n // request without remounting the thread. The transport's `staleAuth`\n // (true right after a refused credential) is forwarded as `force`.\n headers: (ctx) => resolveStatewireAuthHeaders({ apiKey, token, getToken }, ctx),\n isNewChat,\n // Queued entries render in the SDK's queue panel, so only the steer lane\n // projects into the message tail.\n laneProjection: 'steer-only',\n // Durable session: a reload resumes the lane and resends whatever the\n // server has not marked durable. Scoped by host AND principal — a thread\n // id is only meaningful against the backend that minted it, and a stored\n // lane must never resend against another signed-in user. (Consumers\n // server-render this SDK; the core's availability probe keeps those\n // sessions process-local, as before the seam existed.)\n session: {\n kind: 'local-storage',\n key: (id) => statewireSessionKey({ syncUrl, principal, threadId: id }),\n },\n snapshotCache,\n // Head interest (see `ATHENA_SDK_MESSAGE_WINDOW`): the tail of the\n // transcript instead of every message; the chat UI's history edge pages\n // older ranges in and fetches cut tool outputs into `toolOutputs`.\n messageWindow,\n toolOutputs,\n capabilities: { edit: true, reload: true, continue: true },\n // The composer stages the draft each send came from right before it\n // sends; the message carries it as its composer markdown, so its bubble\n // shows the text exactly as typed (`UserMessageText`).\n getMessageMetadata: withStagedComposerMarkdown({ threadId }),\n useClient: useSdkClient,\n onStateChange: (state) => {\n // Statewire publishes what the thread renders before its attach snapshot\n // through here too (the placeholder, a remembered snapshot, and a replay\n // boundary's pre-stream default), and on a thread mid-run it reports\n // `connected` before the snapshot applies: only a state the host sent\n // counts, whatever the connection says.\n if (!isDeepAgentHostState(state)) return;\n // The attach's first one ends the attach span (time-to-history).\n const endSpan = attachSpanRef.current;\n if (endSpan) {\n attachSpanRef.current = null;\n endSpan('statewire.snapshot', { data: { thread_id: threadId } });\n }\n onServerState?.(state);\n },\n onConnectionChange: (connection) => {\n athenaDiagnostics.gauge('statewire.connection', connection?.status ?? null);\n if (connection?.status === 'stopped') {\n athenaDiagnostics.emit('statewire.connection', {\n level: 'warn',\n data: {\n status: connection.status,\n reason: connection.reason,\n message: connection.message,\n },\n });\n }\n onConnectionChange?.(connection);\n },\n onRawConnectionChange,\n onRunningChange,\n // Each model-context notification pulls the latest getter through the\n // core's per-render ref; the value-stable key re-registers (and so\n // notifies) only when the built runConfig actually changed. On a thread\n // with a server config lane, agora keeps only the per-send keys of what\n // this carries; `StatewireThreadConfigLaneSync` writes a changed model,\n // system prompt, tool list, workbench or knowledge base to the lane itself.\n runConfig: {\n get: () => runConfigValue,\n key: runConfigKey,\n },\n onError: (error) => {\n if (!isStatewireChannelTeardown(error)) {\n athenaDiagnostics.error(\n 'statewire.error',\n error,\n {\n code: 'statewire_rejected',\n message: 'The statewire host rejected a command',\n hint: 'Inspect error.detail — agora rejections carry a structured {code, message}.',\n },\n { thread_id: threadId },\n );\n }\n if (onError) {\n onError(error);\n return;\n }\n logUnhandledStatewireError('AthenaSDK', error);\n },\n });\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const react = require("react");
4
+ const conversationList = require("../agent-runtime/src/conversation-list.cjs");
5
+ const OFFLINE_SESSION_LIST_KEY = "session-list";
6
+ const MAX_OFFLINE_SESSION_LIST_PAGES = 4;
7
+ const RESTORED_UPDATED_AT = 1;
8
+ function isSessionListFirstPage(queryKey) {
9
+ return queryKey[0] === conversationList.CONVERSATION_LIST_QUERY_KEY_ROOT && queryKey[4] === "first-page";
10
+ }
11
+ function isFirstPageKey(queryKey) {
12
+ return Array.isArray(queryKey) && queryKey.length === 5 && isSessionListFirstPage(queryKey) && typeof queryKey[1] === "string" && typeof queryKey[2] === "string" && typeof queryKey[3] === "number";
13
+ }
14
+ function isConversationRow(row) {
15
+ if (typeof row !== "object" || row === null) return false;
16
+ const { threadId, title, updatedAt } = row;
17
+ return typeof threadId === "string" && typeof title === "string" && typeof updatedAt === "string";
18
+ }
19
+ function isConversationListPage(value) {
20
+ if (typeof value !== "object" || value === null) return false;
21
+ const { items, hasMore, totalCount } = value;
22
+ return Array.isArray(items) && items.every(isConversationRow) && typeof hasMore === "boolean" && (totalCount === null || typeof totalCount === "number");
23
+ }
24
+ function isKeptPage(value) {
25
+ if (typeof value !== "object" || value === null) return false;
26
+ const { queryKey, data } = value;
27
+ return isFirstPageKey(queryKey) && isConversationListPage(data);
28
+ }
29
+ function useOfflineSessionList({
30
+ queryClient,
31
+ offlineList
32
+ }) {
33
+ const mirror = offlineList == null ? void 0 : offlineList.mirror;
34
+ const ownerId = offlineList == null ? void 0 : offlineList.ownerId;
35
+ const boundOwner = react.useRef(null);
36
+ react.useEffect(() => {
37
+ if (!mirror || !ownerId) return;
38
+ if (boundOwner.current !== null && boundOwner.current !== ownerId) {
39
+ void queryClient.cancelQueries({ queryKey: [conversationList.CONVERSATION_LIST_QUERY_KEY_ROOT] });
40
+ queryClient.removeQueries({ queryKey: [conversationList.CONVERSATION_LIST_QUERY_KEY_ROOT] });
41
+ }
42
+ boundOwner.current = ownerId;
43
+ const boundAt = Date.now();
44
+ let stopped = false;
45
+ void mirror.loadList({ ownerId, listKey: OFFLINE_SESSION_LIST_KEY }).then((kept) => {
46
+ if (stopped || !kept || !Array.isArray(kept.value)) return;
47
+ for (const page of kept.value) {
48
+ if (!isKeptPage(page)) continue;
49
+ if (queryClient.getQueryData(page.queryKey) !== void 0) continue;
50
+ queryClient.setQueryData(page.queryKey, page.data, { updatedAt: RESTORED_UPDATED_AT });
51
+ }
52
+ });
53
+ const write = () => {
54
+ const pages = queryClient.getQueryCache().findAll({ predicate: (query) => isSessionListFirstPage(query.queryKey) }).filter(
55
+ (query) => query.state.status === "success" && query.state.dataUpdatedAt >= boundAt && isConversationListPage(query.state.data)
56
+ ).sort((a, b) => b.state.dataUpdatedAt - a.state.dataUpdatedAt).slice(0, MAX_OFFLINE_SESSION_LIST_PAGES).map((query) => ({ queryKey: query.queryKey, data: query.state.data }));
57
+ if (pages.length > 0) mirror.writeList({ ownerId, listKey: OFFLINE_SESSION_LIST_KEY, value: pages });
58
+ };
59
+ const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
60
+ if (event.type !== "updated" || event.action.type !== "success") return;
61
+ if (!isSessionListFirstPage(event.query.queryKey)) return;
62
+ if (event.query.state.dataUpdatedAt === RESTORED_UPDATED_AT) return;
63
+ write();
64
+ });
65
+ return () => {
66
+ stopped = true;
67
+ unsubscribe();
68
+ };
69
+ }, [queryClient, mirror, ownerId]);
70
+ }
71
+ exports.MAX_OFFLINE_SESSION_LIST_PAGES = MAX_OFFLINE_SESSION_LIST_PAGES;
72
+ exports.OFFLINE_SESSION_LIST_KEY = OFFLINE_SESSION_LIST_KEY;
73
+ exports.isSessionListFirstPage = isSessionListFirstPage;
74
+ exports.useOfflineSessionList = useOfflineSessionList;
75
+ //# sourceMappingURL=offline-session-list.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-session-list.cjs","sources":["../../src/threads/offline-session-list.ts"],"sourcesContent":["import {\n CONVERSATION_LIST_QUERY_KEY_ROOT,\n type ConversationListPage,\n type ThreadSnapshotMirror,\n} from '@athenaintel/agent-runtime';\nimport type { QueryClient, QueryKey } from '@tanstack/react-query';\nimport { useEffect, useRef } from 'react';\n\n/** Under this name the list's pages are kept, per owner. */\nexport const OFFLINE_SESSION_LIST_KEY = 'session-list';\n/** The first pages kept: one per filter the list was read with, the newest first. */\nexport const MAX_OFFLINE_SESSION_LIST_PAGES = 4;\n/**\n * Restored pages are stamped this old, so they paint at once and are read again on mount whenever\n * Athena can be reached (and never replace a page already read in this provider).\n */\nconst RESTORED_UPDATED_AT = 1;\n\ninterface KeptPage {\n queryKey: QueryKey;\n data: ConversationListPage;\n}\n\n/** A conversation list's live first page (`conversationListQueryKeys.firstPage`). */\nexport function isSessionListFirstPage(queryKey: QueryKey): boolean {\n return queryKey[0] === CONVERSATION_LIST_QUERY_KEY_ROOT && queryKey[4] === 'first-page';\n}\n\n/** Exactly `conversationListQueryKeys.firstPage(scopeKey, filterKey, pageSize)`. */\nfunction isFirstPageKey(queryKey: unknown): queryKey is QueryKey {\n return (\n Array.isArray(queryKey) &&\n queryKey.length === 5 &&\n isSessionListFirstPage(queryKey) &&\n typeof queryKey[1] === 'string' &&\n typeof queryKey[2] === 'string' &&\n typeof queryKey[3] === 'number'\n );\n}\n\nfunction isConversationRow(row: unknown): boolean {\n if (typeof row !== 'object' || row === null) return false;\n const { threadId, title, updatedAt } = row as Record<string, unknown>;\n return typeof threadId === 'string' && typeof title === 'string' && typeof updatedAt === 'string';\n}\n\n/** A page the list can render: its rows carry the fields every row reads. */\nfunction isConversationListPage(value: unknown): value is ConversationListPage {\n if (typeof value !== 'object' || value === null) return false;\n const { items, hasMore, totalCount } = value as Record<string, unknown>;\n return (\n Array.isArray(items) &&\n items.every(isConversationRow) &&\n typeof hasMore === 'boolean' &&\n (totalCount === null || typeof totalCount === 'number')\n );\n}\n\nfunction isKeptPage(value: unknown): value is KeptPage {\n if (typeof value !== 'object' || value === null) return false;\n const { queryKey, data } = value as Record<string, unknown>;\n return isFirstPageKey(queryKey) && isConversationListPage(data);\n}\n\n/**\n * The session list's first pages, kept on this device sealed under the host's key\n * (`offlineList`'s mirror, `AthenaProvider`'s `offlineHistory`): restored into the list's own query\n * client before it is read, so the list paints at once and reads while Athena can't be reached,\n * and written back (debounced by the mirror) each time a first page is read. Without `offlineList`,\n * nothing.\n *\n * Owner-bound. A list kept for one owner is never restored for another, and a change of owner\n * drops every page of the list's cache before the new owner's are restored (in API-key mode two\n * people read the same list keys); only pages read since the owner was bound are kept for them. A\n * restored page that is not a list page the rows can render is left out.\n */\nexport function useOfflineSessionList({\n queryClient,\n offlineList,\n}: {\n queryClient: QueryClient;\n offlineList: { mirror: ThreadSnapshotMirror; ownerId: string } | undefined;\n}): void {\n const mirror = offlineList?.mirror;\n const ownerId = offlineList?.ownerId;\n const boundOwner = useRef<string | null>(null);\n useEffect(() => {\n if (!mirror || !ownerId) return;\n if (boundOwner.current !== null && boundOwner.current !== ownerId) {\n void queryClient.cancelQueries({ queryKey: [CONVERSATION_LIST_QUERY_KEY_ROOT] });\n queryClient.removeQueries({ queryKey: [CONVERSATION_LIST_QUERY_KEY_ROOT] });\n }\n boundOwner.current = ownerId;\n const boundAt = Date.now();\n let stopped = false;\n void mirror.loadList({ ownerId, listKey: OFFLINE_SESSION_LIST_KEY }).then((kept) => {\n if (stopped || !kept || !Array.isArray(kept.value)) return;\n for (const page of kept.value) {\n if (!isKeptPage(page)) continue;\n if (queryClient.getQueryData(page.queryKey) !== undefined) continue;\n queryClient.setQueryData(page.queryKey, page.data, { updatedAt: RESTORED_UPDATED_AT });\n }\n });\n const write = (): void => {\n const pages: KeptPage[] = queryClient\n .getQueryCache()\n .findAll({ predicate: (query) => isSessionListFirstPage(query.queryKey) })\n .filter(\n (query) =>\n query.state.status === 'success' &&\n query.state.dataUpdatedAt >= boundAt &&\n isConversationListPage(query.state.data),\n )\n .sort((a, b) => b.state.dataUpdatedAt - a.state.dataUpdatedAt)\n .slice(0, MAX_OFFLINE_SESSION_LIST_PAGES)\n .map((query) => ({ queryKey: query.queryKey, data: query.state.data as ConversationListPage }));\n if (pages.length > 0) mirror.writeList({ ownerId, listKey: OFFLINE_SESSION_LIST_KEY, value: pages });\n };\n const unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n if (event.type !== 'updated' || event.action.type !== 'success') return;\n if (!isSessionListFirstPage(event.query.queryKey)) return;\n // A page restored from the device is not news to write back.\n if (event.query.state.dataUpdatedAt === RESTORED_UPDATED_AT) return;\n write();\n });\n return () => {\n stopped = true;\n unsubscribe();\n };\n }, [queryClient, mirror, ownerId]);\n}\n"],"names":["CONVERSATION_LIST_QUERY_KEY_ROOT","useRef","useEffect"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { type ThreadSnapshotMirror } from '@athenaintel/agent-runtime';
2
+ import type { QueryClient, QueryKey } from '@tanstack/react-query';
3
+ /** Under this name the list's pages are kept, per owner. */
4
+ export declare const OFFLINE_SESSION_LIST_KEY = "session-list";
5
+ /** The first pages kept: one per filter the list was read with, the newest first. */
6
+ export declare const MAX_OFFLINE_SESSION_LIST_PAGES = 4;
7
+ /** A conversation list's live first page (`conversationListQueryKeys.firstPage`). */
8
+ export declare function isSessionListFirstPage(queryKey: QueryKey): boolean;
9
+ /**
10
+ * The session list's first pages, kept on this device sealed under the host's key
11
+ * (`offlineList`'s mirror, `AthenaProvider`'s `offlineHistory`): restored into the list's own query
12
+ * client before it is read, so the list paints at once and reads while Athena can't be reached,
13
+ * and written back (debounced by the mirror) each time a first page is read. Without `offlineList`,
14
+ * nothing.
15
+ *
16
+ * Owner-bound. A list kept for one owner is never restored for another, and a change of owner
17
+ * drops every page of the list's cache before the new owner's are restored (in API-key mode two
18
+ * people read the same list keys); only pages read since the owner was bound are kept for them. A
19
+ * restored page that is not a list page the rows can render is left out.
20
+ */
21
+ export declare function useOfflineSessionList({ queryClient, offlineList, }: {
22
+ queryClient: QueryClient;
23
+ offlineList: {
24
+ mirror: ThreadSnapshotMirror;
25
+ ownerId: string;
26
+ } | undefined;
27
+ }): void;
@@ -0,0 +1,75 @@
1
+ import { useRef, useEffect } from "react";
2
+ import { CONVERSATION_LIST_QUERY_KEY_ROOT } from "../agent-runtime/src/conversation-list.js";
3
+ const OFFLINE_SESSION_LIST_KEY = "session-list";
4
+ const MAX_OFFLINE_SESSION_LIST_PAGES = 4;
5
+ const RESTORED_UPDATED_AT = 1;
6
+ function isSessionListFirstPage(queryKey) {
7
+ return queryKey[0] === CONVERSATION_LIST_QUERY_KEY_ROOT && queryKey[4] === "first-page";
8
+ }
9
+ function isFirstPageKey(queryKey) {
10
+ return Array.isArray(queryKey) && queryKey.length === 5 && isSessionListFirstPage(queryKey) && typeof queryKey[1] === "string" && typeof queryKey[2] === "string" && typeof queryKey[3] === "number";
11
+ }
12
+ function isConversationRow(row) {
13
+ if (typeof row !== "object" || row === null) return false;
14
+ const { threadId, title, updatedAt } = row;
15
+ return typeof threadId === "string" && typeof title === "string" && typeof updatedAt === "string";
16
+ }
17
+ function isConversationListPage(value) {
18
+ if (typeof value !== "object" || value === null) return false;
19
+ const { items, hasMore, totalCount } = value;
20
+ return Array.isArray(items) && items.every(isConversationRow) && typeof hasMore === "boolean" && (totalCount === null || typeof totalCount === "number");
21
+ }
22
+ function isKeptPage(value) {
23
+ if (typeof value !== "object" || value === null) return false;
24
+ const { queryKey, data } = value;
25
+ return isFirstPageKey(queryKey) && isConversationListPage(data);
26
+ }
27
+ function useOfflineSessionList({
28
+ queryClient,
29
+ offlineList
30
+ }) {
31
+ const mirror = offlineList == null ? void 0 : offlineList.mirror;
32
+ const ownerId = offlineList == null ? void 0 : offlineList.ownerId;
33
+ const boundOwner = useRef(null);
34
+ useEffect(() => {
35
+ if (!mirror || !ownerId) return;
36
+ if (boundOwner.current !== null && boundOwner.current !== ownerId) {
37
+ void queryClient.cancelQueries({ queryKey: [CONVERSATION_LIST_QUERY_KEY_ROOT] });
38
+ queryClient.removeQueries({ queryKey: [CONVERSATION_LIST_QUERY_KEY_ROOT] });
39
+ }
40
+ boundOwner.current = ownerId;
41
+ const boundAt = Date.now();
42
+ let stopped = false;
43
+ void mirror.loadList({ ownerId, listKey: OFFLINE_SESSION_LIST_KEY }).then((kept) => {
44
+ if (stopped || !kept || !Array.isArray(kept.value)) return;
45
+ for (const page of kept.value) {
46
+ if (!isKeptPage(page)) continue;
47
+ if (queryClient.getQueryData(page.queryKey) !== void 0) continue;
48
+ queryClient.setQueryData(page.queryKey, page.data, { updatedAt: RESTORED_UPDATED_AT });
49
+ }
50
+ });
51
+ const write = () => {
52
+ const pages = queryClient.getQueryCache().findAll({ predicate: (query) => isSessionListFirstPage(query.queryKey) }).filter(
53
+ (query) => query.state.status === "success" && query.state.dataUpdatedAt >= boundAt && isConversationListPage(query.state.data)
54
+ ).sort((a, b) => b.state.dataUpdatedAt - a.state.dataUpdatedAt).slice(0, MAX_OFFLINE_SESSION_LIST_PAGES).map((query) => ({ queryKey: query.queryKey, data: query.state.data }));
55
+ if (pages.length > 0) mirror.writeList({ ownerId, listKey: OFFLINE_SESSION_LIST_KEY, value: pages });
56
+ };
57
+ const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
58
+ if (event.type !== "updated" || event.action.type !== "success") return;
59
+ if (!isSessionListFirstPage(event.query.queryKey)) return;
60
+ if (event.query.state.dataUpdatedAt === RESTORED_UPDATED_AT) return;
61
+ write();
62
+ });
63
+ return () => {
64
+ stopped = true;
65
+ unsubscribe();
66
+ };
67
+ }, [queryClient, mirror, ownerId]);
68
+ }
69
+ export {
70
+ MAX_OFFLINE_SESSION_LIST_PAGES,
71
+ OFFLINE_SESSION_LIST_KEY,
72
+ isSessionListFirstPage,
73
+ useOfflineSessionList
74
+ };
75
+ //# sourceMappingURL=offline-session-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-session-list.js","sources":["../../src/threads/offline-session-list.ts"],"sourcesContent":["import {\n CONVERSATION_LIST_QUERY_KEY_ROOT,\n type ConversationListPage,\n type ThreadSnapshotMirror,\n} from '@athenaintel/agent-runtime';\nimport type { QueryClient, QueryKey } from '@tanstack/react-query';\nimport { useEffect, useRef } from 'react';\n\n/** Under this name the list's pages are kept, per owner. */\nexport const OFFLINE_SESSION_LIST_KEY = 'session-list';\n/** The first pages kept: one per filter the list was read with, the newest first. */\nexport const MAX_OFFLINE_SESSION_LIST_PAGES = 4;\n/**\n * Restored pages are stamped this old, so they paint at once and are read again on mount whenever\n * Athena can be reached (and never replace a page already read in this provider).\n */\nconst RESTORED_UPDATED_AT = 1;\n\ninterface KeptPage {\n queryKey: QueryKey;\n data: ConversationListPage;\n}\n\n/** A conversation list's live first page (`conversationListQueryKeys.firstPage`). */\nexport function isSessionListFirstPage(queryKey: QueryKey): boolean {\n return queryKey[0] === CONVERSATION_LIST_QUERY_KEY_ROOT && queryKey[4] === 'first-page';\n}\n\n/** Exactly `conversationListQueryKeys.firstPage(scopeKey, filterKey, pageSize)`. */\nfunction isFirstPageKey(queryKey: unknown): queryKey is QueryKey {\n return (\n Array.isArray(queryKey) &&\n queryKey.length === 5 &&\n isSessionListFirstPage(queryKey) &&\n typeof queryKey[1] === 'string' &&\n typeof queryKey[2] === 'string' &&\n typeof queryKey[3] === 'number'\n );\n}\n\nfunction isConversationRow(row: unknown): boolean {\n if (typeof row !== 'object' || row === null) return false;\n const { threadId, title, updatedAt } = row as Record<string, unknown>;\n return typeof threadId === 'string' && typeof title === 'string' && typeof updatedAt === 'string';\n}\n\n/** A page the list can render: its rows carry the fields every row reads. */\nfunction isConversationListPage(value: unknown): value is ConversationListPage {\n if (typeof value !== 'object' || value === null) return false;\n const { items, hasMore, totalCount } = value as Record<string, unknown>;\n return (\n Array.isArray(items) &&\n items.every(isConversationRow) &&\n typeof hasMore === 'boolean' &&\n (totalCount === null || typeof totalCount === 'number')\n );\n}\n\nfunction isKeptPage(value: unknown): value is KeptPage {\n if (typeof value !== 'object' || value === null) return false;\n const { queryKey, data } = value as Record<string, unknown>;\n return isFirstPageKey(queryKey) && isConversationListPage(data);\n}\n\n/**\n * The session list's first pages, kept on this device sealed under the host's key\n * (`offlineList`'s mirror, `AthenaProvider`'s `offlineHistory`): restored into the list's own query\n * client before it is read, so the list paints at once and reads while Athena can't be reached,\n * and written back (debounced by the mirror) each time a first page is read. Without `offlineList`,\n * nothing.\n *\n * Owner-bound. A list kept for one owner is never restored for another, and a change of owner\n * drops every page of the list's cache before the new owner's are restored (in API-key mode two\n * people read the same list keys); only pages read since the owner was bound are kept for them. A\n * restored page that is not a list page the rows can render is left out.\n */\nexport function useOfflineSessionList({\n queryClient,\n offlineList,\n}: {\n queryClient: QueryClient;\n offlineList: { mirror: ThreadSnapshotMirror; ownerId: string } | undefined;\n}): void {\n const mirror = offlineList?.mirror;\n const ownerId = offlineList?.ownerId;\n const boundOwner = useRef<string | null>(null);\n useEffect(() => {\n if (!mirror || !ownerId) return;\n if (boundOwner.current !== null && boundOwner.current !== ownerId) {\n void queryClient.cancelQueries({ queryKey: [CONVERSATION_LIST_QUERY_KEY_ROOT] });\n queryClient.removeQueries({ queryKey: [CONVERSATION_LIST_QUERY_KEY_ROOT] });\n }\n boundOwner.current = ownerId;\n const boundAt = Date.now();\n let stopped = false;\n void mirror.loadList({ ownerId, listKey: OFFLINE_SESSION_LIST_KEY }).then((kept) => {\n if (stopped || !kept || !Array.isArray(kept.value)) return;\n for (const page of kept.value) {\n if (!isKeptPage(page)) continue;\n if (queryClient.getQueryData(page.queryKey) !== undefined) continue;\n queryClient.setQueryData(page.queryKey, page.data, { updatedAt: RESTORED_UPDATED_AT });\n }\n });\n const write = (): void => {\n const pages: KeptPage[] = queryClient\n .getQueryCache()\n .findAll({ predicate: (query) => isSessionListFirstPage(query.queryKey) })\n .filter(\n (query) =>\n query.state.status === 'success' &&\n query.state.dataUpdatedAt >= boundAt &&\n isConversationListPage(query.state.data),\n )\n .sort((a, b) => b.state.dataUpdatedAt - a.state.dataUpdatedAt)\n .slice(0, MAX_OFFLINE_SESSION_LIST_PAGES)\n .map((query) => ({ queryKey: query.queryKey, data: query.state.data as ConversationListPage }));\n if (pages.length > 0) mirror.writeList({ ownerId, listKey: OFFLINE_SESSION_LIST_KEY, value: pages });\n };\n const unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n if (event.type !== 'updated' || event.action.type !== 'success') return;\n if (!isSessionListFirstPage(event.query.queryKey)) return;\n // A page restored from the device is not news to write back.\n if (event.query.state.dataUpdatedAt === RESTORED_UPDATED_AT) return;\n write();\n });\n return () => {\n stopped = true;\n unsubscribe();\n };\n }, [queryClient, mirror, ownerId]);\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
+ const offlineSessionList = require("./offline-session-list.cjs");
4
5
  const reactQuery = require("@tanstack/react-query");
5
6
  const react$1 = require("@assistant-ui/react");
6
7
  const react = require("react");
@@ -72,8 +73,19 @@ function foldConversationIndex(conversations, indexByThread) {
72
73
  return title.normalizeThreadTitle(merged.title) === void 0 ? { ...merged, title: conversation.title } : merged;
73
74
  });
74
75
  }
76
+ function withPinnedConversations({
77
+ conversations,
78
+ pinned
79
+ }) {
80
+ const listed = new Set(conversations.map((conversation) => conversation.threadId));
81
+ const unlisted = pinned.filter(
82
+ (conversation) => conversation.isFavorited !== false && !listed.has(conversation.threadId)
83
+ );
84
+ return unlisted.length === 0 ? conversations : [...conversations, ...unlisted];
85
+ }
75
86
  const AthenaSessionsLiveContext = react.createContext(null);
76
87
  const THREAD_LIST_PAGE_SIZE = 50;
88
+ const MAX_PINNED_PAGES = 10;
77
89
  function StatewireThreadListState({
78
90
  backendUrl,
79
91
  syncUrl,
@@ -137,6 +149,45 @@ function StatewireThreadListState({
137
149
  scopeKey: listScopeKey,
138
150
  enabled: hasAuth
139
151
  });
152
+ const fetchPinnedPage = react.useMemo(
153
+ () => conversationListFetcher.createRestConversationListFetcher({
154
+ backendUrl,
155
+ includePullRequests,
156
+ getAuth,
157
+ extraFilters: { favorites_only: true }
158
+ }),
159
+ [backendUrl, includePullRequests, getAuth]
160
+ );
161
+ const pinnedScopeKey = `${listScopeKey}::pinned`;
162
+ const pinnedList = useConversationList.useConversationList({
163
+ fetchPage: fetchPinnedPage,
164
+ filter,
165
+ pageSize: THREAD_LIST_PAGE_SIZE,
166
+ scopeKey: pinnedScopeKey,
167
+ enabled: hasAuth
168
+ });
169
+ const pinnedPagesRequested = react.useRef({ key: "", count: 0 });
170
+ const pinnedRequestKey = `${pinnedScopeKey}::${JSON.stringify(filter)}`;
171
+ if (pinnedPagesRequested.current.key !== pinnedRequestKey) {
172
+ pinnedPagesRequested.current = { key: pinnedRequestKey, count: 0 };
173
+ }
174
+ const pinnedHasNextPage = pinnedList.hasNextPage;
175
+ const pinnedIsFetchingNextPage = pinnedList.isFetchingNextPage;
176
+ const pinnedNextPageFailed = pinnedList.isFetchNextPageError;
177
+ const pinnedFetchNextPage = pinnedList.fetchNextPage;
178
+ react.useEffect(() => {
179
+ if (!pinnedHasNextPage || pinnedIsFetchingNextPage || pinnedNextPageFailed) return;
180
+ if (pinnedPagesRequested.current.count >= MAX_PINNED_PAGES - 1) return;
181
+ pinnedPagesRequested.current.count += 1;
182
+ void pinnedFetchNextPage();
183
+ }, [pinnedHasNextPage, pinnedIsFetchingNextPage, pinnedNextPageFailed, pinnedFetchNextPage]);
184
+ const listedConversations = react.useMemo(
185
+ () => withPinnedConversations({
186
+ conversations: list.conversations,
187
+ pinned: pinnedList.conversations
188
+ }),
189
+ [list.conversations, pinnedList.conversations]
190
+ );
140
191
  const indexHeaders = react.useCallback(
141
192
  (ctx) => statewireAuthHeaders.resolveStatewireAuthHeaders(
142
193
  { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },
@@ -181,8 +232,8 @@ function StatewireThreadListState({
181
232
  return entries;
182
233
  }, [index.entries]);
183
234
  const mergedConversations = react.useMemo(
184
- () => foldConversationIndex(list.conversations, indexByThread),
185
- [list.conversations, indexByThread]
235
+ () => foldConversationIndex(listedConversations, indexByThread),
236
+ [listedConversations, indexByThread]
186
237
  );
187
238
  const queryClient = reactQuery.useQueryClient();
188
239
  const patchEveryList = react.useCallback(
@@ -192,12 +243,16 @@ function StatewireThreadListState({
192
243
  }),
193
244
  [queryClient]
194
245
  );
246
+ const pinnedListRefresh = pinnedList.refresh;
195
247
  const favorite = useThreadFavorite.useThreadFavorite({
196
248
  backendUrl,
197
249
  principalId: principal,
198
250
  getAuth,
199
251
  rows: mergedConversations,
200
- onServerTruth: ({ threadId, isFavorited }) => patchEveryList(threadId, { isFavorited })
252
+ onServerTruth: ({ threadId, isFavorited }) => {
253
+ patchEveryList(threadId, { isFavorited });
254
+ if (isFavorited) void pinnedListRefresh();
255
+ }
201
256
  });
202
257
  const favoriteOverrides = favorite.overrides;
203
258
  const rename = useThreadRename.useThreadRename({
@@ -279,7 +334,8 @@ function StatewireThreadListState({
279
334
  const listRefresh = list.refresh;
280
335
  const refresh = react.useCallback(() => {
281
336
  void listRefresh();
282
- }, [listRefresh]);
337
+ void pinnedListRefresh();
338
+ }, [listRefresh, pinnedListRefresh]);
283
339
  const listFetchNextPage = list.fetchNextPage;
284
340
  const fetchNextPage = react.useCallback(() => {
285
341
  void listFetchNextPage();
@@ -447,9 +503,11 @@ function AthenaStatewireThreadListProvider({
447
503
  initialThreadId,
448
504
  includePullRequests = true,
449
505
  snapshotCache,
506
+ offlineList,
450
507
  children
451
508
  }) {
452
509
  const [queryClient] = react.useState(() => new reactQuery.QueryClient());
510
+ offlineSessionList.useOfflineSessionList({ queryClient, offlineList });
453
511
  return /* @__PURE__ */ jsxRuntime.jsx(reactQuery.QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsxRuntime.jsx(
454
512
  StatewireThreadListState,
455
513
  {
@@ -522,4 +580,5 @@ exports.indexedThreadsToRefreshFor = indexedThreadsToRefreshFor;
522
580
  exports.threadSummaryFromConversation = threadSummaryFromConversation;
523
581
  exports.useAthenaStatewireThreadList = useAthenaStatewireThreadList;
524
582
  exports.useRefreshThreadList = useRefreshThreadList;
583
+ exports.withPinnedConversations = withPinnedConversations;
525
584
  //# sourceMappingURL=statewire-thread-list.cjs.map