@athenaintel/react 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +29 -4
  2. package/dist/assets/AssetPanel.cjs +3 -2
  3. package/dist/assets/AssetPanel.cjs.map +1 -1
  4. package/dist/assets/AssetPanel.js +3 -2
  5. package/dist/assets/AssetPanel.js.map +1 -1
  6. package/dist/assets/AthenaAssetEmbed.cjs +1 -0
  7. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  8. package/dist/assets/AthenaAssetEmbed.js +1 -0
  9. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  10. package/dist/assets/use-asset-embed.cjs +22 -23
  11. package/dist/assets/use-asset-embed.cjs.map +1 -1
  12. package/dist/assets/use-asset-embed.d.ts +9 -0
  13. package/dist/assets/use-asset-embed.js +22 -23
  14. package/dist/assets/use-asset-embed.js.map +1 -1
  15. package/dist/auth/AthenaAuthProvider.cjs +32 -4
  16. package/dist/auth/AthenaAuthProvider.cjs.map +1 -1
  17. package/dist/auth/AthenaAuthProvider.d.ts +21 -0
  18. package/dist/auth/AthenaAuthProvider.js +33 -5
  19. package/dist/auth/AthenaAuthProvider.js.map +1 -1
  20. package/dist/auth/AthenaCredentialSourceContext.cjs +21 -0
  21. package/dist/auth/AthenaCredentialSourceContext.cjs.map +1 -0
  22. package/dist/auth/AthenaCredentialSourceContext.d.ts +14 -0
  23. package/dist/auth/AthenaCredentialSourceContext.js +21 -0
  24. package/dist/auth/AthenaCredentialSourceContext.js.map +1 -0
  25. package/dist/auth/AthenaSSOProvider.cjs +71 -47
  26. package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
  27. package/dist/auth/AthenaSSOProvider.js +71 -47
  28. package/dist/auth/AthenaSSOProvider.js.map +1 -1
  29. package/dist/auth/credential-source.cjs +120 -0
  30. package/dist/auth/credential-source.cjs.map +1 -0
  31. package/dist/auth/credential-source.d.ts +68 -0
  32. package/dist/auth/credential-source.js +120 -0
  33. package/dist/auth/credential-source.js.map +1 -0
  34. package/dist/auth/types.d.ts +9 -0
  35. package/dist/chat/AthenaChat.cjs +8 -0
  36. package/dist/chat/AthenaChat.cjs.map +1 -1
  37. package/dist/chat/AthenaChat.js +8 -0
  38. package/dist/chat/AthenaChat.js.map +1 -1
  39. package/dist/chat/StatewireApprovalCard.cjs +31 -6
  40. package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
  41. package/dist/chat/StatewireApprovalCard.d.ts +34 -4
  42. package/dist/chat/StatewireApprovalCard.js +33 -8
  43. package/dist/chat/StatewireApprovalCard.js.map +1 -1
  44. package/dist/chat-ui/index.d.ts +1 -1
  45. package/dist/chat-ui/mentions/suggestions/data/api-client.cjs +20 -11
  46. package/dist/chat-ui/mentions/suggestions/data/api-client.cjs.map +1 -1
  47. package/dist/chat-ui/mentions/suggestions/data/api-client.d.ts +11 -2
  48. package/dist/chat-ui/mentions/suggestions/data/api-client.js +20 -11
  49. package/dist/chat-ui/mentions/suggestions/data/api-client.js.map +1 -1
  50. package/dist/chat-ui/statewire.d.ts +1 -1
  51. package/dist/collab/mint.cjs +6 -15
  52. package/dist/collab/mint.cjs.map +1 -1
  53. package/dist/collab/mint.d.ts +8 -0
  54. package/dist/collab/mint.js +6 -15
  55. package/dist/collab/mint.js.map +1 -1
  56. package/dist/collab/presence-mint.cjs +20 -16
  57. package/dist/collab/presence-mint.cjs.map +1 -1
  58. package/dist/collab/presence-mint.js +21 -17
  59. package/dist/collab/presence-mint.js.map +1 -1
  60. package/dist/collab/presence-react.cjs +12 -5
  61. package/dist/collab/presence-react.cjs.map +1 -1
  62. package/dist/collab/presence-react.d.ts +3 -1
  63. package/dist/collab/presence-react.js +12 -5
  64. package/dist/collab/presence-react.js.map +1 -1
  65. package/dist/collab/react.cjs +5 -2
  66. package/dist/collab/react.cjs.map +1 -1
  67. package/dist/collab/react.js +5 -2
  68. package/dist/collab/react.js.map +1 -1
  69. package/dist/dictation/use-athena-dictation-engine.cjs +12 -11
  70. package/dist/dictation/use-athena-dictation-engine.cjs.map +1 -1
  71. package/dist/dictation/use-athena-dictation-engine.d.ts +4 -3
  72. package/dist/dictation/use-athena-dictation-engine.js +12 -11
  73. package/dist/dictation/use-athena-dictation-engine.js.map +1 -1
  74. package/dist/index.cjs +1 -0
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.ts +3 -3
  77. package/dist/index.js +2 -1
  78. package/dist/packages/agent-runtime/src/conversation-list.cjs.map +1 -1
  79. package/dist/packages/agent-runtime/src/conversation-list.js.map +1 -1
  80. package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs +1 -0
  81. package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  82. package/dist/packages/agent-runtime/src/generated/runconfig-contract.js +1 -0
  83. package/dist/packages/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  84. package/dist/packages/agent-runtime/src/interrupts.cjs +22 -2
  85. package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
  86. package/dist/packages/agent-runtime/src/interrupts.js +22 -2
  87. package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
  88. package/dist/packages/agent-runtime/src/messages.cjs +2 -1
  89. package/dist/packages/agent-runtime/src/messages.cjs.map +1 -1
  90. package/dist/packages/agent-runtime/src/messages.js +2 -1
  91. package/dist/packages/agent-runtime/src/messages.js.map +1 -1
  92. package/dist/packages/agent-runtime/src/voice-presentation.cjs +6 -0
  93. package/dist/packages/agent-runtime/src/voice-presentation.cjs.map +1 -1
  94. package/dist/packages/agent-runtime/src/voice-presentation.js +6 -0
  95. package/dist/packages/agent-runtime/src/voice-presentation.js.map +1 -1
  96. package/dist/packages/agent-runtime/src/voice-turn-grouping.cjs +68 -0
  97. package/dist/packages/agent-runtime/src/voice-turn-grouping.cjs.map +1 -0
  98. package/dist/packages/agent-runtime/src/voice-turn-grouping.js +68 -0
  99. package/dist/packages/agent-runtime/src/voice-turn-grouping.js.map +1 -0
  100. package/dist/packages/react/package.json.cjs +1 -1
  101. package/dist/packages/react/package.json.js +1 -1
  102. package/dist/provider/AthenaContext.cjs.map +1 -1
  103. package/dist/provider/AthenaContext.d.ts +7 -1
  104. package/dist/provider/AthenaContext.js.map +1 -1
  105. package/dist/provider/AthenaProvider.cjs +24 -2
  106. package/dist/provider/AthenaProvider.cjs.map +1 -1
  107. package/dist/provider/AthenaProvider.js +24 -2
  108. package/dist/provider/AthenaProvider.js.map +1 -1
  109. package/dist/provider/theme-style-context.cjs +12 -0
  110. package/dist/provider/theme-style-context.cjs.map +1 -0
  111. package/dist/provider/theme-style-context.d.ts +8 -0
  112. package/dist/provider/theme-style-context.js +12 -0
  113. package/dist/provider/theme-style-context.js.map +1 -0
  114. package/dist/runtime/auth-refusal-restart.cjs +66 -0
  115. package/dist/runtime/auth-refusal-restart.cjs.map +1 -0
  116. package/dist/runtime/auth-refusal-restart.d.ts +35 -0
  117. package/dist/runtime/auth-refusal-restart.js +66 -0
  118. package/dist/runtime/auth-refusal-restart.js.map +1 -0
  119. package/dist/runtime/auth.cjs +58 -21
  120. package/dist/runtime/auth.cjs.map +1 -1
  121. package/dist/runtime/auth.d.ts +11 -1
  122. package/dist/runtime/auth.js +59 -22
  123. package/dist/runtime/auth.js.map +1 -1
  124. package/dist/runtime/credential-renewal.cjs +29 -0
  125. package/dist/runtime/credential-renewal.cjs.map +1 -0
  126. package/dist/runtime/credential-renewal.d.ts +35 -0
  127. package/dist/runtime/credential-renewal.js +29 -0
  128. package/dist/runtime/credential-renewal.js.map +1 -0
  129. package/dist/search/useAthenaRepoSearch.cjs +3 -5
  130. package/dist/search/useAthenaRepoSearch.cjs.map +1 -1
  131. package/dist/search/useAthenaRepoSearch.d.ts +7 -7
  132. package/dist/search/useAthenaRepoSearch.js +3 -5
  133. package/dist/search/useAthenaRepoSearch.js.map +1 -1
  134. package/dist/styles.css +1 -1
  135. package/dist/threads/SessionList.cjs +87 -16
  136. package/dist/threads/SessionList.cjs.map +1 -1
  137. package/dist/threads/SessionList.js +88 -17
  138. package/dist/threads/SessionList.js.map +1 -1
  139. package/dist/threads/api.cjs +5 -1
  140. package/dist/threads/api.cjs.map +1 -1
  141. package/dist/threads/api.js +5 -1
  142. package/dist/threads/api.js.map +1 -1
  143. package/dist/threads/conversation-list-fetcher.cjs +5 -5
  144. package/dist/threads/conversation-list-fetcher.cjs.map +1 -1
  145. package/dist/threads/conversation-list-fetcher.d.ts +6 -5
  146. package/dist/threads/conversation-list-fetcher.js +5 -5
  147. package/dist/threads/conversation-list-fetcher.js.map +1 -1
  148. package/dist/threads/session-list-model.cjs +13 -5
  149. package/dist/threads/session-list-model.cjs.map +1 -1
  150. package/dist/threads/session-list-model.d.ts +10 -3
  151. package/dist/threads/session-list-model.js +13 -5
  152. package/dist/threads/session-list-model.js.map +1 -1
  153. package/dist/threads/statewire-thread-list.cjs +13 -14
  154. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  155. package/dist/threads/statewire-thread-list.js +13 -14
  156. package/dist/threads/statewire-thread-list.js.map +1 -1
  157. package/dist/threads/use-rest-conversation-history.cjs +6 -2
  158. package/dist/threads/use-rest-conversation-history.cjs.map +1 -1
  159. package/dist/threads/use-rest-conversation-history.d.ts +3 -1
  160. package/dist/threads/use-rest-conversation-history.js +7 -3
  161. package/dist/threads/use-rest-conversation-history.js.map +1 -1
  162. package/dist/threads/use-statewire-conversation-history.cjs +9 -2
  163. package/dist/threads/use-statewire-conversation-history.cjs.map +1 -1
  164. package/dist/threads/use-statewire-conversation-history.d.ts +3 -1
  165. package/dist/threads/use-statewire-conversation-history.js +10 -3
  166. package/dist/threads/use-statewire-conversation-history.js.map +1 -1
  167. package/dist/threads/use-thread-archive.cjs +2 -1
  168. package/dist/threads/use-thread-archive.cjs.map +1 -1
  169. package/dist/threads/use-thread-archive.d.ts +3 -1
  170. package/dist/threads/use-thread-archive.js +2 -1
  171. package/dist/threads/use-thread-archive.js.map +1 -1
  172. package/dist/threads/use-thread-favorite.cjs.map +1 -1
  173. package/dist/threads/use-thread-favorite.d.ts +3 -1
  174. package/dist/threads/use-thread-favorite.js.map +1 -1
  175. package/dist/threads/use-thread-field-override.cjs +2 -1
  176. package/dist/threads/use-thread-field-override.cjs.map +1 -1
  177. package/dist/threads/use-thread-field-override.d.ts +3 -1
  178. package/dist/threads/use-thread-field-override.js +2 -1
  179. package/dist/threads/use-thread-field-override.js.map +1 -1
  180. package/dist/threads/use-thread-rename.cjs.map +1 -1
  181. package/dist/threads/use-thread-rename.d.ts +3 -1
  182. package/dist/threads/use-thread-rename.js.map +1 -1
  183. package/dist/tools/tool-uis.cjs +11 -15
  184. package/dist/tools/tool-uis.cjs.map +1 -1
  185. package/dist/tools/tool-uis.js +11 -15
  186. package/dist/tools/tool-uis.js.map +1 -1
  187. package/dist/ui/popover.cjs +46 -0
  188. package/dist/ui/popover.cjs.map +1 -0
  189. package/dist/ui/popover.d.ts +6 -0
  190. package/dist/ui/popover.js +46 -0
  191. package/dist/ui/popover.js.map +1 -0
  192. package/dist/voice/use-athena-voice-session.cjs +17 -5
  193. package/dist/voice/use-athena-voice-session.cjs.map +1 -1
  194. package/dist/voice/use-athena-voice-session.d.ts +2 -2
  195. package/dist/voice/use-athena-voice-session.js +17 -5
  196. package/dist/voice/use-athena-voice-session.js.map +1 -1
  197. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"use-thread-field-override.cjs","sources":["../../src/threads/use-thread-field-override.ts"],"sourcesContent":["/**\n * The optimistic per-thread field write behind the pin (`useThreadFavorite`)\n * and the rename (`useThreadRename`): one TanStack mutation over a REST\n * route with an override map layered over the list rows.\n *\n * Why overrides rather than a cache patch alone: a row's field is folded from\n * two sources — the fetched page and the pushed conversation index — and a\n * push that predates the write would clobber a patched page. The override\n * sits over both until the sources report the same value, then drops away so\n * they own the field again (a later write from another client is never\n * masked). A failure restores the previous value; a 404 / 405 marks the\n * write unavailable — the backend predates the route — so the UI withdraws\n * the control instead of failing on every use.\n *\n * Same-thread writes are serialized: while one is in flight, a further write\n * for that thread flips the row at once but only records the desired value,\n * and at most one follow-up write is sent — with that value — once the\n * in-flight one has answered. Two independent writes for one thread never\n * race, so the server cannot end on an earlier request's value.\n *\n * Everything the hook holds is scoped to `(backendUrl, principalId)`: a\n * credential swap under the same list reads an empty override map, and a\n * write that was in flight for the previous identity is ignored when it\n * answers, so a same-id row under the new identity never inherits the old\n * one's value.\n */\nimport { useMutation } from '@tanstack/react-query';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\n\nexport interface ThreadFieldTarget<T> {\n threadId: string;\n value: T;\n}\n\n/** The one field every row must carry: its identity. */\nexport interface ThreadFieldRow {\n threadId: string;\n}\n\nexport interface UseThreadFieldOverrideOptions<T, R extends ThreadFieldRow> {\n /** Names the field: part of the mutation key and of the one-per-backend warning. */\n field: string;\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the read-state mutation keys). */\n principalId: string;\n getAuth: () => RestConversationAuth | Promise<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 R[];\n /** The field as a source row reports it; `undefined` when the row does not carry it. */\n readRow: (row: R) => T | undefined;\n /** The write; resolves to the server's truth after it. */\n write: (auth: RestConversationAuth, target: ThreadFieldTarget<T>) => Promise<ThreadFieldTarget<T>>;\n /** True for the rejection that means the backend has no such route (404 / 405). */\n isUnavailableError: (error: unknown) => boolean;\n /** Logged once per backend when the route is found missing. */\n unavailableWarning: string;\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (truth: ThreadFieldTarget<T>) => void;\n /** Value equality (default `===`). */\n equals?: (a: T, b: T) => boolean;\n}\n\nexport interface ThreadFieldOverride<T> {\n /** Optimistic, then server-confirmed, values layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, T>>;\n /**\n * Write the field. Flips the row at once; while that thread's write is in\n * flight the call only records the desired value (see module doc).\n */\n set: (target: ThreadFieldTarget<T>) => void;\n /** True while any write is in flight. */\n isPending: boolean;\n /** The threads whose write is in flight — a 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 withdraws the control.\n */\n isAvailable: boolean;\n}\n\n/** The hook's state, tagged with the identity it belongs to. */\ninterface ScopedFieldState<T> {\n scope: string;\n overrides: Record<string, T>;\n pending: ReadonlySet<string>;\n}\n\ninterface FieldWrite<T> extends ThreadFieldTarget<T> {\n /** The override before this chain of writes began; `undefined` restores the source value. */\n previous: T | undefined;\n /** The identity the write was issued for; an answer for another one is ignored. */\n scope: string;\n /** This thread's write generation; an answer for an older one is ignored. */\n seq: number;\n}\n\nconst EMPTY_PENDING: ReadonlySet<string> = new Set();\n\nfunction fieldScope(backendUrl: string, principalId: string): string {\n return `${backendUrl}::${principalId}`;\n}\n\nfunction withPending(pending: ReadonlySet<string>, threadId: string, isPending: boolean): ReadonlySet<string> {\n if (pending.has(threadId) === isPending) return pending;\n const next = new Set(pending);\n if (isPending) next.add(threadId);\n else next.delete(threadId);\n return next;\n}\n\nconst strictEquals = <T,>(a: T, b: T): boolean => a === b;\n\n/** `(field, backend)` pairs already warned about, so an older agora logs one line per page load, not one per use. */\nconst warnedUnavailableBackends = new Set<string>();\n\n/** Test seam: forget which backends were warned about. */\nexport function resetThreadFieldUnavailableWarnings(): void {\n warnedUnavailableBackends.clear();\n}\n\nexport function useThreadFieldOverride<T, R extends ThreadFieldRow>({\n field,\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow,\n write,\n isUnavailableError,\n unavailableWarning,\n onServerTruth,\n equals = strictEquals,\n}: UseThreadFieldOverrideOptions<T, R>): ThreadFieldOverride<T> {\n const scope = fieldScope(backendUrl, principalId);\n const [state, setState] = useState<ScopedFieldState<T>>(() => ({\n scope,\n overrides: {},\n pending: EMPTY_PENDING,\n }));\n // An identity swap discards everything held for the previous identity —\n // overrides AND pending rows — rather than merely hiding it: a round trip\n // back to that identity must not revive a row pinned for a write whose\n // answer has since been dropped (it would stay disabled for good). Setting\n // state during render is React's derived-state reset; the render re-runs\n // with the empty state before anything is committed.\n if (state.scope !== scope) {\n setState({ scope, overrides: {}, pending: EMPTY_PENDING });\n }\n const emptyOverridesRef = useRef<Readonly<Record<string, T>>>(Object.freeze({}) as Record<string, T>);\n const overrides = state.scope === scope ? state.overrides : emptyOverridesRef.current;\n const pendingThreadIds = state.scope === scope ? state.pending : EMPTY_PENDING;\n // The backend the route was found missing on. The route is a property of\n // the backend, not of who is asking, so this is keyed by backend alone; a\n // different backend is presumed to have it until it says otherwise.\n const [unavailableBackend, setUnavailableBackend] = useState<string | null>(null);\n\n const scopeRef = useRef(scope);\n scopeRef.current = scope;\n const overridesRef = useRef(overrides);\n overridesRef.current = overrides;\n // The per-render option closures ride refs so a consumer's fresh functions\n // never re-key the mutation or the caught-up effect.\n const readRowRef = useRef(readRow);\n readRowRef.current = readRow;\n const writeRef = useRef(write);\n writeRef.current = write;\n const isUnavailableErrorRef = useRef(isUnavailableError);\n isUnavailableErrorRef.current = isUnavailableError;\n const unavailableWarningRef = useRef(unavailableWarning);\n unavailableWarningRef.current = unavailableWarning;\n const onServerTruthRef = useRef(onServerTruth);\n onServerTruthRef.current = onServerTruth;\n const equalsRef = useRef(equals);\n equalsRef.current = equals;\n // Per-thread write bookkeeping for the CURRENT identity: the generation of\n // the write in flight, and the value a further write asked for meanwhile.\n const inFlightRef = useRef<Map<string, number>>(new Map());\n const queuedRef = useRef<Map<string, T>>(new Map());\n const seqRef = useRef(0);\n const bookkeepingScopeRef = useRef(scope);\n if (bookkeepingScopeRef.current !== scope) {\n // The previous identity's writes are abandoned with its state: their\n // answers are dropped by the scope guard below, and nothing queued\n // behind them is sent.\n bookkeepingScopeRef.current = scope;\n inFlightRef.current = new Map();\n queuedRef.current = new Map();\n }\n\n const update = useCallback(\n (forScope: string, patch: (current: ScopedFieldState<T>) => ScopedFieldState<T>) => {\n setState((current) => {\n const base: ScopedFieldState<T> =\n current.scope === forScope\n ? current\n : { scope: forScope, overrides: {}, pending: EMPTY_PENDING };\n const next = patch(base);\n return next === base && current.scope === forScope ? current : next;\n });\n },\n [],\n );\n\n useEffect(() => {\n const caughtUp = Object.keys(overrides).filter((threadId) => {\n // A row with a write in flight keeps its override whatever the sources\n // say: the first write of a chain patches the page with a value the\n // user has since moved away from, and the row must not flash it.\n if (pendingThreadIds.has(threadId)) return false;\n const row = rows.find((candidate) => candidate.threadId === threadId);\n if (row === undefined) return false;\n const reported = readRowRef.current(row);\n const override = overrides[threadId];\n return reported !== undefined && override !== undefined && equalsRef.current(reported, override);\n });\n if (caughtUp.length === 0) return;\n update(scope, (current) => {\n const next = { ...current.overrides };\n for (const threadId of caughtUp) delete next[threadId];\n return { ...current, overrides: next };\n });\n }, [overrides, pendingThreadIds, rows, scope, update]);\n\n const mutation = useMutation({\n mutationKey: ['conversation-history', field, backendUrl, principalId],\n mutationFn: async ({ threadId, value }: FieldWrite<T>) =>\n writeRef.current(await getAuth(), { threadId, value }),\n onError: (error, write) => {\n if (isUnavailableErrorRef.current(error)) {\n setUnavailableBackend(backendUrl);\n const warnKey = `${field}::${backendUrl}`;\n if (!warnedUnavailableBackends.has(warnKey)) {\n warnedUnavailableBackends.add(warnKey);\n console.warn(unavailableWarningRef.current);\n }\n }\n if (!isCurrentWrite(write)) return;\n inFlightRef.current.delete(write.threadId);\n // Whatever was asked for meanwhile is dropped with the failure: the row\n // goes back to what it showed before the chain began.\n queuedRef.current.delete(write.threadId);\n update(write.scope, (current) => {\n const overridesNext = { ...current.overrides };\n if (write.previous === undefined) delete overridesNext[write.threadId];\n else overridesNext[write.threadId] = write.previous;\n return {\n ...current,\n overrides: overridesNext,\n pending: withPending(current.pending, write.threadId, false),\n };\n });\n },\n onSuccess: (truth, write) => {\n if (!isCurrentWrite(write)) return;\n inFlightRef.current.delete(write.threadId);\n const queued = queuedRef.current.get(write.threadId);\n const hadQueued = queuedRef.current.has(write.threadId);\n queuedRef.current.delete(write.threadId);\n onServerTruthRef.current?.(truth);\n if (hadQueued && queued !== undefined && !equalsRef.current(queued, truth.value)) {\n // The row already shows the value asked for meanwhile; the server\n // does not have it yet, so send it now — one write, the latest value.\n startWrite(write.scope, {\n threadId: write.threadId,\n value: queued,\n previous: truth.value,\n });\n return;\n }\n update(write.scope, (current) => ({\n ...current,\n overrides: { ...current.overrides, [truth.threadId]: truth.value },\n pending: withPending(current.pending, truth.threadId, false),\n }));\n },\n });\n const mutate = mutation.mutate;\n\n /** True when the answer belongs to the current identity and to this thread's latest write. */\n function isCurrentWrite(write: FieldWrite<T>): boolean {\n return (\n scopeRef.current === write.scope && inFlightRef.current.get(write.threadId) === write.seq\n );\n }\n\n function startWrite(forScope: string, write: Omit<FieldWrite<T>, 'scope' | 'seq'>): void {\n const seq = ++seqRef.current;\n inFlightRef.current.set(write.threadId, seq);\n update(forScope, (current) => ({\n ...current,\n pending: withPending(current.pending, write.threadId, true),\n }));\n mutate({ ...write, scope: forScope, seq });\n }\n\n const startWriteRef = useRef(startWrite);\n startWriteRef.current = startWrite;\n\n const set = useCallback(\n ({ threadId, value }: ThreadFieldTarget<T>) => {\n const forScope = scopeRef.current;\n const previous = overridesRef.current[threadId];\n // The row flips at once whether the write goes out now or after the\n // one in flight.\n update(forScope, (current) => ({\n ...current,\n overrides: { ...current.overrides, [threadId]: value },\n }));\n if (inFlightRef.current.has(threadId)) {\n queuedRef.current.set(threadId, value);\n return;\n }\n startWriteRef.current(forScope, { threadId, value, previous });\n },\n [update],\n );\n\n return {\n overrides,\n set,\n isPending: pendingThreadIds.size > 0,\n pendingThreadIds,\n isAvailable: unavailableBackend !== backendUrl,\n };\n}\n"],"names":["useState","useRef","useCallback","useEffect","useMutation","write"],"mappings":"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"}
