@athenaintel/react 0.21.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +13 -1
  2. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +6 -6
  3. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
  4. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +6 -6
  5. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
  6. package/dist/packages/agent-runtime/src/conversation-index.cjs +19 -10
  7. package/dist/packages/agent-runtime/src/conversation-index.cjs.map +1 -1
  8. package/dist/packages/agent-runtime/src/conversation-index.js +19 -10
  9. package/dist/packages/agent-runtime/src/conversation-index.js.map +1 -1
  10. package/dist/packages/react/package.json.cjs +1 -1
  11. package/dist/packages/react/package.json.js +1 -1
  12. package/dist/threads/api.cjs +29 -0
  13. package/dist/threads/api.cjs.map +1 -1
  14. package/dist/threads/api.d.ts +15 -0
  15. package/dist/threads/api.js +29 -0
  16. package/dist/threads/api.js.map +1 -1
  17. package/dist/threads/credential-shape.cjs +15 -0
  18. package/dist/threads/credential-shape.cjs.map +1 -0
  19. package/dist/threads/credential-shape.d.ts +17 -0
  20. package/dist/threads/credential-shape.js +15 -0
  21. package/dist/threads/credential-shape.js.map +1 -0
  22. package/dist/threads/statewire-thread-list.cjs +35 -3
  23. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  24. package/dist/threads/statewire-thread-list.js +36 -4
  25. package/dist/threads/statewire-thread-list.js.map +1 -1
  26. package/dist/threads/use-statewire-conversation-history.cjs +3 -1
  27. package/dist/threads/use-statewire-conversation-history.cjs.map +1 -1
  28. package/dist/threads/use-statewire-conversation-history.d.ts +3 -1
  29. package/dist/threads/use-statewire-conversation-history.js +3 -1
  30. package/dist/threads/use-statewire-conversation-history.js.map +1 -1
  31. package/dist/toolkits/index.cjs +0 -7
  32. package/dist/toolkits/index.cjs.map +1 -1
  33. package/dist/toolkits/index.d.ts +0 -2
  34. package/dist/toolkits/index.js +0 -7
  35. package/dist/toolkits/index.js.map +1 -1
  36. package/dist/voice/index.d.ts +1 -1
  37. package/dist/voice/use-athena-voice-session.cjs.map +1 -1
  38. package/dist/voice/use-athena-voice-session.d.ts +37 -2
  39. package/dist/voice/use-athena-voice-session.js.map +1 -1
  40. package/package.json +2 -2
@@ -63,6 +63,34 @@ async function listThreads(backendUrl, auth, opts = {}) {
63
63
  throw error;
64
64
  }
65
65
  }
