@athenaintel/react 0.24.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/assets/AssetPanel.cjs +3 -2
- package/dist/assets/AssetPanel.cjs.map +1 -1
- package/dist/assets/AssetPanel.js +3 -2
- package/dist/assets/AssetPanel.js.map +1 -1
- package/dist/assets/AthenaAssetEmbed.cjs +1 -0
- package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
- package/dist/assets/AthenaAssetEmbed.js +1 -0
- package/dist/assets/AthenaAssetEmbed.js.map +1 -1
- package/dist/assets/use-asset-embed.cjs +22 -23
- package/dist/assets/use-asset-embed.cjs.map +1 -1
- package/dist/assets/use-asset-embed.d.ts +9 -0
- package/dist/assets/use-asset-embed.js +22 -23
- package/dist/assets/use-asset-embed.js.map +1 -1
- package/dist/auth/AthenaAuthProvider.cjs +32 -4
- package/dist/auth/AthenaAuthProvider.cjs.map +1 -1
- package/dist/auth/AthenaAuthProvider.d.ts +21 -0
- package/dist/auth/AthenaAuthProvider.js +33 -5
- package/dist/auth/AthenaAuthProvider.js.map +1 -1
- package/dist/auth/AthenaCredentialSourceContext.cjs +21 -0
- package/dist/auth/AthenaCredentialSourceContext.cjs.map +1 -0
- package/dist/auth/AthenaCredentialSourceContext.d.ts +14 -0
- package/dist/auth/AthenaCredentialSourceContext.js +21 -0
- package/dist/auth/AthenaCredentialSourceContext.js.map +1 -0
- package/dist/auth/AthenaSSOProvider.cjs +71 -47
- package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
- package/dist/auth/AthenaSSOProvider.js +71 -47
- package/dist/auth/AthenaSSOProvider.js.map +1 -1
- package/dist/auth/credential-source.cjs +120 -0
- package/dist/auth/credential-source.cjs.map +1 -0
- package/dist/auth/credential-source.d.ts +68 -0
- package/dist/auth/credential-source.js +120 -0
- package/dist/auth/credential-source.js.map +1 -0
- package/dist/auth/types.d.ts +9 -0
- package/dist/chat/AthenaChat.cjs +8 -0
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +8 -0
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/api-client.cjs +20 -11
- package/dist/chat-ui/mentions/suggestions/data/api-client.cjs.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/api-client.d.ts +11 -2
- package/dist/chat-ui/mentions/suggestions/data/api-client.js +20 -11
- package/dist/chat-ui/mentions/suggestions/data/api-client.js.map +1 -1
- package/dist/collab/mint.cjs +6 -15
- package/dist/collab/mint.cjs.map +1 -1
- package/dist/collab/mint.d.ts +8 -0
- package/dist/collab/mint.js +6 -15
- package/dist/collab/mint.js.map +1 -1
- package/dist/collab/presence-mint.cjs +20 -16
- package/dist/collab/presence-mint.cjs.map +1 -1
- package/dist/collab/presence-mint.js +21 -17
- package/dist/collab/presence-mint.js.map +1 -1
- package/dist/collab/presence-react.cjs +12 -5
- package/dist/collab/presence-react.cjs.map +1 -1
- package/dist/collab/presence-react.d.ts +3 -1
- package/dist/collab/presence-react.js +12 -5
- package/dist/collab/presence-react.js.map +1 -1
- package/dist/collab/react.cjs +5 -2
- package/dist/collab/react.cjs.map +1 -1
- package/dist/collab/react.js +5 -2
- package/dist/collab/react.js.map +1 -1
- package/dist/dictation/use-athena-dictation-engine.cjs +12 -11
- package/dist/dictation/use-athena-dictation-engine.cjs.map +1 -1
- package/dist/dictation/use-athena-dictation-engine.d.ts +4 -3
- package/dist/dictation/use-athena-dictation-engine.js +12 -11
- package/dist/dictation/use-athena-dictation-engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/dist/provider/AthenaContext.cjs.map +1 -1
- package/dist/provider/AthenaContext.d.ts +7 -1
- package/dist/provider/AthenaContext.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +23 -2
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.js +23 -2
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/runtime/auth-refusal-restart.cjs +66 -0
- package/dist/runtime/auth-refusal-restart.cjs.map +1 -0
- package/dist/runtime/auth-refusal-restart.d.ts +35 -0
- package/dist/runtime/auth-refusal-restart.js +66 -0
- package/dist/runtime/auth-refusal-restart.js.map +1 -0
- package/dist/runtime/auth.cjs +58 -21
- package/dist/runtime/auth.cjs.map +1 -1
- package/dist/runtime/auth.d.ts +11 -1
- package/dist/runtime/auth.js +59 -22
- package/dist/runtime/auth.js.map +1 -1
- package/dist/runtime/credential-renewal.cjs +29 -0
- package/dist/runtime/credential-renewal.cjs.map +1 -0
- package/dist/runtime/credential-renewal.d.ts +35 -0
- package/dist/runtime/credential-renewal.js +29 -0
- package/dist/runtime/credential-renewal.js.map +1 -0
- package/dist/search/useAthenaRepoSearch.cjs +3 -5
- package/dist/search/useAthenaRepoSearch.cjs.map +1 -1
- package/dist/search/useAthenaRepoSearch.d.ts +7 -7
- package/dist/search/useAthenaRepoSearch.js +3 -5
- package/dist/search/useAthenaRepoSearch.js.map +1 -1
- package/dist/threads/api.cjs +5 -1
- package/dist/threads/api.cjs.map +1 -1
- package/dist/threads/api.js +5 -1
- package/dist/threads/api.js.map +1 -1
- package/dist/threads/conversation-list-fetcher.cjs +5 -5
- package/dist/threads/conversation-list-fetcher.cjs.map +1 -1
- package/dist/threads/conversation-list-fetcher.d.ts +6 -5
- package/dist/threads/conversation-list-fetcher.js +5 -5
- package/dist/threads/conversation-list-fetcher.js.map +1 -1
- package/dist/threads/statewire-thread-list.cjs +13 -14
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.js +13 -14
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/dist/threads/use-rest-conversation-history.cjs +6 -2
- package/dist/threads/use-rest-conversation-history.cjs.map +1 -1
- package/dist/threads/use-rest-conversation-history.d.ts +3 -1
- package/dist/threads/use-rest-conversation-history.js +7 -3
- package/dist/threads/use-rest-conversation-history.js.map +1 -1
- package/dist/threads/use-statewire-conversation-history.cjs +9 -2
- package/dist/threads/use-statewire-conversation-history.cjs.map +1 -1
- package/dist/threads/use-statewire-conversation-history.d.ts +3 -1
- package/dist/threads/use-statewire-conversation-history.js +10 -3
- package/dist/threads/use-statewire-conversation-history.js.map +1 -1
- package/dist/threads/use-thread-archive.cjs +2 -1
- package/dist/threads/use-thread-archive.cjs.map +1 -1
- package/dist/threads/use-thread-archive.d.ts +3 -1
- package/dist/threads/use-thread-archive.js +2 -1
- package/dist/threads/use-thread-archive.js.map +1 -1
- package/dist/threads/use-thread-favorite.cjs.map +1 -1
- package/dist/threads/use-thread-favorite.d.ts +3 -1
- package/dist/threads/use-thread-favorite.js.map +1 -1
- package/dist/threads/use-thread-field-override.cjs +2 -1
- package/dist/threads/use-thread-field-override.cjs.map +1 -1
- package/dist/threads/use-thread-field-override.d.ts +3 -1
- package/dist/threads/use-thread-field-override.js +2 -1
- package/dist/threads/use-thread-field-override.js.map +1 -1
- package/dist/threads/use-thread-rename.cjs.map +1 -1
- package/dist/threads/use-thread-rename.d.ts +3 -1
- package/dist/threads/use-thread-rename.js.map +1 -1
- package/dist/tools/tool-uis.cjs +11 -15
- package/dist/tools/tool-uis.cjs.map +1 -1
- package/dist/tools/tool-uis.js +11 -15
- package/dist/tools/tool-uis.js.map +1 -1
- package/dist/voice/use-athena-voice-session.cjs +17 -5
- package/dist/voice/use-athena-voice-session.cjs.map +1 -1
- package/dist/voice/use-athena-voice-session.d.ts +2 -2
- package/dist/voice/use-athena-voice-session.js +17 -5
- package/dist/voice/use-athena-voice-session.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-rest-conversation-history.js","sources":["../../src/threads/use-rest-conversation-history.ts"],"sourcesContent":["import {\n type ConversationSummary,\n useConversationList,\n} from '@athenaintel/agent-runtime';\nimport { useMutation } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport { archiveThread, setThreadReadState } from './api';\nimport { createRestConversationListFetcher } from './conversation-list-fetcher';\nimport { useThreadFavorite, withFavoriteOverrides } from './use-thread-favorite';\n\nexport type RestConversationAuth = {\n apiKey?: string;\n token?: string | null;\n};\n\nexport interface UseRestConversationHistoryOptions {\n backendUrl: string;\n /** Non-secret identity included in the query cache boundary. */\n principalId: string;\n
|
|
1
|
+
{"version":3,"file":"use-rest-conversation-history.js","sources":["../../src/threads/use-rest-conversation-history.ts"],"sourcesContent":["import {\n type ConversationSummary,\n useConversationList,\n} from '@athenaintel/agent-runtime';\nimport { useMutation } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport { type CredentialResolver, withCredentialRenewal } from '../runtime/credential-renewal';\nimport { archiveThread, setThreadReadState } from './api';\nimport { createRestConversationListFetcher } from './conversation-list-fetcher';\nimport { useThreadFavorite, withFavoriteOverrides } from './use-thread-favorite';\n\nexport type RestConversationAuth = {\n apiKey?: string;\n token?: string | null;\n};\n\nexport interface UseRestConversationHistoryOptions {\n backendUrl: string;\n /** Non-secret identity included in the query cache boundary. */\n principalId: string;\n /** Resolved per request; called with `{ force: true }` when the host refuses the token. */\n getAuth: CredentialResolver<RestConversationAuth>;\n search?: string;\n enabled?: boolean;\n pageSize?: number;\n}\n\n/**\n * One inclusive, paginated conversation controller for satellite clients.\n * No app/channel filter is applied: authorization determines the user's rows,\n * while origin and automation metadata remain visible for UI filtering.\n */\nexport function useRestConversationHistory({\n backendUrl,\n principalId,\n getAuth,\n search,\n enabled = true,\n pageSize = 50,\n}: UseRestConversationHistoryOptions) {\n const fetchPage = useMemo(\n () => createRestConversationListFetcher({ backendUrl, getAuth }),\n [backendUrl, getAuth],\n );\n const list = useConversationList({\n fetchPage,\n filter: search?.trim() ? { search: search.trim() } : {},\n pageSize,\n scopeKey: `${backendUrl}::${principalId || 'unknown-principal'}`,\n enabled: enabled && principalId.length > 0,\n });\n\n const archive = useMutation({\n mutationKey: ['conversation-history', 'archive', backendUrl, principalId],\n mutationFn: async (threadId: string) => {\n await withCredentialRenewal(getAuth, (auth) => archiveThread(backendUrl, auth, threadId));\n return threadId;\n },\n onSuccess: (threadId) => list.removeConversation(threadId),\n });\n\n const readState = useMutation({\n mutationKey: ['conversation-history', 'read-state', backendUrl, principalId],\n mutationFn: async ({\n threadId,\n isUnread,\n }: {\n threadId: string;\n isUnread: boolean;\n }) =>\n withCredentialRenewal(getAuth, (auth) =>\n setThreadReadState(backendUrl, auth, { threadId, isUnread }),\n ),\n onSuccess: (result) =>\n list.patchConversation(result.thread_id, { isUnread: result.is_unread }),\n });\n\n // The pin flips optimistically and the server's answer is patched into the\n // page; the override drops once the page reports it.\n const patchConversation = list.patchConversation;\n const favorite = useThreadFavorite({\n backendUrl,\n principalId,\n getAuth,\n rows: list.conversations,\n onServerTruth: ({ threadId, isFavorited }) => patchConversation(threadId, { isFavorited }),\n });\n const favoriteOverrides = favorite.overrides;\n const conversations = useMemo(\n () => withFavoriteOverrides(list.conversations, favoriteOverrides),\n [list.conversations, favoriteOverrides],\n );\n\n return {\n ...list,\n conversations,\n archiveConversation: archive.mutate,\n archivePending: archive.isPending,\n setConversationReadState: readState.mutate,\n readStatePending: readState.isPending,\n /** Pin or unpin a conversation; read-only on a backend without the route (`favoriteToggleAvailable`). */\n setConversationFavorited: favorite.setFavorited,\n favoritePending: favorite.isPending,\n /** The conversations whose pin toggle is in flight. */\n favoritePendingThreadIds: favorite.pendingThreadIds,\n /** `false` once the backend answered 404 / 405 to the favorite route. */\n favoriteToggleAvailable: favorite.isAvailable,\n };\n}\n\nconst GENERIC_TITLES = new Set([\n 'new chat',\n 'new conversation',\n 'untitled',\n 'untitled conversation',\n]);\n\n/** Prefer a useful server title, then a normalized last-message preview. */\nexport function conversationDisplayTitle(\n conversation: Pick<ConversationSummary, 'title' | 'preview'>,\n): string {\n const title = conversation.title.trim();\n if (title && !GENERIC_TITLES.has(title.toLowerCase())) return title;\n const preview = conversation.preview?.replace(/\\s+/g, ' ').trim();\n if (preview) return preview.length > 80 ? `${preview.slice(0, 77)}…` : preview;\n return 'Untitled conversation';\n}\n"],"names":[],"mappings":";;;;;;;AAgCO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,WAAW;AACb,GAAsC;AACpC,QAAM,YAAY;AAAA,IAChB,MAAM,kCAAkC,EAAE,YAAY,SAAS;AAAA,IAC/D,CAAC,YAAY,OAAO;AAAA,EAAA;AAEtB,QAAM,OAAO,oBAAoB;AAAA,IAC/B;AAAA,IACA,SAAQ,iCAAQ,UAAS,EAAE,QAAQ,OAAO,KAAA,EAAK,IAAM,CAAA;AAAA,IACrD;AAAA,IACA,UAAU,GAAG,UAAU,KAAK,eAAe,mBAAmB;AAAA,IAC9D,SAAS,WAAW,YAAY,SAAS;AAAA,EAAA,CAC1C;AAED,QAAM,UAAU,YAAY;AAAA,IAC1B,aAAa,CAAC,wBAAwB,WAAW,YAAY,WAAW;AAAA,IACxE,YAAY,OAAO,aAAqB;AACtC,YAAM,sBAAsB,SAAS,CAAC,SAAS,cAAc,YAAY,MAAM,QAAQ,CAAC;AACxF,aAAO;AAAA,IACT;AAAA,IACA,WAAW,CAAC,aAAa,KAAK,mBAAmB,QAAQ;AAAA,EAAA,CAC1D;AAED,QAAM,YAAY,YAAY;AAAA,IAC5B,aAAa,CAAC,wBAAwB,cAAc,YAAY,WAAW;AAAA,IAC3E,YAAY,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,MAKA;AAAA,MAAsB;AAAA,MAAS,CAAC,SAC9B,mBAAmB,YAAY,MAAM,EAAE,UAAU,UAAU;AAAA,IAAA;AAAA,IAE/D,WAAW,CAAC,WACV,KAAK,kBAAkB,OAAO,WAAW,EAAE,UAAU,OAAO,UAAA,CAAW;AAAA,EAAA,CAC1E;AAID,QAAM,oBAAoB,KAAK;AAC/B,QAAM,WAAW,kBAAkB;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,KAAK;AAAA,IACX,eAAe,CAAC,EAAE,UAAU,YAAA,MAAkB,kBAAkB,UAAU,EAAE,YAAA,CAAa;AAAA,EAAA,CAC1F;AACD,QAAM,oBAAoB,SAAS;AACnC,QAAM,gBAAgB;AAAA,IACpB,MAAM,sBAAsB,KAAK,eAAe,iBAAiB;AAAA,IACjE,CAAC,KAAK,eAAe,iBAAiB;AAAA,EAAA;AAGxC,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,qBAAqB,QAAQ;AAAA,IAC7B,gBAAgB,QAAQ;AAAA,IACxB,0BAA0B,UAAU;AAAA,IACpC,kBAAkB,UAAU;AAAA;AAAA,IAE5B,0BAA0B,SAAS;AAAA,IACnC,iBAAiB,SAAS;AAAA;AAAA,IAE1B,0BAA0B,SAAS;AAAA;AAAA,IAEnC,yBAAyB,SAAS;AAAA,EAAA;AAEtC;AAEA,MAAM,qCAAqB,IAAI;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,yBACd,cACQ;;AACR,QAAM,QAAQ,aAAa,MAAM,KAAA;AACjC,MAAI,SAAS,CAAC,eAAe,IAAI,MAAM,YAAA,CAAa,EAAG,QAAO;AAC9D,QAAM,WAAU,kBAAa,YAAb,mBAAsB,QAAQ,QAAQ,KAAK;AAC3D,MAAI,QAAS,QAAO,QAAQ,SAAS,KAAK,GAAG,QAAQ,MAAM,GAAG,EAAE,CAAC,MAAM;AACvE,SAAO;AACT;"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const reactQuery = require("@tanstack/react-query");
|
|
4
4
|
const react = require("react");
|
|
5
|
+
const credentialRenewal = require("../runtime/credential-renewal.cjs");
|
|
5
6
|
const api = require("./api.cjs");
|
|
6
7
|
const useRestConversationHistory = require("./use-rest-conversation-history.cjs");
|
|
7
8
|
const useThreadFavorite = require("./use-thread-favorite.cjs");
|
|
@@ -158,7 +159,10 @@ function useStatewireConversationHistory({
|
|
|
158
159
|
const archive = reactQuery.useMutation({
|
|
159
160
|
mutationKey: ["conversation-history", "archive", backendUrl, principalId],
|
|
160
161
|
mutationFn: async (threadId) => {
|
|
161
|
-
await
|
|
162
|
+
await credentialRenewal.withCredentialRenewal(
|
|
163
|
+
getAuth,
|
|
164
|
+
(auth) => api.archiveThread(backendUrl, auth, threadId)
|
|
165
|
+
);
|
|
162
166
|
return threadId;
|
|
163
167
|
},
|
|
164
168
|
onMutate: (threadId) => {
|
|
@@ -185,7 +189,10 @@ function useStatewireConversationHistory({
|
|
|
185
189
|
mutationFn: async ({
|
|
186
190
|
threadId,
|
|
187
191
|
isUnread
|
|
188
|
-
}) =>
|
|
192
|
+
}) => credentialRenewal.withCredentialRenewal(
|
|
193
|
+
getAuth,
|
|
194
|
+
(auth) => api.setThreadReadState(backendUrl, auth, { threadId, isUnread })
|
|
195
|
+
),
|
|
189
196
|
onMutate: ({ threadId, isUnread }) => {
|
|
190
197
|
const previous = readOverrides[threadId];
|
|
191
198
|
setReadOverrides((current) => ({ ...current, [threadId]: isUnread }));
|
|
@@ -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\";\nimport { useThreadFavorite, withFavoriteOverrides } from \"./use-thread-favorite\";\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 sourceConversations = useMemo(\n\t\t() =>\n\t\t\tconversationHistorySource({\n\t\t\t\tindexEntries: index.entries,\n\t\t\t\trestConversations: legacyHistory.conversations,\n\t\t\t\tsupportsStatewirePaging,\n\t\t\t\thasSearch,\n\t\t\t}).filter((conversation) => !archivedThreadIds.has(conversation.threadId)),\n\t\t[\n\t\t\tarchivedThreadIds,\n\t\t\tindex.entries,\n\t\t\tlegacyHistory.conversations,\n\t\t\thasSearch,\n\t\t\tsupportsStatewirePaging,\n\t\t],\n\t);\n\t// The pin flips optimistically over both sources; the Statewire index\n\t// pushes the confirmed value, the REST page is patched with it, and the\n\t// override drops once the rendered source reports it.\n\tconst patchLegacyConversation = legacyHistory.patchConversation;\n\tconst favorite = useThreadFavorite({\n\t\tbackendUrl,\n\t\tprincipalId,\n\t\tgetAuth,\n\t\trows: sourceConversations,\n\t\tonServerTruth: ({ threadId, isFavorited }) =>\n\t\t\tpatchLegacyConversation(threadId, { isFavorited }),\n\t});\n\tconst favoriteOverrides = favorite.overrides;\n\n\tconst conversations = useMemo(\n\t\t() =>\n\t\t\twithFavoriteOverrides(sourceConversations, favoriteOverrides).map(\n\t\t\t\t(conversation) => ({\n\t\t\t\t\t...conversation,\n\t\t\t\t\tisUnread:\n\t\t\t\t\t\treadOverrides[conversation.threadId] ?? conversation.isUnread,\n\t\t\t\t}),\n\t\t\t),\n\t\t[sourceConversations, favoriteOverrides, readOverrides],\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\t/** Pin or unpin a conversation; read-only on a backend without the route (`favoriteToggleAvailable`). */\n\t\tsetConversationFavorited: favorite.setFavorited,\n\t\tfavoritePending: favorite.isPending,\n\t\t/** The conversations whose pin toggle is in flight. */\n\t\tfavoritePendingThreadIds: favorite.pendingThreadIds,\n\t\t/** `false` once the backend answered 404 / 405 to the favorite route. */\n\t\tfavoriteToggleAvailable: favorite.isAvailable,\n\t\tconnection: index.connection,\n\t};\n}\n"],"names":["useConversationIndex","useRestConversationHistory","useState","useEffect","useMemo","useThreadFavorite","withFavoriteOverrides","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 {\n\ttype CredentialResolver,\n\twithCredentialRenewal,\n} from \"../runtime/credential-renewal\";\nimport { archiveThread, setThreadReadState } from \"./api\";\nimport {\n\ttype RestConversationAuth,\n\tuseRestConversationHistory,\n} from \"./use-rest-conversation-history\";\nimport { useThreadFavorite, withFavoriteOverrides } from \"./use-thread-favorite\";\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\t/** Resolved per request; called with `{ force: true }` when the host refuses the token. */\n\tgetAuth: CredentialResolver<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 sourceConversations = useMemo(\n\t\t() =>\n\t\t\tconversationHistorySource({\n\t\t\t\tindexEntries: index.entries,\n\t\t\t\trestConversations: legacyHistory.conversations,\n\t\t\t\tsupportsStatewirePaging,\n\t\t\t\thasSearch,\n\t\t\t}).filter((conversation) => !archivedThreadIds.has(conversation.threadId)),\n\t\t[\n\t\t\tarchivedThreadIds,\n\t\t\tindex.entries,\n\t\t\tlegacyHistory.conversations,\n\t\t\thasSearch,\n\t\t\tsupportsStatewirePaging,\n\t\t],\n\t);\n\t// The pin flips optimistically over both sources; the Statewire index\n\t// pushes the confirmed value, the REST page is patched with it, and the\n\t// override drops once the rendered source reports it.\n\tconst patchLegacyConversation = legacyHistory.patchConversation;\n\tconst favorite = useThreadFavorite({\n\t\tbackendUrl,\n\t\tprincipalId,\n\t\tgetAuth,\n\t\trows: sourceConversations,\n\t\tonServerTruth: ({ threadId, isFavorited }) =>\n\t\t\tpatchLegacyConversation(threadId, { isFavorited }),\n\t});\n\tconst favoriteOverrides = favorite.overrides;\n\n\tconst conversations = useMemo(\n\t\t() =>\n\t\t\twithFavoriteOverrides(sourceConversations, favoriteOverrides).map(\n\t\t\t\t(conversation) => ({\n\t\t\t\t\t...conversation,\n\t\t\t\t\tisUnread:\n\t\t\t\t\t\treadOverrides[conversation.threadId] ?? conversation.isUnread,\n\t\t\t\t}),\n\t\t\t),\n\t\t[sourceConversations, favoriteOverrides, readOverrides],\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 withCredentialRenewal(getAuth, (auth) =>\n\t\t\t\tarchiveThread(backendUrl, auth, threadId),\n\t\t\t);\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\twithCredentialRenewal(getAuth, (auth) =>\n\t\t\t\tsetThreadReadState(backendUrl, auth, { threadId, isUnread }),\n\t\t\t),\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\t/** Pin or unpin a conversation; read-only on a backend without the route (`favoriteToggleAvailable`). */\n\t\tsetConversationFavorited: favorite.setFavorited,\n\t\tfavoritePending: favorite.isPending,\n\t\t/** The conversations whose pin toggle is in flight. */\n\t\tfavoritePendingThreadIds: favorite.pendingThreadIds,\n\t\t/** `false` once the backend answered 404 / 405 to the favorite route. */\n\t\tfavoriteToggleAvailable: favorite.isAvailable,\n\t\tconnection: index.connection,\n\t};\n}\n"],"names":["useConversationIndex","useRestConversationHistory","useState","useEffect","useMemo","useThreadFavorite","withFavoriteOverrides","useMutation","withCredentialRenewal","archiveThread","setThreadReadState","useCallback"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ConversationIndexEntry, type ConversationSummary, type UseConversationIndexOptions } from "@athenaintel/agent-runtime";
|
|
2
|
+
import { type CredentialResolver } from "../runtime/credential-renewal";
|
|
2
3
|
import { type RestConversationAuth } from "./use-rest-conversation-history";
|
|
3
4
|
export interface UseStatewireConversationHistoryOptions {
|
|
4
5
|
backendUrl: string;
|
|
@@ -6,7 +7,8 @@ export interface UseStatewireConversationHistoryOptions {
|
|
|
6
7
|
/** The workspace being viewed; the index is one instance per (workspace, user). */
|
|
7
8
|
workspaceId: string;
|
|
8
9
|
principalId: string;
|
|
9
|
-
|
|
10
|
+
/** Resolved per request; called with `{ force: true }` when the host refuses the token. */
|
|
11
|
+
getAuth: CredentialResolver<RestConversationAuth>;
|
|
10
12
|
headers: UseConversationIndexOptions["headers"];
|
|
11
13
|
search?: string;
|
|
12
14
|
enabled?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMutation } from "@tanstack/react-query";
|
|
2
2
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { withCredentialRenewal } from "../runtime/credential-renewal.js";
|
|
4
|
+
import { setThreadReadState, archiveThread } from "./api.js";
|
|
4
5
|
import { useRestConversationHistory } from "./use-rest-conversation-history.js";
|
|
5
6
|
import { useThreadFavorite, withFavoriteOverrides } from "./use-thread-favorite.js";
|
|
6
7
|
import { useConversationIndex } from "../packages/agent-runtime/src/conversation-index.js";
|
|
@@ -156,7 +157,10 @@ function useStatewireConversationHistory({
|
|
|
156
157
|
const archive = useMutation({
|
|
157
158
|
mutationKey: ["conversation-history", "archive", backendUrl, principalId],
|
|
158
159
|
mutationFn: async (threadId) => {
|
|
159
|
-
await
|
|
160
|
+
await withCredentialRenewal(
|
|
161
|
+
getAuth,
|
|
162
|
+
(auth) => archiveThread(backendUrl, auth, threadId)
|
|
163
|
+
);
|
|
160
164
|
return threadId;
|
|
161
165
|
},
|
|
162
166
|
onMutate: (threadId) => {
|
|
@@ -183,7 +187,10 @@ function useStatewireConversationHistory({
|
|
|
183
187
|
mutationFn: async ({
|
|
184
188
|
threadId,
|
|
185
189
|
isUnread
|
|
186
|
-
}) =>
|
|
190
|
+
}) => withCredentialRenewal(
|
|
191
|
+
getAuth,
|
|
192
|
+
(auth) => setThreadReadState(backendUrl, auth, { threadId, isUnread })
|
|
193
|
+
),
|
|
187
194
|
onMutate: ({ threadId, isUnread }) => {
|
|
188
195
|
const previous = readOverrides[threadId];
|
|
189
196
|
setReadOverrides((current) => ({ ...current, [threadId]: isUnread }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-statewire-conversation-history.js","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\";\nimport { useThreadFavorite, withFavoriteOverrides } from \"./use-thread-favorite\";\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 sourceConversations = useMemo(\n\t\t() =>\n\t\t\tconversationHistorySource({\n\t\t\t\tindexEntries: index.entries,\n\t\t\t\trestConversations: legacyHistory.conversations,\n\t\t\t\tsupportsStatewirePaging,\n\t\t\t\thasSearch,\n\t\t\t}).filter((conversation) => !archivedThreadIds.has(conversation.threadId)),\n\t\t[\n\t\t\tarchivedThreadIds,\n\t\t\tindex.entries,\n\t\t\tlegacyHistory.conversations,\n\t\t\thasSearch,\n\t\t\tsupportsStatewirePaging,\n\t\t],\n\t);\n\t// The pin flips optimistically over both sources; the Statewire index\n\t// pushes the confirmed value, the REST page is patched with it, and the\n\t// override drops once the rendered source reports it.\n\tconst patchLegacyConversation = legacyHistory.patchConversation;\n\tconst favorite = useThreadFavorite({\n\t\tbackendUrl,\n\t\tprincipalId,\n\t\tgetAuth,\n\t\trows: sourceConversations,\n\t\tonServerTruth: ({ threadId, isFavorited }) =>\n\t\t\tpatchLegacyConversation(threadId, { isFavorited }),\n\t});\n\tconst favoriteOverrides = favorite.overrides;\n\n\tconst conversations = useMemo(\n\t\t() =>\n\t\t\twithFavoriteOverrides(sourceConversations, favoriteOverrides).map(\n\t\t\t\t(conversation) => ({\n\t\t\t\t\t...conversation,\n\t\t\t\t\tisUnread:\n\t\t\t\t\t\treadOverrides[conversation.threadId] ?? conversation.isUnread,\n\t\t\t\t}),\n\t\t\t),\n\t\t[sourceConversations, favoriteOverrides, readOverrides],\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\t/** Pin or unpin a conversation; read-only on a backend without the route (`favoriteToggleAvailable`). */\n\t\tsetConversationFavorited: favorite.setFavorited,\n\t\tfavoritePending: favorite.isPending,\n\t\t/** The conversations whose pin toggle is in flight. */\n\t\tfavoritePendingThreadIds: favorite.pendingThreadIds,\n\t\t/** `false` once the backend answered 404 / 405 to the favorite route. */\n\t\tfavoriteToggleAvailable: favorite.isAvailable,\n\t\tconnection: index.connection,\n\t};\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-statewire-conversation-history.js","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 {\n\ttype CredentialResolver,\n\twithCredentialRenewal,\n} from \"../runtime/credential-renewal\";\nimport { archiveThread, setThreadReadState } from \"./api\";\nimport {\n\ttype RestConversationAuth,\n\tuseRestConversationHistory,\n} from \"./use-rest-conversation-history\";\nimport { useThreadFavorite, withFavoriteOverrides } from \"./use-thread-favorite\";\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\t/** Resolved per request; called with `{ force: true }` when the host refuses the token. */\n\tgetAuth: CredentialResolver<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 sourceConversations = useMemo(\n\t\t() =>\n\t\t\tconversationHistorySource({\n\t\t\t\tindexEntries: index.entries,\n\t\t\t\trestConversations: legacyHistory.conversations,\n\t\t\t\tsupportsStatewirePaging,\n\t\t\t\thasSearch,\n\t\t\t}).filter((conversation) => !archivedThreadIds.has(conversation.threadId)),\n\t\t[\n\t\t\tarchivedThreadIds,\n\t\t\tindex.entries,\n\t\t\tlegacyHistory.conversations,\n\t\t\thasSearch,\n\t\t\tsupportsStatewirePaging,\n\t\t],\n\t);\n\t// The pin flips optimistically over both sources; the Statewire index\n\t// pushes the confirmed value, the REST page is patched with it, and the\n\t// override drops once the rendered source reports it.\n\tconst patchLegacyConversation = legacyHistory.patchConversation;\n\tconst favorite = useThreadFavorite({\n\t\tbackendUrl,\n\t\tprincipalId,\n\t\tgetAuth,\n\t\trows: sourceConversations,\n\t\tonServerTruth: ({ threadId, isFavorited }) =>\n\t\t\tpatchLegacyConversation(threadId, { isFavorited }),\n\t});\n\tconst favoriteOverrides = favorite.overrides;\n\n\tconst conversations = useMemo(\n\t\t() =>\n\t\t\twithFavoriteOverrides(sourceConversations, favoriteOverrides).map(\n\t\t\t\t(conversation) => ({\n\t\t\t\t\t...conversation,\n\t\t\t\t\tisUnread:\n\t\t\t\t\t\treadOverrides[conversation.threadId] ?? conversation.isUnread,\n\t\t\t\t}),\n\t\t\t),\n\t\t[sourceConversations, favoriteOverrides, readOverrides],\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 withCredentialRenewal(getAuth, (auth) =>\n\t\t\t\tarchiveThread(backendUrl, auth, threadId),\n\t\t\t);\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\twithCredentialRenewal(getAuth, (auth) =>\n\t\t\t\tsetThreadReadState(backendUrl, auth, { threadId, isUnread }),\n\t\t\t),\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\t/** Pin or unpin a conversation; read-only on a backend without the route (`favoriteToggleAvailable`). */\n\t\tsetConversationFavorited: favorite.setFavorited,\n\t\tfavoritePending: favorite.isPending,\n\t\t/** The conversations whose pin toggle is in flight. */\n\t\tfavoritePendingThreadIds: favorite.pendingThreadIds,\n\t\t/** `false` once the backend answered 404 / 405 to the favorite route. */\n\t\tfavoriteToggleAvailable: favorite.isAvailable,\n\t\tconnection: index.connection,\n\t};\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const reactQuery = require("@tanstack/react-query");
|
|
4
4
|
const react = require("react");
|
|
5
|
+
const credentialRenewal = require("../runtime/credential-renewal.cjs");
|
|
5
6
|
const api = require("./api.cjs");
|
|
6
7
|
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
7
8
|
function archiveScope(backendUrl, principalId) {
|
|
@@ -52,7 +53,7 @@ function useThreadArchive({
|
|
|
52
53
|
const mutation = reactQuery.useMutation({
|
|
53
54
|
mutationKey: ["conversation-history", "archive", backendUrl, principalId],
|
|
54
55
|
mutationFn: async (write) => {
|
|
55
|
-
await api.archiveThread(backendUrl,
|
|
56
|
+
await credentialRenewal.withCredentialRenewal(getAuth, (auth) => api.archiveThread(backendUrl, auth, write.threadId));
|
|
56
57
|
},
|
|
57
58
|
onSuccess: (_result, write) => {
|
|
58
59
|
var _a;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-thread-archive.cjs","sources":["../../src/threads/use-thread-archive.ts"],"sourcesContent":["/**\n * The archive action behind `SessionList`: one TanStack mutation over\n * `POST /api/conversations/threads/archive` with an optimistic hidden set.\n *\n * The row leaves the list the moment the user asks and stays hidden until\n * the caller reports that neither list source (the fetched page, the pushed\n * conversation index) lists it any more (`release`) — the page is patched on\n * the server's answer, but the index may still carry the row until the\n * publish that follows the commit lands. A failure drops the id and the row\n * comes back from the still-cached page. A 405, or a 404 that does not name\n * the thread, marks the route unavailable — the backend predates it, or has\n * no checkpointer — so the UI withdraws the action; a 404 for a thread that\n * is gone is a per-row failure (`isThreadArchiveUnavailableError`).\n *\n * One write per thread at a time: a second `archive` for a thread whose\n * write is in flight is a no-op (the guard is synchronous, so two calls in\n * one tick send one request). Everything is scoped to `(backendUrl,\n * principalId)`: a credential swap discards the hidden and pending sets, and\n * an answer for the previous identity is ignored when it lands, so a same-id\n * row under the new identity is never removed and the new user is never\n * moved off their active thread by the old user's archive.\n */\nimport { useMutation } from '@tanstack/react-query';\nimport { useCallback, useRef, useState } from 'react';\nimport { archiveThread, isThreadArchiveUnavailableError } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\n\nexport interface UseThreadArchiveOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the other thread writes key). */\n principalId: string;\n getAuth: () => RestConversationAuth | Promise<RestConversationAuth>;\n /** The server confirmed the archive: patch the caches, leave the thread if it is active. */\n onArchived?: (threadId: string) => void;\n}\n\nexport interface ThreadArchive {\n /** Threads hidden from the rows: asked to archive, not yet forgotten by every source. */\n hiddenThreadIds: ReadonlySet<string>;\n /** Archive a thread; a no-op while that thread's write is in flight. */\n archive: (threadId: string) => void;\n /** Drop hidden ids the caller has seen every source forget (never while their write is in flight). */\n release: (threadIds: readonly string[]) => void;\n /** True while any archive is in flight. */\n isPending: boolean;\n /** The threads whose archive is in flight — the action disables itself for these. */\n pendingThreadIds: ReadonlySet<string>;\n /** `false` once the backend answered that it has no archive route. */\n isAvailable: boolean;\n}\n\ninterface ScopedArchiveState {\n scope: string;\n hidden: ReadonlySet<string>;\n pending: ReadonlySet<string>;\n}\n\ninterface ArchiveWrite {\n threadId: string;\n /** The identity the write was issued for; an answer for another one is ignored. */\n scope: string;\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\nfunction archiveScope(backendUrl: string, principalId: string): string {\n return `${backendUrl}::${principalId}`;\n}\n\nfunction withId(ids: ReadonlySet<string>, threadId: string, present: boolean): ReadonlySet<string> {\n if (ids.has(threadId) === present) return ids;\n const next = new Set(ids);\n if (present) next.add(threadId);\n else next.delete(threadId);\n return next;\n}\n\n/** Backends already warned about, so an older agora logs one line per page load, not one per click. */\nconst warnedUnavailableBackends = new Set<string>();\n\n/** Test seam: forget which backends were warned about. */\nexport function resetThreadArchiveUnavailableWarnings(): void {\n warnedUnavailableBackends.clear();\n}\n\nexport function useThreadArchive({\n backendUrl,\n principalId,\n getAuth,\n onArchived,\n}: UseThreadArchiveOptions): ThreadArchive {\n const scope = archiveScope(backendUrl, principalId);\n const [state, setState] = useState<ScopedArchiveState>(() => ({\n scope,\n hidden: EMPTY_IDS,\n pending: EMPTY_IDS,\n }));\n // An identity swap discards everything held for the previous identity\n // (derived-state reset during render; see `useThreadFieldOverride`).\n if (state.scope !== scope) {\n setState({ scope, hidden: EMPTY_IDS, pending: EMPTY_IDS });\n }\n const hiddenThreadIds = state.scope === scope ? state.hidden : EMPTY_IDS;\n const pendingThreadIds = state.scope === scope ? state.pending : EMPTY_IDS;\n // The route is a property of the backend, not of who is asking.\n const [unavailableBackend, setUnavailableBackend] = useState<string | null>(null);\n\n const scopeRef = useRef(scope);\n scopeRef.current = scope;\n const onArchivedRef = useRef(onArchived);\n onArchivedRef.current = onArchived;\n // Writes in flight, keyed by identity and thread: the synchronous guard\n // behind the one-write-per-thread rule. A previous identity's entries are\n // left to expire with their answers (which the scope guard drops).\n const inFlightRef = useRef<Set<string>>(new Set());\n const writeKey = (write: ArchiveWrite) => `${write.scope}::${write.threadId}`;\n\n const update = useCallback(\n (forScope: string, patch: (current: ScopedArchiveState) => ScopedArchiveState) => {\n setState((current) => {\n const base: ScopedArchiveState =\n current.scope === forScope ? current : { scope: forScope, hidden: EMPTY_IDS, pending: EMPTY_IDS };\n const next = patch(base);\n return next === base && current.scope === forScope ? current : next;\n });\n },\n [],\n );\n\n const mutation = useMutation({\n mutationKey: ['conversation-history', 'archive', backendUrl, principalId],\n mutationFn: async (write: ArchiveWrite) => {\n await archiveThread(backendUrl, await getAuth(), write.threadId);\n },\n onSuccess: (_result, write) => {\n inFlightRef.current.delete(writeKey(write));\n if (scopeRef.current !== write.scope) return;\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, write.threadId, false),\n }));\n onArchivedRef.current?.(write.threadId);\n },\n onError: (error, write) => {\n inFlightRef.current.delete(writeKey(write));\n if (isThreadArchiveUnavailableError(error)) {\n setUnavailableBackend(backendUrl);\n if (!warnedUnavailableBackends.has(backendUrl)) {\n warnedUnavailableBackends.add(backendUrl);\n console.warn(\n '[AthenaSDK] This backend has no POST /api/conversations/threads/archive route (older agora, or no checkpointer); sessions cannot be archived from here.',\n );\n }\n }\n if (scopeRef.current !== write.scope) return;\n // The row comes back from the still-cached page.\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, write.threadId, false),\n hidden: withId(current.hidden, write.threadId, false),\n }));\n },\n });\n const mutate = mutation.mutate;\n\n const archive = useCallback(\n (threadId: string) => {\n if (!threadId) return;\n const write: ArchiveWrite = { threadId, scope: scopeRef.current };\n const key = writeKey(write);\n if (inFlightRef.current.has(key)) return;\n inFlightRef.current.add(key);\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, threadId, true),\n hidden: withId(current.hidden, threadId, true),\n }));\n mutate(write);\n },\n [mutate, update],\n );\n\n const release = useCallback(\n (threadIds: readonly string[]) => {\n if (threadIds.length === 0) return;\n update(scopeRef.current, (current) => {\n // A hidden row with a write in flight stays hidden whatever the\n // sources say: they have not seen the archive yet.\n const releasable = threadIds.filter(\n (threadId) => current.hidden.has(threadId) && !current.pending.has(threadId),\n );\n if (releasable.length === 0) return current;\n const hidden = new Set(current.hidden);\n for (const threadId of releasable) hidden.delete(threadId);\n return { ...current, hidden };\n });\n },\n [update],\n );\n\n return {\n hiddenThreadIds,\n archive,\n release,\n isPending: pendingThreadIds.size > 0,\n pendingThreadIds,\n isAvailable: unavailableBackend !== backendUrl,\n };\n}\n"],"names":["useState","useRef","useCallback","useMutation","archiveThread","isThreadArchiveUnavailableError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-thread-archive.cjs","sources":["../../src/threads/use-thread-archive.ts"],"sourcesContent":["/**\n * The archive action behind `SessionList`: one TanStack mutation over\n * `POST /api/conversations/threads/archive` with an optimistic hidden set.\n *\n * The row leaves the list the moment the user asks and stays hidden until\n * the caller reports that neither list source (the fetched page, the pushed\n * conversation index) lists it any more (`release`) — the page is patched on\n * the server's answer, but the index may still carry the row until the\n * publish that follows the commit lands. A failure drops the id and the row\n * comes back from the still-cached page. A 405, or a 404 that does not name\n * the thread, marks the route unavailable — the backend predates it, or has\n * no checkpointer — so the UI withdraws the action; a 404 for a thread that\n * is gone is a per-row failure (`isThreadArchiveUnavailableError`).\n *\n * One write per thread at a time: a second `archive` for a thread whose\n * write is in flight is a no-op (the guard is synchronous, so two calls in\n * one tick send one request). Everything is scoped to `(backendUrl,\n * principalId)`: a credential swap discards the hidden and pending sets, and\n * an answer for the previous identity is ignored when it lands, so a same-id\n * row under the new identity is never removed and the new user is never\n * moved off their active thread by the old user's archive.\n */\nimport { useMutation } from '@tanstack/react-query';\nimport { useCallback, useRef, useState } from 'react';\nimport { type CredentialResolver, withCredentialRenewal } from '../runtime/credential-renewal';\nimport { archiveThread, isThreadArchiveUnavailableError } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\n\nexport interface UseThreadArchiveOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the other thread writes key). */\n principalId: string;\n /** Resolved per request; called with `{ force: true }` when the host refuses the token. */\n getAuth: CredentialResolver<RestConversationAuth>;\n /** The server confirmed the archive: patch the caches, leave the thread if it is active. */\n onArchived?: (threadId: string) => void;\n}\n\nexport interface ThreadArchive {\n /** Threads hidden from the rows: asked to archive, not yet forgotten by every source. */\n hiddenThreadIds: ReadonlySet<string>;\n /** Archive a thread; a no-op while that thread's write is in flight. */\n archive: (threadId: string) => void;\n /** Drop hidden ids the caller has seen every source forget (never while their write is in flight). */\n release: (threadIds: readonly string[]) => void;\n /** True while any archive is in flight. */\n isPending: boolean;\n /** The threads whose archive is in flight — the action disables itself for these. */\n pendingThreadIds: ReadonlySet<string>;\n /** `false` once the backend answered that it has no archive route. */\n isAvailable: boolean;\n}\n\ninterface ScopedArchiveState {\n scope: string;\n hidden: ReadonlySet<string>;\n pending: ReadonlySet<string>;\n}\n\ninterface ArchiveWrite {\n threadId: string;\n /** The identity the write was issued for; an answer for another one is ignored. */\n scope: string;\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\nfunction archiveScope(backendUrl: string, principalId: string): string {\n return `${backendUrl}::${principalId}`;\n}\n\nfunction withId(ids: ReadonlySet<string>, threadId: string, present: boolean): ReadonlySet<string> {\n if (ids.has(threadId) === present) return ids;\n const next = new Set(ids);\n if (present) next.add(threadId);\n else next.delete(threadId);\n return next;\n}\n\n/** Backends already warned about, so an older agora logs one line per page load, not one per click. */\nconst warnedUnavailableBackends = new Set<string>();\n\n/** Test seam: forget which backends were warned about. */\nexport function resetThreadArchiveUnavailableWarnings(): void {\n warnedUnavailableBackends.clear();\n}\n\nexport function useThreadArchive({\n backendUrl,\n principalId,\n getAuth,\n onArchived,\n}: UseThreadArchiveOptions): ThreadArchive {\n const scope = archiveScope(backendUrl, principalId);\n const [state, setState] = useState<ScopedArchiveState>(() => ({\n scope,\n hidden: EMPTY_IDS,\n pending: EMPTY_IDS,\n }));\n // An identity swap discards everything held for the previous identity\n // (derived-state reset during render; see `useThreadFieldOverride`).\n if (state.scope !== scope) {\n setState({ scope, hidden: EMPTY_IDS, pending: EMPTY_IDS });\n }\n const hiddenThreadIds = state.scope === scope ? state.hidden : EMPTY_IDS;\n const pendingThreadIds = state.scope === scope ? state.pending : EMPTY_IDS;\n // The route is a property of the backend, not of who is asking.\n const [unavailableBackend, setUnavailableBackend] = useState<string | null>(null);\n\n const scopeRef = useRef(scope);\n scopeRef.current = scope;\n const onArchivedRef = useRef(onArchived);\n onArchivedRef.current = onArchived;\n // Writes in flight, keyed by identity and thread: the synchronous guard\n // behind the one-write-per-thread rule. A previous identity's entries are\n // left to expire with their answers (which the scope guard drops).\n const inFlightRef = useRef<Set<string>>(new Set());\n const writeKey = (write: ArchiveWrite) => `${write.scope}::${write.threadId}`;\n\n const update = useCallback(\n (forScope: string, patch: (current: ScopedArchiveState) => ScopedArchiveState) => {\n setState((current) => {\n const base: ScopedArchiveState =\n current.scope === forScope ? current : { scope: forScope, hidden: EMPTY_IDS, pending: EMPTY_IDS };\n const next = patch(base);\n return next === base && current.scope === forScope ? current : next;\n });\n },\n [],\n );\n\n const mutation = useMutation({\n mutationKey: ['conversation-history', 'archive', backendUrl, principalId],\n mutationFn: async (write: ArchiveWrite) => {\n await withCredentialRenewal(getAuth, (auth) => archiveThread(backendUrl, auth, write.threadId));\n },\n onSuccess: (_result, write) => {\n inFlightRef.current.delete(writeKey(write));\n if (scopeRef.current !== write.scope) return;\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, write.threadId, false),\n }));\n onArchivedRef.current?.(write.threadId);\n },\n onError: (error, write) => {\n inFlightRef.current.delete(writeKey(write));\n if (isThreadArchiveUnavailableError(error)) {\n setUnavailableBackend(backendUrl);\n if (!warnedUnavailableBackends.has(backendUrl)) {\n warnedUnavailableBackends.add(backendUrl);\n console.warn(\n '[AthenaSDK] This backend has no POST /api/conversations/threads/archive route (older agora, or no checkpointer); sessions cannot be archived from here.',\n );\n }\n }\n if (scopeRef.current !== write.scope) return;\n // The row comes back from the still-cached page.\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, write.threadId, false),\n hidden: withId(current.hidden, write.threadId, false),\n }));\n },\n });\n const mutate = mutation.mutate;\n\n const archive = useCallback(\n (threadId: string) => {\n if (!threadId) return;\n const write: ArchiveWrite = { threadId, scope: scopeRef.current };\n const key = writeKey(write);\n if (inFlightRef.current.has(key)) return;\n inFlightRef.current.add(key);\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, threadId, true),\n hidden: withId(current.hidden, threadId, true),\n }));\n mutate(write);\n },\n [mutate, update],\n );\n\n const release = useCallback(\n (threadIds: readonly string[]) => {\n if (threadIds.length === 0) return;\n update(scopeRef.current, (current) => {\n // A hidden row with a write in flight stays hidden whatever the\n // sources say: they have not seen the archive yet.\n const releasable = threadIds.filter(\n (threadId) => current.hidden.has(threadId) && !current.pending.has(threadId),\n );\n if (releasable.length === 0) return current;\n const hidden = new Set(current.hidden);\n for (const threadId of releasable) hidden.delete(threadId);\n return { ...current, hidden };\n });\n },\n [update],\n );\n\n return {\n hiddenThreadIds,\n archive,\n release,\n isPending: pendingThreadIds.size > 0,\n pendingThreadIds,\n isAvailable: unavailableBackend !== backendUrl,\n };\n}\n"],"names":["useState","useRef","useCallback","useMutation","withCredentialRenewal","archiveThread","isThreadArchiveUnavailableError"],"mappings":"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"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type CredentialResolver } from '../runtime/credential-renewal';
|
|
1
2
|
import type { RestConversationAuth } from './use-rest-conversation-history';
|
|
2
3
|
export interface UseThreadArchiveOptions {
|
|
3
4
|
backendUrl: string;
|
|
4
5
|
/** Non-secret identity in the mutation key (as the other thread writes key). */
|
|
5
6
|
principalId: string;
|
|
6
|
-
|
|
7
|
+
/** Resolved per request; called with `{ force: true }` when the host refuses the token. */
|
|
8
|
+
getAuth: CredentialResolver<RestConversationAuth>;
|
|
7
9
|
/** The server confirmed the archive: patch the caches, leave the thread if it is active. */
|
|
8
10
|
onArchived?: (threadId: string) => void;
|
|
9
11
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMutation } from "@tanstack/react-query";
|
|
2
2
|
import { useState, useRef, useCallback } from "react";
|
|
3
|
+
import { withCredentialRenewal } from "../runtime/credential-renewal.js";
|
|
3
4
|
import { isThreadArchiveUnavailableError, archiveThread } from "./api.js";
|
|
4
5
|
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
5
6
|
function archiveScope(backendUrl, principalId) {
|
|
@@ -50,7 +51,7 @@ function useThreadArchive({
|
|
|
50
51
|
const mutation = useMutation({
|
|
51
52
|
mutationKey: ["conversation-history", "archive", backendUrl, principalId],
|
|
52
53
|
mutationFn: async (write) => {
|
|
53
|
-
await
|
|
54
|
+
await withCredentialRenewal(getAuth, (auth) => archiveThread(backendUrl, auth, write.threadId));
|
|
54
55
|
},
|
|
55
56
|
onSuccess: (_result, write) => {
|
|
56
57
|
var _a;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-thread-archive.js","sources":["../../src/threads/use-thread-archive.ts"],"sourcesContent":["/**\n * The archive action behind `SessionList`: one TanStack mutation over\n * `POST /api/conversations/threads/archive` with an optimistic hidden set.\n *\n * The row leaves the list the moment the user asks and stays hidden until\n * the caller reports that neither list source (the fetched page, the pushed\n * conversation index) lists it any more (`release`) — the page is patched on\n * the server's answer, but the index may still carry the row until the\n * publish that follows the commit lands. A failure drops the id and the row\n * comes back from the still-cached page. A 405, or a 404 that does not name\n * the thread, marks the route unavailable — the backend predates it, or has\n * no checkpointer — so the UI withdraws the action; a 404 for a thread that\n * is gone is a per-row failure (`isThreadArchiveUnavailableError`).\n *\n * One write per thread at a time: a second `archive` for a thread whose\n * write is in flight is a no-op (the guard is synchronous, so two calls in\n * one tick send one request). Everything is scoped to `(backendUrl,\n * principalId)`: a credential swap discards the hidden and pending sets, and\n * an answer for the previous identity is ignored when it lands, so a same-id\n * row under the new identity is never removed and the new user is never\n * moved off their active thread by the old user's archive.\n */\nimport { useMutation } from '@tanstack/react-query';\nimport { useCallback, useRef, useState } from 'react';\nimport { archiveThread, isThreadArchiveUnavailableError } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\n\nexport interface UseThreadArchiveOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the other thread writes key). */\n principalId: string;\n getAuth: () => RestConversationAuth | Promise<RestConversationAuth>;\n /** The server confirmed the archive: patch the caches, leave the thread if it is active. */\n onArchived?: (threadId: string) => void;\n}\n\nexport interface ThreadArchive {\n /** Threads hidden from the rows: asked to archive, not yet forgotten by every source. */\n hiddenThreadIds: ReadonlySet<string>;\n /** Archive a thread; a no-op while that thread's write is in flight. */\n archive: (threadId: string) => void;\n /** Drop hidden ids the caller has seen every source forget (never while their write is in flight). */\n release: (threadIds: readonly string[]) => void;\n /** True while any archive is in flight. */\n isPending: boolean;\n /** The threads whose archive is in flight — the action disables itself for these. */\n pendingThreadIds: ReadonlySet<string>;\n /** `false` once the backend answered that it has no archive route. */\n isAvailable: boolean;\n}\n\ninterface ScopedArchiveState {\n scope: string;\n hidden: ReadonlySet<string>;\n pending: ReadonlySet<string>;\n}\n\ninterface ArchiveWrite {\n threadId: string;\n /** The identity the write was issued for; an answer for another one is ignored. */\n scope: string;\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\nfunction archiveScope(backendUrl: string, principalId: string): string {\n return `${backendUrl}::${principalId}`;\n}\n\nfunction withId(ids: ReadonlySet<string>, threadId: string, present: boolean): ReadonlySet<string> {\n if (ids.has(threadId) === present) return ids;\n const next = new Set(ids);\n if (present) next.add(threadId);\n else next.delete(threadId);\n return next;\n}\n\n/** Backends already warned about, so an older agora logs one line per page load, not one per click. */\nconst warnedUnavailableBackends = new Set<string>();\n\n/** Test seam: forget which backends were warned about. */\nexport function resetThreadArchiveUnavailableWarnings(): void {\n warnedUnavailableBackends.clear();\n}\n\nexport function useThreadArchive({\n backendUrl,\n principalId,\n getAuth,\n onArchived,\n}: UseThreadArchiveOptions): ThreadArchive {\n const scope = archiveScope(backendUrl, principalId);\n const [state, setState] = useState<ScopedArchiveState>(() => ({\n scope,\n hidden: EMPTY_IDS,\n pending: EMPTY_IDS,\n }));\n // An identity swap discards everything held for the previous identity\n // (derived-state reset during render; see `useThreadFieldOverride`).\n if (state.scope !== scope) {\n setState({ scope, hidden: EMPTY_IDS, pending: EMPTY_IDS });\n }\n const hiddenThreadIds = state.scope === scope ? state.hidden : EMPTY_IDS;\n const pendingThreadIds = state.scope === scope ? state.pending : EMPTY_IDS;\n // The route is a property of the backend, not of who is asking.\n const [unavailableBackend, setUnavailableBackend] = useState<string | null>(null);\n\n const scopeRef = useRef(scope);\n scopeRef.current = scope;\n const onArchivedRef = useRef(onArchived);\n onArchivedRef.current = onArchived;\n // Writes in flight, keyed by identity and thread: the synchronous guard\n // behind the one-write-per-thread rule. A previous identity's entries are\n // left to expire with their answers (which the scope guard drops).\n const inFlightRef = useRef<Set<string>>(new Set());\n const writeKey = (write: ArchiveWrite) => `${write.scope}::${write.threadId}`;\n\n const update = useCallback(\n (forScope: string, patch: (current: ScopedArchiveState) => ScopedArchiveState) => {\n setState((current) => {\n const base: ScopedArchiveState =\n current.scope === forScope ? current : { scope: forScope, hidden: EMPTY_IDS, pending: EMPTY_IDS };\n const next = patch(base);\n return next === base && current.scope === forScope ? current : next;\n });\n },\n [],\n );\n\n const mutation = useMutation({\n mutationKey: ['conversation-history', 'archive', backendUrl, principalId],\n mutationFn: async (write: ArchiveWrite) => {\n await archiveThread(backendUrl, await getAuth(), write.threadId);\n },\n onSuccess: (_result, write) => {\n inFlightRef.current.delete(writeKey(write));\n if (scopeRef.current !== write.scope) return;\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, write.threadId, false),\n }));\n onArchivedRef.current?.(write.threadId);\n },\n onError: (error, write) => {\n inFlightRef.current.delete(writeKey(write));\n if (isThreadArchiveUnavailableError(error)) {\n setUnavailableBackend(backendUrl);\n if (!warnedUnavailableBackends.has(backendUrl)) {\n warnedUnavailableBackends.add(backendUrl);\n console.warn(\n '[AthenaSDK] This backend has no POST /api/conversations/threads/archive route (older agora, or no checkpointer); sessions cannot be archived from here.',\n );\n }\n }\n if (scopeRef.current !== write.scope) return;\n // The row comes back from the still-cached page.\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, write.threadId, false),\n hidden: withId(current.hidden, write.threadId, false),\n }));\n },\n });\n const mutate = mutation.mutate;\n\n const archive = useCallback(\n (threadId: string) => {\n if (!threadId) return;\n const write: ArchiveWrite = { threadId, scope: scopeRef.current };\n const key = writeKey(write);\n if (inFlightRef.current.has(key)) return;\n inFlightRef.current.add(key);\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, threadId, true),\n hidden: withId(current.hidden, threadId, true),\n }));\n mutate(write);\n },\n [mutate, update],\n );\n\n const release = useCallback(\n (threadIds: readonly string[]) => {\n if (threadIds.length === 0) return;\n update(scopeRef.current, (current) => {\n // A hidden row with a write in flight stays hidden whatever the\n // sources say: they have not seen the archive yet.\n const releasable = threadIds.filter(\n (threadId) => current.hidden.has(threadId) && !current.pending.has(threadId),\n );\n if (releasable.length === 0) return current;\n const hidden = new Set(current.hidden);\n for (const threadId of releasable) hidden.delete(threadId);\n return { ...current, hidden };\n });\n },\n [update],\n );\n\n return {\n hiddenThreadIds,\n archive,\n release,\n isPending: pendingThreadIds.size > 0,\n pendingThreadIds,\n isAvailable: unavailableBackend !== backendUrl,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-thread-archive.js","sources":["../../src/threads/use-thread-archive.ts"],"sourcesContent":["/**\n * The archive action behind `SessionList`: one TanStack mutation over\n * `POST /api/conversations/threads/archive` with an optimistic hidden set.\n *\n * The row leaves the list the moment the user asks and stays hidden until\n * the caller reports that neither list source (the fetched page, the pushed\n * conversation index) lists it any more (`release`) — the page is patched on\n * the server's answer, but the index may still carry the row until the\n * publish that follows the commit lands. A failure drops the id and the row\n * comes back from the still-cached page. A 405, or a 404 that does not name\n * the thread, marks the route unavailable — the backend predates it, or has\n * no checkpointer — so the UI withdraws the action; a 404 for a thread that\n * is gone is a per-row failure (`isThreadArchiveUnavailableError`).\n *\n * One write per thread at a time: a second `archive` for a thread whose\n * write is in flight is a no-op (the guard is synchronous, so two calls in\n * one tick send one request). Everything is scoped to `(backendUrl,\n * principalId)`: a credential swap discards the hidden and pending sets, and\n * an answer for the previous identity is ignored when it lands, so a same-id\n * row under the new identity is never removed and the new user is never\n * moved off their active thread by the old user's archive.\n */\nimport { useMutation } from '@tanstack/react-query';\nimport { useCallback, useRef, useState } from 'react';\nimport { type CredentialResolver, withCredentialRenewal } from '../runtime/credential-renewal';\nimport { archiveThread, isThreadArchiveUnavailableError } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\n\nexport interface UseThreadArchiveOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the other thread writes key). */\n principalId: string;\n /** Resolved per request; called with `{ force: true }` when the host refuses the token. */\n getAuth: CredentialResolver<RestConversationAuth>;\n /** The server confirmed the archive: patch the caches, leave the thread if it is active. */\n onArchived?: (threadId: string) => void;\n}\n\nexport interface ThreadArchive {\n /** Threads hidden from the rows: asked to archive, not yet forgotten by every source. */\n hiddenThreadIds: ReadonlySet<string>;\n /** Archive a thread; a no-op while that thread's write is in flight. */\n archive: (threadId: string) => void;\n /** Drop hidden ids the caller has seen every source forget (never while their write is in flight). */\n release: (threadIds: readonly string[]) => void;\n /** True while any archive is in flight. */\n isPending: boolean;\n /** The threads whose archive is in flight — the action disables itself for these. */\n pendingThreadIds: ReadonlySet<string>;\n /** `false` once the backend answered that it has no archive route. */\n isAvailable: boolean;\n}\n\ninterface ScopedArchiveState {\n scope: string;\n hidden: ReadonlySet<string>;\n pending: ReadonlySet<string>;\n}\n\ninterface ArchiveWrite {\n threadId: string;\n /** The identity the write was issued for; an answer for another one is ignored. */\n scope: string;\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\nfunction archiveScope(backendUrl: string, principalId: string): string {\n return `${backendUrl}::${principalId}`;\n}\n\nfunction withId(ids: ReadonlySet<string>, threadId: string, present: boolean): ReadonlySet<string> {\n if (ids.has(threadId) === present) return ids;\n const next = new Set(ids);\n if (present) next.add(threadId);\n else next.delete(threadId);\n return next;\n}\n\n/** Backends already warned about, so an older agora logs one line per page load, not one per click. */\nconst warnedUnavailableBackends = new Set<string>();\n\n/** Test seam: forget which backends were warned about. */\nexport function resetThreadArchiveUnavailableWarnings(): void {\n warnedUnavailableBackends.clear();\n}\n\nexport function useThreadArchive({\n backendUrl,\n principalId,\n getAuth,\n onArchived,\n}: UseThreadArchiveOptions): ThreadArchive {\n const scope = archiveScope(backendUrl, principalId);\n const [state, setState] = useState<ScopedArchiveState>(() => ({\n scope,\n hidden: EMPTY_IDS,\n pending: EMPTY_IDS,\n }));\n // An identity swap discards everything held for the previous identity\n // (derived-state reset during render; see `useThreadFieldOverride`).\n if (state.scope !== scope) {\n setState({ scope, hidden: EMPTY_IDS, pending: EMPTY_IDS });\n }\n const hiddenThreadIds = state.scope === scope ? state.hidden : EMPTY_IDS;\n const pendingThreadIds = state.scope === scope ? state.pending : EMPTY_IDS;\n // The route is a property of the backend, not of who is asking.\n const [unavailableBackend, setUnavailableBackend] = useState<string | null>(null);\n\n const scopeRef = useRef(scope);\n scopeRef.current = scope;\n const onArchivedRef = useRef(onArchived);\n onArchivedRef.current = onArchived;\n // Writes in flight, keyed by identity and thread: the synchronous guard\n // behind the one-write-per-thread rule. A previous identity's entries are\n // left to expire with their answers (which the scope guard drops).\n const inFlightRef = useRef<Set<string>>(new Set());\n const writeKey = (write: ArchiveWrite) => `${write.scope}::${write.threadId}`;\n\n const update = useCallback(\n (forScope: string, patch: (current: ScopedArchiveState) => ScopedArchiveState) => {\n setState((current) => {\n const base: ScopedArchiveState =\n current.scope === forScope ? current : { scope: forScope, hidden: EMPTY_IDS, pending: EMPTY_IDS };\n const next = patch(base);\n return next === base && current.scope === forScope ? current : next;\n });\n },\n [],\n );\n\n const mutation = useMutation({\n mutationKey: ['conversation-history', 'archive', backendUrl, principalId],\n mutationFn: async (write: ArchiveWrite) => {\n await withCredentialRenewal(getAuth, (auth) => archiveThread(backendUrl, auth, write.threadId));\n },\n onSuccess: (_result, write) => {\n inFlightRef.current.delete(writeKey(write));\n if (scopeRef.current !== write.scope) return;\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, write.threadId, false),\n }));\n onArchivedRef.current?.(write.threadId);\n },\n onError: (error, write) => {\n inFlightRef.current.delete(writeKey(write));\n if (isThreadArchiveUnavailableError(error)) {\n setUnavailableBackend(backendUrl);\n if (!warnedUnavailableBackends.has(backendUrl)) {\n warnedUnavailableBackends.add(backendUrl);\n console.warn(\n '[AthenaSDK] This backend has no POST /api/conversations/threads/archive route (older agora, or no checkpointer); sessions cannot be archived from here.',\n );\n }\n }\n if (scopeRef.current !== write.scope) return;\n // The row comes back from the still-cached page.\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, write.threadId, false),\n hidden: withId(current.hidden, write.threadId, false),\n }));\n },\n });\n const mutate = mutation.mutate;\n\n const archive = useCallback(\n (threadId: string) => {\n if (!threadId) return;\n const write: ArchiveWrite = { threadId, scope: scopeRef.current };\n const key = writeKey(write);\n if (inFlightRef.current.has(key)) return;\n inFlightRef.current.add(key);\n update(write.scope, (current) => ({\n ...current,\n pending: withId(current.pending, threadId, true),\n hidden: withId(current.hidden, threadId, true),\n }));\n mutate(write);\n },\n [mutate, update],\n );\n\n const release = useCallback(\n (threadIds: readonly string[]) => {\n if (threadIds.length === 0) return;\n update(scopeRef.current, (current) => {\n // A hidden row with a write in flight stays hidden whatever the\n // sources say: they have not seen the archive yet.\n const releasable = threadIds.filter(\n (threadId) => current.hidden.has(threadId) && !current.pending.has(threadId),\n );\n if (releasable.length === 0) return current;\n const hidden = new Set(current.hidden);\n for (const threadId of releasable) hidden.delete(threadId);\n return { ...current, hidden };\n });\n },\n [update],\n );\n\n return {\n hiddenThreadIds,\n archive,\n release,\n isPending: pendingThreadIds.size > 0,\n pendingThreadIds,\n isAvailable: unavailableBackend !== backendUrl,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-thread-favorite.cjs","sources":["../../src/threads/use-thread-favorite.ts"],"sourcesContent":["/**\n * The pin toggle behind `SessionList` and the conversation-history hooks:\n * `useThreadFieldOverride` over `POST /api/conversations/threads/favorite`\n * (see that module for the optimistic-override, rollback, serialization and\n * identity-scoping contract). A 404 / 405 marks the toggle unavailable — the\n * backend predates the route — so the UI falls back to the read-only pin\n * instead of failing on every click.\n */\nimport { useCallback } from 'react';\nimport { isThreadFavoriteUnavailableError, setThreadFavorite } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\nimport {\n resetThreadFieldUnavailableWarnings,\n type ThreadFieldTarget,\n useThreadFieldOverride,\n} from './use-thread-field-override';\n\nexport interface ThreadFavoriteTarget {\n threadId: string;\n isFavorited: boolean;\n}\n\n/** The two fields the override layer reads and writes on a list row. */\nexport interface FavoriteRow {\n threadId: string;\n isFavorited?: boolean;\n}\n\nexport interface UseThreadFavoriteOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the read-state mutation keys). */\n principalId: string;\n
|
|
1
|
+
{"version":3,"file":"use-thread-favorite.cjs","sources":["../../src/threads/use-thread-favorite.ts"],"sourcesContent":["/**\n * The pin toggle behind `SessionList` and the conversation-history hooks:\n * `useThreadFieldOverride` over `POST /api/conversations/threads/favorite`\n * (see that module for the optimistic-override, rollback, serialization and\n * identity-scoping contract). A 404 / 405 marks the toggle unavailable — the\n * backend predates the route — so the UI falls back to the read-only pin\n * instead of failing on every click.\n */\nimport { useCallback } from 'react';\nimport type { CredentialResolver } from '../runtime/credential-renewal';\nimport { isThreadFavoriteUnavailableError, setThreadFavorite } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\nimport {\n resetThreadFieldUnavailableWarnings,\n type ThreadFieldTarget,\n useThreadFieldOverride,\n} from './use-thread-field-override';\n\nexport interface ThreadFavoriteTarget {\n threadId: string;\n isFavorited: boolean;\n}\n\n/** The two fields the override layer reads and writes on a list row. */\nexport interface FavoriteRow {\n threadId: string;\n isFavorited?: boolean;\n}\n\nexport interface UseThreadFavoriteOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the read-state mutation keys). */\n principalId: string;\n /** Resolved per request; called with `{ force: true }` when the host refuses the token. */\n getAuth: CredentialResolver<RestConversationAuth>;\n /**\n * The rows as the list sources currently report them, BEFORE overrides. An\n * override is dropped once its row reports the same value.\n */\n rows: readonly FavoriteRow[];\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (result: ThreadFavoriteTarget) => void;\n}\n\nexport interface ThreadFavoriteToggle {\n /** Optimistic, then server-confirmed, values layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, boolean>>;\n /**\n * Pin or unpin a thread. Flips the row at once; while that thread's write\n * is in flight the call only records the desired value (see module doc).\n */\n setFavorited: (target: ThreadFavoriteTarget) => void;\n /** True while any toggle is in flight. */\n isPending: boolean;\n /** The threads whose toggle is in flight — a pin control disables itself for these. */\n pendingThreadIds: ReadonlySet<string>;\n /**\n * `false` once the backend answered 404 / 405: the route is not deployed\n * there, and the UI shows the read-only pin instead of a toggle.\n */\n isAvailable: boolean;\n}\n\n/** Test seam: forget which backends were warned about (shared with the other field writes). */\nexport const resetThreadFavoriteUnavailableWarnings = resetThreadFieldUnavailableWarnings;\n\nconst FAVORITE_UNAVAILABLE_WARNING =\n '[AthenaSDK] This backend has no POST /api/conversations/threads/favorite route yet; session pins stay read-only until it deploys.';\n\nconst readFavorite = (row: FavoriteRow): boolean => row.isFavorited ?? false;\n\nexport function useThreadFavorite({\n backendUrl,\n principalId,\n getAuth,\n rows,\n onServerTruth,\n}: UseThreadFavoriteOptions): ThreadFavoriteToggle {\n const field = useThreadFieldOverride<boolean, FavoriteRow>({\n field: 'favorite',\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow: readFavorite,\n write: async (auth, { threadId, value }) => {\n const result = await setThreadFavorite(backendUrl, auth, { threadId, isFavorited: value });\n return { threadId: result.thread_id, value: result.is_favorited };\n },\n isUnavailableError: isThreadFavoriteUnavailableError,\n unavailableWarning: FAVORITE_UNAVAILABLE_WARNING,\n onServerTruth: onServerTruth\n ? ({ threadId, value }: ThreadFieldTarget<boolean>) =>\n onServerTruth({ threadId, isFavorited: value })\n : undefined,\n });\n const set = field.set;\n const setFavorited = useCallback(\n ({ threadId, isFavorited }: ThreadFavoriteTarget) => set({ threadId, value: isFavorited }),\n [set],\n );\n return {\n overrides: field.overrides,\n setFavorited,\n isPending: field.isPending,\n pendingThreadIds: field.pendingThreadIds,\n isAvailable: field.isAvailable,\n };\n}\n\n/** The row with its override applied, or the row itself when it has none. */\nexport function withFavoriteOverride<T extends FavoriteRow>(\n row: T,\n overrides: Readonly<Record<string, boolean>>,\n): T {\n const override = overrides[row.threadId];\n return override === undefined || override === row.isFavorited\n ? row\n : { ...row, isFavorited: override };\n}\n\n/** Every row with its override applied. */\nexport function withFavoriteOverrides<T extends FavoriteRow>(\n rows: readonly T[],\n overrides: Readonly<Record<string, boolean>>,\n): T[] {\n return rows.map((row) => withFavoriteOverride(row, overrides));\n}\n"],"names":["useThreadFieldOverride","setThreadFavorite","isThreadFavoriteUnavailableError","useCallback"],"mappings":";;;;;AAkEA,MAAM,+BACJ;AAEF,MAAM,eAAe,CAAC,QAA8B,IAAI,eAAe;AAEhE,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAmD;AACjD,QAAM,QAAQA,uBAAAA,uBAA6C;AAAA,IACzD,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,OAAO,OAAO,MAAM,EAAE,UAAU,YAAY;AAC1C,YAAM,SAAS,MAAMC,IAAAA,kBAAkB,YAAY,MAAM,EAAE,UAAU,aAAa,OAAO;AACzF,aAAO,EAAE,UAAU,OAAO,WAAW,OAAO,OAAO,aAAA;AAAA,IACrD;AAAA,IACA,oBAAoBC,IAAAA;AAAAA,IACpB,oBAAoB;AAAA,IACpB,eAAe,gBACX,CAAC,EAAE,UAAU,MAAA,MACX,cAAc,EAAE,UAAU,aAAa,MAAA,CAAO,IAChD;AAAA,EAAA,CACL;AACD,QAAM,MAAM,MAAM;AAClB,QAAM,eAAeC,MAAAA;AAAAA,IACnB,CAAC,EAAE,UAAU,kBAAwC,IAAI,EAAE,UAAU,OAAO,aAAa;AAAA,IACzF,CAAC,GAAG;AAAA,EAAA;AAEN,SAAO;AAAA,IACL,WAAW,MAAM;AAAA,IACjB;AAAA,IACA,WAAW,MAAM;AAAA,IACjB,kBAAkB,MAAM;AAAA,IACxB,aAAa,MAAM;AAAA,EAAA;AAEvB;AAGO,SAAS,qBACd,KACA,WACG;AACH,QAAM,WAAW,UAAU,IAAI,QAAQ;AACvC,SAAO,aAAa,UAAa,aAAa,IAAI,cAC9C,MACA,EAAE,GAAG,KAAK,aAAa,SAAA;AAC7B;AAGO,SAAS,sBACd,MACA,WACK;AACL,SAAO,KAAK,IAAI,CAAC,QAAQ,qBAAqB,KAAK,SAAS,CAAC;AAC/D;;;;"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CredentialResolver } from '../runtime/credential-renewal';
|
|
1
2
|
import type { RestConversationAuth } from './use-rest-conversation-history';
|
|
2
3
|
import { resetThreadFieldUnavailableWarnings } from './use-thread-field-override';
|
|
3
4
|
export interface ThreadFavoriteTarget {
|
|
@@ -13,7 +14,8 @@ export interface UseThreadFavoriteOptions {
|
|
|
13
14
|
backendUrl: string;
|
|
14
15
|
/** Non-secret identity in the mutation key (as the read-state mutation keys). */
|
|
15
16
|
principalId: string;
|
|
16
|
-
|
|
17
|
+
/** Resolved per request; called with `{ force: true }` when the host refuses the token. */
|
|
18
|
+
getAuth: CredentialResolver<RestConversationAuth>;
|
|
17
19
|
/**
|
|
18
20
|
* The rows as the list sources currently report them, BEFORE overrides. An
|
|
19
21
|
* override is dropped once its row reports the same value.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-thread-favorite.js","sources":["../../src/threads/use-thread-favorite.ts"],"sourcesContent":["/**\n * The pin toggle behind `SessionList` and the conversation-history hooks:\n * `useThreadFieldOverride` over `POST /api/conversations/threads/favorite`\n * (see that module for the optimistic-override, rollback, serialization and\n * identity-scoping contract). A 404 / 405 marks the toggle unavailable — the\n * backend predates the route — so the UI falls back to the read-only pin\n * instead of failing on every click.\n */\nimport { useCallback } from 'react';\nimport { isThreadFavoriteUnavailableError, setThreadFavorite } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\nimport {\n resetThreadFieldUnavailableWarnings,\n type ThreadFieldTarget,\n useThreadFieldOverride,\n} from './use-thread-field-override';\n\nexport interface ThreadFavoriteTarget {\n threadId: string;\n isFavorited: boolean;\n}\n\n/** The two fields the override layer reads and writes on a list row. */\nexport interface FavoriteRow {\n threadId: string;\n isFavorited?: boolean;\n}\n\nexport interface UseThreadFavoriteOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the read-state mutation keys). */\n principalId: string;\n
|
|
1
|
+
{"version":3,"file":"use-thread-favorite.js","sources":["../../src/threads/use-thread-favorite.ts"],"sourcesContent":["/**\n * The pin toggle behind `SessionList` and the conversation-history hooks:\n * `useThreadFieldOverride` over `POST /api/conversations/threads/favorite`\n * (see that module for the optimistic-override, rollback, serialization and\n * identity-scoping contract). A 404 / 405 marks the toggle unavailable — the\n * backend predates the route — so the UI falls back to the read-only pin\n * instead of failing on every click.\n */\nimport { useCallback } from 'react';\nimport type { CredentialResolver } from '../runtime/credential-renewal';\nimport { isThreadFavoriteUnavailableError, setThreadFavorite } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\nimport {\n resetThreadFieldUnavailableWarnings,\n type ThreadFieldTarget,\n useThreadFieldOverride,\n} from './use-thread-field-override';\n\nexport interface ThreadFavoriteTarget {\n threadId: string;\n isFavorited: boolean;\n}\n\n/** The two fields the override layer reads and writes on a list row. */\nexport interface FavoriteRow {\n threadId: string;\n isFavorited?: boolean;\n}\n\nexport interface UseThreadFavoriteOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the read-state mutation keys). */\n principalId: string;\n /** Resolved per request; called with `{ force: true }` when the host refuses the token. */\n getAuth: CredentialResolver<RestConversationAuth>;\n /**\n * The rows as the list sources currently report them, BEFORE overrides. An\n * override is dropped once its row reports the same value.\n */\n rows: readonly FavoriteRow[];\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (result: ThreadFavoriteTarget) => void;\n}\n\nexport interface ThreadFavoriteToggle {\n /** Optimistic, then server-confirmed, values layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, boolean>>;\n /**\n * Pin or unpin a thread. Flips the row at once; while that thread's write\n * is in flight the call only records the desired value (see module doc).\n */\n setFavorited: (target: ThreadFavoriteTarget) => void;\n /** True while any toggle is in flight. */\n isPending: boolean;\n /** The threads whose toggle is in flight — a pin control disables itself for these. */\n pendingThreadIds: ReadonlySet<string>;\n /**\n * `false` once the backend answered 404 / 405: the route is not deployed\n * there, and the UI shows the read-only pin instead of a toggle.\n */\n isAvailable: boolean;\n}\n\n/** Test seam: forget which backends were warned about (shared with the other field writes). */\nexport const resetThreadFavoriteUnavailableWarnings = resetThreadFieldUnavailableWarnings;\n\nconst FAVORITE_UNAVAILABLE_WARNING =\n '[AthenaSDK] This backend has no POST /api/conversations/threads/favorite route yet; session pins stay read-only until it deploys.';\n\nconst readFavorite = (row: FavoriteRow): boolean => row.isFavorited ?? false;\n\nexport function useThreadFavorite({\n backendUrl,\n principalId,\n getAuth,\n rows,\n onServerTruth,\n}: UseThreadFavoriteOptions): ThreadFavoriteToggle {\n const field = useThreadFieldOverride<boolean, FavoriteRow>({\n field: 'favorite',\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow: readFavorite,\n write: async (auth, { threadId, value }) => {\n const result = await setThreadFavorite(backendUrl, auth, { threadId, isFavorited: value });\n return { threadId: result.thread_id, value: result.is_favorited };\n },\n isUnavailableError: isThreadFavoriteUnavailableError,\n unavailableWarning: FAVORITE_UNAVAILABLE_WARNING,\n onServerTruth: onServerTruth\n ? ({ threadId, value }: ThreadFieldTarget<boolean>) =>\n onServerTruth({ threadId, isFavorited: value })\n : undefined,\n });\n const set = field.set;\n const setFavorited = useCallback(\n ({ threadId, isFavorited }: ThreadFavoriteTarget) => set({ threadId, value: isFavorited }),\n [set],\n );\n return {\n overrides: field.overrides,\n setFavorited,\n isPending: field.isPending,\n pendingThreadIds: field.pendingThreadIds,\n isAvailable: field.isAvailable,\n };\n}\n\n/** The row with its override applied, or the row itself when it has none. */\nexport function withFavoriteOverride<T extends FavoriteRow>(\n row: T,\n overrides: Readonly<Record<string, boolean>>,\n): T {\n const override = overrides[row.threadId];\n return override === undefined || override === row.isFavorited\n ? row\n : { ...row, isFavorited: override };\n}\n\n/** Every row with its override applied. */\nexport function withFavoriteOverrides<T extends FavoriteRow>(\n rows: readonly T[],\n overrides: Readonly<Record<string, boolean>>,\n): T[] {\n return rows.map((row) => withFavoriteOverride(row, overrides));\n}\n"],"names":[],"mappings":";;;AAkEA,MAAM,+BACJ;AAEF,MAAM,eAAe,CAAC,QAA8B,IAAI,eAAe;AAEhE,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAmD;AACjD,QAAM,QAAQ,uBAA6C;AAAA,IACzD,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,OAAO,OAAO,MAAM,EAAE,UAAU,YAAY;AAC1C,YAAM,SAAS,MAAM,kBAAkB,YAAY,MAAM,EAAE,UAAU,aAAa,OAAO;AACzF,aAAO,EAAE,UAAU,OAAO,WAAW,OAAO,OAAO,aAAA;AAAA,IACrD;AAAA,IACA,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,IACpB,eAAe,gBACX,CAAC,EAAE,UAAU,MAAA,MACX,cAAc,EAAE,UAAU,aAAa,MAAA,CAAO,IAChD;AAAA,EAAA,CACL;AACD,QAAM,MAAM,MAAM;AAClB,QAAM,eAAe;AAAA,IACnB,CAAC,EAAE,UAAU,kBAAwC,IAAI,EAAE,UAAU,OAAO,aAAa;AAAA,IACzF,CAAC,GAAG;AAAA,EAAA;AAEN,SAAO;AAAA,IACL,WAAW,MAAM;AAAA,IACjB;AAAA,IACA,WAAW,MAAM;AAAA,IACjB,kBAAkB,MAAM;AAAA,IACxB,aAAa,MAAM;AAAA,EAAA;AAEvB;AAGO,SAAS,qBACd,KACA,WACG;AACH,QAAM,WAAW,UAAU,IAAI,QAAQ;AACvC,SAAO,aAAa,UAAa,aAAa,IAAI,cAC9C,MACA,EAAE,GAAG,KAAK,aAAa,SAAA;AAC7B;AAGO,SAAS,sBACd,MACA,WACK;AACL,SAAO,KAAK,IAAI,CAAC,QAAQ,qBAAqB,KAAK,SAAS,CAAC;AAC/D;"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const reactQuery = require("@tanstack/react-query");
|
|
4
4
|
const react = require("react");
|
|
5
|
+
const credentialRenewal = require("../runtime/credential-renewal.cjs");
|
|
5
6
|
const EMPTY_PENDING = /* @__PURE__ */ new Set();
|
|
6
7
|
function fieldScope(backendUrl, principalId) {
|
|
7
8
|
return `${backendUrl}::${principalId}`;
|
|
@@ -94,7 +95,7 @@ function useThreadFieldOverride({
|
|
|
94
95
|
}, [overrides, pendingThreadIds, rows, scope, update]);
|
|
95
96
|
const mutation = reactQuery.useMutation({
|
|
96
97
|
mutationKey: ["conversation-history", field, backendUrl, principalId],
|
|
97
|
-
mutationFn: async ({ threadId, value }) =>
|
|
98
|
+
mutationFn: async ({ threadId, value }) => credentialRenewal.withCredentialRenewal(getAuth, (auth) => writeRef.current(auth, { threadId, value })),
|
|
98
99
|
onError: (error, write2) => {
|
|
99
100
|
if (isUnavailableErrorRef.current(error)) {
|
|
100
101
|
setUnavailableBackend(backendUrl);
|