1
+ {"version":3,"file":"use-thread-field-override.cjs","sources":["../../src/threads/use-thread-field-override.ts"],"sourcesContent":["/**\n * The optimistic per-thread field write behind the pin (`useThreadFavorite`)\n * and the rename (`useThreadRename`): one TanStack mutation over a REST\n * route with an override map layered over the list rows.\n *\n * Why overrides rather than a cache patch alone: a row's field is folded from\n * two sources — the fetched page and the pushed conversation index — and a\n * push that predates the write would clobber a patched page. The override\n * sits over both until the sources report the same value, then drops away so\n * they own the field again (a later write from another client is never\n * masked). A failure restores the previous value; a 404 / 405 marks the\n * write unavailable — the backend predates the route — so the UI withdraws\n * the control instead of failing on every use.\n *\n * Same-thread writes are serialized: while one is in flight, a further write\n * for that thread flips the row at once but only records the desired value,\n * and at most one follow-up write is sent — with that value — once the\n * in-flight one has answered. Two independent writes for one thread never\n * race, so the server cannot end on an earlier request's value.\n *\n * Everything the hook holds is scoped to `(backendUrl, principalId)`: a\n * credential swap under the same list reads an empty override map, and a\n * write that was in flight for the previous identity is ignored when it\n * answers, so a same-id row under the new identity never inherits the old\n * one's value.\n */\nimport { useMutation } from '@tanstack/react-query';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { type CredentialResolver, withCredentialRenewal } from '../runtime/credential-renewal';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\n\nexport interface ThreadFieldTarget<T> {\n threadId: string;\n value: T;\n}\n\n/** The one field every row must carry: its identity. */\nexport interface ThreadFieldRow {\n threadId: string;\n}\n\nexport interface UseThreadFieldOverrideOptions<T, R extends ThreadFieldRow> {\n /** Names the field: part of the mutation key and of the one-per-backend warning. */\n field: string;\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 R[];\n /** The field as a source row reports it; `undefined` when the row does not carry it. */\n readRow: (row: R) => T | undefined;\n /** The write; resolves to the server's truth after it. */\n write: (auth: RestConversationAuth, target: ThreadFieldTarget<T>) => Promise<ThreadFieldTarget<T>>;\n /** True for the rejection that means the backend has no such route (404 / 405). */\n isUnavailableError: (error: unknown) => boolean;\n /** Logged once per backend when the route is found missing. */\n unavailableWarning: string;\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (truth: ThreadFieldTarget<T>) => void;\n /** Value equality (default `===`). */\n equals?: (a: T, b: T) => boolean;\n}\n\nexport interface ThreadFieldOverride<T> {\n /** Optimistic, then server-confirmed, values layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, T>>;\n /**\n * Write the field. Flips the row at once; while that thread's write is in\n * flight the call only records the desired value (see module doc).\n */\n set: (target: ThreadFieldTarget<T>) => void;\n /** True while any write is in flight. */\n isPending: boolean;\n /** The threads whose write is in flight — a 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 withdraws the control.\n */\n isAvailable: boolean;\n}\n\n/** The hook's state, tagged with the identity it belongs to. */\ninterface ScopedFieldState<T> {\n scope: string;\n overrides: Record<string, T>;\n pending: ReadonlySet<string>;\n}\n\ninterface FieldWrite<T> extends ThreadFieldTarget<T> {\n /** The override before this chain of writes began; `undefined` restores the source value. */\n previous: T | undefined;\n /** The identity the write was issued for; an answer for another one is ignored. */\n scope: string;\n /** This thread's write generation; an answer for an older one is ignored. */\n seq: number;\n}\n\nconst EMPTY_PENDING: ReadonlySet<string> = new Set();\n\nfunction fieldScope(backendUrl: string, principalId: string): string {\n return `${backendUrl}::${principalId}`;\n}\n\nfunction withPending(pending: ReadonlySet<string>, threadId: string, isPending: boolean): ReadonlySet<string> {\n if (pending.has(threadId) === isPending) return pending;\n const next = new Set(pending);\n if (isPending) next.add(threadId);\n else next.delete(threadId);\n return next;\n}\n\nconst strictEquals = <T,>(a: T, b: T): boolean => a === b;\n\n/** `(field, backend)` pairs already warned about, so an older agora logs one line per page load, not one per use. */\nconst warnedUnavailableBackends = new Set<string>();\n\n/** Test seam: forget which backends were warned about. */\nexport function resetThreadFieldUnavailableWarnings(): void {\n warnedUnavailableBackends.clear();\n}\n\nexport function useThreadFieldOverride<T, R extends ThreadFieldRow>({\n field,\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow,\n write,\n isUnavailableError,\n unavailableWarning,\n onServerTruth,\n equals = strictEquals,\n}: UseThreadFieldOverrideOptions<T, R>): ThreadFieldOverride<T> {\n const scope = fieldScope(backendUrl, principalId);\n const [state, setState] = useState<ScopedFieldState<T>>(() => ({\n scope,\n overrides: {},\n pending: EMPTY_PENDING,\n }));\n // An identity swap discards everything held for the previous identity —\n // overrides AND pending rows — rather than merely hiding it: a round trip\n // back to that identity must not revive a row pinned for a write whose\n // answer has since been dropped (it would stay disabled for good). Setting\n // state during render is React's derived-state reset; the render re-runs\n // with the empty state before anything is committed.\n if (state.scope !== scope) {\n setState({ scope, overrides: {}, pending: EMPTY_PENDING });\n }\n const emptyOverridesRef = useRef<Readonly<Record<string, T>>>(Object.freeze({}) as Record<string, T>);\n const overrides = state.scope === scope ? state.overrides : emptyOverridesRef.current;\n const pendingThreadIds = state.scope === scope ? state.pending : EMPTY_PENDING;\n // The backend the route was found missing on. The route is a property of\n // the backend, not of who is asking, so this is keyed by backend alone; a\n // different backend is presumed to have it until it says otherwise.\n const [unavailableBackend, setUnavailableBackend] = useState<string | null>(null);\n\n const scopeRef = useRef(scope);\n scopeRef.current = scope;\n const overridesRef = useRef(overrides);\n overridesRef.current = overrides;\n // The per-render option closures ride refs so a consumer's fresh functions\n // never re-key the mutation or the caught-up effect.\n const readRowRef = useRef(readRow);\n readRowRef.current = readRow;\n const writeRef = useRef(write);\n writeRef.current = write;\n const isUnavailableErrorRef = useRef(isUnavailableError);\n isUnavailableErrorRef.current = isUnavailableError;\n const unavailableWarningRef = useRef(unavailableWarning);\n unavailableWarningRef.current = unavailableWarning;\n const onServerTruthRef = useRef(onServerTruth);\n onServerTruthRef.current = onServerTruth;\n const equalsRef = useRef(equals);\n equalsRef.current = equals;\n // Per-thread write bookkeeping for the CURRENT identity: the generation of\n // the write in flight, and the value a further write asked for meanwhile.\n const inFlightRef = useRef<Map<string, number>>(new Map());\n const queuedRef = useRef<Map<string, T>>(new Map());\n const seqRef = useRef(0);\n const bookkeepingScopeRef = useRef(scope);\n if (bookkeepingScopeRef.current !== scope) {\n // The previous identity's writes are abandoned with its state: their\n // answers are dropped by the scope guard below, and nothing queued\n // behind them is sent.\n bookkeepingScopeRef.current = scope;\n inFlightRef.current = new Map();\n queuedRef.current = new Map();\n }\n\n const update = useCallback(\n (forScope: string, patch: (current: ScopedFieldState<T>) => ScopedFieldState<T>) => {\n setState((current) => {\n const base: ScopedFieldState<T> =\n current.scope === forScope\n ? current\n : { scope: forScope, overrides: {}, pending: EMPTY_PENDING };\n const next = patch(base);\n return next === base && current.scope === forScope ? current : next;\n });\n },\n [],\n );\n\n useEffect(() => {\n const caughtUp = Object.keys(overrides).filter((threadId) => {\n // A row with a write in flight keeps its override whatever the sources\n // say: the first write of a chain patches the page with a value the\n // user has since moved away from, and the row must not flash it.\n if (pendingThreadIds.has(threadId)) return false;\n const row = rows.find((candidate) => candidate.threadId === threadId);\n if (row === undefined) return false;\n const reported = readRowRef.current(row);\n const override = overrides[threadId];\n return reported !== undefined && override !== undefined && equalsRef.current(reported, override);\n });\n if (caughtUp.length === 0) return;\n update(scope, (current) => {\n const next = { ...current.overrides };\n for (const threadId of caughtUp) delete next[threadId];\n return { ...current, overrides: next };\n });\n }, [overrides, pendingThreadIds, rows, scope, update]);\n\n const mutation = useMutation({\n mutationKey: ['conversation-history', field, backendUrl, principalId],\n mutationFn: async ({ threadId, value }: FieldWrite<T>) =>\n withCredentialRenewal(getAuth, (auth) => writeRef.current(auth, { threadId, value })),\n onError: (error, write) => {\n if (isUnavailableErrorRef.current(error)) {\n setUnavailableBackend(backendUrl);\n const warnKey = `${field}::${backendUrl}`;\n if (!warnedUnavailableBackends.has(warnKey)) {\n warnedUnavailableBackends.add(warnKey);\n console.warn(unavailableWarningRef.current);\n }\n }\n if (!isCurrentWrite(write)) return;\n inFlightRef.current.delete(write.threadId);\n // Whatever was asked for meanwhile is dropped with the failure: the row\n // goes back to what it showed before the chain began.\n queuedRef.current.delete(write.threadId);\n update(write.scope, (current) => {\n const overridesNext = { ...current.overrides };\n if (write.previous === undefined) delete overridesNext[write.threadId];\n else overridesNext[write.threadId] = write.previous;\n return {\n ...current,\n overrides: overridesNext,\n pending: withPending(current.pending, write.threadId, false),\n };\n });\n },\n onSuccess: (truth, write) => {\n if (!isCurrentWrite(write)) return;\n inFlightRef.current.delete(write.threadId);\n const queued = queuedRef.current.get(write.threadId);\n const hadQueued = queuedRef.current.has(write.threadId);\n queuedRef.current.delete(write.threadId);\n onServerTruthRef.current?.(truth);\n if (hadQueued && queued !== undefined && !equalsRef.current(queued, truth.value)) {\n // The row already shows the value asked for meanwhile; the server\n // does not have it yet, so send it now — one write, the latest value.\n startWrite(write.scope, {\n threadId: write.threadId,\n value: queued,\n previous: truth.value,\n });\n return;\n }\n update(write.scope, (current) => ({\n ...current,\n overrides: { ...current.overrides, [truth.threadId]: truth.value },\n pending: withPending(current.pending, truth.threadId, false),\n }));\n },\n });\n const mutate = mutation.mutate;\n\n /** True when the answer belongs to the current identity and to this thread's latest write. */\n function isCurrentWrite(write: FieldWrite<T>): boolean {\n return (\n scopeRef.current === write.scope && inFlightRef.current.get(write.threadId) === write.seq\n );\n }\n\n function startWrite(forScope: string, write: Omit<FieldWrite<T>, 'scope' | 'seq'>): void {\n const seq = ++seqRef.current;\n inFlightRef.current.set(write.threadId, seq);\n update(forScope, (current) => ({\n ...current,\n pending: withPending(current.pending, write.threadId, true),\n }));\n mutate({ ...write, scope: forScope, seq });\n }\n\n const startWriteRef = useRef(startWrite);\n startWriteRef.current = startWrite;\n\n const set = useCallback(\n ({ threadId, value }: ThreadFieldTarget<T>) => {\n const forScope = scopeRef.current;\n const previous = overridesRef.current[threadId];\n // The row flips at once whether the write goes out now or after the\n // one in flight.\n update(forScope, (current) => ({\n ...current,\n overrides: { ...current.overrides, [threadId]: value },\n }));\n if (inFlightRef.current.has(threadId)) {\n queuedRef.current.set(threadId, value);\n return;\n }\n startWriteRef.current(forScope, { threadId, value, previous });\n },\n [update],\n );\n\n return {\n overrides,\n set,\n isPending: pendingThreadIds.size > 0,\n pendingThreadIds,\n isAvailable: unavailableBackend !== backendUrl,\n };\n}\n"],"names":["useState","useRef","useCallback","useEffect","useMutation","withCredentialRenewal","write"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { type CredentialResolver } from '../runtime/credential-renewal';
1
2
  import type { RestConversationAuth } from './use-rest-conversation-history';
2
3
  export interface ThreadFieldTarget<T> {
3
4
  threadId: string;
@@ -13,7 +14,8 @@ export interface UseThreadFieldOverrideOptions<T, R extends ThreadFieldRow> {
13
14
  backendUrl: string;
14
15
  /** Non-secret identity in the mutation key (as the read-state mutation keys). */
15
16
  principalId: string;
16
- getAuth: () => RestConversationAuth | Promise<RestConversationAuth>;
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,5 +1,6 @@
1
1
  import { useMutation } from "@tanstack/react-query";
2
2
  import { useState, useRef, useCallback, useEffect } from "react";
3
+ import { withCredentialRenewal } from "../runtime/credential-renewal.js";
3
4
  const EMPTY_PENDING = /* @__PURE__ */ new Set();
4
5
  function fieldScope(backendUrl, principalId) {
5
6
  return `${backendUrl}::${principalId}`;
@@ -92,7 +93,7 @@ function useThreadFieldOverride({
92
93
  }, [overrides, pendingThreadIds, rows, scope, update]);
93
94
  const mutation = useMutation({
94
95
  mutationKey: ["conversation-history", field, backendUrl, principalId],
95
- mutationFn: async ({ threadId, value }) => writeRef.current(await getAuth(), { threadId, value }),
96
+ mutationFn: async ({ threadId, value }) => withCredentialRenewal(getAuth, (auth) => writeRef.current(auth, { threadId, value })),
96
97
  onError: (error, write2) => {
97
98
  if (isUnavailableErrorRef.current(error)) {
98
99
  setUnavailableBackend(backendUrl);
@@ -1 +1 @@
1
- {"version":3,"file":"use-thread-field-override.js","sources":["../../src/threads/use-thread-field-override.ts"],"sourcesContent":["/**\n * The optimistic per-thread field write behind the pin (`useThreadFavorite`)\n * and the rename (`useThreadRename`): one TanStack mutation over a REST\n * route with an override map layered over the list rows.\n *\n * Why overrides rather than a cache patch alone: a row's field is folded from\n * two sources — the fetched page and the pushed conversation index — and a\n * push that predates the write would clobber a patched page. The override\n * sits over both until the sources report the same value, then drops away so\n * they own the field again (a later write from another client is never\n * masked). A failure restores the previous value; a 404 / 405 marks the\n * write unavailable — the backend predates the route — so the UI withdraws\n * the control instead of failing on every use.\n *\n * Same-thread writes are serialized: while one is in flight, a further write\n * for that thread flips the row at once but only records the desired value,\n * and at most one follow-up write is sent — with that value — once the\n * in-flight one has answered. Two independent writes for one thread never\n * race, so the server cannot end on an earlier request's value.\n *\n * Everything the hook holds is scoped to `(backendUrl, principalId)`: a\n * credential swap under the same list reads an empty override map, and a\n * write that was in flight for the previous identity is ignored when it\n * answers, so a same-id row under the new identity never inherits the old\n * one's value.\n */\nimport { useMutation } from '@tanstack/react-query';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\n\nexport interface ThreadFieldTarget<T> {\n threadId: string;\n value: T;\n}\n\n/** The one field every row must carry: its identity. */\nexport interface ThreadFieldRow {\n threadId: string;\n}\n\nexport interface UseThreadFieldOverrideOptions<T, R extends ThreadFieldRow> {\n /** Names the field: part of the mutation key and of the one-per-backend warning. */\n field: string;\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the read-state mutation keys). */\n principalId: string;\n getAuth: () => RestConversationAuth | Promise<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 R[];\n /** The field as a source row reports it; `undefined` when the row does not carry it. */\n readRow: (row: R) => T | undefined;\n /** The write; resolves to the server's truth after it. */\n write: (auth: RestConversationAuth, target: ThreadFieldTarget<T>) => Promise<ThreadFieldTarget<T>>;\n /** True for the rejection that means the backend has no such route (404 / 405). */\n isUnavailableError: (error: unknown) => boolean;\n /** Logged once per backend when the route is found missing. */\n unavailableWarning: string;\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (truth: ThreadFieldTarget<T>) => void;\n /** Value equality (default `===`). */\n equals?: (a: T, b: T) => boolean;\n}\n\nexport interface ThreadFieldOverride<T> {\n /** Optimistic, then server-confirmed, values layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, T>>;\n /**\n * Write the field. Flips the row at once; while that thread's write is in\n * flight the call only records the desired value (see module doc).\n */\n set: (target: ThreadFieldTarget<T>) => void;\n /** True while any write is in flight. */\n isPending: boolean;\n /** The threads whose write is in flight — a 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 withdraws the control.\n */\n isAvailable: boolean;\n}\n\n/** The hook's state, tagged with the identity it belongs to. */\ninterface ScopedFieldState<T> {\n scope: string;\n overrides: Record<string, T>;\n pending: ReadonlySet<string>;\n}\n\ninterface FieldWrite<T> extends ThreadFieldTarget<T> {\n /** The override before this chain of writes began; `undefined` restores the source value. */\n previous: T | undefined;\n /** The identity the write was issued for; an answer for another one is ignored. */\n scope: string;\n /** This thread's write generation; an answer for an older one is ignored. */\n seq: number;\n}\n\nconst EMPTY_PENDING: ReadonlySet<string> = new Set();\n\nfunction fieldScope(backendUrl: string, principalId: string): string {\n return `${backendUrl}::${principalId}`;\n}\n\nfunction withPending(pending: ReadonlySet<string>, threadId: string, isPending: boolean): ReadonlySet<string> {\n if (pending.has(threadId) === isPending) return pending;\n const next = new Set(pending);\n if (isPending) next.add(threadId);\n else next.delete(threadId);\n return next;\n}\n\nconst strictEquals = <T,>(a: T, b: T): boolean => a === b;\n\n/** `(field, backend)` pairs already warned about, so an older agora logs one line per page load, not one per use. */\nconst warnedUnavailableBackends = new Set<string>();\n\n/** Test seam: forget which backends were warned about. */\nexport function resetThreadFieldUnavailableWarnings(): void {\n warnedUnavailableBackends.clear();\n}\n\nexport function useThreadFieldOverride<T, R extends ThreadFieldRow>({\n field,\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow,\n write,\n isUnavailableError,\n unavailableWarning,\n onServerTruth,\n equals = strictEquals,\n}: UseThreadFieldOverrideOptions<T, R>): ThreadFieldOverride<T> {\n const scope = fieldScope(backendUrl, principalId);\n const [state, setState] = useState<ScopedFieldState<T>>(() => ({\n scope,\n overrides: {},\n pending: EMPTY_PENDING,\n }));\n // An identity swap discards everything held for the previous identity —\n // overrides AND pending rows — rather than merely hiding it: a round trip\n // back to that identity must not revive a row pinned for a write whose\n // answer has since been dropped (it would stay disabled for good). Setting\n // state during render is React's derived-state reset; the render re-runs\n // with the empty state before anything is committed.\n if (state.scope !== scope) {\n setState({ scope, overrides: {}, pending: EMPTY_PENDING });\n }\n const emptyOverridesRef = useRef<Readonly<Record<string, T>>>(Object.freeze({}) as Record<string, T>);\n const overrides = state.scope === scope ? state.overrides : emptyOverridesRef.current;\n const pendingThreadIds = state.scope === scope ? state.pending : EMPTY_PENDING;\n // The backend the route was found missing on. The route is a property of\n // the backend, not of who is asking, so this is keyed by backend alone; a\n // different backend is presumed to have it until it says otherwise.\n const [unavailableBackend, setUnavailableBackend] = useState<string | null>(null);\n\n const scopeRef = useRef(scope);\n scopeRef.current = scope;\n const overridesRef = useRef(overrides);\n overridesRef.current = overrides;\n // The per-render option closures ride refs so a consumer's fresh functions\n // never re-key the mutation or the caught-up effect.\n const readRowRef = useRef(readRow);\n readRowRef.current = readRow;\n const writeRef = useRef(write);\n writeRef.current = write;\n const isUnavailableErrorRef = useRef(isUnavailableError);\n isUnavailableErrorRef.current = isUnavailableError;\n const unavailableWarningRef = useRef(unavailableWarning);\n unavailableWarningRef.current = unavailableWarning;\n const onServerTruthRef = useRef(onServerTruth);\n onServerTruthRef.current = onServerTruth;\n const equalsRef = useRef(equals);\n equalsRef.current = equals;\n // Per-thread write bookkeeping for the CURRENT identity: the generation of\n // the write in flight, and the value a further write asked for meanwhile.\n const inFlightRef = useRef<Map<string, number>>(new Map());\n const queuedRef = useRef<Map<string, T>>(new Map());\n const seqRef = useRef(0);\n const bookkeepingScopeRef = useRef(scope);\n if (bookkeepingScopeRef.current !== scope) {\n // The previous identity's writes are abandoned with its state: their\n // answers are dropped by the scope guard below, and nothing queued\n // behind them is sent.\n bookkeepingScopeRef.current = scope;\n inFlightRef.current = new Map();\n queuedRef.current = new Map();\n }\n\n const update = useCallback(\n (forScope: string, patch: (current: ScopedFieldState<T>) => ScopedFieldState<T>) => {\n setState((current) => {\n const base: ScopedFieldState<T> =\n current.scope === forScope\n ? current\n : { scope: forScope, overrides: {}, pending: EMPTY_PENDING };\n const next = patch(base);\n return next === base && current.scope === forScope ? current : next;\n });\n },\n [],\n );\n\n useEffect(() => {\n const caughtUp = Object.keys(overrides).filter((threadId) => {\n // A row with a write in flight keeps its override whatever the sources\n // say: the first write of a chain patches the page with a value the\n // user has since moved away from, and the row must not flash it.\n if (pendingThreadIds.has(threadId)) return false;\n const row = rows.find((candidate) => candidate.threadId === threadId);\n if (row === undefined) return false;\n const reported = readRowRef.current(row);\n const override = overrides[threadId];\n return reported !== undefined && override !== undefined && equalsRef.current(reported, override);\n });\n if (caughtUp.length === 0) return;\n update(scope, (current) => {\n const next = { ...current.overrides };\n for (const threadId of caughtUp) delete next[threadId];\n return { ...current, overrides: next };\n });\n }, [overrides, pendingThreadIds, rows, scope, update]);\n\n const mutation = useMutation({\n mutationKey: ['conversation-history', field, backendUrl, principalId],\n mutationFn: async ({ threadId, value }: FieldWrite<T>) =>\n writeRef.current(await getAuth(), { threadId, value }),\n onError: (error, write) => {\n if (isUnavailableErrorRef.current(error)) {\n setUnavailableBackend(backendUrl);\n const warnKey = `${field}::${backendUrl}`;\n if (!warnedUnavailableBackends.has(warnKey)) {\n warnedUnavailableBackends.add(warnKey);\n console.warn(unavailableWarningRef.current);\n }\n }\n if (!isCurrentWrite(write)) return;\n inFlightRef.current.delete(write.threadId);\n // Whatever was asked for meanwhile is dropped with the failure: the row\n // goes back to what it showed before the chain began.\n queuedRef.current.delete(write.threadId);\n update(write.scope, (current) => {\n const overridesNext = { ...current.overrides };\n if (write.previous === undefined) delete overridesNext[write.threadId];\n else overridesNext[write.threadId] = write.previous;\n return {\n ...current,\n overrides: overridesNext,\n pending: withPending(current.pending, write.threadId, false),\n };\n });\n },\n onSuccess: (truth, write) => {\n if (!isCurrentWrite(write)) return;\n inFlightRef.current.delete(write.threadId);\n const queued = queuedRef.current.get(write.threadId);\n const hadQueued = queuedRef.current.has(write.threadId);\n queuedRef.current.delete(write.threadId);\n onServerTruthRef.current?.(truth);\n if (hadQueued && queued !== undefined && !equalsRef.current(queued, truth.value)) {\n // The row already shows the value asked for meanwhile; the server\n // does not have it yet, so send it now — one write, the latest value.\n startWrite(write.scope, {\n threadId: write.threadId,\n value: queued,\n previous: truth.value,\n });\n return;\n }\n update(write.scope, (current) => ({\n ...current,\n overrides: { ...current.overrides, [truth.threadId]: truth.value },\n pending: withPending(current.pending, truth.threadId, false),\n }));\n },\n });\n const mutate = mutation.mutate;\n\n /** True when the answer belongs to the current identity and to this thread's latest write. */\n function isCurrentWrite(write: FieldWrite<T>): boolean {\n return (\n scopeRef.current === write.scope && inFlightRef.current.get(write.threadId) === write.seq\n );\n }\n\n function startWrite(forScope: string, write: Omit<FieldWrite<T>, 'scope' | 'seq'>): void {\n const seq = ++seqRef.current;\n inFlightRef.current.set(write.threadId, seq);\n update(forScope, (current) => ({\n ...current,\n pending: withPending(current.pending, write.threadId, true),\n }));\n mutate({ ...write, scope: forScope, seq });\n }\n\n const startWriteRef = useRef(startWrite);\n startWriteRef.current = startWrite;\n\n const set = useCallback(\n ({ threadId, value }: ThreadFieldTarget<T>) => {\n const forScope = scopeRef.current;\n const previous = overridesRef.current[threadId];\n // The row flips at once whether the write goes out now or after the\n // one in flight.\n update(forScope, (current) => ({\n ...current,\n overrides: { ...current.overrides, [threadId]: value },\n }));\n if (inFlightRef.current.has(threadId)) {\n queuedRef.current.set(threadId, value);\n return;\n }\n startWriteRef.current(forScope, { threadId, value, previous });\n },\n [update],\n );\n\n return {\n overrides,\n set,\n isPending: pendingThreadIds.size > 0,\n pendingThreadIds,\n isAvailable: unavailableBackend !== backendUrl,\n };\n}\n"],"names":["write"],"mappings":";;AAqGA,MAAM,oCAAyC,IAAA;AAE/C,SAAS,WAAW,YAAoB,aAA6B;AACnE,SAAO,GAAG,UAAU,KAAK,WAAW;AACtC;AAEA,SAAS,YAAY,SAA8B,UAAkB,WAAyC;AAC5G,MAAI,QAAQ,IAAI,QAAQ,MAAM,UAAW,QAAO;AAChD,QAAM,OAAO,IAAI,IAAI,OAAO;AAC5B,MAAI,UAAW,MAAK,IAAI,QAAQ;AAAA,MAC3B,MAAK,OAAO,QAAQ;AACzB,SAAO;AACT;AAEA,MAAM,eAAe,CAAK,GAAM,MAAkB,MAAM;AAGxD,MAAM,gDAAgC,IAAA;AAO/B,SAAS,uBAAoD;AAAA,EAClE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AACX,GAAgE;AAC9D,QAAM,QAAQ,WAAW,YAAY,WAAW;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA8B,OAAO;AAAA,IAC7D;AAAA,IACA,WAAW,CAAA;AAAA,IACX,SAAS;AAAA,EAAA,EACT;AAOF,MAAI,MAAM,UAAU,OAAO;AACzB,aAAS,EAAE,OAAO,WAAW,CAAA,GAAI,SAAS,eAAe;AAAA,EAC3D;AACA,QAAM,oBAAoB,OAAoC,OAAO,OAAO,CAAA,CAAE,CAAsB;AACpG,QAAM,YAAY,MAAM,UAAU,QAAQ,MAAM,YAAY,kBAAkB;AAC9E,QAAM,mBAAmB,MAAM,UAAU,QAAQ,MAAM,UAAU;AAIjE,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAwB,IAAI;AAEhF,QAAM,WAAW,OAAO,KAAK;AAC7B,WAAS,UAAU;AACnB,QAAM,eAAe,OAAO,SAAS;AACrC,eAAa,UAAU;AAGvB,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AACrB,QAAM,WAAW,OAAO,KAAK;AAC7B,WAAS,UAAU;AACnB,QAAM,wBAAwB,OAAO,kBAAkB;AACvD,wBAAsB,UAAU;AAChC,QAAM,wBAAwB,OAAO,kBAAkB;AACvD,wBAAsB,UAAU;AAChC,QAAM,mBAAmB,OAAO,aAAa;AAC7C,mBAAiB,UAAU;AAC3B,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AAGpB,QAAM,cAAc,OAA4B,oBAAI,KAAK;AACzD,QAAM,YAAY,OAAuB,oBAAI,KAAK;AAClD,QAAM,SAAS,OAAO,CAAC;AACvB,QAAM,sBAAsB,OAAO,KAAK;AACxC,MAAI,oBAAoB,YAAY,OAAO;AAIzC,wBAAoB,UAAU;AAC9B,gBAAY,8BAAc,IAAA;AAC1B,cAAU,8BAAc,IAAA;AAAA,EAC1B;AAEA,QAAM,SAAS;AAAA,IACb,CAAC,UAAkB,UAAiE;AAClF,eAAS,CAAC,YAAY;AACpB,cAAM,OACJ,QAAQ,UAAU,WACd,UACA,EAAE,OAAO,UAAU,WAAW,IAAI,SAAS,cAAA;AACjD,cAAM,OAAO,MAAM,IAAI;AACvB,eAAO,SAAS,QAAQ,QAAQ,UAAU,WAAW,UAAU;AAAA,MACjE,CAAC;AAAA,IACH;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,YAAU,MAAM;AACd,UAAM,WAAW,OAAO,KAAK,SAAS,EAAE,OAAO,CAAC,aAAa;AAI3D,UAAI,iBAAiB,IAAI,QAAQ,EAAG,QAAO;AAC3C,YAAM,MAAM,KAAK,KAAK,CAAC,cAAc,UAAU,aAAa,QAAQ;AACpE,UAAI,QAAQ,OAAW,QAAO;AAC9B,YAAM,WAAW,WAAW,QAAQ,GAAG;AACvC,YAAM,WAAW,UAAU,QAAQ;AACnC,aAAO,aAAa,UAAa,aAAa,UAAa,UAAU,QAAQ,UAAU,QAAQ;AAAA,IACjG,CAAC;AACD,QAAI,SAAS,WAAW,EAAG;AAC3B,WAAO,OAAO,CAAC,YAAY;AACzB,YAAM,OAAO,EAAE,GAAG,QAAQ,UAAA;AAC1B,iBAAW,YAAY,SAAU,QAAO,KAAK,QAAQ;AACrD,aAAO,EAAE,GAAG,SAAS,WAAW,KAAA;AAAA,IAClC,CAAC;AAAA,EACH,GAAG,CAAC,WAAW,kBAAkB,MAAM,OAAO,MAAM,CAAC;AAErD,QAAM,WAAW,YAAY;AAAA,IAC3B,aAAa,CAAC,wBAAwB,OAAO,YAAY,WAAW;AAAA,IACpE,YAAY,OAAO,EAAE,UAAU,YAC7B,SAAS,QAAQ,MAAM,QAAA,GAAW,EAAE,UAAU,OAAO;AAAA,IACvD,SAAS,CAAC,OAAOA,WAAU;AACzB,UAAI,sBAAsB,QAAQ,KAAK,GAAG;AACxC,8BAAsB,UAAU;AAChC,cAAM,UAAU,GAAG,KAAK,KAAK,UAAU;AACvC,YAAI,CAAC,0BAA0B,IAAI,OAAO,GAAG;AAC3C,oCAA0B,IAAI,OAAO;AACrC,kBAAQ,KAAK,sBAAsB,OAAO;AAAA,QAC5C;AAAA,MACF;AACA,UAAI,CAAC,eAAeA,MAAK,EAAG;AAC5B,kBAAY,QAAQ,OAAOA,OAAM,QAAQ;AAGzC,gBAAU,QAAQ,OAAOA,OAAM,QAAQ;AACvC,aAAOA,OAAM,OAAO,CAAC,YAAY;AAC/B,cAAM,gBAAgB,EAAE,GAAG,QAAQ,UAAA;AACnC,YAAIA,OAAM,aAAa,OAAW,QAAO,cAAcA,OAAM,QAAQ;AAAA,YAChE,eAAcA,OAAM,QAAQ,IAAIA,OAAM;AAC3C,eAAO;AAAA,UACL,GAAG;AAAA,UACH,WAAW;AAAA,UACX,SAAS,YAAY,QAAQ,SAASA,OAAM,UAAU,KAAK;AAAA,QAAA;AAAA,MAE/D,CAAC;AAAA,IACH;AAAA,IACA,WAAW,CAAC,OAAOA,WAAU;;AAC3B,UAAI,CAAC,eAAeA,MAAK,EAAG;AAC5B,kBAAY,QAAQ,OAAOA,OAAM,QAAQ;AACzC,YAAM,SAAS,UAAU,QAAQ,IAAIA,OAAM,QAAQ;AACnD,YAAM,YAAY,UAAU,QAAQ,IAAIA,OAAM,QAAQ;AACtD,gBAAU,QAAQ,OAAOA,OAAM,QAAQ;AACvC,6BAAiB,YAAjB,0CAA2B;AAC3B,UAAI,aAAa,WAAW,UAAa,CAAC,UAAU,QAAQ,QAAQ,MAAM,KAAK,GAAG;AAGhF,mBAAWA,OAAM,OAAO;AAAA,UACtB,UAAUA,OAAM;AAAA,UAChB,OAAO;AAAA,UACP,UAAU,MAAM;AAAA,QAAA,CACjB;AACD;AAAA,MACF;AACA,aAAOA,OAAM,OAAO,CAAC,aAAa;AAAA,QAChC,GAAG;AAAA,QACH,WAAW,EAAE,GAAG,QAAQ,WAAW,CAAC,MAAM,QAAQ,GAAG,MAAM,MAAA;AAAA,QAC3D,SAAS,YAAY,QAAQ,SAAS,MAAM,UAAU,KAAK;AAAA,MAAA,EAC3D;AAAA,IACJ;AAAA,EAAA,CACD;AACD,QAAM,SAAS,SAAS;AAGxB,WAAS,eAAeA,QAA+B;AACrD,WACE,SAAS,YAAYA,OAAM,SAAS,YAAY,QAAQ,IAAIA,OAAM,QAAQ,MAAMA,OAAM;AAAA,EAE1F;AAEA,WAAS,WAAW,UAAkBA,QAAmD;AACvF,UAAM,MAAM,EAAE,OAAO;AACrB,gBAAY,QAAQ,IAAIA,OAAM,UAAU,GAAG;AAC3C,WAAO,UAAU,CAAC,aAAa;AAAA,MAC7B,GAAG;AAAA,MACH,SAAS,YAAY,QAAQ,SAASA,OAAM,UAAU,IAAI;AAAA,IAAA,EAC1D;AACF,WAAO,EAAE,GAAGA,QAAO,OAAO,UAAU,KAAK;AAAA,EAC3C;AAEA,QAAM,gBAAgB,OAAO,UAAU;AACvC,gBAAc,UAAU;AAExB,QAAM,MAAM;AAAA,IACV,CAAC,EAAE,UAAU,YAAkC;AAC7C,YAAM,WAAW,SAAS;AAC1B,YAAM,WAAW,aAAa,QAAQ,QAAQ;AAG9C,aAAO,UAAU,CAAC,aAAa;AAAA,QAC7B,GAAG;AAAA,QACH,WAAW,EAAE,GAAG,QAAQ,WAAW,CAAC,QAAQ,GAAG,MAAA;AAAA,MAAM,EACrD;AACF,UAAI,YAAY,QAAQ,IAAI,QAAQ,GAAG;AACrC,kBAAU,QAAQ,IAAI,UAAU,KAAK;AACrC;AAAA,MACF;AACA,oBAAc,QAAQ,UAAU,EAAE,UAAU,OAAO,UAAU;AAAA,IAC/D;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,iBAAiB,OAAO;AAAA,IACnC;AAAA,IACA,aAAa,uBAAuB;AAAA,EAAA;AAExC;"}
1
+ {"version":3,"file":"use-thread-field-override.js","sources":["../../src/threads/use-thread-field-override.ts"],"sourcesContent":["/**\n * The optimistic per-thread field write behind the pin (`useThreadFavorite`)\n * and the rename (`useThreadRename`): one TanStack mutation over a REST\n * route with an override map layered over the list rows.\n *\n * Why overrides rather than a cache patch alone: a row's field is folded from\n * two sources — the fetched page and the pushed conversation index — and a\n * push that predates the write would clobber a patched page. The override\n * sits over both until the sources report the same value, then drops away so\n * they own the field again (a later write from another client is never\n * masked). A failure restores the previous value; a 404 / 405 marks the\n * write unavailable — the backend predates the route — so the UI withdraws\n * the control instead of failing on every use.\n *\n * Same-thread writes are serialized: while one is in flight, a further write\n * for that thread flips the row at once but only records the desired value,\n * and at most one follow-up write is sent — with that value — once the\n * in-flight one has answered. Two independent writes for one thread never\n * race, so the server cannot end on an earlier request's value.\n *\n * Everything the hook holds is scoped to `(backendUrl, principalId)`: a\n * credential swap under the same list reads an empty override map, and a\n * write that was in flight for the previous identity is ignored when it\n * answers, so a same-id row under the new identity never inherits the old\n * one's value.\n */\nimport { useMutation } from '@tanstack/react-query';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { type CredentialResolver, withCredentialRenewal } from '../runtime/credential-renewal';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\n\nexport interface ThreadFieldTarget<T> {\n threadId: string;\n value: T;\n}\n\n/** The one field every row must carry: its identity. */\nexport interface ThreadFieldRow {\n threadId: string;\n}\n\nexport interface UseThreadFieldOverrideOptions<T, R extends ThreadFieldRow> {\n /** Names the field: part of the mutation key and of the one-per-backend warning. */\n field: string;\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 R[];\n /** The field as a source row reports it; `undefined` when the row does not carry it. */\n readRow: (row: R) => T | undefined;\n /** The write; resolves to the server's truth after it. */\n write: (auth: RestConversationAuth, target: ThreadFieldTarget<T>) => Promise<ThreadFieldTarget<T>>;\n /** True for the rejection that means the backend has no such route (404 / 405). */\n isUnavailableError: (error: unknown) => boolean;\n /** Logged once per backend when the route is found missing. */\n unavailableWarning: string;\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (truth: ThreadFieldTarget<T>) => void;\n /** Value equality (default `===`). */\n equals?: (a: T, b: T) => boolean;\n}\n\nexport interface ThreadFieldOverride<T> {\n /** Optimistic, then server-confirmed, values layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, T>>;\n /**\n * Write the field. Flips the row at once; while that thread's write is in\n * flight the call only records the desired value (see module doc).\n */\n set: (target: ThreadFieldTarget<T>) => void;\n /** True while any write is in flight. */\n isPending: boolean;\n /** The threads whose write is in flight — a 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 withdraws the control.\n */\n isAvailable: boolean;\n}\n\n/** The hook's state, tagged with the identity it belongs to. */\ninterface ScopedFieldState<T> {\n scope: string;\n overrides: Record<string, T>;\n pending: ReadonlySet<string>;\n}\n\ninterface FieldWrite<T> extends ThreadFieldTarget<T> {\n /** The override before this chain of writes began; `undefined` restores the source value. */\n previous: T | undefined;\n /** The identity the write was issued for; an answer for another one is ignored. */\n scope: string;\n /** This thread's write generation; an answer for an older one is ignored. */\n seq: number;\n}\n\nconst EMPTY_PENDING: ReadonlySet<string> = new Set();\n\nfunction fieldScope(backendUrl: string, principalId: string): string {\n return `${backendUrl}::${principalId}`;\n}\n\nfunction withPending(pending: ReadonlySet<string>, threadId: string, isPending: boolean): ReadonlySet<string> {\n if (pending.has(threadId) === isPending) return pending;\n const next = new Set(pending);\n if (isPending) next.add(threadId);\n else next.delete(threadId);\n return next;\n}\n\nconst strictEquals = <T,>(a: T, b: T): boolean => a === b;\n\n/** `(field, backend)` pairs already warned about, so an older agora logs one line per page load, not one per use. */\nconst warnedUnavailableBackends = new Set<string>();\n\n/** Test seam: forget which backends were warned about. */\nexport function resetThreadFieldUnavailableWarnings(): void {\n warnedUnavailableBackends.clear();\n}\n\nexport function useThreadFieldOverride<T, R extends ThreadFieldRow>({\n field,\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow,\n write,\n isUnavailableError,\n unavailableWarning,\n onServerTruth,\n equals = strictEquals,\n}: UseThreadFieldOverrideOptions<T, R>): ThreadFieldOverride<T> {\n const scope = fieldScope(backendUrl, principalId);\n const [state, setState] = useState<ScopedFieldState<T>>(() => ({\n scope,\n overrides: {},\n pending: EMPTY_PENDING,\n }));\n // An identity swap discards everything held for the previous identity —\n // overrides AND pending rows — rather than merely hiding it: a round trip\n // back to that identity must not revive a row pinned for a write whose\n // answer has since been dropped (it would stay disabled for good). Setting\n // state during render is React's derived-state reset; the render re-runs\n // with the empty state before anything is committed.\n if (state.scope !== scope) {\n setState({ scope, overrides: {}, pending: EMPTY_PENDING });\n }\n const emptyOverridesRef = useRef<Readonly<Record<string, T>>>(Object.freeze({}) as Record<string, T>);\n const overrides = state.scope === scope ? state.overrides : emptyOverridesRef.current;\n const pendingThreadIds = state.scope === scope ? state.pending : EMPTY_PENDING;\n // The backend the route was found missing on. The route is a property of\n // the backend, not of who is asking, so this is keyed by backend alone; a\n // different backend is presumed to have it until it says otherwise.\n const [unavailableBackend, setUnavailableBackend] = useState<string | null>(null);\n\n const scopeRef = useRef(scope);\n scopeRef.current = scope;\n const overridesRef = useRef(overrides);\n overridesRef.current = overrides;\n // The per-render option closures ride refs so a consumer's fresh functions\n // never re-key the mutation or the caught-up effect.\n const readRowRef = useRef(readRow);\n readRowRef.current = readRow;\n const writeRef = useRef(write);\n writeRef.current = write;\n const isUnavailableErrorRef = useRef(isUnavailableError);\n isUnavailableErrorRef.current = isUnavailableError;\n const unavailableWarningRef = useRef(unavailableWarning);\n unavailableWarningRef.current = unavailableWarning;\n const onServerTruthRef = useRef(onServerTruth);\n onServerTruthRef.current = onServerTruth;\n const equalsRef = useRef(equals);\n equalsRef.current = equals;\n // Per-thread write bookkeeping for the CURRENT identity: the generation of\n // the write in flight, and the value a further write asked for meanwhile.\n const inFlightRef = useRef<Map<string, number>>(new Map());\n const queuedRef = useRef<Map<string, T>>(new Map());\n const seqRef = useRef(0);\n const bookkeepingScopeRef = useRef(scope);\n if (bookkeepingScopeRef.current !== scope) {\n // The previous identity's writes are abandoned with its state: their\n // answers are dropped by the scope guard below, and nothing queued\n // behind them is sent.\n bookkeepingScopeRef.current = scope;\n inFlightRef.current = new Map();\n queuedRef.current = new Map();\n }\n\n const update = useCallback(\n (forScope: string, patch: (current: ScopedFieldState<T>) => ScopedFieldState<T>) => {\n setState((current) => {\n const base: ScopedFieldState<T> =\n current.scope === forScope\n ? current\n : { scope: forScope, overrides: {}, pending: EMPTY_PENDING };\n const next = patch(base);\n return next === base && current.scope === forScope ? current : next;\n });\n },\n [],\n );\n\n useEffect(() => {\n const caughtUp = Object.keys(overrides).filter((threadId) => {\n // A row with a write in flight keeps its override whatever the sources\n // say: the first write of a chain patches the page with a value the\n // user has since moved away from, and the row must not flash it.\n if (pendingThreadIds.has(threadId)) return false;\n const row = rows.find((candidate) => candidate.threadId === threadId);\n if (row === undefined) return false;\n const reported = readRowRef.current(row);\n const override = overrides[threadId];\n return reported !== undefined && override !== undefined && equalsRef.current(reported, override);\n });\n if (caughtUp.length === 0) return;\n update(scope, (current) => {\n const next = { ...current.overrides };\n for (const threadId of caughtUp) delete next[threadId];\n return { ...current, overrides: next };\n });\n }, [overrides, pendingThreadIds, rows, scope, update]);\n\n const mutation = useMutation({\n mutationKey: ['conversation-history', field, backendUrl, principalId],\n mutationFn: async ({ threadId, value }: FieldWrite<T>) =>\n withCredentialRenewal(getAuth, (auth) => writeRef.current(auth, { threadId, value })),\n onError: (error, write) => {\n if (isUnavailableErrorRef.current(error)) {\n setUnavailableBackend(backendUrl);\n const warnKey = `${field}::${backendUrl}`;\n if (!warnedUnavailableBackends.has(warnKey)) {\n warnedUnavailableBackends.add(warnKey);\n console.warn(unavailableWarningRef.current);\n }\n }\n if (!isCurrentWrite(write)) return;\n inFlightRef.current.delete(write.threadId);\n // Whatever was asked for meanwhile is dropped with the failure: the row\n // goes back to what it showed before the chain began.\n queuedRef.current.delete(write.threadId);\n update(write.scope, (current) => {\n const overridesNext = { ...current.overrides };\n if (write.previous === undefined) delete overridesNext[write.threadId];\n else overridesNext[write.threadId] = write.previous;\n return {\n ...current,\n overrides: overridesNext,\n pending: withPending(current.pending, write.threadId, false),\n };\n });\n },\n onSuccess: (truth, write) => {\n if (!isCurrentWrite(write)) return;\n inFlightRef.current.delete(write.threadId);\n const queued = queuedRef.current.get(write.threadId);\n const hadQueued = queuedRef.current.has(write.threadId);\n queuedRef.current.delete(write.threadId);\n onServerTruthRef.current?.(truth);\n if (hadQueued && queued !== undefined && !equalsRef.current(queued, truth.value)) {\n // The row already shows the value asked for meanwhile; the server\n // does not have it yet, so send it now — one write, the latest value.\n startWrite(write.scope, {\n threadId: write.threadId,\n value: queued,\n previous: truth.value,\n });\n return;\n }\n update(write.scope, (current) => ({\n ...current,\n overrides: { ...current.overrides, [truth.threadId]: truth.value },\n pending: withPending(current.pending, truth.threadId, false),\n }));\n },\n });\n const mutate = mutation.mutate;\n\n /** True when the answer belongs to the current identity and to this thread's latest write. */\n function isCurrentWrite(write: FieldWrite<T>): boolean {\n return (\n scopeRef.current === write.scope && inFlightRef.current.get(write.threadId) === write.seq\n );\n }\n\n function startWrite(forScope: string, write: Omit<FieldWrite<T>, 'scope' | 'seq'>): void {\n const seq = ++seqRef.current;\n inFlightRef.current.set(write.threadId, seq);\n update(forScope, (current) => ({\n ...current,\n pending: withPending(current.pending, write.threadId, true),\n }));\n mutate({ ...write, scope: forScope, seq });\n }\n\n const startWriteRef = useRef(startWrite);\n startWriteRef.current = startWrite;\n\n const set = useCallback(\n ({ threadId, value }: ThreadFieldTarget<T>) => {\n const forScope = scopeRef.current;\n const previous = overridesRef.current[threadId];\n // The row flips at once whether the write goes out now or after the\n // one in flight.\n update(forScope, (current) => ({\n ...current,\n overrides: { ...current.overrides, [threadId]: value },\n }));\n if (inFlightRef.current.has(threadId)) {\n queuedRef.current.set(threadId, value);\n return;\n }\n startWriteRef.current(forScope, { threadId, value, previous });\n },\n [update],\n );\n\n return {\n overrides,\n set,\n isPending: pendingThreadIds.size > 0,\n pendingThreadIds,\n isAvailable: unavailableBackend !== backendUrl,\n };\n}\n"],"names":["write"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-thread-rename.cjs","sources":["../../src/threads/use-thread-rename.ts"],"sourcesContent":["/**\n * The inline rename behind `SessionList`: `useThreadFieldOverride` over\n * `POST /api/conversations/threads/rename` (see that module for the\n * optimistic-override, rollback, serialization and identity-scoping\n * contract). The row shows the new title on commit; the server's stored\n * title (trimmed) is written back, and a failure restores the previous one.\n * A 404 / 405 marks the rename unavailable — the backend predates the route\n * — so the list withdraws the action instead of failing on every commit.\n */\nimport { useCallback } from 'react';\nimport { isThreadRenameUnavailableError, renameThread } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\nimport { type ThreadFieldTarget, useThreadFieldOverride } from './use-thread-field-override';\n\nexport interface ThreadRenameTarget {\n threadId: string;\n /** The new title; the caller trims it (the server trims again and bounds it to 1–200 characters). */\n title: string;\n}\n\n/** The two fields the override layer reads and writes on a list row. */\nexport interface TitleRow {\n threadId: string;\n title: string;\n}\n\nexport interface UseThreadRenameOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the read-state mutation keys). */\n principalId: string;\n getAuth: () => RestConversationAuth | Promise<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 title.\n */\n rows: readonly TitleRow[];\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (result: ThreadRenameTarget) => void;\n}\n\nexport interface ThreadRename {\n /** Optimistic, then server-confirmed, titles layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, string>>;\n /**\n * Rename a thread. The row shows the title at once; while that thread's\n * write is in flight the call only records the desired title (see the\n * field-override module doc).\n */\n rename: (target: ThreadRenameTarget) => void;\n /** True while any rename is in flight. */\n isPending: boolean;\n /** The threads whose rename is in flight — the row's editor stays closed for these. */\n pendingThreadIds: ReadonlySet<string>;\n /**\n * `false` once the backend answered 404 / 405: the route is not deployed\n * there, and the list offers no rename action.\n */\n isAvailable: boolean;\n}\n\nconst RENAME_UNAVAILABLE_WARNING =\n '[AthenaSDK] This backend has no POST /api/conversations/threads/rename route yet; sessions cannot be renamed from here until it deploys.';\n\nconst readTitle = (row: TitleRow): string => row.title;\n\nexport function useThreadRename({\n backendUrl,\n principalId,\n getAuth,\n rows,\n onServerTruth,\n}: UseThreadRenameOptions): ThreadRename {\n const field = useThreadFieldOverride<string, TitleRow>({\n field: 'rename',\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow: readTitle,\n write: async (auth, { threadId, value }) => {\n const result = await renameThread(backendUrl, auth, { threadId, title: value });\n return { threadId: result.thread_id, value: result.title };\n },\n isUnavailableError: isThreadRenameUnavailableError,\n unavailableWarning: RENAME_UNAVAILABLE_WARNING,\n onServerTruth: onServerTruth\n ? ({ threadId, value }: ThreadFieldTarget<string>) => onServerTruth({ threadId, title: value })\n : undefined,\n });\n const set = field.set;\n const rename = useCallback(\n ({ threadId, title }: ThreadRenameTarget) => set({ threadId, value: title }),\n [set],\n );\n return {\n overrides: field.overrides,\n rename,\n isPending: field.isPending,\n pendingThreadIds: field.pendingThreadIds,\n isAvailable: field.isAvailable,\n };\n}\n\n/** The row with its title override applied, or the row itself when it has none. */\nexport function withTitleOverride<T extends TitleRow>(\n row: T,\n overrides: Readonly<Record<string, string>>,\n): T {\n const override = overrides[row.threadId];\n return override === undefined || override === row.title ? row : { ...row, title: override };\n}\n\n/** Every row with its title override applied. */\nexport function withTitleOverrides<T extends TitleRow>(\n rows: readonly T[],\n overrides: Readonly<Record<string, string>>,\n): T[] {\n return rows.map((row) => withTitleOverride(row, overrides));\n}\n"],"names":["useThreadFieldOverride","renameThread","isThreadRenameUnavailableError","useCallback"],"mappings":";;;;;AA4DA,MAAM,6BACJ;AAEF,MAAM,YAAY,CAAC,QAA0B,IAAI;AAE1C,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAyC;AACvC,QAAM,QAAQA,uBAAAA,uBAAyC;AAAA,IACrD,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,aAAa,YAAY,MAAM,EAAE,UAAU,OAAO,OAAO;AAC9E,aAAO,EAAE,UAAU,OAAO,WAAW,OAAO,OAAO,MAAA;AAAA,IACrD;AAAA,IACA,oBAAoBC,IAAAA;AAAAA,IACpB,oBAAoB;AAAA,IACpB,eAAe,gBACX,CAAC,EAAE,UAAU,MAAA,MAAuC,cAAc,EAAE,UAAU,OAAO,MAAA,CAAO,IAC5F;AAAA,EAAA,CACL;AACD,QAAM,MAAM,MAAM;AAClB,QAAM,SAASC,MAAAA;AAAAA,IACb,CAAC,EAAE,UAAU,YAAgC,IAAI,EAAE,UAAU,OAAO,OAAO;AAAA,IAC3E,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,kBACd,KACA,WACG;AACH,QAAM,WAAW,UAAU,IAAI,QAAQ;AACvC,SAAO,aAAa,UAAa,aAAa,IAAI,QAAQ,MAAM,EAAE,GAAG,KAAK,OAAO,SAAA;AACnF;AAGO,SAAS,mBACd,MACA,WACK;AACL,SAAO,KAAK,IAAI,CAAC,QAAQ,kBAAkB,KAAK,SAAS,CAAC;AAC5D;;;;"}
1
+ {"version":3,"file":"use-thread-rename.cjs","sources":["../../src/threads/use-thread-rename.ts"],"sourcesContent":["/**\n * The inline rename behind `SessionList`: `useThreadFieldOverride` over\n * `POST /api/conversations/threads/rename` (see that module for the\n * optimistic-override, rollback, serialization and identity-scoping\n * contract). The row shows the new title on commit; the server's stored\n * title (trimmed) is written back, and a failure restores the previous one.\n * A 404 / 405 marks the rename unavailable — the backend predates the route\n * — so the list withdraws the action instead of failing on every commit.\n */\nimport { useCallback } from 'react';\nimport type { CredentialResolver } from '../runtime/credential-renewal';\nimport { isThreadRenameUnavailableError, renameThread } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\nimport { type ThreadFieldTarget, useThreadFieldOverride } from './use-thread-field-override';\n\nexport interface ThreadRenameTarget {\n threadId: string;\n /** The new title; the caller trims it (the server trims again and bounds it to 1–200 characters). */\n title: string;\n}\n\n/** The two fields the override layer reads and writes on a list row. */\nexport interface TitleRow {\n threadId: string;\n title: string;\n}\n\nexport interface UseThreadRenameOptions {\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 title.\n */\n rows: readonly TitleRow[];\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (result: ThreadRenameTarget) => void;\n}\n\nexport interface ThreadRename {\n /** Optimistic, then server-confirmed, titles layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, string>>;\n /**\n * Rename a thread. The row shows the title at once; while that thread's\n * write is in flight the call only records the desired title (see the\n * field-override module doc).\n */\n rename: (target: ThreadRenameTarget) => void;\n /** True while any rename is in flight. */\n isPending: boolean;\n /** The threads whose rename is in flight — the row's editor stays closed for these. */\n pendingThreadIds: ReadonlySet<string>;\n /**\n * `false` once the backend answered 404 / 405: the route is not deployed\n * there, and the list offers no rename action.\n */\n isAvailable: boolean;\n}\n\nconst RENAME_UNAVAILABLE_WARNING =\n '[AthenaSDK] This backend has no POST /api/conversations/threads/rename route yet; sessions cannot be renamed from here until it deploys.';\n\nconst readTitle = (row: TitleRow): string => row.title;\n\nexport function useThreadRename({\n backendUrl,\n principalId,\n getAuth,\n rows,\n onServerTruth,\n}: UseThreadRenameOptions): ThreadRename {\n const field = useThreadFieldOverride<string, TitleRow>({\n field: 'rename',\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow: readTitle,\n write: async (auth, { threadId, value }) => {\n const result = await renameThread(backendUrl, auth, { threadId, title: value });\n return { threadId: result.thread_id, value: result.title };\n },\n isUnavailableError: isThreadRenameUnavailableError,\n unavailableWarning: RENAME_UNAVAILABLE_WARNING,\n onServerTruth: onServerTruth\n ? ({ threadId, value }: ThreadFieldTarget<string>) => onServerTruth({ threadId, title: value })\n : undefined,\n });\n const set = field.set;\n const rename = useCallback(\n ({ threadId, title }: ThreadRenameTarget) => set({ threadId, value: title }),\n [set],\n );\n return {\n overrides: field.overrides,\n rename,\n isPending: field.isPending,\n pendingThreadIds: field.pendingThreadIds,\n isAvailable: field.isAvailable,\n };\n}\n\n/** The row with its title override applied, or the row itself when it has none. */\nexport function withTitleOverride<T extends TitleRow>(\n row: T,\n overrides: Readonly<Record<string, string>>,\n): T {\n const override = overrides[row.threadId];\n return override === undefined || override === row.title ? row : { ...row, title: override };\n}\n\n/** Every row with its title override applied. */\nexport function withTitleOverrides<T extends TitleRow>(\n rows: readonly T[],\n overrides: Readonly<Record<string, string>>,\n): T[] {\n return rows.map((row) => withTitleOverride(row, overrides));\n}\n"],"names":["useThreadFieldOverride","renameThread","isThreadRenameUnavailableError","useCallback"],"mappings":";;;;;AA8DA,MAAM,6BACJ;AAEF,MAAM,YAAY,CAAC,QAA0B,IAAI;AAE1C,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAyC;AACvC,QAAM,QAAQA,uBAAAA,uBAAyC;AAAA,IACrD,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,aAAa,YAAY,MAAM,EAAE,UAAU,OAAO,OAAO;AAC9E,aAAO,EAAE,UAAU,OAAO,WAAW,OAAO,OAAO,MAAA;AAAA,IACrD;AAAA,IACA,oBAAoBC,IAAAA;AAAAA,IACpB,oBAAoB;AAAA,IACpB,eAAe,gBACX,CAAC,EAAE,UAAU,MAAA,MAAuC,cAAc,EAAE,UAAU,OAAO,MAAA,CAAO,IAC5F;AAAA,EAAA,CACL;AACD,QAAM,MAAM,MAAM;AAClB,QAAM,SAASC,MAAAA;AAAAA,IACb,CAAC,EAAE,UAAU,YAAgC,IAAI,EAAE,UAAU,OAAO,OAAO;AAAA,IAC3E,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,kBACd,KACA,WACG;AACH,QAAM,WAAW,UAAU,IAAI,QAAQ;AACvC,SAAO,aAAa,UAAa,aAAa,IAAI,QAAQ,MAAM,EAAE,GAAG,KAAK,OAAO,SAAA;AACnF;AAGO,SAAS,mBACd,MACA,WACK;AACL,SAAO,KAAK,IAAI,CAAC,QAAQ,kBAAkB,KAAK,SAAS,CAAC;AAC5D;;;;"}
@@ -1,3 +1,4 @@
1
+ import type { CredentialResolver } from '../runtime/credential-renewal';
1
2
  import type { RestConversationAuth } from './use-rest-conversation-history';
2
3
  export interface ThreadRenameTarget {
3
4
  threadId: string;
@@ -13,7 +14,8 @@ export interface UseThreadRenameOptions {
13
14
  backendUrl: string;
14
15
  /** Non-secret identity in the mutation key (as the read-state mutation keys). */
15
16
  principalId: string;
16
- getAuth: () => RestConversationAuth | Promise<RestConversationAuth>;
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 title.
@@ -1 +1 @@
1
- {"version":3,"file":"use-thread-rename.js","sources":["../../src/threads/use-thread-rename.ts"],"sourcesContent":["/**\n * The inline rename behind `SessionList`: `useThreadFieldOverride` over\n * `POST /api/conversations/threads/rename` (see that module for the\n * optimistic-override, rollback, serialization and identity-scoping\n * contract). The row shows the new title on commit; the server's stored\n * title (trimmed) is written back, and a failure restores the previous one.\n * A 404 / 405 marks the rename unavailable — the backend predates the route\n * — so the list withdraws the action instead of failing on every commit.\n */\nimport { useCallback } from 'react';\nimport { isThreadRenameUnavailableError, renameThread } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\nimport { type ThreadFieldTarget, useThreadFieldOverride } from './use-thread-field-override';\n\nexport interface ThreadRenameTarget {\n threadId: string;\n /** The new title; the caller trims it (the server trims again and bounds it to 1–200 characters). */\n title: string;\n}\n\n/** The two fields the override layer reads and writes on a list row. */\nexport interface TitleRow {\n threadId: string;\n title: string;\n}\n\nexport interface UseThreadRenameOptions {\n backendUrl: string;\n /** Non-secret identity in the mutation key (as the read-state mutation keys). */\n principalId: string;\n getAuth: () => RestConversationAuth | Promise<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 title.\n */\n rows: readonly TitleRow[];\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (result: ThreadRenameTarget) => void;\n}\n\nexport interface ThreadRename {\n /** Optimistic, then server-confirmed, titles layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, string>>;\n /**\n * Rename a thread. The row shows the title at once; while that thread's\n * write is in flight the call only records the desired title (see the\n * field-override module doc).\n */\n rename: (target: ThreadRenameTarget) => void;\n /** True while any rename is in flight. */\n isPending: boolean;\n /** The threads whose rename is in flight — the row's editor stays closed for these. */\n pendingThreadIds: ReadonlySet<string>;\n /**\n * `false` once the backend answered 404 / 405: the route is not deployed\n * there, and the list offers no rename action.\n */\n isAvailable: boolean;\n}\n\nconst RENAME_UNAVAILABLE_WARNING =\n '[AthenaSDK] This backend has no POST /api/conversations/threads/rename route yet; sessions cannot be renamed from here until it deploys.';\n\nconst readTitle = (row: TitleRow): string => row.title;\n\nexport function useThreadRename({\n backendUrl,\n principalId,\n getAuth,\n rows,\n onServerTruth,\n}: UseThreadRenameOptions): ThreadRename {\n const field = useThreadFieldOverride<string, TitleRow>({\n field: 'rename',\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow: readTitle,\n write: async (auth, { threadId, value }) => {\n const result = await renameThread(backendUrl, auth, { threadId, title: value });\n return { threadId: result.thread_id, value: result.title };\n },\n isUnavailableError: isThreadRenameUnavailableError,\n unavailableWarning: RENAME_UNAVAILABLE_WARNING,\n onServerTruth: onServerTruth\n ? ({ threadId, value }: ThreadFieldTarget<string>) => onServerTruth({ threadId, title: value })\n : undefined,\n });\n const set = field.set;\n const rename = useCallback(\n ({ threadId, title }: ThreadRenameTarget) => set({ threadId, value: title }),\n [set],\n );\n return {\n overrides: field.overrides,\n rename,\n isPending: field.isPending,\n pendingThreadIds: field.pendingThreadIds,\n isAvailable: field.isAvailable,\n };\n}\n\n/** The row with its title override applied, or the row itself when it has none. */\nexport function withTitleOverride<T extends TitleRow>(\n row: T,\n overrides: Readonly<Record<string, string>>,\n): T {\n const override = overrides[row.threadId];\n return override === undefined || override === row.title ? row : { ...row, title: override };\n}\n\n/** Every row with its title override applied. */\nexport function withTitleOverrides<T extends TitleRow>(\n rows: readonly T[],\n overrides: Readonly<Record<string, string>>,\n): T[] {\n return rows.map((row) => withTitleOverride(row, overrides));\n}\n"],"names":[],"mappings":";;;AA4DA,MAAM,6BACJ;AAEF,MAAM,YAAY,CAAC,QAA0B,IAAI;AAE1C,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAyC;AACvC,QAAM,QAAQ,uBAAyC;AAAA,IACrD,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,OAAO,OAAO,MAAM,EAAE,UAAU,YAAY;AAC1C,YAAM,SAAS,MAAM,aAAa,YAAY,MAAM,EAAE,UAAU,OAAO,OAAO;AAC9E,aAAO,EAAE,UAAU,OAAO,WAAW,OAAO,OAAO,MAAA;AAAA,IACrD;AAAA,IACA,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,IACpB,eAAe,gBACX,CAAC,EAAE,UAAU,MAAA,MAAuC,cAAc,EAAE,UAAU,OAAO,MAAA,CAAO,IAC5F;AAAA,EAAA,CACL;AACD,QAAM,MAAM,MAAM;AAClB,QAAM,SAAS;AAAA,IACb,CAAC,EAAE,UAAU,YAAgC,IAAI,EAAE,UAAU,OAAO,OAAO;AAAA,IAC3E,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,kBACd,KACA,WACG;AACH,QAAM,WAAW,UAAU,IAAI,QAAQ;AACvC,SAAO,aAAa,UAAa,aAAa,IAAI,QAAQ,MAAM,EAAE,GAAG,KAAK,OAAO,SAAA;AACnF;AAGO,SAAS,mBACd,MACA,WACK;AACL,SAAO,KAAK,IAAI,CAAC,QAAQ,kBAAkB,KAAK,SAAS,CAAC;AAC5D;"}
1
+ {"version":3,"file":"use-thread-rename.js","sources":["../../src/threads/use-thread-rename.ts"],"sourcesContent":["/**\n * The inline rename behind `SessionList`: `useThreadFieldOverride` over\n * `POST /api/conversations/threads/rename` (see that module for the\n * optimistic-override, rollback, serialization and identity-scoping\n * contract). The row shows the new title on commit; the server's stored\n * title (trimmed) is written back, and a failure restores the previous one.\n * A 404 / 405 marks the rename unavailable — the backend predates the route\n * — so the list withdraws the action instead of failing on every commit.\n */\nimport { useCallback } from 'react';\nimport type { CredentialResolver } from '../runtime/credential-renewal';\nimport { isThreadRenameUnavailableError, renameThread } from './api';\nimport type { RestConversationAuth } from './use-rest-conversation-history';\nimport { type ThreadFieldTarget, useThreadFieldOverride } from './use-thread-field-override';\n\nexport interface ThreadRenameTarget {\n threadId: string;\n /** The new title; the caller trims it (the server trims again and bounds it to 1–200 characters). */\n title: string;\n}\n\n/** The two fields the override layer reads and writes on a list row. */\nexport interface TitleRow {\n threadId: string;\n title: string;\n}\n\nexport interface UseThreadRenameOptions {\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 title.\n */\n rows: readonly TitleRow[];\n /** Write the server's answer into the list caches (`patchConversation`). */\n onServerTruth?: (result: ThreadRenameTarget) => void;\n}\n\nexport interface ThreadRename {\n /** Optimistic, then server-confirmed, titles layered over the list sources, keyed by thread id. */\n overrides: Readonly<Record<string, string>>;\n /**\n * Rename a thread. The row shows the title at once; while that thread's\n * write is in flight the call only records the desired title (see the\n * field-override module doc).\n */\n rename: (target: ThreadRenameTarget) => void;\n /** True while any rename is in flight. */\n isPending: boolean;\n /** The threads whose rename is in flight — the row's editor stays closed for these. */\n pendingThreadIds: ReadonlySet<string>;\n /**\n * `false` once the backend answered 404 / 405: the route is not deployed\n * there, and the list offers no rename action.\n */\n isAvailable: boolean;\n}\n\nconst RENAME_UNAVAILABLE_WARNING =\n '[AthenaSDK] This backend has no POST /api/conversations/threads/rename route yet; sessions cannot be renamed from here until it deploys.';\n\nconst readTitle = (row: TitleRow): string => row.title;\n\nexport function useThreadRename({\n backendUrl,\n principalId,\n getAuth,\n rows,\n onServerTruth,\n}: UseThreadRenameOptions): ThreadRename {\n const field = useThreadFieldOverride<string, TitleRow>({\n field: 'rename',\n backendUrl,\n principalId,\n getAuth,\n rows,\n readRow: readTitle,\n write: async (auth, { threadId, value }) => {\n const result = await renameThread(backendUrl, auth, { threadId, title: value });\n return { threadId: result.thread_id, value: result.title };\n },\n isUnavailableError: isThreadRenameUnavailableError,\n unavailableWarning: RENAME_UNAVAILABLE_WARNING,\n onServerTruth: onServerTruth\n ? ({ threadId, value }: ThreadFieldTarget<string>) => onServerTruth({ threadId, title: value })\n : undefined,\n });\n const set = field.set;\n const rename = useCallback(\n ({ threadId, title }: ThreadRenameTarget) => set({ threadId, value: title }),\n [set],\n );\n return {\n overrides: field.overrides,\n rename,\n isPending: field.isPending,\n pendingThreadIds: field.pendingThreadIds,\n isAvailable: field.isAvailable,\n };\n}\n\n/** The row with its title override applied, or the row itself when it has none. */\nexport function withTitleOverride<T extends TitleRow>(\n row: T,\n overrides: Readonly<Record<string, string>>,\n): T {\n const override = overrides[row.threadId];\n return override === undefined || override === row.title ? row : { ...row, title: override };\n}\n\n/** Every row with its title override applied. */\nexport function withTitleOverrides<T extends TitleRow>(\n rows: readonly T[],\n overrides: Readonly<Record<string, string>>,\n): T[] {\n return rows.map((row) => withTitleOverride(row, overrides));\n}\n"],"names":[],"mappings":";;;AA8DA,MAAM,6BACJ;AAEF,MAAM,YAAY,CAAC,QAA0B,IAAI;AAE1C,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAyC;AACvC,QAAM,QAAQ,uBAAyC;AAAA,IACrD,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,OAAO,OAAO,MAAM,EAAE,UAAU,YAAY;AAC1C,YAAM,SAAS,MAAM,aAAa,YAAY,MAAM,EAAE,UAAU,OAAO,OAAO;AAC9E,aAAO,EAAE,UAAU,OAAO,WAAW,OAAO,OAAO,MAAA;AAAA,IACrD;AAAA,IACA,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,IACpB,eAAe,gBACX,CAAC,EAAE,UAAU,MAAA,MAAuC,cAAc,EAAE,UAAU,OAAO,MAAA,CAAO,IAC5F;AAAA,EAAA,CACL;AACD,QAAM,MAAM,MAAM;AAClB,QAAM,SAAS;AAAA,IACb,CAAC,EAAE,UAAU,YAAgC,IAAI,EAAE,UAAU,OAAO,OAAO;AAAA,IAC3E,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,kBACd,KACA,WACG;AACH,QAAM,WAAW,UAAU,IAAI,QAAQ;AACvC,SAAO,aAAa,UAAa,aAAa,IAAI,QAAQ,MAAM,EAAE,GAAG,KAAK,OAAO,SAAA;AACnF;AAGO,SAAS,mBACd,MACA,WACK;AACL,SAAO,KAAK,IAAI,CAAC,QAAQ,kBAAkB,KAAK,SAAS,CAAC;AAC5D;"}
@@ -10,6 +10,7 @@ const writeTodosToolUi = require("./write-todos-tool-ui.cjs");
10
10
  const miscToolUis = require("./misc-tool-uis.cjs");
11
11
  const assetPanelStore = require("../assets/asset-panel-store.cjs");
12
12
  const AthenaContext = require("../provider/AthenaContext.cjs");
13
+ const credentialRenewal = require("../runtime/credential-renewal.cjs");
13
14
  const cnfast = require("cnfast");
14
15
  function isSdkToolDebugEnabled() {
15
16
  var _a, _b;
@@ -2697,22 +2698,17 @@ function useFreshPresignedUrl({
2697
2698
  let cancelled = false;
2698
2699
  (async () => {
2699
2700
  try {
2700
- const { backendUrl, token, apiKey } = configRef.current;
2701
- const baseUrl = backendUrl.replace(/\/api\/assistant-ui\/?$/, "");
2701
+ const config2 = configRef.current;
2702
+ const baseUrl = config2.backendUrl.replace(/\/api\/assistant-ui\/?$/, "");
2702
2703
  const url = `${baseUrl}/api/asset/fetch-attachment-presigned-url`;
2703
- const headers = {
2704
- "Content-Type": "application/json"
2705
- };
2706
- if (token) {
2707
- headers["Authorization"] = `Bearer ${token}`;
2708
- } else if (apiKey) {
2709
- headers["X-API-KEY"] = apiKey;
2710
- }
2711
- const resp = await fetch(url, {
2712
- method: "POST",
2713
- headers,
2714
- body: JSON.stringify({ s3_key: s3Key })
2715
- });
2704
+ const resp = await credentialRenewal.sendWithCredentialRenewal(
2705
+ config2,
2706
+ (headers) => fetch(url, {
2707
+ method: "POST",
2708
+ headers: { "Content-Type": "application/json", ...headers },
2709
+ body: JSON.stringify({ s3_key: s3Key })
2710
+ })
2711
+ );
2716
2712
  if (!cancelled && resp.ok) {
2717
2713
  const data = await resp.json();
2718
2714
  if (!cancelled && data.presigned_url) {