66
+ async function fetchAccountIdentity(backendUrl, auth, opts = {}) {
67
+ const base = getAthenaApiBaseUrl(backendUrl);
68
+ const res = await fetch(`${base}/api/v0/me`, {
69
+ headers: getAuthHeaders(auth),
70
+ signal: opts.signal ?? null
71
+ });
72
+ if (!res.ok) {
73
+ throw new AthenaSdkError({
74
+ code: res.status === 401 || res.status === 403 ? "auth_rejected" : "threads_list_failed",
75
+ message: `Failed to resolve account identity: ${res.status}`,
76
+ status: res.status,
77
+ hint: res.status === 401 || res.status === 403 ? "The credential was refused. Check the token/API key and that the user belongs to the workspace." : "Check backendUrl points at the Agora API base.",
78
+ context: { auth_mode: getAuthMode(auth) }
79
+ });
80
+ }
81
+ const body = await res.json();
82
+ if (typeof body.user_id !== "string" || body.user_id.length === 0) {
83
+ throw new AthenaSdkError({
84
+ code: "threads_list_failed",
85
+ message: "Account identity lookup returned no user id.",
86
+ context: { auth_mode: getAuthMode(auth) }
87
+ });
88
+ }
89
+ return {
90
+ userId: body.user_id,
91
+ workspaceId: typeof body.workspace_id === "string" ? body.workspace_id : null
92
+ };
93
+ }
66
94
  async function setThreadReadState(backendUrl, auth, opts) {
67
95
  const base = getAthenaApiBaseUrl(backendUrl);
68
96
  const res = await fetch(`${base}/api/conversations/threads/read-state`, {
@@ -201,6 +229,7 @@ async function archiveThread(backendUrl, auth, threadId) {
201
229
  export {
202
230
  archiveThread,
203
231
  createThread,
232
+ fetchAccountIdentity,
204
233
  getThreadState,
205
234
  listThreads,
206
235
  setThreadReadState
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sources":["../../src/threads/api.ts"],"sourcesContent":["/**\n * Athena thread API client.\n * Calls existing REST endpoints for thread CRUD.\n */\n\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { AthenaSdkError } from '../diagnostics/errors';\n\nexport interface ThreadSummary {\n thread_id: string;\n title: string;\n created_at: string;\n updated_at: string;\n last_message: string | null;\n message_count: number;\n state: string | null;\n start_channel?: string | null;\n last_channel?: string | null;\n /** Client app that created the session (SESSION_APP_ID_GUIDE). */\n app_id?: string | null;\n /** Trigger marker for automated/background sessions. */\n trigger_type?: string | null;\n /** Whether the calling user has seen this thread since it last changed. */\n is_unread?: boolean;\n}\n\n/**\n * True for fetch cancellations (unmount/remount aborts, StrictMode double\n * effects). These are lifecycle noise, not failures: emitting them as\n * `*.error` diagnostics floods the console with\n * \"signal is aborted without reason\" for requests the caller itself aborted.\n */\nfunction isAbortError(error: unknown): boolean {\n return (\n (error instanceof DOMException && error.name === 'AbortError') ||\n (error instanceof Error && error.name === 'AbortError')\n );\n}\n\nexport interface ThreadListResponse {\n threads: ThreadSummary[];\n /** Server lookahead result. Older servers may omit this field. */\n has_more?: boolean;\n}\n\nexport interface ThreadCreateResponse {\n thread_id: string;\n}\n\ninterface AuthConfig {\n apiKey?: string;\n token?: string | null;\n}\n\nfunction getAuthHeaders(auth: AuthConfig): Record<string, string> {\n if (auth.token) {\n return { Authorization: `Bearer ${auth.token}` };\n }\n if (auth.apiKey) {\n return { 'X-API-KEY': auth.apiKey };\n }\n return {};\n}\n\n/**\n * Resolves the Athena API base URL from backendUrl.\n * Thread endpoints live under `/api/conversations/threads/*` and `/api/unstable/threads/`.\n * If backendUrl points at a nested Athena endpoint, trim it back to the API base first.\n */\nfunction getAthenaApiBaseUrl(backendUrl: string): string {\n const stripped = backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '');\n // If the suffix wasn't found, assume the URL already points to the API base\n if (stripped === backendUrl) {\n return backendUrl.replace(/\\/$/, '');\n }\n return stripped;\n}\n\nexport async function listThreads(\n backendUrl: string,\n auth: AuthConfig,\n opts: {\n limit?: number;\n offset?: number;\n exclude_triggered?: boolean;\n app_id?: string;\n start_channel?: string;\n search?: string;\n signal?: AbortSignal;\n } = {},\n): Promise<ThreadListResponse> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const endSpan = athenaDiagnostics.span('threads.list', 'threads.list.start', {\n auth_mode: getAuthMode(auth),\n app_id: opts.app_id,\n });\n try {\n const res = await fetch(`${base}/api/conversations/threads/list`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n signal: opts.signal ?? null,\n body: JSON.stringify({\n limit: opts.limit ?? 50,\n offset: opts.offset ?? 0,\n // Default to the full recent-conversations set unless a caller\n // explicitly asks to hide triggered/background sessions.\n exclude_triggered: opts.exclude_triggered ?? false,\n ...(opts.app_id ? { app_id: opts.app_id } : {}),\n ...(opts.start_channel ? { start_channel: opts.start_channel } : {}),\n ...(opts.search ? { search: opts.search } : {}),\n }),\n });\n if (!res.ok) {\n throw new AthenaSdkError({\n code: res.status === 401 || res.status === 403 ? 'auth_rejected' : 'threads_list_failed',\n message: `Failed to list threads: ${res.status}`,\n status: res.status,\n hint:\n res.status === 401 || res.status === 403\n ? 'The credential was refused. Check the token/API key and that the user belongs to the workspace.'\n : 'Check backendUrl points at the Agora API base.',\n context: { auth_mode: getAuthMode(auth) },\n });\n }\n const data = (await res.json()) as ThreadListResponse;\n endSpan('threads.list.done', { data: { count: data.threads?.length ?? 0 } });\n return data;\n } catch (error) {\n // Cancellation is lifecycle noise (unmount/remount aborts): log it as an\n // informational abort, not an error — passing `error` would force the\n // span end to error level and spam the console.\n if (isAbortError(error)) {\n endSpan('threads.list.aborted', { data: { reason: 'request aborted' } });\n } else {\n endSpan('threads.list.error', { error });\n }\n throw error;\n }\n}\n\nexport interface ThreadReadStateResponse {\n thread_id: string;\n is_unread: boolean;\n}\n\n/** Store the calling user's read receipt for a thread. */\nexport async function setThreadReadState(\n backendUrl: string,\n auth: AuthConfig,\n opts: { threadId: string; isUnread: boolean },\n): Promise<ThreadReadStateResponse> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const res = await fetch(`${base}/api/conversations/threads/read-state`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n body: JSON.stringify({\n thread_id: opts.threadId,\n is_unread: opts.isUnread,\n }),\n });\n if (!res.ok) {\n throw new Error(`[AthenaSDK] Failed to set thread read state: ${res.status}`);\n }\n return res.json();\n}\n\nexport async function createThread(\n backendUrl: string,\n auth: AuthConfig,\n opts: { systemPrompt?: string; enabledTools?: string[]; appId?: string } = {},\n): Promise<ThreadCreateResponse> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const res = await fetch(`${base}/api/unstable/threads/`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n body: JSON.stringify({\n config: {\n system_prompt: opts.systemPrompt ?? '',\n enabled_tools: opts.enabledTools,\n ...(opts.appId ? { app_id: opts.appId } : {}),\n },\n }),\n });\n if (!res.ok) {\n throw new Error(`[AthenaSDK] Failed to create thread: ${res.status}`);\n }\n return res.json();\n}\n\nexport interface ThreadStateResponse {\n thread_id: string;\n messages: unknown[];\n status?: string;\n [key: string]: unknown;\n}\n\ninterface ConversationThreadResponse {\n thread?: {\n thread_id?: string;\n state?: string | null;\n channel_values?: Record<string, unknown> | null;\n messages?: unknown[] | null;\n } | null;\n thread_found?: boolean;\n}\n\n/**\n * Deserialize a LangChain serialized message.\n * The API returns messages in `{lc, type: \"constructor\", id, kwargs}` format.\n * We need the plain format: `{type: \"human\", content: \"...\", id: \"...\"}`.\n */\nfunction deserializeMessage(msg: unknown): unknown {\n if (\n msg &&\n typeof msg === 'object' &&\n 'lc' in msg &&\n 'kwargs' in msg &&\n (msg as Record<string, unknown>).type === 'constructor'\n ) {\n const kwargs = (msg as { kwargs: Record<string, unknown> }).kwargs;\n\n // Recursively deserialize nested content without mutating the original\n const deserializedToolCalls = Array.isArray(kwargs.tool_calls)\n ? kwargs.tool_calls.map((tc: unknown) => {\n if (tc && typeof tc === 'object' && 'lc' in tc && 'kwargs' in tc) {\n return (tc as { kwargs: unknown }).kwargs;\n }\n return tc;\n })\n : kwargs.tool_calls;\n\n return { ...kwargs, tool_calls: deserializedToolCalls };\n }\n return msg;\n}\n\n/** Return a non-sensitive auth label for request diagnostics. */\nfunction getAuthMode(auth: AuthConfig): 'bearer' | 'api-key' | 'none' {\n if (auth.token) {\n return 'bearer';\n }\n if (auth.apiKey) {\n return 'api-key';\n }\n return 'none';\n}\n\n/** Safely read a response body for error messages without masking the root failure. */\nfunction getResponseErrorBody(response: Response): Promise<string> {\n return response.text().catch(() => '');\n}\n\n/**\n * Fetch persisted thread state from the conversations API.\n *\n * This endpoint is the same source used by Athena's conversation sidebar and\n * supports API-key auth plus BYO SSO session tokens sent as Bearer tokens.\n */\nexport async function getThreadState(\n backendUrl: string,\n auth: AuthConfig,\n threadId: string,\n): Promise<ThreadStateResponse> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const authMode = getAuthMode(auth);\n const endpoint = `${base}/api/conversations/threads/get`;\n\n const endSpan = athenaDiagnostics.span('threads.history', 'threads.history.start', {\n thread_id: threadId,\n auth_mode: authMode,\n });\n\n let res: Response;\n try {\n res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n body: JSON.stringify({\n thread_id: threadId,\n skip_cache: true,\n }),\n });\n } catch (error) {\n if (isAbortError(error)) {\n endSpan('threads.history.aborted', {\n data: { thread_id: threadId, reason: 'request aborted' },\n });\n } else {\n endSpan('threads.history.error', { error, data: { thread_id: threadId } });\n }\n throw error;\n }\n if (!res.ok) {\n const body = await getResponseErrorBody(res);\n const error = new AthenaSdkError({\n code: res.status === 401 || res.status === 403 ? 'auth_rejected' : 'thread_load_failed',\n message: `Failed to get thread state: ${res.status} (${authMode})${\n body ? `: ${body.slice(0, 300)}` : ''\n }`,\n status: res.status,\n hint: 'A brand-new thread may not be persisted yet; anything else means the credential or backendUrl is wrong.',\n context: { thread_id: threadId, auth_mode: authMode },\n });\n endSpan('threads.history.error', { error, data: { thread_id: threadId } });\n throw error;\n }\n const data = (await res.json()) as ConversationThreadResponse;\n const channelMessages = data.thread?.channel_values?.messages;\n endSpan('threads.history.done', {\n data: {\n thread_id: threadId,\n found: data.thread_found !== false,\n message_count: Array.isArray(channelMessages)\n ? channelMessages.length\n : (data.thread?.messages?.length ?? 0),\n },\n });\n const thread = data.thread;\n\n if (data.thread_found === false || !thread) {\n return {\n thread_id: threadId,\n messages: [],\n };\n }\n\n const channelValues = thread.channel_values ?? {};\n const rawMessages = Array.isArray(channelValues.messages)\n ? channelValues.messages\n : Array.isArray(thread.messages)\n ? thread.messages\n : [];\n\n return {\n ...channelValues,\n thread_id: thread.thread_id ?? threadId,\n messages: rawMessages.map(deserializeMessage),\n ...(thread.state ? { status: thread.state } : {}),\n };\n}\n\nexport async function archiveThread(\n backendUrl: string,\n auth: AuthConfig,\n threadId: string,\n): Promise<void> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const res = await fetch(`${base}/api/conversations/threads/archive`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n body: JSON.stringify({ thread_id: threadId }),\n });\n if (!res.ok) {\n throw new Error(`[AthenaSDK] Failed to archive thread: ${res.status}`);\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sources":["../../src/threads/api.ts"],"sourcesContent":["/**\n * Athena thread API client.\n * Calls existing REST endpoints for thread CRUD.\n */\n\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { AthenaSdkError } from '../diagnostics/errors';\n\nexport interface ThreadSummary {\n thread_id: string;\n title: string;\n created_at: string;\n updated_at: string;\n last_message: string | null;\n message_count: number;\n state: string | null;\n start_channel?: string | null;\n last_channel?: string | null;\n /** Client app that created the session (SESSION_APP_ID_GUIDE). */\n app_id?: string | null;\n /** Trigger marker for automated/background sessions. */\n trigger_type?: string | null;\n /** Whether the calling user has seen this thread since it last changed. */\n is_unread?: boolean;\n}\n\n/**\n * True for fetch cancellations (unmount/remount aborts, StrictMode double\n * effects). These are lifecycle noise, not failures: emitting them as\n * `*.error` diagnostics floods the console with\n * \"signal is aborted without reason\" for requests the caller itself aborted.\n */\nfunction isAbortError(error: unknown): boolean {\n return (\n (error instanceof DOMException && error.name === 'AbortError') ||\n (error instanceof Error && error.name === 'AbortError')\n );\n}\n\nexport interface ThreadListResponse {\n threads: ThreadSummary[];\n /** Server lookahead result. Older servers may omit this field. */\n has_more?: boolean;\n}\n\nexport interface ThreadCreateResponse {\n thread_id: string;\n}\n\ninterface AuthConfig {\n apiKey?: string;\n token?: string | null;\n}\n\nfunction getAuthHeaders(auth: AuthConfig): Record<string, string> {\n if (auth.token) {\n return { Authorization: `Bearer ${auth.token}` };\n }\n if (auth.apiKey) {\n return { 'X-API-KEY': auth.apiKey };\n }\n return {};\n}\n\n/**\n * Resolves the Athena API base URL from backendUrl.\n * Thread endpoints live under `/api/conversations/threads/*` and `/api/unstable/threads/`.\n * If backendUrl points at a nested Athena endpoint, trim it back to the API base first.\n */\nfunction getAthenaApiBaseUrl(backendUrl: string): string {\n const stripped = backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '');\n // If the suffix wasn't found, assume the URL already points to the API base\n if (stripped === backendUrl) {\n return backendUrl.replace(/\\/$/, '');\n }\n return stripped;\n}\n\nexport async function listThreads(\n backendUrl: string,\n auth: AuthConfig,\n opts: {\n limit?: number;\n offset?: number;\n exclude_triggered?: boolean;\n app_id?: string;\n start_channel?: string;\n search?: string;\n signal?: AbortSignal;\n } = {},\n): Promise<ThreadListResponse> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const endSpan = athenaDiagnostics.span('threads.list', 'threads.list.start', {\n auth_mode: getAuthMode(auth),\n app_id: opts.app_id,\n });\n try {\n const res = await fetch(`${base}/api/conversations/threads/list`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n signal: opts.signal ?? null,\n body: JSON.stringify({\n limit: opts.limit ?? 50,\n offset: opts.offset ?? 0,\n // Default to the full recent-conversations set unless a caller\n // explicitly asks to hide triggered/background sessions.\n exclude_triggered: opts.exclude_triggered ?? false,\n ...(opts.app_id ? { app_id: opts.app_id } : {}),\n ...(opts.start_channel ? { start_channel: opts.start_channel } : {}),\n ...(opts.search ? { search: opts.search } : {}),\n }),\n });\n if (!res.ok) {\n throw new AthenaSdkError({\n code: res.status === 401 || res.status === 403 ? 'auth_rejected' : 'threads_list_failed',\n message: `Failed to list threads: ${res.status}`,\n status: res.status,\n hint:\n res.status === 401 || res.status === 403\n ? 'The credential was refused. Check the token/API key and that the user belongs to the workspace.'\n : 'Check backendUrl points at the Agora API base.',\n context: { auth_mode: getAuthMode(auth) },\n });\n }\n const data = (await res.json()) as ThreadListResponse;\n endSpan('threads.list.done', { data: { count: data.threads?.length ?? 0 } });\n return data;\n } catch (error) {\n // Cancellation is lifecycle noise (unmount/remount aborts): log it as an\n // informational abort, not an error — passing `error` would force the\n // span end to error level and spam the console.\n if (isAbortError(error)) {\n endSpan('threads.list.aborted', { data: { reason: 'request aborted' } });\n } else {\n endSpan('threads.list.error', { error });\n }\n throw error;\n }\n}\n\n/** The `(workspace, user)` pair the credential resolves to on the backend. */\nexport interface AccountIdentity {\n userId: string;\n /** `null` for a user with no current workspace. */\n workspaceId: string | null;\n}\n\n/**\n * Resolve the credential's agora identity. The conversation index is one\n * statewire instance per `(workspace, user)`, and neither half is knowable\n * from the credential alone: the SDK's auth context carries an identity\n * provider user id, not agora's, and never a workspace.\n */\nexport async function fetchAccountIdentity(\n backendUrl: string,\n auth: AuthConfig,\n opts: { signal?: AbortSignal } = {},\n): Promise<AccountIdentity> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const res = await fetch(`${base}/api/v0/me`, {\n headers: getAuthHeaders(auth),\n signal: opts.signal ?? null,\n });\n if (!res.ok) {\n throw new AthenaSdkError({\n code: res.status === 401 || res.status === 403 ? 'auth_rejected' : 'threads_list_failed',\n message: `Failed to resolve account identity: ${res.status}`,\n status: res.status,\n hint:\n res.status === 401 || res.status === 403\n ? 'The credential was refused. Check the token/API key and that the user belongs to the workspace.'\n : 'Check backendUrl points at the Agora API base.',\n context: { auth_mode: getAuthMode(auth) },\n });\n }\n const body = (await res.json()) as { user_id?: unknown; workspace_id?: unknown };\n if (typeof body.user_id !== 'string' || body.user_id.length === 0) {\n throw new AthenaSdkError({\n code: 'threads_list_failed',\n message: 'Account identity lookup returned no user id.',\n context: { auth_mode: getAuthMode(auth) },\n });\n }\n return {\n userId: body.user_id,\n workspaceId: typeof body.workspace_id === 'string' ? body.workspace_id : null,\n };\n}\n\nexport interface ThreadReadStateResponse {\n thread_id: string;\n is_unread: boolean;\n}\n\n/** Store the calling user's read receipt for a thread. */\nexport async function setThreadReadState(\n backendUrl: string,\n auth: AuthConfig,\n opts: { threadId: string; isUnread: boolean },\n): Promise<ThreadReadStateResponse> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const res = await fetch(`${base}/api/conversations/threads/read-state`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n body: JSON.stringify({\n thread_id: opts.threadId,\n is_unread: opts.isUnread,\n }),\n });\n if (!res.ok) {\n throw new Error(`[AthenaSDK] Failed to set thread read state: ${res.status}`);\n }\n return res.json();\n}\n\nexport async function createThread(\n backendUrl: string,\n auth: AuthConfig,\n opts: { systemPrompt?: string; enabledTools?: string[]; appId?: string } = {},\n): Promise<ThreadCreateResponse> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const res = await fetch(`${base}/api/unstable/threads/`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n body: JSON.stringify({\n config: {\n system_prompt: opts.systemPrompt ?? '',\n enabled_tools: opts.enabledTools,\n ...(opts.appId ? { app_id: opts.appId } : {}),\n },\n }),\n });\n if (!res.ok) {\n throw new Error(`[AthenaSDK] Failed to create thread: ${res.status}`);\n }\n return res.json();\n}\n\nexport interface ThreadStateResponse {\n thread_id: string;\n messages: unknown[];\n status?: string;\n [key: string]: unknown;\n}\n\ninterface ConversationThreadResponse {\n thread?: {\n thread_id?: string;\n state?: string | null;\n channel_values?: Record<string, unknown> | null;\n messages?: unknown[] | null;\n } | null;\n thread_found?: boolean;\n}\n\n/**\n * Deserialize a LangChain serialized message.\n * The API returns messages in `{lc, type: \"constructor\", id, kwargs}` format.\n * We need the plain format: `{type: \"human\", content: \"...\", id: \"...\"}`.\n */\nfunction deserializeMessage(msg: unknown): unknown {\n if (\n msg &&\n typeof msg === 'object' &&\n 'lc' in msg &&\n 'kwargs' in msg &&\n (msg as Record<string, unknown>).type === 'constructor'\n ) {\n const kwargs = (msg as { kwargs: Record<string, unknown> }).kwargs;\n\n // Recursively deserialize nested content without mutating the original\n const deserializedToolCalls = Array.isArray(kwargs.tool_calls)\n ? kwargs.tool_calls.map((tc: unknown) => {\n if (tc && typeof tc === 'object' && 'lc' in tc && 'kwargs' in tc) {\n return (tc as { kwargs: unknown }).kwargs;\n }\n return tc;\n })\n : kwargs.tool_calls;\n\n return { ...kwargs, tool_calls: deserializedToolCalls };\n }\n return msg;\n}\n\n/** Return a non-sensitive auth label for request diagnostics. */\nfunction getAuthMode(auth: AuthConfig): 'bearer' | 'api-key' | 'none' {\n if (auth.token) {\n return 'bearer';\n }\n if (auth.apiKey) {\n return 'api-key';\n }\n return 'none';\n}\n\n/** Safely read a response body for error messages without masking the root failure. */\nfunction getResponseErrorBody(response: Response): Promise<string> {\n return response.text().catch(() => '');\n}\n\n/**\n * Fetch persisted thread state from the conversations API.\n *\n * This endpoint is the same source used by Athena's conversation sidebar and\n * supports API-key auth plus BYO SSO session tokens sent as Bearer tokens.\n */\nexport async function getThreadState(\n backendUrl: string,\n auth: AuthConfig,\n threadId: string,\n): Promise<ThreadStateResponse> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const authMode = getAuthMode(auth);\n const endpoint = `${base}/api/conversations/threads/get`;\n\n const endSpan = athenaDiagnostics.span('threads.history', 'threads.history.start', {\n thread_id: threadId,\n auth_mode: authMode,\n });\n\n let res: Response;\n try {\n res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n body: JSON.stringify({\n thread_id: threadId,\n skip_cache: true,\n }),\n });\n } catch (error) {\n if (isAbortError(error)) {\n endSpan('threads.history.aborted', {\n data: { thread_id: threadId, reason: 'request aborted' },\n });\n } else {\n endSpan('threads.history.error', { error, data: { thread_id: threadId } });\n }\n throw error;\n }\n if (!res.ok) {\n const body = await getResponseErrorBody(res);\n const error = new AthenaSdkError({\n code: res.status === 401 || res.status === 403 ? 'auth_rejected' : 'thread_load_failed',\n message: `Failed to get thread state: ${res.status} (${authMode})${\n body ? `: ${body.slice(0, 300)}` : ''\n }`,\n status: res.status,\n hint: 'A brand-new thread may not be persisted yet; anything else means the credential or backendUrl is wrong.',\n context: { thread_id: threadId, auth_mode: authMode },\n });\n endSpan('threads.history.error', { error, data: { thread_id: threadId } });\n throw error;\n }\n const data = (await res.json()) as ConversationThreadResponse;\n const channelMessages = data.thread?.channel_values?.messages;\n endSpan('threads.history.done', {\n data: {\n thread_id: threadId,\n found: data.thread_found !== false,\n message_count: Array.isArray(channelMessages)\n ? channelMessages.length\n : (data.thread?.messages?.length ?? 0),\n },\n });\n const thread = data.thread;\n\n if (data.thread_found === false || !thread) {\n return {\n thread_id: threadId,\n messages: [],\n };\n }\n\n const channelValues = thread.channel_values ?? {};\n const rawMessages = Array.isArray(channelValues.messages)\n ? channelValues.messages\n : Array.isArray(thread.messages)\n ? thread.messages\n : [];\n\n return {\n ...channelValues,\n thread_id: thread.thread_id ?? threadId,\n messages: rawMessages.map(deserializeMessage),\n ...(thread.state ? { status: thread.state } : {}),\n };\n}\n\nexport async function archiveThread(\n backendUrl: string,\n auth: AuthConfig,\n threadId: string,\n): Promise<void> {\n const base = getAthenaApiBaseUrl(backendUrl);\n const res = await fetch(`${base}/api/conversations/threads/archive`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...getAuthHeaders(auth) },\n body: JSON.stringify({ thread_id: threadId }),\n });\n if (!res.ok) {\n throw new Error(`[AthenaSDK] Failed to archive thread: ${res.status}`);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ function credentialShape(credentials) {
4
+ return {
5
+ apiKey: credentials.apiKey,
6
+ token: credentials.token,
7
+ hasGetToken: credentials.getToken !== void 0
8
+ };
9
+ }
10
+ function sameCredentialShape(a, b) {
11
+ return a.apiKey === b.apiKey && a.token === b.token && a.hasGetToken === b.hasGetToken;
12
+ }
13
+ exports.credentialShape = credentialShape;
14
+ exports.sameCredentialShape = sameCredentialShape;
15
+ //# sourceMappingURL=credential-shape.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-shape.cjs","sources":["../../src/threads/credential-shape.ts"],"sourcesContent":["import type { AthenaStatewireTokenGetter } from '../runtime/statewire-auth-headers';\n\n/**\n * What a host's credentials look like, without the secrets: enough to notice\n * a swap. `getToken` counts by presence, not identity — an inline getter is a\n * new function every render, and keying on it would refetch forever.\n */\nexport interface CredentialShape {\n apiKey: string | undefined;\n token: string | null | undefined;\n hasGetToken: boolean;\n}\n\nexport function credentialShape(credentials: {\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n}): CredentialShape {\n return {\n apiKey: credentials.apiKey,\n token: credentials.token,\n hasGetToken: credentials.getToken !== undefined,\n };\n}\n\nexport function sameCredentialShape(a: CredentialShape, b: CredentialShape): boolean {\n return a.apiKey === b.apiKey && a.token === b.token && a.hasGetToken === b.hasGetToken;\n}\n"],"names":[],"mappings":";;AAaO,SAAS,gBAAgB,aAIZ;AAClB,SAAO;AAAA,IACL,QAAQ,YAAY;AAAA,IACpB,OAAO,YAAY;AAAA,IACnB,aAAa,YAAY,aAAa;AAAA,EAAA;AAE1C;AAEO,SAAS,oBAAoB,GAAoB,GAA6B;AACnF,SAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE;AAC7E;;;"}
@@ -0,0 +1,17 @@
1
+ import type { AthenaStatewireTokenGetter } from '../runtime/statewire-auth-headers';
2
+ /**
3
+ * What a host's credentials look like, without the secrets: enough to notice
4
+ * a swap. `getToken` counts by presence, not identity — an inline getter is a
5
+ * new function every render, and keying on it would refetch forever.
6
+ */
7
+ export interface CredentialShape {
8
+ apiKey: string | undefined;
9
+ token: string | null | undefined;
10
+ hasGetToken: boolean;
11
+ }
12
+ export declare function credentialShape(credentials: {
13
+ apiKey?: string;
14
+ token?: string | null;
15
+ getToken?: AthenaStatewireTokenGetter;
16
+ }): CredentialShape;
17
+ export declare function sameCredentialShape(a: CredentialShape, b: CredentialShape): boolean;
@@ -0,0 +1,15 @@
1
+ function credentialShape(credentials) {
2
+ return {
3
+ apiKey: credentials.apiKey,
4
+ token: credentials.token,
5
+ hasGetToken: credentials.getToken !== void 0
6
+ };
7
+ }
8
+ function sameCredentialShape(a, b) {
9
+ return a.apiKey === b.apiKey && a.token === b.token && a.hasGetToken === b.hasGetToken;
10
+ }
11
+ export {
12
+ credentialShape,
13
+ sameCredentialShape
14
+ };
15
+ //# sourceMappingURL=credential-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-shape.js","sources":["../../src/threads/credential-shape.ts"],"sourcesContent":["import type { AthenaStatewireTokenGetter } from '../runtime/statewire-auth-headers';\n\n/**\n * What a host's credentials look like, without the secrets: enough to notice\n * a swap. `getToken` counts by presence, not identity — an inline getter is a\n * new function every render, and keying on it would refetch forever.\n */\nexport interface CredentialShape {\n apiKey: string | undefined;\n token: string | null | undefined;\n hasGetToken: boolean;\n}\n\nexport function credentialShape(credentials: {\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n}): CredentialShape {\n return {\n apiKey: credentials.apiKey,\n token: credentials.token,\n hasGetToken: credentials.getToken !== undefined,\n };\n}\n\nexport function sameCredentialShape(a: CredentialShape, b: CredentialShape): boolean {\n return a.apiKey === b.apiKey && a.token === b.token && a.hasGetToken === b.hasGetToken;\n}\n"],"names":[],"mappings":"AAaO,SAAS,gBAAgB,aAIZ;AAClB,SAAO;AAAA,IACL,QAAQ,YAAY;AAAA,IACpB,OAAO,YAAY;AAAA,IACnB,aAAa,YAAY,aAAa;AAAA,EAAA;AAE1C;AAEO,SAAS,oBAAoB,GAAoB,GAA6B;AACnF,SAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE;AAC7E;"}
@@ -21,7 +21,9 @@ const reactQuery = require("@tanstack/react-query");
21
21
  const react$1 = require("@assistant-ui/react");
22
22
  const AthenaAuthContext = require("../auth/AthenaAuthContext.cjs");
23
23
  const statewireAuthHeaders = require("../runtime/statewire-auth-headers.cjs");
24
+ const api = require("./api.cjs");
24
25
  const conversationListFetcher = require("./conversation-list-fetcher.cjs");
26
+ const credentialShape = require("./credential-shape.cjs");
25
27
  const mintThreadId = require("./mint-thread-id.cjs");
26
28
  const title = require("./title.cjs");
27
29
  const AthenaStatewireThreadListContext = react.createContext(null);
@@ -43,7 +45,7 @@ function StatewireThreadListState({
43
45
  initialThreadId,
44
46
  children
45
47
  }) {
46
- var _a;
48
+ var _a, _b, _c;
47
49
  const [active, setActive] = react.useState(
48
50
  () => initialThreadId ? { id: initialThreadId, isNew: false } : { id: mintThreadId.mintThreadId(), isNew: true }
49
51
  );
@@ -89,10 +91,40 @@ function StatewireThreadListState({
89
91
  ),
90
92
  []
91
93
  );
92
- const indexEnabled = syncUrl !== void 0 && principal.length > 0 && hasAuth;
94
+ const credentialsRef = react.useRef(credentialShape.credentialShape({ apiKey, token, getToken }));
95
+ const credentialGenerationRef = react.useRef(0);
96
+ const currentCredentials = credentialShape.credentialShape({ apiKey, token, getToken });
97
+ if (!credentialShape.sameCredentialShape(credentialsRef.current, currentCredentials)) {
98
+ credentialsRef.current = currentCredentials;
99
+ credentialGenerationRef.current += 1;
100
+ }
101
+ const credentialGeneration = credentialGenerationRef.current;
102
+ const identityEnabled = syncUrl !== void 0 && principal.length > 0 && hasAuth;
103
+ const identity = reactQuery.useQuery({
104
+ // Keyed by the non-secret principal and credential generation, never the
105
+ // credential itself.
106
+ queryKey: ["athena-sdk", "account-identity", backendUrl, principal, credentialGeneration],
107
+ queryFn: async ({ signal }) => api.fetchAccountIdentity(
108
+ backendUrl,
109
+ {
110
+ apiKey: apiKeyRef.current,
111
+ token: await statewireAuthHeaders.resolveStatewireAuthToken(
112
+ { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },
113
+ { staleAuth: false }
114
+ )
115
+ },
116
+ { signal }
117
+ ),
118
+ enabled: identityEnabled,
119
+ staleTime: Number.POSITIVE_INFINITY
120
+ });
121
+ const indexWorkspaceId = ((_b = identity.data) == null ? void 0 : _b.workspaceId) ?? "";
122
+ const indexUserId = ((_c = identity.data) == null ? void 0 : _c.userId) ?? "";
123
+ const indexEnabled = identityEnabled && indexWorkspaceId.length > 0 && indexUserId.length > 0;
93
124
  const index = conversationIndex.useConversationIndex({
94
125
  baseUrl: syncUrl === void 0 ? "" : conversationIndex.conversationIndexUrlFromStatewireUrl(syncUrl),
95
- userId: principal || "pending-user",
126
+ workspaceId: indexWorkspaceId || "pending-workspace",
127
+ userId: indexUserId || "pending-user",
96
128
  headers: indexHeaders,
97
129
  enabled: indexEnabled
98
130
  });
@@ -1 +1 @@
1
- {"version":3,"file":"statewire-thread-list.cjs","sources":["../../src/threads/statewire-thread-list.tsx"],"sourcesContent":["import {\n type ConversationListFilter,\n conversationIndexUrlFromStatewireUrl,\n type DeepAgentHeadersResolver,\n useConversationIndex,\n useConversationList,\n} from '@athenaintel/agent-runtime';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { useAuiState } from '@assistant-ui/react';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport { AthenaAuthContext } from '../auth/AthenaAuthContext';\nimport {\n type AthenaStatewireTokenGetter,\n resolveStatewireAuthHeaders,\n resolveStatewireAuthToken,\n} from '../runtime/statewire-auth-headers';\nimport { createRestConversationListFetcher } from './conversation-list-fetcher';\nimport { mintThreadId } from './mint-thread-id';\nimport { isSerializedContentPartsTitle, normalizeThreadTitle } from './title';\n\nexport interface AthenaStatewireThreadSummary {\n id: string;\n title: string | null;\n}\n\n/**\n * Thread-list state. The list is the persisted Agora session list; the ACTIVE\n * thread is a plain id the statewire runtime attaches to — switching threads\n * remounts the statewire session against the new `…/v2/threads/{id}` URL\n * (the same model the mobile app uses).\n */\nexport interface AthenaStatewireThreadListState {\n threads: AthenaStatewireThreadSummary[];\n isLoading: boolean;\n activeThreadId: string;\n /** True while the active thread is a locally-minted chat with no server history. */\n isNewChat: boolean;\n selectThread: (threadId: string) => void;\n newThread: () => void;\n refresh: () => void;\n /**\n * Where titles come from. `'index'` once the signed-in user's live\n * conversation index has replicated — titles and newly created threads then\n * arrive as pushed updates; `'poll'` while it has not (no signed-in\n * principal, e.g. API-key mode, or before its snapshot), when a\n * freshly-started thread's title is picked up by the bounded REST poll.\n */\n titleSource: 'index' | 'poll';\n}\n\nexport const AthenaStatewireThreadListContext =\n createContext<AthenaStatewireThreadListState | null>(null);\n\n/** Read the statewire thread-list state, or null outside that mode. */\nexport function useAthenaStatewireThreadList(): AthenaStatewireThreadListState | null {\n return useContext(AthenaStatewireThreadListContext);\n}\n\n/** Trigger a re-fetch of the thread list. `null` outside `enableThreadList` mode. */\nexport function useRefreshThreadList(): (() => void) | null {\n return useAthenaStatewireThreadList()?.refresh ?? null;\n}\n\n/**\n * The statewire thread list reads one live page through the shared\n * conversation-list contract; the REST default page size predates the shared\n * pager and is preserved here.\n */\nconst THREAD_LIST_PAGE_SIZE = 50;\n\nfunction StatewireThreadListState({\n backendUrl,\n syncUrl,\n apiKey,\n token,\n getToken,\n appId,\n initialThreadId,\n children,\n}: {\n backendUrl: string;\n syncUrl?: string;\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n appId?: string;\n initialThreadId?: string;\n children: ReactNode;\n}) {\n const [active, setActive] = useState<{ id: string; isNew: boolean }>(() =>\n initialThreadId\n ? { id: initialThreadId, isNew: false }\n : { id: mintThreadId(), isNew: true },\n );\n\n const tokenRef = useRef(token);\n tokenRef.current = token;\n const apiKeyRef = useRef(apiKey);\n apiKeyRef.current = apiKey;\n const getTokenRef = useRef(getToken);\n getTokenRef.current = getToken;\n\n // The cache must be keyed by WHO is asking. Keying on `backendUrl`/`appId`\n // alone let a token swap to a different user read the previous user's thread\n // titles out of the cache. The principal is a non-secret user id, so no\n // credential enters the key (or React Query's devtools). The shared pager\n // never serves placeholder rows across keys, so another principal's list is\n // never shown while the new one loads.\n const authContext = useContext(AthenaAuthContext);\n const principal = authContext?.user?.userId ?? '';\n // A live token getter is a credential too: a `getToken`-only host\n // authenticates its chat, so its list must not stay empty.\n const hasAuth = !!(apiKey || token || getToken);\n\n const fetchPage = useMemo(\n () =>\n createRestConversationListFetcher({\n backendUrl,\n // Resolved per request with the attach's token semantics (getter first,\n // host-held token as the fallback) so a rotated token is never stale.\n getAuth: async () => ({\n apiKey: apiKeyRef.current,\n token: await resolveStatewireAuthToken(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n { staleAuth: false },\n ),\n }),\n }),\n [backendUrl],\n );\n const filter = useMemo<ConversationListFilter>(\n () => (appId ? { appId } : {}),\n [appId],\n );\n const list = useConversationList({\n fetchPage,\n filter,\n pageSize: THREAD_LIST_PAGE_SIZE,\n scopeKey: `${backendUrl}::${principal}::${hasAuth ? 'auth' : 'anon'}`,\n enabled: hasAuth,\n });\n\n // Live titles ride the signed-in user's conversation index — the same push\n // channel the conversation-history panel renders from: the host replicates\n // title, lifecycle, and creation changes as ops, so a freshly-started\n // thread's server-generated title lands without polling. The index is keyed\n // by the principal, so it only attaches once one is known; API-key-only\n // hosts keep the bounded REST poll (`titleSource: 'poll'`).\n const indexHeaders = useCallback<DeepAgentHeadersResolver>(\n (ctx) =>\n resolveStatewireAuthHeaders(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n ctx,\n ),\n [],\n );\n const indexEnabled = syncUrl !== undefined && principal.length > 0 && hasAuth;\n const index = useConversationIndex({\n baseUrl: syncUrl === undefined ? '' : conversationIndexUrlFromStatewireUrl(syncUrl),\n userId: principal || 'pending-user',\n headers: indexHeaders,\n enabled: indexEnabled,\n });\n const indexTitles = useMemo(() => {\n const titles = new Map<string, string | null>();\n for (const entry of index.entries ?? []) {\n titles.set(entry.thread_id, normalizeThreadTitle(entry.title) ?? null);\n }\n return titles;\n }, [index.entries]);\n\n const data = useMemo(\n () =>\n list.conversations.map((conversation) => ({\n id: conversation.threadId,\n title:\n indexTitles.get(conversation.threadId) ??\n normalizeThreadTitle(conversation.title) ??\n null,\n })),\n [list.conversations, indexTitles],\n );\n const isLoading = list.isLoading;\n\n const listRefresh = list.refresh;\n const refresh = useCallback(() => {\n void listRefresh();\n }, [listRefresh]);\n\n // A thread the index knows — created here on first send, or elsewhere —\n // that the REST page does not list yet: refresh once per such id so the row\n // appears without the user pressing refresh. Rows for other app ids belong\n // to other lists and never trigger one.\n const refreshedForRef = useRef<Set<string>>(new Set());\n useEffect(() => {\n if (index.entries === null) return;\n const listed = new Set(list.conversations.map((conversation) => conversation.threadId));\n let pending = false;\n for (const entry of index.entries) {\n if (appId && entry.app_id !== appId) continue;\n if (listed.has(entry.thread_id) || refreshedForRef.current.has(entry.thread_id)) continue;\n refreshedForRef.current.add(entry.thread_id);\n pending = true;\n }\n if (pending) void listRefresh();\n }, [index.entries, list.conversations, appId, listRefresh]);\n\n const selectThread = useCallback((threadId: string) => {\n if (!threadId) return;\n setActive((current) =>\n current.id === threadId ? current : { id: threadId, isNew: false },\n );\n }, []);\n\n const newThread = useCallback(() => {\n setActive({ id: mintThreadId(), isNew: true });\n }, []);\n\n // A minted thread stops being \"new\" once the backend lists it: until then\n // `useAthenaThreadManager` reports a null id, so URL sync cannot name it.\n const isPersisted = data.some((thread) => thread.id === active.id);\n useEffect(() => {\n if (isPersisted) {\n setActive((current) => (current.isNew ? { ...current, isNew: false } : current));\n }\n }, [isPersisted]);\n\n const titleSource: AthenaStatewireThreadListState['titleSource'] =\n index.entries !== null ? 'index' : 'poll';\n\n const value = useMemo<AthenaStatewireThreadListState>(\n () => ({\n threads: data,\n isLoading,\n activeThreadId: active.id,\n isNewChat: active.isNew,\n selectThread,\n newThread,\n refresh,\n titleSource,\n }),\n [data, isLoading, active.id, active.isNew, selectThread, newThread, refresh, titleSource],\n );\n\n return (\n <AthenaStatewireThreadListContext.Provider value={value}>\n {children}\n </AthenaStatewireThreadListContext.Provider>\n );\n}\n\n/**\n * Provides the statewire thread-list state, with an SDK-scoped QueryClient so\n * consumers do not need their own QueryClientProvider. `syncUrl` (the\n * `…/v2/threads` mount) locates the conversation index beside it; without it\n * titles fall back to the REST poll.\n */\nexport function AthenaStatewireThreadListProvider({\n backendUrl,\n syncUrl,\n apiKey,\n token,\n getToken,\n appId,\n initialThreadId,\n children,\n}: {\n backendUrl: string;\n syncUrl?: string;\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n appId?: string;\n initialThreadId?: string;\n children: ReactNode;\n}) {\n const [queryClient] = useState(() => new QueryClient());\n return (\n <QueryClientProvider client={queryClient}>\n <StatewireThreadListState\n backendUrl={backendUrl}\n syncUrl={syncUrl}\n apiKey={apiKey}\n token={token}\n getToken={getToken}\n appId={appId}\n initialThreadId={initialThreadId}\n >\n {children}\n </StatewireThreadListState>\n </QueryClientProvider>\n );\n}\n\nconst TITLE_POLL_DELAY_MS = 5000;\nconst TITLE_POLL_INTERVAL_MS = 2000;\nconst TITLE_POLL_MAX_DURATION_MS = 60_000;\n\n/**\n * Fallback for hosts without a live conversation index (`titleSource: 'poll'`,\n * i.e. no signed-in principal): refreshes the thread list until a\n * freshly-started thread's server-generated title appears. Idle while the\n * index carries titles. Mount inside the statewire runtime scope (needs aui\n * state).\n */\nexport function StatewireThreadTitlePoller(): null {\n const list = useAthenaStatewireThreadList();\n const hasMessages = useAuiState((s) => (s.optional.thread?.messages.length ?? 0) > 0);\n\n const activeThreadId = list?.activeThreadId ?? null;\n const titleSource = list?.titleSource ?? 'poll';\n const entry = list?.threads.find((thread) => thread.id === activeThreadId);\n const title = entry?.title?.trim() ?? '';\n const hasTitle = title.length > 0 && !isSerializedContentPartsTitle(title);\n\n const polledThreadsRef = useRef<Set<string>>(new Set());\n const refreshRef = useRef(list?.refresh);\n refreshRef.current = list?.refresh;\n\n useEffect(() => {\n if (!activeThreadId || !hasMessages || hasTitle || titleSource === 'index') return;\n if (polledThreadsRef.current.has(activeThreadId)) return;\n polledThreadsRef.current.add(activeThreadId);\n\n let intervalId: ReturnType<typeof setInterval> | null = null;\n const startId = setTimeout(() => {\n intervalId = setInterval(() => refreshRef.current?.(), TITLE_POLL_INTERVAL_MS);\n }, TITLE_POLL_DELAY_MS);\n const stopId = setTimeout(() => {\n if (intervalId !== null) clearInterval(intervalId);\n }, TITLE_POLL_MAX_DURATION_MS);\n\n return () => {\n clearTimeout(startId);\n clearTimeout(stopId);\n if (intervalId !== null) clearInterval(intervalId);\n };\n }, [activeThreadId, hasMessages, hasTitle, titleSource]);\n\n return null;\n}\n"],"names":["createContext","useContext","useState","mintThreadId","useRef","AthenaAuthContext","useMemo","createRestConversationListFetcher","resolveStatewireAuthToken","useConversationList","useCallback","resolveStatewireAuthHeaders","useConversationIndex","conversationIndexUrlFromStatewireUrl","normalizeThreadTitle","useEffect","jsx","QueryClient","QueryClientProvider","useAuiState","_a","title","isSerializedContentPartsTitle"],"mappings":"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"}
1
+ {"version":3,"file":"statewire-thread-list.cjs","sources":["../../src/threads/statewire-thread-list.tsx"],"sourcesContent":["import {\n type ConversationListFilter,\n conversationIndexUrlFromStatewireUrl,\n type DeepAgentHeadersResolver,\n useConversationIndex,\n useConversationList,\n} from '@athenaintel/agent-runtime';\nimport { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';\nimport { useAuiState } from '@assistant-ui/react';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport { AthenaAuthContext } from '../auth/AthenaAuthContext';\nimport {\n type AthenaStatewireTokenGetter,\n resolveStatewireAuthHeaders,\n resolveStatewireAuthToken,\n} from '../runtime/statewire-auth-headers';\nimport { fetchAccountIdentity } from './api';\nimport { createRestConversationListFetcher } from './conversation-list-fetcher';\nimport { credentialShape, sameCredentialShape } from './credential-shape';\nimport { mintThreadId } from './mint-thread-id';\nimport { isSerializedContentPartsTitle, normalizeThreadTitle } from './title';\n\nexport interface AthenaStatewireThreadSummary {\n id: string;\n title: string | null;\n}\n\n/**\n * Thread-list state. The list is the persisted Agora session list; the ACTIVE\n * thread is a plain id the statewire runtime attaches to — switching threads\n * remounts the statewire session against the new `…/v2/threads/{id}` URL\n * (the same model the mobile app uses).\n */\nexport interface AthenaStatewireThreadListState {\n threads: AthenaStatewireThreadSummary[];\n isLoading: boolean;\n activeThreadId: string;\n /** True while the active thread is a locally-minted chat with no server history. */\n isNewChat: boolean;\n selectThread: (threadId: string) => void;\n newThread: () => void;\n refresh: () => void;\n /**\n * Where titles come from. `'index'` once the signed-in user's live\n * conversation index has replicated — titles and newly created threads then\n * arrive as pushed updates; `'poll'` while it has not (no signed-in\n * principal, e.g. API-key mode, or before its snapshot), when a\n * freshly-started thread's title is picked up by the bounded REST poll.\n */\n titleSource: 'index' | 'poll';\n}\n\nexport const AthenaStatewireThreadListContext =\n createContext<AthenaStatewireThreadListState | null>(null);\n\n/** Read the statewire thread-list state, or null outside that mode. */\nexport function useAthenaStatewireThreadList(): AthenaStatewireThreadListState | null {\n return useContext(AthenaStatewireThreadListContext);\n}\n\n/** Trigger a re-fetch of the thread list. `null` outside `enableThreadList` mode. */\nexport function useRefreshThreadList(): (() => void) | null {\n return useAthenaStatewireThreadList()?.refresh ?? null;\n}\n\n/**\n * The statewire thread list reads one live page through the shared\n * conversation-list contract; the REST default page size predates the shared\n * pager and is preserved here.\n */\nconst THREAD_LIST_PAGE_SIZE = 50;\n\nfunction StatewireThreadListState({\n backendUrl,\n syncUrl,\n apiKey,\n token,\n getToken,\n appId,\n initialThreadId,\n children,\n}: {\n backendUrl: string;\n syncUrl?: string;\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n appId?: string;\n initialThreadId?: string;\n children: ReactNode;\n}) {\n const [active, setActive] = useState<{ id: string; isNew: boolean }>(() =>\n initialThreadId\n ? { id: initialThreadId, isNew: false }\n : { id: mintThreadId(), isNew: true },\n );\n\n const tokenRef = useRef(token);\n tokenRef.current = token;\n const apiKeyRef = useRef(apiKey);\n apiKeyRef.current = apiKey;\n const getTokenRef = useRef(getToken);\n getTokenRef.current = getToken;\n\n // The cache must be keyed by WHO is asking. Keying on `backendUrl`/`appId`\n // alone let a token swap to a different user read the previous user's thread\n // titles out of the cache. The principal is a non-secret user id, so no\n // credential enters the key (or React Query's devtools). The shared pager\n // never serves placeholder rows across keys, so another principal's list is\n // never shown while the new one loads.\n const authContext = useContext(AthenaAuthContext);\n const principal = authContext?.user?.userId ?? '';\n // A live token getter is a credential too: a `getToken`-only host\n // authenticates its chat, so its list must not stay empty.\n const hasAuth = !!(apiKey || token || getToken);\n\n const fetchPage = useMemo(\n () =>\n createRestConversationListFetcher({\n backendUrl,\n // Resolved per request with the attach's token semantics (getter first,\n // host-held token as the fallback) so a rotated token is never stale.\n getAuth: async () => ({\n apiKey: apiKeyRef.current,\n token: await resolveStatewireAuthToken(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n { staleAuth: false },\n ),\n }),\n }),\n [backendUrl],\n );\n const filter = useMemo<ConversationListFilter>(\n () => (appId ? { appId } : {}),\n [appId],\n );\n const list = useConversationList({\n fetchPage,\n filter,\n pageSize: THREAD_LIST_PAGE_SIZE,\n scopeKey: `${backendUrl}::${principal}::${hasAuth ? 'auth' : 'anon'}`,\n enabled: hasAuth,\n });\n\n // Live titles ride the signed-in user's conversation index — the same push\n // channel the conversation-history panel renders from: the host replicates\n // title, lifecycle, and creation changes as ops, so a freshly-started\n // thread's server-generated title lands without polling. The index is one\n // instance per `(workspace, user)` in agora's ids, so the pair is resolved\n // from the backend once a principal is known and the index only attaches\n // once both halves are; API-key-only hosts keep the bounded REST poll\n // (`titleSource: 'poll'`).\n const indexHeaders = useCallback<DeepAgentHeadersResolver>(\n (ctx) =>\n resolveStatewireAuthHeaders(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n ctx,\n ),\n [],\n );\n // The workspace half is the CREDENTIAL's: a scoped credential (sandbox,\n // preview, Office session) pins its own workspace, so swapping one for\n // another under the same principal must re-resolve the pair. The query is\n // therefore keyed by a credential generation — a counter that advances\n // whenever the host's credentials change — so no secret enters the key.\n const credentialsRef = useRef(credentialShape({ apiKey, token, getToken }));\n const credentialGenerationRef = useRef(0);\n const currentCredentials = credentialShape({ apiKey, token, getToken });\n if (!sameCredentialShape(credentialsRef.current, currentCredentials)) {\n credentialsRef.current = currentCredentials;\n credentialGenerationRef.current += 1;\n }\n const credentialGeneration = credentialGenerationRef.current;\n const identityEnabled = syncUrl !== undefined && principal.length > 0 && hasAuth;\n const identity = useQuery({\n // Keyed by the non-secret principal and credential generation, never the\n // credential itself.\n queryKey: ['athena-sdk', 'account-identity', backendUrl, principal, credentialGeneration],\n queryFn: async ({ signal }) =>\n fetchAccountIdentity(\n backendUrl,\n {\n apiKey: apiKeyRef.current,\n token: await resolveStatewireAuthToken(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n { staleAuth: false },\n ),\n },\n { signal },\n ),\n enabled: identityEnabled,\n staleTime: Number.POSITIVE_INFINITY,\n });\n const indexWorkspaceId = identity.data?.workspaceId ?? '';\n const indexUserId = identity.data?.userId ?? '';\n const indexEnabled = identityEnabled && indexWorkspaceId.length > 0 && indexUserId.length > 0;\n const index = useConversationIndex({\n baseUrl: syncUrl === undefined ? '' : conversationIndexUrlFromStatewireUrl(syncUrl),\n workspaceId: indexWorkspaceId || 'pending-workspace',\n userId: indexUserId || 'pending-user',\n headers: indexHeaders,\n enabled: indexEnabled,\n });\n const indexTitles = useMemo(() => {\n const titles = new Map<string, string | null>();\n for (const entry of index.entries ?? []) {\n titles.set(entry.thread_id, normalizeThreadTitle(entry.title) ?? null);\n }\n return titles;\n }, [index.entries]);\n\n const data = useMemo(\n () =>\n list.conversations.map((conversation) => ({\n id: conversation.threadId,\n title:\n indexTitles.get(conversation.threadId) ??\n normalizeThreadTitle(conversation.title) ??\n null,\n })),\n [list.conversations, indexTitles],\n );\n const isLoading = list.isLoading;\n\n const listRefresh = list.refresh;\n const refresh = useCallback(() => {\n void listRefresh();\n }, [listRefresh]);\n\n // A thread the index knows — created here on first send, or elsewhere —\n // that the REST page does not list yet: refresh once per such id so the row\n // appears without the user pressing refresh. Rows for other app ids belong\n // to other lists and never trigger one.\n const refreshedForRef = useRef<Set<string>>(new Set());\n useEffect(() => {\n if (index.entries === null) return;\n const listed = new Set(list.conversations.map((conversation) => conversation.threadId));\n let pending = false;\n for (const entry of index.entries) {\n if (appId && entry.app_id !== appId) continue;\n if (listed.has(entry.thread_id) || refreshedForRef.current.has(entry.thread_id)) continue;\n refreshedForRef.current.add(entry.thread_id);\n pending = true;\n }\n if (pending) void listRefresh();\n }, [index.entries, list.conversations, appId, listRefresh]);\n\n const selectThread = useCallback((threadId: string) => {\n if (!threadId) return;\n setActive((current) =>\n current.id === threadId ? current : { id: threadId, isNew: false },\n );\n }, []);\n\n const newThread = useCallback(() => {\n setActive({ id: mintThreadId(), isNew: true });\n }, []);\n\n // A minted thread stops being \"new\" once the backend lists it: until then\n // `useAthenaThreadManager` reports a null id, so URL sync cannot name it.\n const isPersisted = data.some((thread) => thread.id === active.id);\n useEffect(() => {\n if (isPersisted) {\n setActive((current) => (current.isNew ? { ...current, isNew: false } : current));\n }\n }, [isPersisted]);\n\n const titleSource: AthenaStatewireThreadListState['titleSource'] =\n index.entries !== null ? 'index' : 'poll';\n\n const value = useMemo<AthenaStatewireThreadListState>(\n () => ({\n threads: data,\n isLoading,\n activeThreadId: active.id,\n isNewChat: active.isNew,\n selectThread,\n newThread,\n refresh,\n titleSource,\n }),\n [data, isLoading, active.id, active.isNew, selectThread, newThread, refresh, titleSource],\n );\n\n return (\n <AthenaStatewireThreadListContext.Provider value={value}>\n {children}\n </AthenaStatewireThreadListContext.Provider>\n );\n}\n\n/**\n * Provides the statewire thread-list state, with an SDK-scoped QueryClient so\n * consumers do not need their own QueryClientProvider. `syncUrl` (the\n * `…/v2/threads` mount) locates the conversation index beside it; without it\n * titles fall back to the REST poll.\n */\nexport function AthenaStatewireThreadListProvider({\n backendUrl,\n syncUrl,\n apiKey,\n token,\n getToken,\n appId,\n initialThreadId,\n children,\n}: {\n backendUrl: string;\n syncUrl?: string;\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n appId?: string;\n initialThreadId?: string;\n children: ReactNode;\n}) {\n const [queryClient] = useState(() => new QueryClient());\n return (\n <QueryClientProvider client={queryClient}>\n <StatewireThreadListState\n backendUrl={backendUrl}\n syncUrl={syncUrl}\n apiKey={apiKey}\n token={token}\n getToken={getToken}\n appId={appId}\n initialThreadId={initialThreadId}\n >\n {children}\n </StatewireThreadListState>\n </QueryClientProvider>\n );\n}\n\nconst TITLE_POLL_DELAY_MS = 5000;\nconst TITLE_POLL_INTERVAL_MS = 2000;\nconst TITLE_POLL_MAX_DURATION_MS = 60_000;\n\n/**\n * Fallback for hosts without a live conversation index (`titleSource: 'poll'`,\n * i.e. no signed-in principal): refreshes the thread list until a\n * freshly-started thread's server-generated title appears. Idle while the\n * index carries titles. Mount inside the statewire runtime scope (needs aui\n * state).\n */\nexport function StatewireThreadTitlePoller(): null {\n const list = useAthenaStatewireThreadList();\n const hasMessages = useAuiState((s) => (s.optional.thread?.messages.length ?? 0) > 0);\n\n const activeThreadId = list?.activeThreadId ?? null;\n const titleSource = list?.titleSource ?? 'poll';\n const entry = list?.threads.find((thread) => thread.id === activeThreadId);\n const title = entry?.title?.trim() ?? '';\n const hasTitle = title.length > 0 && !isSerializedContentPartsTitle(title);\n\n const polledThreadsRef = useRef<Set<string>>(new Set());\n const refreshRef = useRef(list?.refresh);\n refreshRef.current = list?.refresh;\n\n useEffect(() => {\n if (!activeThreadId || !hasMessages || hasTitle || titleSource === 'index') return;\n if (polledThreadsRef.current.has(activeThreadId)) return;\n polledThreadsRef.current.add(activeThreadId);\n\n let intervalId: ReturnType<typeof setInterval> | null = null;\n const startId = setTimeout(() => {\n intervalId = setInterval(() => refreshRef.current?.(), TITLE_POLL_INTERVAL_MS);\n }, TITLE_POLL_DELAY_MS);\n const stopId = setTimeout(() => {\n if (intervalId !== null) clearInterval(intervalId);\n }, TITLE_POLL_MAX_DURATION_MS);\n\n return () => {\n clearTimeout(startId);\n clearTimeout(stopId);\n if (intervalId !== null) clearInterval(intervalId);\n };\n }, [activeThreadId, hasMessages, hasTitle, titleSource]);\n\n return null;\n}\n"],"names":["createContext","useContext","useState","mintThreadId","useRef","AthenaAuthContext","useMemo","createRestConversationListFetcher","resolveStatewireAuthToken","useConversationList","useCallback","resolveStatewireAuthHeaders","credentialShape","sameCredentialShape","useQuery","fetchAccountIdentity","useConversationIndex","conversationIndexUrlFromStatewireUrl","normalizeThreadTitle","useEffect","jsx","QueryClient","QueryClientProvider","useAuiState","_a","title","isSerializedContentPartsTitle"],"mappings":"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"}
@@ -15,11 +15,13 @@ import "statewire";
15
15
  import "../packages/agent-runtime/src/thread-status.js";
16
16
  import "@assistant-ui/core";
17
17
  import "../packages/agent-runtime/src/thread-handoff.js";
18
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
18
+ import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
19
19
  import { useAuiState } from "@assistant-ui/react";
20
20
  import { AthenaAuthContext } from "../auth/AthenaAuthContext.js";
21
21
  import { resolveStatewireAuthToken, resolveStatewireAuthHeaders } from "../runtime/statewire-auth-headers.js";
22
+ import { fetchAccountIdentity } from "./api.js";
22
23
  import { createRestConversationListFetcher } from "./conversation-list-fetcher.js";
24
+ import { credentialShape, sameCredentialShape } from "./credential-shape.js";
23
25
  import { mintThreadId } from "./mint-thread-id.js";
24
26
  import { normalizeThreadTitle, isSerializedContentPartsTitle } from "./title.js";
25
27
  const AthenaStatewireThreadListContext = createContext(null);
@@ -41,7 +43,7 @@ function StatewireThreadListState({
41
43
  initialThreadId,
42
44
  children
43
45
  }) {
44
- var _a;
46
+ var _a, _b, _c;
45
47
  const [active, setActive] = useState(
46
48
  () => initialThreadId ? { id: initialThreadId, isNew: false } : { id: mintThreadId(), isNew: true }
47
49
  );
@@ -87,10 +89,40 @@ function StatewireThreadListState({
87
89
  ),
88
90
  []
89
91
  );
90
- const indexEnabled = syncUrl !== void 0 && principal.length > 0 && hasAuth;
92
+ const credentialsRef = useRef(credentialShape({ apiKey, token, getToken }));
93
+ const credentialGenerationRef = useRef(0);
94
+ const currentCredentials = credentialShape({ apiKey, token, getToken });
95
+ if (!sameCredentialShape(credentialsRef.current, currentCredentials)) {
96
+ credentialsRef.current = currentCredentials;
97
+ credentialGenerationRef.current += 1;
98
+ }
99
+ const credentialGeneration = credentialGenerationRef.current;
100
+ const identityEnabled = syncUrl !== void 0 && principal.length > 0 && hasAuth;
101
+ const identity = useQuery({
102
+ // Keyed by the non-secret principal and credential generation, never the
103
+ // credential itself.
104
+ queryKey: ["athena-sdk", "account-identity", backendUrl, principal, credentialGeneration],
105
+ queryFn: async ({ signal }) => fetchAccountIdentity(
106
+ backendUrl,
107
+ {
108
+ apiKey: apiKeyRef.current,
109
+ token: await resolveStatewireAuthToken(
110
+ { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },
111
+ { staleAuth: false }
112
+ )
113
+ },
114
+ { signal }
115
+ ),
116
+ enabled: identityEnabled,
117
+ staleTime: Number.POSITIVE_INFINITY
118
+ });
119
+ const indexWorkspaceId = ((_b = identity.data) == null ? void 0 : _b.workspaceId) ?? "";
120
+ const indexUserId = ((_c = identity.data) == null ? void 0 : _c.userId) ?? "";
121
+ const indexEnabled = identityEnabled && indexWorkspaceId.length > 0 && indexUserId.length > 0;
91
122
  const index = useConversationIndex({
92
123
  baseUrl: syncUrl === void 0 ? "" : conversationIndexUrlFromStatewireUrl(syncUrl),
93
- userId: principal || "pending-user",
124
+ workspaceId: indexWorkspaceId || "pending-workspace",
125
+ userId: indexUserId || "pending-user",
94
126
  headers: indexHeaders,
95
127
  enabled: indexEnabled
96
128
  });
@@ -1 +1 @@
1
- {"version":3,"file":"statewire-thread-list.js","sources":["../../src/threads/statewire-thread-list.tsx"],"sourcesContent":["import {\n type ConversationListFilter,\n conversationIndexUrlFromStatewireUrl,\n type DeepAgentHeadersResolver,\n useConversationIndex,\n useConversationList,\n} from '@athenaintel/agent-runtime';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { useAuiState } from '@assistant-ui/react';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport { AthenaAuthContext } from '../auth/AthenaAuthContext';\nimport {\n type AthenaStatewireTokenGetter,\n resolveStatewireAuthHeaders,\n resolveStatewireAuthToken,\n} from '../runtime/statewire-auth-headers';\nimport { createRestConversationListFetcher } from './conversation-list-fetcher';\nimport { mintThreadId } from './mint-thread-id';\nimport { isSerializedContentPartsTitle, normalizeThreadTitle } from './title';\n\nexport interface AthenaStatewireThreadSummary {\n id: string;\n title: string | null;\n}\n\n/**\n * Thread-list state. The list is the persisted Agora session list; the ACTIVE\n * thread is a plain id the statewire runtime attaches to — switching threads\n * remounts the statewire session against the new `…/v2/threads/{id}` URL\n * (the same model the mobile app uses).\n */\nexport interface AthenaStatewireThreadListState {\n threads: AthenaStatewireThreadSummary[];\n isLoading: boolean;\n activeThreadId: string;\n /** True while the active thread is a locally-minted chat with no server history. */\n isNewChat: boolean;\n selectThread: (threadId: string) => void;\n newThread: () => void;\n refresh: () => void;\n /**\n * Where titles come from. `'index'` once the signed-in user's live\n * conversation index has replicated — titles and newly created threads then\n * arrive as pushed updates; `'poll'` while it has not (no signed-in\n * principal, e.g. API-key mode, or before its snapshot), when a\n * freshly-started thread's title is picked up by the bounded REST poll.\n */\n titleSource: 'index' | 'poll';\n}\n\nexport const AthenaStatewireThreadListContext =\n createContext<AthenaStatewireThreadListState | null>(null);\n\n/** Read the statewire thread-list state, or null outside that mode. */\nexport function useAthenaStatewireThreadList(): AthenaStatewireThreadListState | null {\n return useContext(AthenaStatewireThreadListContext);\n}\n\n/** Trigger a re-fetch of the thread list. `null` outside `enableThreadList` mode. */\nexport function useRefreshThreadList(): (() => void) | null {\n return useAthenaStatewireThreadList()?.refresh ?? null;\n}\n\n/**\n * The statewire thread list reads one live page through the shared\n * conversation-list contract; the REST default page size predates the shared\n * pager and is preserved here.\n */\nconst THREAD_LIST_PAGE_SIZE = 50;\n\nfunction StatewireThreadListState({\n backendUrl,\n syncUrl,\n apiKey,\n token,\n getToken,\n appId,\n initialThreadId,\n children,\n}: {\n backendUrl: string;\n syncUrl?: string;\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n appId?: string;\n initialThreadId?: string;\n children: ReactNode;\n}) {\n const [active, setActive] = useState<{ id: string; isNew: boolean }>(() =>\n initialThreadId\n ? { id: initialThreadId, isNew: false }\n : { id: mintThreadId(), isNew: true },\n );\n\n const tokenRef = useRef(token);\n tokenRef.current = token;\n const apiKeyRef = useRef(apiKey);\n apiKeyRef.current = apiKey;\n const getTokenRef = useRef(getToken);\n getTokenRef.current = getToken;\n\n // The cache must be keyed by WHO is asking. Keying on `backendUrl`/`appId`\n // alone let a token swap to a different user read the previous user's thread\n // titles out of the cache. The principal is a non-secret user id, so no\n // credential enters the key (or React Query's devtools). The shared pager\n // never serves placeholder rows across keys, so another principal's list is\n // never shown while the new one loads.\n const authContext = useContext(AthenaAuthContext);\n const principal = authContext?.user?.userId ?? '';\n // A live token getter is a credential too: a `getToken`-only host\n // authenticates its chat, so its list must not stay empty.\n const hasAuth = !!(apiKey || token || getToken);\n\n const fetchPage = useMemo(\n () =>\n createRestConversationListFetcher({\n backendUrl,\n // Resolved per request with the attach's token semantics (getter first,\n // host-held token as the fallback) so a rotated token is never stale.\n getAuth: async () => ({\n apiKey: apiKeyRef.current,\n token: await resolveStatewireAuthToken(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n { staleAuth: false },\n ),\n }),\n }),\n [backendUrl],\n );\n const filter = useMemo<ConversationListFilter>(\n () => (appId ? { appId } : {}),\n [appId],\n );\n const list = useConversationList({\n fetchPage,\n filter,\n pageSize: THREAD_LIST_PAGE_SIZE,\n scopeKey: `${backendUrl}::${principal}::${hasAuth ? 'auth' : 'anon'}`,\n enabled: hasAuth,\n });\n\n // Live titles ride the signed-in user's conversation index — the same push\n // channel the conversation-history panel renders from: the host replicates\n // title, lifecycle, and creation changes as ops, so a freshly-started\n // thread's server-generated title lands without polling. The index is keyed\n // by the principal, so it only attaches once one is known; API-key-only\n // hosts keep the bounded REST poll (`titleSource: 'poll'`).\n const indexHeaders = useCallback<DeepAgentHeadersResolver>(\n (ctx) =>\n resolveStatewireAuthHeaders(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n ctx,\n ),\n [],\n );\n const indexEnabled = syncUrl !== undefined && principal.length > 0 && hasAuth;\n const index = useConversationIndex({\n baseUrl: syncUrl === undefined ? '' : conversationIndexUrlFromStatewireUrl(syncUrl),\n userId: principal || 'pending-user',\n headers: indexHeaders,\n enabled: indexEnabled,\n });\n const indexTitles = useMemo(() => {\n const titles = new Map<string, string | null>();\n for (const entry of index.entries ?? []) {\n titles.set(entry.thread_id, normalizeThreadTitle(entry.title) ?? null);\n }\n return titles;\n }, [index.entries]);\n\n const data = useMemo(\n () =>\n list.conversations.map((conversation) => ({\n id: conversation.threadId,\n title:\n indexTitles.get(conversation.threadId) ??\n normalizeThreadTitle(conversation.title) ??\n null,\n })),\n [list.conversations, indexTitles],\n );\n const isLoading = list.isLoading;\n\n const listRefresh = list.refresh;\n const refresh = useCallback(() => {\n void listRefresh();\n }, [listRefresh]);\n\n // A thread the index knows — created here on first send, or elsewhere —\n // that the REST page does not list yet: refresh once per such id so the row\n // appears without the user pressing refresh. Rows for other app ids belong\n // to other lists and never trigger one.\n const refreshedForRef = useRef<Set<string>>(new Set());\n useEffect(() => {\n if (index.entries === null) return;\n const listed = new Set(list.conversations.map((conversation) => conversation.threadId));\n let pending = false;\n for (const entry of index.entries) {\n if (appId && entry.app_id !== appId) continue;\n if (listed.has(entry.thread_id) || refreshedForRef.current.has(entry.thread_id)) continue;\n refreshedForRef.current.add(entry.thread_id);\n pending = true;\n }\n if (pending) void listRefresh();\n }, [index.entries, list.conversations, appId, listRefresh]);\n\n const selectThread = useCallback((threadId: string) => {\n if (!threadId) return;\n setActive((current) =>\n current.id === threadId ? current : { id: threadId, isNew: false },\n );\n }, []);\n\n const newThread = useCallback(() => {\n setActive({ id: mintThreadId(), isNew: true });\n }, []);\n\n // A minted thread stops being \"new\" once the backend lists it: until then\n // `useAthenaThreadManager` reports a null id, so URL sync cannot name it.\n const isPersisted = data.some((thread) => thread.id === active.id);\n useEffect(() => {\n if (isPersisted) {\n setActive((current) => (current.isNew ? { ...current, isNew: false } : current));\n }\n }, [isPersisted]);\n\n const titleSource: AthenaStatewireThreadListState['titleSource'] =\n index.entries !== null ? 'index' : 'poll';\n\n const value = useMemo<AthenaStatewireThreadListState>(\n () => ({\n threads: data,\n isLoading,\n activeThreadId: active.id,\n isNewChat: active.isNew,\n selectThread,\n newThread,\n refresh,\n titleSource,\n }),\n [data, isLoading, active.id, active.isNew, selectThread, newThread, refresh, titleSource],\n );\n\n return (\n <AthenaStatewireThreadListContext.Provider value={value}>\n {children}\n </AthenaStatewireThreadListContext.Provider>\n );\n}\n\n/**\n * Provides the statewire thread-list state, with an SDK-scoped QueryClient so\n * consumers do not need their own QueryClientProvider. `syncUrl` (the\n * `…/v2/threads` mount) locates the conversation index beside it; without it\n * titles fall back to the REST poll.\n */\nexport function AthenaStatewireThreadListProvider({\n backendUrl,\n syncUrl,\n apiKey,\n token,\n getToken,\n appId,\n initialThreadId,\n children,\n}: {\n backendUrl: string;\n syncUrl?: string;\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n appId?: string;\n initialThreadId?: string;\n children: ReactNode;\n}) {\n const [queryClient] = useState(() => new QueryClient());\n return (\n <QueryClientProvider client={queryClient}>\n <StatewireThreadListState\n backendUrl={backendUrl}\n syncUrl={syncUrl}\n apiKey={apiKey}\n token={token}\n getToken={getToken}\n appId={appId}\n initialThreadId={initialThreadId}\n >\n {children}\n </StatewireThreadListState>\n </QueryClientProvider>\n );\n}\n\nconst TITLE_POLL_DELAY_MS = 5000;\nconst TITLE_POLL_INTERVAL_MS = 2000;\nconst TITLE_POLL_MAX_DURATION_MS = 60_000;\n\n/**\n * Fallback for hosts without a live conversation index (`titleSource: 'poll'`,\n * i.e. no signed-in principal): refreshes the thread list until a\n * freshly-started thread's server-generated title appears. Idle while the\n * index carries titles. Mount inside the statewire runtime scope (needs aui\n * state).\n */\nexport function StatewireThreadTitlePoller(): null {\n const list = useAthenaStatewireThreadList();\n const hasMessages = useAuiState((s) => (s.optional.thread?.messages.length ?? 0) > 0);\n\n const activeThreadId = list?.activeThreadId ?? null;\n const titleSource = list?.titleSource ?? 'poll';\n const entry = list?.threads.find((thread) => thread.id === activeThreadId);\n const title = entry?.title?.trim() ?? '';\n const hasTitle = title.length > 0 && !isSerializedContentPartsTitle(title);\n\n const polledThreadsRef = useRef<Set<string>>(new Set());\n const refreshRef = useRef(list?.refresh);\n refreshRef.current = list?.refresh;\n\n useEffect(() => {\n if (!activeThreadId || !hasMessages || hasTitle || titleSource === 'index') return;\n if (polledThreadsRef.current.has(activeThreadId)) return;\n polledThreadsRef.current.add(activeThreadId);\n\n let intervalId: ReturnType<typeof setInterval> | null = null;\n const startId = setTimeout(() => {\n intervalId = setInterval(() => refreshRef.current?.(), TITLE_POLL_INTERVAL_MS);\n }, TITLE_POLL_DELAY_MS);\n const stopId = setTimeout(() => {\n if (intervalId !== null) clearInterval(intervalId);\n }, TITLE_POLL_MAX_DURATION_MS);\n\n return () => {\n clearTimeout(startId);\n clearTimeout(stopId);\n if (intervalId !== null) clearInterval(intervalId);\n };\n }, [activeThreadId, hasMessages, hasTitle, titleSource]);\n\n return null;\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"statewire-thread-list.js","sources":["../../src/threads/statewire-thread-list.tsx"],"sourcesContent":["import {\n type ConversationListFilter,\n conversationIndexUrlFromStatewireUrl,\n type DeepAgentHeadersResolver,\n useConversationIndex,\n useConversationList,\n} from '@athenaintel/agent-runtime';\nimport { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';\nimport { useAuiState } from '@assistant-ui/react';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport { AthenaAuthContext } from '../auth/AthenaAuthContext';\nimport {\n type AthenaStatewireTokenGetter,\n resolveStatewireAuthHeaders,\n resolveStatewireAuthToken,\n} from '../runtime/statewire-auth-headers';\nimport { fetchAccountIdentity } from './api';\nimport { createRestConversationListFetcher } from './conversation-list-fetcher';\nimport { credentialShape, sameCredentialShape } from './credential-shape';\nimport { mintThreadId } from './mint-thread-id';\nimport { isSerializedContentPartsTitle, normalizeThreadTitle } from './title';\n\nexport interface AthenaStatewireThreadSummary {\n id: string;\n title: string | null;\n}\n\n/**\n * Thread-list state. The list is the persisted Agora session list; the ACTIVE\n * thread is a plain id the statewire runtime attaches to — switching threads\n * remounts the statewire session against the new `…/v2/threads/{id}` URL\n * (the same model the mobile app uses).\n */\nexport interface AthenaStatewireThreadListState {\n threads: AthenaStatewireThreadSummary[];\n isLoading: boolean;\n activeThreadId: string;\n /** True while the active thread is a locally-minted chat with no server history. */\n isNewChat: boolean;\n selectThread: (threadId: string) => void;\n newThread: () => void;\n refresh: () => void;\n /**\n * Where titles come from. `'index'` once the signed-in user's live\n * conversation index has replicated — titles and newly created threads then\n * arrive as pushed updates; `'poll'` while it has not (no signed-in\n * principal, e.g. API-key mode, or before its snapshot), when a\n * freshly-started thread's title is picked up by the bounded REST poll.\n */\n titleSource: 'index' | 'poll';\n}\n\nexport const AthenaStatewireThreadListContext =\n createContext<AthenaStatewireThreadListState | null>(null);\n\n/** Read the statewire thread-list state, or null outside that mode. */\nexport function useAthenaStatewireThreadList(): AthenaStatewireThreadListState | null {\n return useContext(AthenaStatewireThreadListContext);\n}\n\n/** Trigger a re-fetch of the thread list. `null` outside `enableThreadList` mode. */\nexport function useRefreshThreadList(): (() => void) | null {\n return useAthenaStatewireThreadList()?.refresh ?? null;\n}\n\n/**\n * The statewire thread list reads one live page through the shared\n * conversation-list contract; the REST default page size predates the shared\n * pager and is preserved here.\n */\nconst THREAD_LIST_PAGE_SIZE = 50;\n\nfunction StatewireThreadListState({\n backendUrl,\n syncUrl,\n apiKey,\n token,\n getToken,\n appId,\n initialThreadId,\n children,\n}: {\n backendUrl: string;\n syncUrl?: string;\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n appId?: string;\n initialThreadId?: string;\n children: ReactNode;\n}) {\n const [active, setActive] = useState<{ id: string; isNew: boolean }>(() =>\n initialThreadId\n ? { id: initialThreadId, isNew: false }\n : { id: mintThreadId(), isNew: true },\n );\n\n const tokenRef = useRef(token);\n tokenRef.current = token;\n const apiKeyRef = useRef(apiKey);\n apiKeyRef.current = apiKey;\n const getTokenRef = useRef(getToken);\n getTokenRef.current = getToken;\n\n // The cache must be keyed by WHO is asking. Keying on `backendUrl`/`appId`\n // alone let a token swap to a different user read the previous user's thread\n // titles out of the cache. The principal is a non-secret user id, so no\n // credential enters the key (or React Query's devtools). The shared pager\n // never serves placeholder rows across keys, so another principal's list is\n // never shown while the new one loads.\n const authContext = useContext(AthenaAuthContext);\n const principal = authContext?.user?.userId ?? '';\n // A live token getter is a credential too: a `getToken`-only host\n // authenticates its chat, so its list must not stay empty.\n const hasAuth = !!(apiKey || token || getToken);\n\n const fetchPage = useMemo(\n () =>\n createRestConversationListFetcher({\n backendUrl,\n // Resolved per request with the attach's token semantics (getter first,\n // host-held token as the fallback) so a rotated token is never stale.\n getAuth: async () => ({\n apiKey: apiKeyRef.current,\n token: await resolveStatewireAuthToken(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n { staleAuth: false },\n ),\n }),\n }),\n [backendUrl],\n );\n const filter = useMemo<ConversationListFilter>(\n () => (appId ? { appId } : {}),\n [appId],\n );\n const list = useConversationList({\n fetchPage,\n filter,\n pageSize: THREAD_LIST_PAGE_SIZE,\n scopeKey: `${backendUrl}::${principal}::${hasAuth ? 'auth' : 'anon'}`,\n enabled: hasAuth,\n });\n\n // Live titles ride the signed-in user's conversation index — the same push\n // channel the conversation-history panel renders from: the host replicates\n // title, lifecycle, and creation changes as ops, so a freshly-started\n // thread's server-generated title lands without polling. The index is one\n // instance per `(workspace, user)` in agora's ids, so the pair is resolved\n // from the backend once a principal is known and the index only attaches\n // once both halves are; API-key-only hosts keep the bounded REST poll\n // (`titleSource: 'poll'`).\n const indexHeaders = useCallback<DeepAgentHeadersResolver>(\n (ctx) =>\n resolveStatewireAuthHeaders(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n ctx,\n ),\n [],\n );\n // The workspace half is the CREDENTIAL's: a scoped credential (sandbox,\n // preview, Office session) pins its own workspace, so swapping one for\n // another under the same principal must re-resolve the pair. The query is\n // therefore keyed by a credential generation — a counter that advances\n // whenever the host's credentials change — so no secret enters the key.\n const credentialsRef = useRef(credentialShape({ apiKey, token, getToken }));\n const credentialGenerationRef = useRef(0);\n const currentCredentials = credentialShape({ apiKey, token, getToken });\n if (!sameCredentialShape(credentialsRef.current, currentCredentials)) {\n credentialsRef.current = currentCredentials;\n credentialGenerationRef.current += 1;\n }\n const credentialGeneration = credentialGenerationRef.current;\n const identityEnabled = syncUrl !== undefined && principal.length > 0 && hasAuth;\n const identity = useQuery({\n // Keyed by the non-secret principal and credential generation, never the\n // credential itself.\n queryKey: ['athena-sdk', 'account-identity', backendUrl, principal, credentialGeneration],\n queryFn: async ({ signal }) =>\n fetchAccountIdentity(\n backendUrl,\n {\n apiKey: apiKeyRef.current,\n token: await resolveStatewireAuthToken(\n { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },\n { staleAuth: false },\n ),\n },\n { signal },\n ),\n enabled: identityEnabled,\n staleTime: Number.POSITIVE_INFINITY,\n });\n const indexWorkspaceId = identity.data?.workspaceId ?? '';\n const indexUserId = identity.data?.userId ?? '';\n const indexEnabled = identityEnabled && indexWorkspaceId.length > 0 && indexUserId.length > 0;\n const index = useConversationIndex({\n baseUrl: syncUrl === undefined ? '' : conversationIndexUrlFromStatewireUrl(syncUrl),\n workspaceId: indexWorkspaceId || 'pending-workspace',\n userId: indexUserId || 'pending-user',\n headers: indexHeaders,\n enabled: indexEnabled,\n });\n const indexTitles = useMemo(() => {\n const titles = new Map<string, string | null>();\n for (const entry of index.entries ?? []) {\n titles.set(entry.thread_id, normalizeThreadTitle(entry.title) ?? null);\n }\n return titles;\n }, [index.entries]);\n\n const data = useMemo(\n () =>\n list.conversations.map((conversation) => ({\n id: conversation.threadId,\n title:\n indexTitles.get(conversation.threadId) ??\n normalizeThreadTitle(conversation.title) ??\n null,\n })),\n [list.conversations, indexTitles],\n );\n const isLoading = list.isLoading;\n\n const listRefresh = list.refresh;\n const refresh = useCallback(() => {\n void listRefresh();\n }, [listRefresh]);\n\n // A thread the index knows — created here on first send, or elsewhere —\n // that the REST page does not list yet: refresh once per such id so the row\n // appears without the user pressing refresh. Rows for other app ids belong\n // to other lists and never trigger one.\n const refreshedForRef = useRef<Set<string>>(new Set());\n useEffect(() => {\n if (index.entries === null) return;\n const listed = new Set(list.conversations.map((conversation) => conversation.threadId));\n let pending = false;\n for (const entry of index.entries) {\n if (appId && entry.app_id !== appId) continue;\n if (listed.has(entry.thread_id) || refreshedForRef.current.has(entry.thread_id)) continue;\n refreshedForRef.current.add(entry.thread_id);\n pending = true;\n }\n if (pending) void listRefresh();\n }, [index.entries, list.conversations, appId, listRefresh]);\n\n const selectThread = useCallback((threadId: string) => {\n if (!threadId) return;\n setActive((current) =>\n current.id === threadId ? current : { id: threadId, isNew: false },\n );\n }, []);\n\n const newThread = useCallback(() => {\n setActive({ id: mintThreadId(), isNew: true });\n }, []);\n\n // A minted thread stops being \"new\" once the backend lists it: until then\n // `useAthenaThreadManager` reports a null id, so URL sync cannot name it.\n const isPersisted = data.some((thread) => thread.id === active.id);\n useEffect(() => {\n if (isPersisted) {\n setActive((current) => (current.isNew ? { ...current, isNew: false } : current));\n }\n }, [isPersisted]);\n\n const titleSource: AthenaStatewireThreadListState['titleSource'] =\n index.entries !== null ? 'index' : 'poll';\n\n const value = useMemo<AthenaStatewireThreadListState>(\n () => ({\n threads: data,\n isLoading,\n activeThreadId: active.id,\n isNewChat: active.isNew,\n selectThread,\n newThread,\n refresh,\n titleSource,\n }),\n [data, isLoading, active.id, active.isNew, selectThread, newThread, refresh, titleSource],\n );\n\n return (\n <AthenaStatewireThreadListContext.Provider value={value}>\n {children}\n </AthenaStatewireThreadListContext.Provider>\n );\n}\n\n/**\n * Provides the statewire thread-list state, with an SDK-scoped QueryClient so\n * consumers do not need their own QueryClientProvider. `syncUrl` (the\n * `…/v2/threads` mount) locates the conversation index beside it; without it\n * titles fall back to the REST poll.\n */\nexport function AthenaStatewireThreadListProvider({\n backendUrl,\n syncUrl,\n apiKey,\n token,\n getToken,\n appId,\n initialThreadId,\n children,\n}: {\n backendUrl: string;\n syncUrl?: string;\n apiKey?: string;\n token?: string | null;\n getToken?: AthenaStatewireTokenGetter;\n appId?: string;\n initialThreadId?: string;\n children: ReactNode;\n}) {\n const [queryClient] = useState(() => new QueryClient());\n return (\n <QueryClientProvider client={queryClient}>\n <StatewireThreadListState\n backendUrl={backendUrl}\n syncUrl={syncUrl}\n apiKey={apiKey}\n token={token}\n getToken={getToken}\n appId={appId}\n initialThreadId={initialThreadId}\n >\n {children}\n </StatewireThreadListState>\n </QueryClientProvider>\n );\n}\n\nconst TITLE_POLL_DELAY_MS = 5000;\nconst TITLE_POLL_INTERVAL_MS = 2000;\nconst TITLE_POLL_MAX_DURATION_MS = 60_000;\n\n/**\n * Fallback for hosts without a live conversation index (`titleSource: 'poll'`,\n * i.e. no signed-in principal): refreshes the thread list until a\n * freshly-started thread's server-generated title appears. Idle while the\n * index carries titles. Mount inside the statewire runtime scope (needs aui\n * state).\n */\nexport function StatewireThreadTitlePoller(): null {\n const list = useAthenaStatewireThreadList();\n const hasMessages = useAuiState((s) => (s.optional.thread?.messages.length ?? 0) > 0);\n\n const activeThreadId = list?.activeThreadId ?? null;\n const titleSource = list?.titleSource ?? 'poll';\n const entry = list?.threads.find((thread) => thread.id === activeThreadId);\n const title = entry?.title?.trim() ?? '';\n const hasTitle = title.length > 0 && !isSerializedContentPartsTitle(title);\n\n const polledThreadsRef = useRef<Set<string>>(new Set());\n const refreshRef = useRef(list?.refresh);\n refreshRef.current = list?.refresh;\n\n useEffect(() => {\n if (!activeThreadId || !hasMessages || hasTitle || titleSource === 'index') return;\n if (polledThreadsRef.current.has(activeThreadId)) return;\n polledThreadsRef.current.add(activeThreadId);\n\n let intervalId: ReturnType<typeof setInterval> | null = null;\n const startId = setTimeout(() => {\n intervalId = setInterval(() => refreshRef.current?.(), TITLE_POLL_INTERVAL_MS);\n }, TITLE_POLL_DELAY_MS);\n const stopId = setTimeout(() => {\n if (intervalId !== null) clearInterval(intervalId);\n }, TITLE_POLL_MAX_DURATION_MS);\n\n return () => {\n clearTimeout(startId);\n clearTimeout(stopId);\n if (intervalId !== null) clearInterval(intervalId);\n };\n }, [activeThreadId, hasMessages, hasTitle, titleSource]);\n\n return null;\n}\n"],"names":["_a"],"mappings":"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"}
@@ -70,6 +70,7 @@ function shouldUseRestConversationHistory({
70
70
  function useStatewireConversationHistory({
71
71
  backendUrl,
72
72
  conversationIndexBaseUrl,
73
+ workspaceId,
73
74
  principalId,
74
75
  getAuth,
75
76
  headers,
@@ -79,9 +80,10 @@ function useStatewireConversationHistory({
79
80
  }) {
80
81
  const index = conversationIndex.useConversationIndex({
81
82
  baseUrl: conversationIndexBaseUrl,
83
+ workspaceId: workspaceId || "pending-workspace",
82
84
  userId: principalId || "pending-user",
83
85
  headers,
84
- enabled: enabled && principalId.length > 0,
86
+ enabled: enabled && workspaceId.length > 0 && principalId.length > 0,
85
87
  sse
86
88
  });
87
89
  const supportsStatewirePaging = index.totalCount !== null;
@@ -1 +1 @@
1
- {"version":3,"file":"use-statewire-conversation-history.cjs","sources":["../../src/threads/use-statewire-conversation-history.ts"],"sourcesContent":["import {\n\ttype ConversationIndexEntry,\n\ttype ConversationSummary,\n\ttype UseConversationIndexOptions,\n\tuseConversationIndex,\n} from \"@athenaintel/agent-runtime\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { archiveThread, setThreadReadState } from \"./api\";\nimport {\n\ttype RestConversationAuth,\n\tuseRestConversationHistory,\n} from \"./use-rest-conversation-history\";\n\nexport interface UseStatewireConversationHistoryOptions {\n\tbackendUrl: string;\n\tconversationIndexBaseUrl: string;\n\tprincipalId: string;\n\tgetAuth: () => RestConversationAuth | Promise<RestConversationAuth>;\n\theaders: UseConversationIndexOptions[\"headers\"];\n\tsearch?: string;\n\tenabled?: boolean;\n\tsse?: boolean;\n}\n\n/** Convert the replicated wire row into the shared cross-surface contract. */\nexport function conversationSummaryFromIndexEntry(\n\tentry: ConversationIndexEntry,\n): ConversationSummary {\n\treturn {\n\t\tthreadId: entry.thread_id,\n\t\ttitle: entry.title,\n\t\tupdatedAt: entry.updated_at,\n\t\tcreatedAt: null,\n\t\tstate: entry.state,\n\t\truntime: entry.runtime,\n\t\tsourceChannel: entry.start_channel,\n\t\tassetType: entry.asset_type,\n\t\tassetId: entry.asset_id ?? null,\n\t\tappId: entry.app_id ?? null,\n\t\ttriggerType: entry.trigger_type ?? null,\n\t\tpreview: entry.last_message_preview ?? null,\n\t\tmessageCount: entry.message_count ?? null,\n\t\tisUnread: entry.is_unread ?? false,\n\t\tsessionStatus: entry.session_status ?? null,\n\t\trunStatus: entry.run_status ?? null,\n\t};\n}\n\nexport function conversationHistorySource({\n\tindexEntries,\n\trestConversations,\n\tsupportsStatewirePaging,\n\thasSearch,\n}: {\n\tindexEntries: readonly ConversationIndexEntry[] | null;\n\trestConversations: readonly ConversationSummary[];\n\tsupportsStatewirePaging: boolean;\n\thasSearch: boolean;\n}): ConversationSummary[] {\n\t// Search is server-backed so it covers the user's entire authorized history,\n\t// including rows older than the Statewire live window. An empty query uses\n\t// Statewire as the render source; REST only supplements rolling old hosts.\n\tif (hasSearch) return [...restConversations];\n\tconst statewireItems = (indexEntries ?? []).map(\n\t\tconversationSummaryFromIndexEntry,\n\t);\n\tif (supportsStatewirePaging) return statewireItems;\n\tconst statewireIds = new Set(statewireItems.map((item) => item.threadId));\n\treturn [\n\t\t...statewireItems,\n\t\t...restConversations.filter(\n\t\t\t(conversation) => !statewireIds.has(conversation.threadId),\n\t\t),\n\t];\n}\n\nexport function shouldUseRestConversationHistory({\n\thasSearch,\n\tindexEntries,\n\tsupportsStatewirePaging,\n\tconnectionStatus,\n}: {\n\thasSearch: boolean;\n\tindexEntries: readonly ConversationIndexEntry[] | null;\n\tsupportsStatewirePaging: boolean;\n\tconnectionStatus: string;\n}): boolean {\n\tif (hasSearch) return true;\n\tif (supportsStatewirePaging) return false;\n\t// An old host is identifiable once its snapshot arrives. A transport that\n\t// fails or is parked before any snapshot also needs the independent REST\n\t// path; otherwise entries stays null and the history panel loads forever.\n\treturn (\n\t\tindexEntries !== null ||\n\t\tconnectionStatus === \"reconnecting\" ||\n\t\tconnectionStatus === \"gone\"\n\t);\n}\n\n/**\n * Inclusive satellite conversation history backed by the user's Pinned\n * Statewire instance. The replicated window is the render source; refreshes,\n * pagination, thread creation, lifecycle changes, and title changes arrive on\n * that same channel. A non-empty search deliberately uses the authorized REST\n * list endpoint so matches are not limited to the replicated newest window.\n */\nexport function useStatewireConversationHistory({\n\tbackendUrl,\n\tconversationIndexBaseUrl,\n\tprincipalId,\n\tgetAuth,\n\theaders,\n\tsearch,\n\tenabled = true,\n\tsse = false,\n}: UseStatewireConversationHistoryOptions) {\n\tconst index = useConversationIndex({\n\t\tbaseUrl: conversationIndexBaseUrl,\n\t\tuserId: principalId || \"pending-user\",\n\t\theaders,\n\t\tenabled: enabled && principalId.length > 0,\n\t\tsse,\n\t});\n\t// Rolling deploy compatibility: the pre-paging index snapshot has no\n\t// total_count. It still renders live rows immediately, while the old REST\n\t// pager remains available only until the upgraded host activates.\n\tconst supportsStatewirePaging = index.totalCount !== null;\n\tconst normalizedSearch = search?.trim().toLowerCase() ?? \"\";\n\tconst hasSearch = normalizedSearch.length > 0;\n\tconst usesRestHistory = shouldUseRestConversationHistory({\n\t\thasSearch,\n\t\tindexEntries: index.entries,\n\t\tsupportsStatewirePaging,\n\t\tconnectionStatus: index.connection.status,\n\t});\n\tconst legacyHistory = useRestConversationHistory({\n\t\tbackendUrl,\n\t\tprincipalId,\n\t\tgetAuth,\n\t\tsearch,\n\t\tenabled: enabled && principalId.length > 0 && usesRestHistory,\n\t});\n\tconst [readOverrides, setReadOverrides] = useState<Record<string, boolean>>(\n\t\t{},\n\t);\n\tconst [archivedThreadIds, setArchivedThreadIds] = useState<\n\t\tReadonlySet<string>\n\t>(() => new Set());\n\n\tuseEffect(() => {\n\t\tif (index.entries === null || archivedThreadIds.size === 0) return;\n\t\tconst visibleIds = new Set(index.entries.map((entry) => entry.thread_id));\n\t\tsetArchivedThreadIds((current) => {\n\t\t\tconst next = new Set(\n\t\t\t\t[...current].filter((threadId) => visibleIds.has(threadId)),\n\t\t\t);\n\t\t\treturn next.size === current.size ? current : next;\n\t\t});\n\t}, [archivedThreadIds.size, index.entries]);\n\n\tconst conversations = useMemo(() => {\n\t\treturn conversationHistorySource({\n\t\t\tindexEntries: index.entries,\n\t\t\trestConversations: legacyHistory.conversations,\n\t\t\tsupportsStatewirePaging,\n\t\t\thasSearch,\n\t\t})\n\t\t\t.filter((conversation) => !archivedThreadIds.has(conversation.threadId))\n\t\t\t.map((conversation) => ({\n\t\t\t\t...conversation,\n\t\t\t\tisUnread: readOverrides[conversation.threadId] ?? conversation.isUnread,\n\t\t\t}));\n\t}, [\n\t\tarchivedThreadIds,\n\t\tindex.entries,\n\t\tlegacyHistory.conversations,\n\t\thasSearch,\n\t\treadOverrides,\n\t\tsupportsStatewirePaging,\n\t]);\n\n\tconst refreshMutation = useMutation({\n\t\tmutationKey: [\n\t\t\t\"conversation-index\",\n\t\t\t\"refresh\",\n\t\t\tconversationIndexBaseUrl,\n\t\t\tprincipalId,\n\t\t],\n\t\tmutationFn: () =>\n\t\t\tusesRestHistory ? legacyHistory.refresh() : index.refresh(),\n\t});\n\tconst loadMoreMutation = useMutation({\n\t\tmutationKey: [\n\t\t\t\"conversation-index\",\n\t\t\t\"load-more\",\n\t\t\tconversationIndexBaseUrl,\n\t\t\tprincipalId,\n\t\t],\n\t\tmutationFn: () =>\n\t\t\tusesRestHistory ? legacyHistory.fetchNextPage() : index.loadMore(),\n\t});\n\tconst archive = useMutation({\n\t\tmutationKey: [\"conversation-history\", \"archive\", backendUrl, principalId],\n\t\tmutationFn: async (threadId: string) => {\n\t\t\tawait archiveThread(backendUrl, await getAuth(), threadId);\n\t\t\treturn threadId;\n\t\t},\n\t\tonMutate: (threadId) => {\n\t\t\tsetArchivedThreadIds((current) => new Set(current).add(threadId));\n\t\t},\n\t\tonError: (_error, threadId) => {\n\t\t\tsetArchivedThreadIds((current) => {\n\t\t\t\tconst next = new Set(current);\n\t\t\t\tnext.delete(threadId);\n\t\t\t\treturn next;\n\t\t\t});\n\t\t},\n\t\tonSuccess: () => {\n\t\t\trefreshMutation.mutate();\n\t\t},\n\t});\n\tconst readState = useMutation({\n\t\tmutationKey: [\n\t\t\t\"conversation-history\",\n\t\t\t\"read-state\",\n\t\t\tbackendUrl,\n\t\t\tprincipalId,\n\t\t],\n\t\tmutationFn: async ({\n\t\t\tthreadId,\n\t\t\tisUnread,\n\t\t}: {\n\t\t\tthreadId: string;\n\t\t\tisUnread: boolean;\n\t\t}) =>\n\t\t\tsetThreadReadState(backendUrl, await getAuth(), { threadId, isUnread }),\n\t\tonMutate: ({ threadId, isUnread }) => {\n\t\t\tconst previous = readOverrides[threadId];\n\t\t\tsetReadOverrides((current) => ({ ...current, [threadId]: isUnread }));\n\t\t\treturn { previous, threadId };\n\t\t},\n\t\tonError: (_error, _variables, context) => {\n\t\t\tif (!context) return;\n\t\t\tsetReadOverrides((current) => {\n\t\t\t\tconst next = { ...current };\n\t\t\t\tif (context.previous === undefined) delete next[context.threadId];\n\t\t\t\telse next[context.threadId] = context.previous;\n\t\t\t\treturn next;\n\t\t\t});\n\t\t},\n\t\tonSuccess: (result) => {\n\t\t\tsetReadOverrides((current) => ({\n\t\t\t\t...current,\n\t\t\t\t[result.thread_id]: result.is_unread,\n\t\t\t}));\n\t\t\trefreshMutation.mutate(undefined, {\n\t\t\t\tonSettled: () => {\n\t\t\t\t\tsetReadOverrides((current) => {\n\t\t\t\t\t\tconst next = { ...current };\n\t\t\t\t\t\tdelete next[result.thread_id];\n\t\t\t\t\t\treturn next;\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t});\n\n\tconst refresh = useCallback(\n\t\t() => refreshMutation.mutateAsync(),\n\t\t[refreshMutation.mutateAsync],\n\t);\n\tconst fetchNextPage = useCallback(\n\t\t() => loadMoreMutation.mutateAsync(),\n\t\t[loadMoreMutation.mutateAsync],\n\t);\n\n\treturn {\n\t\tconversations,\n\t\ttotalCount: usesRestHistory ? legacyHistory.totalCount : index.totalCount,\n\t\tisLoading:\n\t\t\tenabled &&\n\t\t\t(usesRestHistory\n\t\t\t\t? !legacyHistory.hasResolvedFirstPage\n\t\t\t\t: index.entries === null),\n\t\tisFetching:\n\t\t\trefreshMutation.isPending ||\n\t\t\tloadMoreMutation.isPending ||\n\t\t\tlegacyHistory.isFetching,\n\t\tisRefreshing: refreshMutation.isPending,\n\t\thasResolvedFirstPage: usesRestHistory\n\t\t\t? legacyHistory.hasResolvedFirstPage\n\t\t\t: index.entries !== null,\n\t\thasNextPage: usesRestHistory\n\t\t\t? legacyHistory.hasNextPage\n\t\t\t: index.hasNextPage,\n\t\tisFetchingNextPage: loadMoreMutation.isPending,\n\t\tfetchNextPage,\n\t\trefresh,\n\t\tarchiveConversation: archive.mutate,\n\t\tarchivePending: archive.isPending,\n\t\tsetConversationReadState: readState.mutate,\n\t\treadStatePending: readState.isPending,\n\t\tconnection: index.connection,\n\t};\n}\n"],"names":["useConversationIndex","useRestConversationHistory","useState","useEffect","useMemo","useMutation","archiveThread","setThreadReadState","useCallback"],"mappings":"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"}
1
+ {"version":3,"file":"use-statewire-conversation-history.cjs","sources":["../../src/threads/use-statewire-conversation-history.ts"],"sourcesContent":["import {\n\ttype ConversationIndexEntry,\n\ttype ConversationSummary,\n\ttype UseConversationIndexOptions,\n\tuseConversationIndex,\n} from \"@athenaintel/agent-runtime\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { archiveThread, setThreadReadState } from \"./api\";\nimport {\n\ttype RestConversationAuth,\n\tuseRestConversationHistory,\n} from \"./use-rest-conversation-history\";\n\nexport interface UseStatewireConversationHistoryOptions {\n\tbackendUrl: string;\n\tconversationIndexBaseUrl: string;\n\t/** The workspace being viewed; the index is one instance per (workspace, user). */\n\tworkspaceId: string;\n\tprincipalId: string;\n\tgetAuth: () => RestConversationAuth | Promise<RestConversationAuth>;\n\theaders: UseConversationIndexOptions[\"headers\"];\n\tsearch?: string;\n\tenabled?: boolean;\n\tsse?: boolean;\n}\n\n/** Convert the replicated wire row into the shared cross-surface contract. */\nexport function conversationSummaryFromIndexEntry(\n\tentry: ConversationIndexEntry,\n): ConversationSummary {\n\treturn {\n\t\tthreadId: entry.thread_id,\n\t\ttitle: entry.title,\n\t\tupdatedAt: entry.updated_at,\n\t\tcreatedAt: null,\n\t\tstate: entry.state,\n\t\truntime: entry.runtime,\n\t\tsourceChannel: entry.start_channel,\n\t\tassetType: entry.asset_type,\n\t\tassetId: entry.asset_id ?? null,\n\t\tappId: entry.app_id ?? null,\n\t\ttriggerType: entry.trigger_type ?? null,\n\t\tpreview: entry.last_message_preview ?? null,\n\t\tmessageCount: entry.message_count ?? null,\n\t\tisUnread: entry.is_unread ?? false,\n\t\tsessionStatus: entry.session_status ?? null,\n\t\trunStatus: entry.run_status ?? null,\n\t};\n}\n\nexport function conversationHistorySource({\n\tindexEntries,\n\trestConversations,\n\tsupportsStatewirePaging,\n\thasSearch,\n}: {\n\tindexEntries: readonly ConversationIndexEntry[] | null;\n\trestConversations: readonly ConversationSummary[];\n\tsupportsStatewirePaging: boolean;\n\thasSearch: boolean;\n}): ConversationSummary[] {\n\t// Search is server-backed so it covers the user's entire authorized history,\n\t// including rows older than the Statewire live window. An empty query uses\n\t// Statewire as the render source; REST only supplements rolling old hosts.\n\tif (hasSearch) return [...restConversations];\n\tconst statewireItems = (indexEntries ?? []).map(\n\t\tconversationSummaryFromIndexEntry,\n\t);\n\tif (supportsStatewirePaging) return statewireItems;\n\tconst statewireIds = new Set(statewireItems.map((item) => item.threadId));\n\treturn [\n\t\t...statewireItems,\n\t\t...restConversations.filter(\n\t\t\t(conversation) => !statewireIds.has(conversation.threadId),\n\t\t),\n\t];\n}\n\nexport function shouldUseRestConversationHistory({\n\thasSearch,\n\tindexEntries,\n\tsupportsStatewirePaging,\n\tconnectionStatus,\n}: {\n\thasSearch: boolean;\n\tindexEntries: readonly ConversationIndexEntry[] | null;\n\tsupportsStatewirePaging: boolean;\n\tconnectionStatus: string;\n}): boolean {\n\tif (hasSearch) return true;\n\tif (supportsStatewirePaging) return false;\n\t// An old host is identifiable once its snapshot arrives. A transport that\n\t// fails or is parked before any snapshot also needs the independent REST\n\t// path; otherwise entries stays null and the history panel loads forever.\n\treturn (\n\t\tindexEntries !== null ||\n\t\tconnectionStatus === \"reconnecting\" ||\n\t\tconnectionStatus === \"gone\"\n\t);\n}\n\n/**\n * Inclusive satellite conversation history backed by the user's Pinned\n * Statewire instance. The replicated window is the render source; refreshes,\n * pagination, thread creation, lifecycle changes, and title changes arrive on\n * that same channel. A non-empty search deliberately uses the authorized REST\n * list endpoint so matches are not limited to the replicated newest window.\n */\nexport function useStatewireConversationHistory({\n\tbackendUrl,\n\tconversationIndexBaseUrl,\n\tworkspaceId,\n\tprincipalId,\n\tgetAuth,\n\theaders,\n\tsearch,\n\tenabled = true,\n\tsse = false,\n}: UseStatewireConversationHistoryOptions) {\n\tconst index = useConversationIndex({\n\t\tbaseUrl: conversationIndexBaseUrl,\n\t\tworkspaceId: workspaceId || \"pending-workspace\",\n\t\tuserId: principalId || \"pending-user\",\n\t\theaders,\n\t\tenabled: enabled && workspaceId.length > 0 && principalId.length > 0,\n\t\tsse,\n\t});\n\t// Rolling deploy compatibility: the pre-paging index snapshot has no\n\t// total_count. It still renders live rows immediately, while the old REST\n\t// pager remains available only until the upgraded host activates.\n\tconst supportsStatewirePaging = index.totalCount !== null;\n\tconst normalizedSearch = search?.trim().toLowerCase() ?? \"\";\n\tconst hasSearch = normalizedSearch.length > 0;\n\tconst usesRestHistory = shouldUseRestConversationHistory({\n\t\thasSearch,\n\t\tindexEntries: index.entries,\n\t\tsupportsStatewirePaging,\n\t\tconnectionStatus: index.connection.status,\n\t});\n\tconst legacyHistory = useRestConversationHistory({\n\t\tbackendUrl,\n\t\tprincipalId,\n\t\tgetAuth,\n\t\tsearch,\n\t\tenabled: enabled && principalId.length > 0 && usesRestHistory,\n\t});\n\tconst [readOverrides, setReadOverrides] = useState<Record<string, boolean>>(\n\t\t{},\n\t);\n\tconst [archivedThreadIds, setArchivedThreadIds] = useState<\n\t\tReadonlySet<string>\n\t>(() => new Set());\n\n\tuseEffect(() => {\n\t\tif (index.entries === null || archivedThreadIds.size === 0) return;\n\t\tconst visibleIds = new Set(index.entries.map((entry) => entry.thread_id));\n\t\tsetArchivedThreadIds((current) => {\n\t\t\tconst next = new Set(\n\t\t\t\t[...current].filter((threadId) => visibleIds.has(threadId)),\n\t\t\t);\n\t\t\treturn next.size === current.size ? current : next;\n\t\t});\n\t}, [archivedThreadIds.size, index.entries]);\n\n\tconst conversations = useMemo(() => {\n\t\treturn conversationHistorySource({\n\t\t\tindexEntries: index.entries,\n\t\t\trestConversations: legacyHistory.conversations,\n\t\t\tsupportsStatewirePaging,\n\t\t\thasSearch,\n\t\t})\n\t\t\t.filter((conversation) => !archivedThreadIds.has(conversation.threadId))\n\t\t\t.map((conversation) => ({\n\t\t\t\t...conversation,\n\t\t\t\tisUnread: readOverrides[conversation.threadId] ?? conversation.isUnread,\n\t\t\t}));\n\t}, [\n\t\tarchivedThreadIds,\n\t\tindex.entries,\n\t\tlegacyHistory.conversations,\n\t\thasSearch,\n\t\treadOverrides,\n\t\tsupportsStatewirePaging,\n\t]);\n\n\tconst refreshMutation = useMutation({\n\t\tmutationKey: [\n\t\t\t\"conversation-index\",\n\t\t\t\"refresh\",\n\t\t\tconversationIndexBaseUrl,\n\t\t\tprincipalId,\n\t\t],\n\t\tmutationFn: () =>\n\t\t\tusesRestHistory ? legacyHistory.refresh() : index.refresh(),\n\t});\n\tconst loadMoreMutation = useMutation({\n\t\tmutationKey: [\n\t\t\t\"conversation-index\",\n\t\t\t\"load-more\",\n\t\t\tconversationIndexBaseUrl,\n\t\t\tprincipalId,\n\t\t],\n\t\tmutationFn: () =>\n\t\t\tusesRestHistory ? legacyHistory.fetchNextPage() : index.loadMore(),\n\t});\n\tconst archive = useMutation({\n\t\tmutationKey: [\"conversation-history\", \"archive\", backendUrl, principalId],\n\t\tmutationFn: async (threadId: string) => {\n\t\t\tawait archiveThread(backendUrl, await getAuth(), threadId);\n\t\t\treturn threadId;\n\t\t},\n\t\tonMutate: (threadId) => {\n\t\t\tsetArchivedThreadIds((current) => new Set(current).add(threadId));\n\t\t},\n\t\tonError: (_error, threadId) => {\n\t\t\tsetArchivedThreadIds((current) => {\n\t\t\t\tconst next = new Set(current);\n\t\t\t\tnext.delete(threadId);\n\t\t\t\treturn next;\n\t\t\t});\n\t\t},\n\t\tonSuccess: () => {\n\t\t\trefreshMutation.mutate();\n\t\t},\n\t});\n\tconst readState = useMutation({\n\t\tmutationKey: [\n\t\t\t\"conversation-history\",\n\t\t\t\"read-state\",\n\t\t\tbackendUrl,\n\t\t\tprincipalId,\n\t\t],\n\t\tmutationFn: async ({\n\t\t\tthreadId,\n\t\t\tisUnread,\n\t\t}: {\n\t\t\tthreadId: string;\n\t\t\tisUnread: boolean;\n\t\t}) =>\n\t\t\tsetThreadReadState(backendUrl, await getAuth(), { threadId, isUnread }),\n\t\tonMutate: ({ threadId, isUnread }) => {\n\t\t\tconst previous = readOverrides[threadId];\n\t\t\tsetReadOverrides((current) => ({ ...current, [threadId]: isUnread }));\n\t\t\treturn { previous, threadId };\n\t\t},\n\t\tonError: (_error, _variables, context) => {\n\t\t\tif (!context) return;\n\t\t\tsetReadOverrides((current) => {\n\t\t\t\tconst next = { ...current };\n\t\t\t\tif (context.previous === undefined) delete next[context.threadId];\n\t\t\t\telse next[context.threadId] = context.previous;\n\t\t\t\treturn next;\n\t\t\t});\n\t\t},\n\t\tonSuccess: (result) => {\n\t\t\tsetReadOverrides((current) => ({\n\t\t\t\t...current,\n\t\t\t\t[result.thread_id]: result.is_unread,\n\t\t\t}));\n\t\t\trefreshMutation.mutate(undefined, {\n\t\t\t\tonSettled: () => {\n\t\t\t\t\tsetReadOverrides((current) => {\n\t\t\t\t\t\tconst next = { ...current };\n\t\t\t\t\t\tdelete next[result.thread_id];\n\t\t\t\t\t\treturn next;\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t});\n\n\tconst refresh = useCallback(\n\t\t() => refreshMutation.mutateAsync(),\n\t\t[refreshMutation.mutateAsync],\n\t);\n\tconst fetchNextPage = useCallback(\n\t\t() => loadMoreMutation.mutateAsync(),\n\t\t[loadMoreMutation.mutateAsync],\n\t);\n\n\treturn {\n\t\tconversations,\n\t\ttotalCount: usesRestHistory ? legacyHistory.totalCount : index.totalCount,\n\t\tisLoading:\n\t\t\tenabled &&\n\t\t\t(usesRestHistory\n\t\t\t\t? !legacyHistory.hasResolvedFirstPage\n\t\t\t\t: index.entries === null),\n\t\tisFetching:\n\t\t\trefreshMutation.isPending ||\n\t\t\tloadMoreMutation.isPending ||\n\t\t\tlegacyHistory.isFetching,\n\t\tisRefreshing: refreshMutation.isPending,\n\t\thasResolvedFirstPage: usesRestHistory\n\t\t\t? legacyHistory.hasResolvedFirstPage\n\t\t\t: index.entries !== null,\n\t\thasNextPage: usesRestHistory\n\t\t\t? legacyHistory.hasNextPage\n\t\t\t: index.hasNextPage,\n\t\tisFetchingNextPage: loadMoreMutation.isPending,\n\t\tfetchNextPage,\n\t\trefresh,\n\t\tarchiveConversation: archive.mutate,\n\t\tarchivePending: archive.isPending,\n\t\tsetConversationReadState: readState.mutate,\n\t\treadStatePending: readState.isPending,\n\t\tconnection: index.connection,\n\t};\n}\n"],"names":["useConversationIndex","useRestConversationHistory","useState","useEffect","useMemo","useMutation","archiveThread","setThreadReadState","useCallback"],"mappings":"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"}