@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":"conversation-list-fetcher.cjs","sources":["../../src/threads/conversation-list-fetcher.ts"],"sourcesContent":["/**\n * REST adapter for the shared conversation-list contract\n * (`@athenaintel/agent-runtime` — thread-list-contract-convergence).\n *\n * Translates the canonical {@link ConversationListFetchParams} onto Agora's\n * `POST /api/conversations/threads/list` and maps its `ThreadSummary` rows\n * into the canonical {@link ConversationSummary} shape. Filter fields this\n * transport cannot express throw instead of being ignored — a dropped filter\n * would return wrong rows under a cache key that claims otherwise.\n */\n\nimport type {\n ConversationListFetcher,\n ConversationListFetchParams,\n ConversationListFilter,\n ConversationListPage,\n ConversationPullRequest,\n ConversationSummary,\n ConversationWake,\n} from '@athenaintel/agent-runtime';\nimport { listThreads, type ThreadPullRequest, type ThreadSummary, type ThreadWake } from './api';\n\nexport interface ThreadListRequestBody {\n limit: number;\n offset: number;\n exclude_triggered?: boolean;\n app_id?: string;\n start_channel?: string;\n search?: string;\n}\n\n/**\n * Map the canonical filter onto the REST thread-list request body.\n * Exported for tests; throws on filter fields the endpoint cannot express.\n */\nexport function threadListRequestBody(\n params: Pick<ConversationListFetchParams, 'limit' | 'offset' | 'filter'>,\n): ThreadListRequestBody {\n const { filter } = params;\n const unsupported: string[] = [];\n if (filter.assetTypes && filter.assetTypes.length > 0) unsupported.push('assetTypes');\n if (filter.states && filter.states.length > 0) unsupported.push('states');\n if (filter.sourceChannels && filter.sourceChannels.length > 1) {\n unsupported.push('sourceChannels (more than one)');\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n unsupported.push('excludeSourceChannels');\n }\n if (filter.computerAssetId) unsupported.push('computerAssetId');\n if (unsupported.length > 0) {\n throw new Error(\n `[AthenaSDK] The REST thread list cannot express filter field(s): ${unsupported.join(', ')}`,\n );\n }\n return {\n limit: params.limit,\n offset: params.offset,\n ...(filter.excludeTriggered !== undefined\n ? { exclude_triggered: filter.excludeTriggered }\n : {}),\n ...(filter.appId ? { app_id: filter.appId } : {}),\n ...(filter.sourceChannels?.[0] ? { start_channel: filter.sourceChannels[0] } : {}),\n ...(filter.search ? { search: filter.search } : {}),\n };\n}\n\n/** Map one REST pull-request record onto the canonical camelCase shape. */\nexport function conversationPullRequestFromThreadPullRequest(\n record: ThreadPullRequest,\n): ConversationPullRequest {\n return {\n repoFullName: record.repo_full_name,\n number: record.number,\n url: record.url,\n title: record.title,\n state: record.state,\n isDraft: record.is_draft,\n reviewDecision: record.review_decision ?? null,\n checksState: record.checks_state,\n updatedAt: record.updated_at,\n };\n}\n\nfunction conversationWakeFromThreadWake(wake: ThreadWake): ConversationWake {\n return { source: wake.source ?? null, reason: wake.reason ?? null, at: wake.at ?? null };\n}\n\n/**\n * Map a REST `ThreadSummary` row into the canonical contract shape. Fields\n * the server did not send stay `undefined` (an older server, or a list that\n * did not opt into pull requests), so a consumer can tell \"not reported\"\n * from \"reported as none\".\n */\nexport function conversationSummaryFromThreadSummary(\n thread: ThreadSummary,\n filter: ConversationListFilter,\n): ConversationSummary {\n return {\n threadId: thread.thread_id,\n title: thread.title,\n updatedAt: thread.updated_at,\n createdAt: thread.created_at ?? null,\n state: thread.state,\n // Not reported by this endpoint.\n runtime: null,\n sourceChannel: thread.start_channel ?? null,\n assetType: null,\n assetId: null,\n appId: thread.app_id ?? filter.appId ?? null,\n triggerType: thread.trigger_type ?? null,\n preview: thread.last_message ?? null,\n messageCount: thread.message_count ?? null,\n isUnread: thread.is_unread ?? false,\n // Older servers omit the status planes; `isConversationRunning` then falls back to `state`.\n sessionStatus: thread.session_status ?? null,\n runStatus: thread.run_status ?? null,\n attention: thread.attention,\n nextAction: thread.next_action,\n attentionSource: thread.attention_source,\n isFavorited: thread.is_favorited,\n lastWake:\n thread.last_wake === undefined || thread.last_wake === null\n ? thread.last_wake\n : conversationWakeFromThreadWake(thread.last_wake),\n pullRequests:\n thread.pull_requests === undefined || thread.pull_requests === null\n ? thread.pull_requests\n : thread.pull_requests.map(conversationPullRequestFromThreadPullRequest),\n };\n}\n\nexport interface RestConversationListFetcherOptions {\n backendUrl: string;\n /** Resolved per request so rotating tokens never go stale in the fetcher. */\n getAuth: () =>\n | { apiKey?: string; token?: string | null }\n | Promise<{ apiKey?: string; token?: string | null }>;\n /**\n * Ask the server for live GitHub state on each row's `pullRequests`\n * (`include_pull_requests`). Off by default: it costs the server a lookup,\n * so only a list that renders pull-request chips opts in.\n */\n includePullRequests?: boolean;\n}\n\n/**\n * Build a {@link ConversationListFetcher} over the REST thread list. The\n * endpoint reports no `hasMore`/total, so `hasMore` is the standard offset\n * heuristic (a full page implies more may exist) and `totalCount` is null.\n */\nexport function createRestConversationListFetcher(\n options: RestConversationListFetcherOptions,\n): ConversationListFetcher {\n return async (params): Promise<ConversationListPage> => {\n const body = threadListRequestBody(params);\n const { threads, has_more: hasMore } = await listThreads(\n options.backendUrl,\n await options.getAuth(),\n {\n ...body,\n ...(options.includePullRequests ? { include_pull_requests: true } : {}),\n signal: params.signal,\n },\n );\n return {\n items: threads.map((thread) => conversationSummaryFromThreadSummary(thread, params.filter)),\n hasMore: hasMore ?? threads.length >= params.limit,\n totalCount: null,\n };\n };\n}\n"],"names":["listThreads"],"mappings":"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"}
1
+ {"version":3,"file":"conversation-list-fetcher.cjs","sources":["../../src/threads/conversation-list-fetcher.ts"],"sourcesContent":["/**\n * REST adapter for the shared conversation-list contract\n * (`@athenaintel/agent-runtime` — thread-list-contract-convergence).\n *\n * Translates the canonical {@link ConversationListFetchParams} onto Agora's\n * `POST /api/conversations/threads/list` and maps its `ThreadSummary` rows\n * into the canonical {@link ConversationSummary} shape. Filter fields this\n * transport cannot express throw instead of being ignored — a dropped filter\n * would return wrong rows under a cache key that claims otherwise.\n */\n\nimport type {\n ConversationListFetcher,\n ConversationListFetchParams,\n ConversationListFilter,\n ConversationListPage,\n ConversationPullRequest,\n ConversationSummary,\n ConversationWake,\n} from '@athenaintel/agent-runtime';\nimport { type CredentialResolver, withCredentialRenewal } from '../runtime/credential-renewal';\nimport { listThreads, type ThreadPullRequest, type ThreadSummary, type ThreadWake } from './api';\n\nexport interface ThreadListRequestBody {\n limit: number;\n offset: number;\n exclude_triggered?: boolean;\n app_id?: string;\n start_channel?: string;\n search?: string;\n}\n\n/**\n * Map the canonical filter onto the REST thread-list request body.\n * Exported for tests; throws on filter fields the endpoint cannot express.\n */\nexport function threadListRequestBody(\n params: Pick<ConversationListFetchParams, 'limit' | 'offset' | 'filter'>,\n): ThreadListRequestBody {\n const { filter } = params;\n const unsupported: string[] = [];\n if (filter.assetTypes && filter.assetTypes.length > 0) unsupported.push('assetTypes');\n if (filter.states && filter.states.length > 0) unsupported.push('states');\n if (filter.sourceChannels && filter.sourceChannels.length > 1) {\n unsupported.push('sourceChannels (more than one)');\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n unsupported.push('excludeSourceChannels');\n }\n if (filter.computerAssetId) unsupported.push('computerAssetId');\n if (unsupported.length > 0) {\n throw new Error(\n `[AthenaSDK] The REST thread list cannot express filter field(s): ${unsupported.join(', ')}`,\n );\n }\n return {\n limit: params.limit,\n offset: params.offset,\n ...(filter.excludeTriggered !== undefined\n ? { exclude_triggered: filter.excludeTriggered }\n : {}),\n ...(filter.appId ? { app_id: filter.appId } : {}),\n ...(filter.sourceChannels?.[0] ? { start_channel: filter.sourceChannels[0] } : {}),\n ...(filter.search ? { search: filter.search } : {}),\n };\n}\n\n/** Map one REST pull-request record onto the canonical camelCase shape. */\nexport function conversationPullRequestFromThreadPullRequest(\n record: ThreadPullRequest,\n): ConversationPullRequest {\n return {\n repoFullName: record.repo_full_name,\n number: record.number,\n url: record.url,\n title: record.title,\n state: record.state,\n isDraft: record.is_draft,\n reviewDecision: record.review_decision ?? null,\n checksState: record.checks_state,\n updatedAt: record.updated_at,\n };\n}\n\nfunction conversationWakeFromThreadWake(wake: ThreadWake): ConversationWake {\n return { source: wake.source ?? null, reason: wake.reason ?? null, at: wake.at ?? null };\n}\n\n/**\n * Map a REST `ThreadSummary` row into the canonical contract shape. Fields\n * the server did not send stay `undefined` (an older server, or a list that\n * did not opt into pull requests), so a consumer can tell \"not reported\"\n * from \"reported as none\".\n */\nexport function conversationSummaryFromThreadSummary(\n thread: ThreadSummary,\n filter: ConversationListFilter,\n): ConversationSummary {\n return {\n threadId: thread.thread_id,\n title: thread.title,\n updatedAt: thread.updated_at,\n createdAt: thread.created_at ?? null,\n state: thread.state,\n // Not reported by this endpoint.\n runtime: null,\n sourceChannel: thread.start_channel ?? null,\n assetType: null,\n assetId: null,\n appId: thread.app_id ?? filter.appId ?? null,\n triggerType: thread.trigger_type ?? null,\n preview: thread.last_message ?? null,\n messageCount: thread.message_count ?? null,\n isUnread: thread.is_unread ?? false,\n // Older servers omit the status planes; `isConversationRunning` then falls back to `state`.\n sessionStatus: thread.session_status ?? null,\n runStatus: thread.run_status ?? null,\n attention: thread.attention,\n nextAction: thread.next_action,\n attentionSource: thread.attention_source,\n isFavorited: thread.is_favorited,\n lastWake:\n thread.last_wake === undefined || thread.last_wake === null\n ? thread.last_wake\n : conversationWakeFromThreadWake(thread.last_wake),\n pullRequests:\n thread.pull_requests === undefined || thread.pull_requests === null\n ? thread.pull_requests\n : thread.pull_requests.map(conversationPullRequestFromThreadPullRequest),\n };\n}\n\nexport interface RestConversationListFetcherOptions {\n backendUrl: string;\n /**\n * Resolved per request so rotating tokens never go stale in the fetcher;\n * called again with `{ force: true }` when the host refuses the token.\n */\n getAuth: CredentialResolver<{ apiKey?: string; token?: string | null }>;\n /**\n * Ask the server for live GitHub state on each row's `pullRequests`\n * (`include_pull_requests`). Off by default: it costs the server a lookup,\n * so only a list that renders pull-request chips opts in.\n */\n includePullRequests?: boolean;\n}\n\n/**\n * Build a {@link ConversationListFetcher} over the REST thread list. The\n * endpoint reports no `hasMore`/total, so `hasMore` is the standard offset\n * heuristic (a full page implies more may exist) and `totalCount` is null.\n */\nexport function createRestConversationListFetcher(\n options: RestConversationListFetcherOptions,\n): ConversationListFetcher {\n return async (params): Promise<ConversationListPage> => {\n const body = threadListRequestBody(params);\n const { threads, has_more: hasMore } = await withCredentialRenewal(options.getAuth, (auth) =>\n listThreads(options.backendUrl, auth, {\n ...body,\n ...(options.includePullRequests ? { include_pull_requests: true } : {}),\n signal: params.signal,\n }),\n );\n return {\n items: threads.map((thread) => conversationSummaryFromThreadSummary(thread, params.filter)),\n hasMore: hasMore ?? threads.length >= params.limit,\n totalCount: null,\n };\n };\n}\n"],"names":["withCredentialRenewal","listThreads"],"mappings":"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"}
@@ -9,6 +9,7 @@
9
9
  * would return wrong rows under a cache key that claims otherwise.
10
10
  */
11
11
  import type { ConversationListFetcher, ConversationListFetchParams, ConversationListFilter, ConversationPullRequest, ConversationSummary } from '@athenaintel/agent-runtime';
12
+ import { type CredentialResolver } from '../runtime/credential-renewal';
12
13
  import { type ThreadPullRequest, type ThreadSummary } from './api';
13
14
  export interface ThreadListRequestBody {
14
15
  limit: number;
@@ -34,11 +35,11 @@ export declare function conversationPullRequestFromThreadPullRequest(record: Thr
34
35
  export declare function conversationSummaryFromThreadSummary(thread: ThreadSummary, filter: ConversationListFilter): ConversationSummary;
35
36
  export interface RestConversationListFetcherOptions {
36
37
  backendUrl: string;
37
- /** Resolved per request so rotating tokens never go stale in the fetcher. */
38
- getAuth: () => {
39
- apiKey?: string;
40
- token?: string | null;
41
- } | Promise<{
38
+ /**
39
+ * Resolved per request so rotating tokens never go stale in the fetcher;
40
+ * called again with `{ force: true }` when the host refuses the token.
41
+ */
42
+ getAuth: CredentialResolver<{
42
43
  apiKey?: string;
43
44
  token?: string | null;
44
45
  }>;
@@ -1,3 +1,4 @@
1
+ import { withCredentialRenewal } from "../runtime/credential-renewal.js";
1
2
  import { listThreads } from "./api.js";
2
3
  function threadListRequestBody(params) {
3
4
  var _a;
@@ -73,14 +74,13 @@ function conversationSummaryFromThreadSummary(thread, filter) {
73
74
  function createRestConversationListFetcher(options) {
74
75
  return async (params) => {
75
76
  const body = threadListRequestBody(params);
76
- const { threads, has_more: hasMore } = await listThreads(
77
- options.backendUrl,
78
- await options.getAuth(),
79
- {
77
+ const { threads, has_more: hasMore } = await withCredentialRenewal(
78
+ options.getAuth,
79
+ (auth) => listThreads(options.backendUrl, auth, {
80
80
  ...body,
81
81
  ...options.includePullRequests ? { include_pull_requests: true } : {},
82
82
  signal: params.signal
83
- }
83
+ })
84
84
  );
85
85
  return {
86
86
  items: threads.map((thread) => conversationSummaryFromThreadSummary(thread, params.filter)),
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-list-fetcher.js","sources":["../../src/threads/conversation-list-fetcher.ts"],"sourcesContent":["/**\n * REST adapter for the shared conversation-list contract\n * (`@athenaintel/agent-runtime` — thread-list-contract-convergence).\n *\n * Translates the canonical {@link ConversationListFetchParams} onto Agora's\n * `POST /api/conversations/threads/list` and maps its `ThreadSummary` rows\n * into the canonical {@link ConversationSummary} shape. Filter fields this\n * transport cannot express throw instead of being ignored — a dropped filter\n * would return wrong rows under a cache key that claims otherwise.\n */\n\nimport type {\n ConversationListFetcher,\n ConversationListFetchParams,\n ConversationListFilter,\n ConversationListPage,\n ConversationPullRequest,\n ConversationSummary,\n ConversationWake,\n} from '@athenaintel/agent-runtime';\nimport { listThreads, type ThreadPullRequest, type ThreadSummary, type ThreadWake } from './api';\n\nexport interface ThreadListRequestBody {\n limit: number;\n offset: number;\n exclude_triggered?: boolean;\n app_id?: string;\n start_channel?: string;\n search?: string;\n}\n\n/**\n * Map the canonical filter onto the REST thread-list request body.\n * Exported for tests; throws on filter fields the endpoint cannot express.\n */\nexport function threadListRequestBody(\n params: Pick<ConversationListFetchParams, 'limit' | 'offset' | 'filter'>,\n): ThreadListRequestBody {\n const { filter } = params;\n const unsupported: string[] = [];\n if (filter.assetTypes && filter.assetTypes.length > 0) unsupported.push('assetTypes');\n if (filter.states && filter.states.length > 0) unsupported.push('states');\n if (filter.sourceChannels && filter.sourceChannels.length > 1) {\n unsupported.push('sourceChannels (more than one)');\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n unsupported.push('excludeSourceChannels');\n }\n if (filter.computerAssetId) unsupported.push('computerAssetId');\n if (unsupported.length > 0) {\n throw new Error(\n `[AthenaSDK] The REST thread list cannot express filter field(s): ${unsupported.join(', ')}`,\n );\n }\n return {\n limit: params.limit,\n offset: params.offset,\n ...(filter.excludeTriggered !== undefined\n ? { exclude_triggered: filter.excludeTriggered }\n : {}),\n ...(filter.appId ? { app_id: filter.appId } : {}),\n ...(filter.sourceChannels?.[0] ? { start_channel: filter.sourceChannels[0] } : {}),\n ...(filter.search ? { search: filter.search } : {}),\n };\n}\n\n/** Map one REST pull-request record onto the canonical camelCase shape. */\nexport function conversationPullRequestFromThreadPullRequest(\n record: ThreadPullRequest,\n): ConversationPullRequest {\n return {\n repoFullName: record.repo_full_name,\n number: record.number,\n url: record.url,\n title: record.title,\n state: record.state,\n isDraft: record.is_draft,\n reviewDecision: record.review_decision ?? null,\n checksState: record.checks_state,\n updatedAt: record.updated_at,\n };\n}\n\nfunction conversationWakeFromThreadWake(wake: ThreadWake): ConversationWake {\n return { source: wake.source ?? null, reason: wake.reason ?? null, at: wake.at ?? null };\n}\n\n/**\n * Map a REST `ThreadSummary` row into the canonical contract shape. Fields\n * the server did not send stay `undefined` (an older server, or a list that\n * did not opt into pull requests), so a consumer can tell \"not reported\"\n * from \"reported as none\".\n */\nexport function conversationSummaryFromThreadSummary(\n thread: ThreadSummary,\n filter: ConversationListFilter,\n): ConversationSummary {\n return {\n threadId: thread.thread_id,\n title: thread.title,\n updatedAt: thread.updated_at,\n createdAt: thread.created_at ?? null,\n state: thread.state,\n // Not reported by this endpoint.\n runtime: null,\n sourceChannel: thread.start_channel ?? null,\n assetType: null,\n assetId: null,\n appId: thread.app_id ?? filter.appId ?? null,\n triggerType: thread.trigger_type ?? null,\n preview: thread.last_message ?? null,\n messageCount: thread.message_count ?? null,\n isUnread: thread.is_unread ?? false,\n // Older servers omit the status planes; `isConversationRunning` then falls back to `state`.\n sessionStatus: thread.session_status ?? null,\n runStatus: thread.run_status ?? null,\n attention: thread.attention,\n nextAction: thread.next_action,\n attentionSource: thread.attention_source,\n isFavorited: thread.is_favorited,\n lastWake:\n thread.last_wake === undefined || thread.last_wake === null\n ? thread.last_wake\n : conversationWakeFromThreadWake(thread.last_wake),\n pullRequests:\n thread.pull_requests === undefined || thread.pull_requests === null\n ? thread.pull_requests\n : thread.pull_requests.map(conversationPullRequestFromThreadPullRequest),\n };\n}\n\nexport interface RestConversationListFetcherOptions {\n backendUrl: string;\n /** Resolved per request so rotating tokens never go stale in the fetcher. */\n getAuth: () =>\n | { apiKey?: string; token?: string | null }\n | Promise<{ apiKey?: string; token?: string | null }>;\n /**\n * Ask the server for live GitHub state on each row's `pullRequests`\n * (`include_pull_requests`). Off by default: it costs the server a lookup,\n * so only a list that renders pull-request chips opts in.\n */\n includePullRequests?: boolean;\n}\n\n/**\n * Build a {@link ConversationListFetcher} over the REST thread list. The\n * endpoint reports no `hasMore`/total, so `hasMore` is the standard offset\n * heuristic (a full page implies more may exist) and `totalCount` is null.\n */\nexport function createRestConversationListFetcher(\n options: RestConversationListFetcherOptions,\n): ConversationListFetcher {\n return async (params): Promise<ConversationListPage> => {\n const body = threadListRequestBody(params);\n const { threads, has_more: hasMore } = await listThreads(\n options.backendUrl,\n await options.getAuth(),\n {\n ...body,\n ...(options.includePullRequests ? { include_pull_requests: true } : {}),\n signal: params.signal,\n },\n );\n return {\n items: threads.map((thread) => conversationSummaryFromThreadSummary(thread, params.filter)),\n hasMore: hasMore ?? threads.length >= params.limit,\n totalCount: null,\n };\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"conversation-list-fetcher.js","sources":["../../src/threads/conversation-list-fetcher.ts"],"sourcesContent":["/**\n * REST adapter for the shared conversation-list contract\n * (`@athenaintel/agent-runtime` — thread-list-contract-convergence).\n *\n * Translates the canonical {@link ConversationListFetchParams} onto Agora's\n * `POST /api/conversations/threads/list` and maps its `ThreadSummary` rows\n * into the canonical {@link ConversationSummary} shape. Filter fields this\n * transport cannot express throw instead of being ignored — a dropped filter\n * would return wrong rows under a cache key that claims otherwise.\n */\n\nimport type {\n ConversationListFetcher,\n ConversationListFetchParams,\n ConversationListFilter,\n ConversationListPage,\n ConversationPullRequest,\n ConversationSummary,\n ConversationWake,\n} from '@athenaintel/agent-runtime';\nimport { type CredentialResolver, withCredentialRenewal } from '../runtime/credential-renewal';\nimport { listThreads, type ThreadPullRequest, type ThreadSummary, type ThreadWake } from './api';\n\nexport interface ThreadListRequestBody {\n limit: number;\n offset: number;\n exclude_triggered?: boolean;\n app_id?: string;\n start_channel?: string;\n search?: string;\n}\n\n/**\n * Map the canonical filter onto the REST thread-list request body.\n * Exported for tests; throws on filter fields the endpoint cannot express.\n */\nexport function threadListRequestBody(\n params: Pick<ConversationListFetchParams, 'limit' | 'offset' | 'filter'>,\n): ThreadListRequestBody {\n const { filter } = params;\n const unsupported: string[] = [];\n if (filter.assetTypes && filter.assetTypes.length > 0) unsupported.push('assetTypes');\n if (filter.states && filter.states.length > 0) unsupported.push('states');\n if (filter.sourceChannels && filter.sourceChannels.length > 1) {\n unsupported.push('sourceChannels (more than one)');\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n unsupported.push('excludeSourceChannels');\n }\n if (filter.computerAssetId) unsupported.push('computerAssetId');\n if (unsupported.length > 0) {\n throw new Error(\n `[AthenaSDK] The REST thread list cannot express filter field(s): ${unsupported.join(', ')}`,\n );\n }\n return {\n limit: params.limit,\n offset: params.offset,\n ...(filter.excludeTriggered !== undefined\n ? { exclude_triggered: filter.excludeTriggered }\n : {}),\n ...(filter.appId ? { app_id: filter.appId } : {}),\n ...(filter.sourceChannels?.[0] ? { start_channel: filter.sourceChannels[0] } : {}),\n ...(filter.search ? { search: filter.search } : {}),\n };\n}\n\n/** Map one REST pull-request record onto the canonical camelCase shape. */\nexport function conversationPullRequestFromThreadPullRequest(\n record: ThreadPullRequest,\n): ConversationPullRequest {\n return {\n repoFullName: record.repo_full_name,\n number: record.number,\n url: record.url,\n title: record.title,\n state: record.state,\n isDraft: record.is_draft,\n reviewDecision: record.review_decision ?? null,\n checksState: record.checks_state,\n updatedAt: record.updated_at,\n };\n}\n\nfunction conversationWakeFromThreadWake(wake: ThreadWake): ConversationWake {\n return { source: wake.source ?? null, reason: wake.reason ?? null, at: wake.at ?? null };\n}\n\n/**\n * Map a REST `ThreadSummary` row into the canonical contract shape. Fields\n * the server did not send stay `undefined` (an older server, or a list that\n * did not opt into pull requests), so a consumer can tell \"not reported\"\n * from \"reported as none\".\n */\nexport function conversationSummaryFromThreadSummary(\n thread: ThreadSummary,\n filter: ConversationListFilter,\n): ConversationSummary {\n return {\n threadId: thread.thread_id,\n title: thread.title,\n updatedAt: thread.updated_at,\n createdAt: thread.created_at ?? null,\n state: thread.state,\n // Not reported by this endpoint.\n runtime: null,\n sourceChannel: thread.start_channel ?? null,\n assetType: null,\n assetId: null,\n appId: thread.app_id ?? filter.appId ?? null,\n triggerType: thread.trigger_type ?? null,\n preview: thread.last_message ?? null,\n messageCount: thread.message_count ?? null,\n isUnread: thread.is_unread ?? false,\n // Older servers omit the status planes; `isConversationRunning` then falls back to `state`.\n sessionStatus: thread.session_status ?? null,\n runStatus: thread.run_status ?? null,\n attention: thread.attention,\n nextAction: thread.next_action,\n attentionSource: thread.attention_source,\n isFavorited: thread.is_favorited,\n lastWake:\n thread.last_wake === undefined || thread.last_wake === null\n ? thread.last_wake\n : conversationWakeFromThreadWake(thread.last_wake),\n pullRequests:\n thread.pull_requests === undefined || thread.pull_requests === null\n ? thread.pull_requests\n : thread.pull_requests.map(conversationPullRequestFromThreadPullRequest),\n };\n}\n\nexport interface RestConversationListFetcherOptions {\n backendUrl: string;\n /**\n * Resolved per request so rotating tokens never go stale in the fetcher;\n * called again with `{ force: true }` when the host refuses the token.\n */\n getAuth: CredentialResolver<{ apiKey?: string; token?: string | null }>;\n /**\n * Ask the server for live GitHub state on each row's `pullRequests`\n * (`include_pull_requests`). Off by default: it costs the server a lookup,\n * so only a list that renders pull-request chips opts in.\n */\n includePullRequests?: boolean;\n}\n\n/**\n * Build a {@link ConversationListFetcher} over the REST thread list. The\n * endpoint reports no `hasMore`/total, so `hasMore` is the standard offset\n * heuristic (a full page implies more may exist) and `totalCount` is null.\n */\nexport function createRestConversationListFetcher(\n options: RestConversationListFetcherOptions,\n): ConversationListFetcher {\n return async (params): Promise<ConversationListPage> => {\n const body = threadListRequestBody(params);\n const { threads, has_more: hasMore } = await withCredentialRenewal(options.getAuth, (auth) =>\n listThreads(options.backendUrl, auth, {\n ...body,\n ...(options.includePullRequests ? { include_pull_requests: true } : {}),\n signal: params.signal,\n }),\n );\n return {\n items: threads.map((thread) => conversationSummaryFromThreadSummary(thread, params.filter)),\n hasMore: hasMore ?? threads.length >= params.limit,\n totalCount: null,\n };\n };\n}\n"],"names":[],"mappings":"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"}
@@ -58,15 +58,22 @@ function pullRequestChipState(pullRequest) {
58
58
  if (pullRequest.state === "CLOSED") return "closed";
59
59
  return pullRequest.isDraft ? "draft" : "open";
60
60
  }
61
- function primaryPullRequest(pullRequests) {
62
- const rank = (pullRequest) => pullRequest.state === "OPEN" ? pullRequest.isDraft ? 1 : 0 : 2;
61
+ function pullRequestRank(pullRequest) {
62
+ return pullRequest.state === "OPEN" ? pullRequest.isDraft ? 1 : 0 : 2;
63
+ }
64
+ function updatedAtMs(pullRequest) {
65
+ const timestamp = Date.parse(pullRequest.updatedAt);
66
+ return Number.isFinite(timestamp) ? timestamp : 0;
67
+ }
68
+ function orderedPullRequests(pullRequests) {
69
+ if (!pullRequests) return [];
63
70
  return [...pullRequests].sort(
64
- (a, b) => rank(a) - rank(b) || Date.parse(b.updatedAt) - Date.parse(a.updatedAt)
65
- )[0];
71
+ (a, b) => pullRequestRank(a) - pullRequestRank(b) || updatedAtMs(b) - updatedAtMs(a)
72
+ );
66
73
  }
67
74
  function pullRequestChip(pullRequests) {
68
75
  if (!pullRequests || pullRequests.length === 0) return null;
69
- const primary = primaryPullRequest(pullRequests);
76
+ const primary = orderedPullRequests(pullRequests)[0];
70
77
  if (!primary) return null;
71
78
  return {
72
79
  state: pullRequestChipState(primary),
@@ -120,6 +127,7 @@ function sessionOriginLabel(thread) {
120
127
  }
121
128
  exports.SESSION_LIST_GROUPS = SESSION_LIST_GROUPS;
122
129
  exports.groupSessions = groupSessions;
130
+ exports.orderedPullRequests = orderedPullRequests;
123
131
  exports.pullRequestChip = pullRequestChip;
124
132
  exports.pullRequestChipState = pullRequestChipState;
125
133
  exports.relativeTime = relativeTime;
@@ -1 +1 @@
1
- {"version":3,"file":"session-list-model.cjs","sources":["../../src/threads/session-list-model.ts"],"sourcesContent":["/**\n * Pure derivations behind `SessionList`: which group a session lands in,\n * which status indicator it shows, what its second line says, how its pull\n * requests collapse into one chip, and how its origin is labelled. Kept\n * React-free so the render tests stay thin and hosts that draw their own rows\n * (`useAthenaSessionAttention`) can reuse the same rules.\n */\nimport type { ConversationPullRequest } from '@athenaintel/agent-runtime';\nimport type { AthenaStatewireThreadSummary } from './statewire-thread-list';\n\nexport type SessionListGroup = 'Needs you' | 'Working' | 'Pinned' | 'Recent';\n\n/** Display order; a session appears once, in the FIRST group that applies. */\nexport const SESSION_LIST_GROUPS: readonly SessionListGroup[] = [\n 'Needs you',\n 'Working',\n 'Pinned',\n 'Recent',\n];\n\nexport function sessionListGroupOf(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'isFavorited'>,\n): SessionListGroup {\n if (thread.attention === 'needs_you') return 'Needs you';\n if (thread.attention === 'working') return 'Working';\n if (thread.isFavorited) return 'Pinned';\n return 'Recent';\n}\n\nexport interface SessionListSection {\n /** `null` for the single ungrouped section. */\n group: SessionListGroup | null;\n items: AthenaStatewireThreadSummary[];\n}\n\n/** Sections in display order; empty groups are omitted and row order is kept within a group. */\nexport function groupSessions(\n threads: readonly AthenaStatewireThreadSummary[],\n grouped: boolean,\n): SessionListSection[] {\n if (!grouped) return threads.length > 0 ? [{ group: null, items: [...threads] }] : [];\n const byGroup = new Map<SessionListGroup, AthenaStatewireThreadSummary[]>();\n for (const thread of threads) {\n const group = sessionListGroupOf(thread);\n const items = byGroup.get(group) ?? [];\n items.push(thread);\n byGroup.set(group, items);\n }\n return SESSION_LIST_GROUPS.flatMap((group) => {\n const items = byGroup.get(group);\n return items ? [{ group, items }] : [];\n });\n}\n\n/**\n * The status indicator at the head of a row: `needs_you` an orange dot,\n * `working` a spinner, `unread` (and not needs-you) a blue dot, `watching` a\n * clock, otherwise none. One indicator per row, in that precedence.\n */\nexport type SessionIndicator = 'needs_you' | 'working' | 'unread' | 'watching' | null;\n\nexport function sessionIndicator(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'isUnread'>,\n): SessionIndicator {\n if (thread.attention === 'needs_you') return 'needs_you';\n if (thread.attention === 'working') return 'working';\n if (thread.isUnread) return 'unread';\n if (thread.attention === 'watching') return 'watching';\n return null;\n}\n\nexport type SessionLineTone = 'needs_you' | 'ready' | 'error' | 'muted';\n\nexport interface SessionSecondaryLine {\n kind: 'next_action' | 'updated';\n text: string;\n tone: SessionLineTone;\n}\n\n/** \"5m ago\" / \"3h ago\" / \"2d ago\"; a locale date past a week; empty for an unparseable value. */\nexport function relativeTime(value: string, now: number = Date.now()): string {\n const timestamp = Date.parse(value);\n if (!Number.isFinite(timestamp)) return '';\n const minutes = Math.floor(Math.max(0, now - timestamp) / 60_000);\n if (minutes < 1) return 'just now';\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n return days < 7 ? `${days}d ago` : new Date(timestamp).toLocaleDateString();\n}\n\n/**\n * The row's second line: the server's next action coloured by attention\n * (needs-you orange, ready green, error red), else the muted relative time\n * of the last update.\n */\nexport function sessionSecondaryLine(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'nextAction' | 'updatedAt'>,\n now: number = Date.now(),\n): SessionSecondaryLine {\n const nextAction = thread.nextAction?.trim();\n if (nextAction) {\n const tone: SessionLineTone =\n thread.attention === 'needs_you'\n ? 'needs_you'\n : thread.attention === 'ready'\n ? 'ready'\n : thread.attention === 'error'\n ? 'error'\n : 'muted';\n return { kind: 'next_action', text: nextAction, tone };\n }\n return { kind: 'updated', text: relativeTime(thread.updatedAt, now), tone: 'muted' };\n}\n\nexport type PullRequestChipState = 'open' | 'draft' | 'merged' | 'closed';\n\nexport function pullRequestChipState(\n pullRequest: Pick<ConversationPullRequest, 'state' | 'isDraft'>,\n): PullRequestChipState {\n if (pullRequest.state === 'MERGED') return 'merged';\n if (pullRequest.state === 'CLOSED') return 'closed';\n return pullRequest.isDraft ? 'draft' : 'open';\n}\n\nexport interface PullRequestChip {\n /** State of the primary pull request (the one the chip links to). */\n state: PullRequestChipState;\n count: number;\n /** `#123` for one pull request, `3 PRs` for several. */\n label: string;\n url: string;\n title: string;\n checksState: ConversationPullRequest['checksState'];\n reviewDecision: string | null;\n}\n\n/**\n * The pull request a chip stands for when a session opened several: the one\n * still in flight (open before draft), then the most recently updated.\n */\nfunction primaryPullRequest(\n pullRequests: readonly ConversationPullRequest[],\n): ConversationPullRequest | undefined {\n const rank = (pullRequest: ConversationPullRequest): number =>\n pullRequest.state === 'OPEN' ? (pullRequest.isDraft ? 1 : 0) : 2;\n return [...pullRequests].sort(\n (a, b) => rank(a) - rank(b) || Date.parse(b.updatedAt) - Date.parse(a.updatedAt),\n )[0];\n}\n\n/** One chip per row, or `null` when the session opened no pull request (or none is known). */\nexport function pullRequestChip(\n pullRequests: readonly ConversationPullRequest[] | null | undefined,\n): PullRequestChip | null {\n if (!pullRequests || pullRequests.length === 0) return null;\n const primary = primaryPullRequest(pullRequests);\n if (!primary) return null;\n return {\n state: pullRequestChipState(primary),\n count: pullRequests.length,\n label: pullRequests.length === 1 ? `#${primary.number}` : `${pullRequests.length} PRs`,\n url: primary.url,\n title: primary.title,\n checksState: primary.checksState,\n reviewDecision: primary.reviewDecision,\n };\n}\n\nconst CHANNEL_LABELS: Record<string, string> = {\n web: 'Athena',\n olympus: 'Athena',\n github: 'GitHub',\n api: 'API',\n email: 'Email',\n agent_email: 'Email',\n slack: 'Slack',\n sms: 'SMS',\n teams: 'Teams',\n task: 'Task',\n orchestration: 'Orchestration',\n computer: 'Computer',\n voice: 'Voice',\n mobile: 'Mobile',\n};\n\nfunction humanize(value: string): string {\n const words = value.replace(/^athena[-_]/, '').replace(/[-_.]+/g, ' ').trim();\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : '';\n}\n\n/**\n * The origin badge: the client app when it is a recognizable surface\n * (Chrome, an Office host, mobile, a computer asset), else the start channel,\n * else a non-manual trigger; `null` when nothing is known.\n */\nexport function sessionOriginLabel(\n thread: Pick<AthenaStatewireThreadSummary, 'startChannel' | 'appId' | 'triggerType'>,\n): string | null {\n const appId = thread.appId?.trim().toLowerCase() ?? '';\n if (appId) {\n if (appId.includes('chrome')) return 'Chrome';\n if (appId.includes('powerpoint')) return 'PowerPoint';\n if (appId.includes('word')) return 'Word';\n if (appId.includes('excel')) return 'Excel';\n if (appId.includes('office')) return 'Office';\n if (appId.includes('mobile')) return 'Mobile';\n if (appId.startsWith('asset_')) return 'Computer';\n if (CHANNEL_LABELS[appId]) return CHANNEL_LABELS[appId];\n }\n const channel = thread.startChannel?.trim().toLowerCase() ?? '';\n if (channel) return CHANNEL_LABELS[channel] ?? humanize(channel);\n if (appId) return humanize(appId) || null;\n const trigger = thread.triggerType?.trim().toLowerCase() ?? '';\n if (trigger && trigger !== 'manual') return humanize(trigger) || null;\n return null;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-list-model.cjs","sources":["../../src/threads/session-list-model.ts"],"sourcesContent":["/**\n * Pure derivations behind `SessionList`: which group a session lands in,\n * which status indicator it shows, what its second line says, how its pull\n * requests collapse into one chip (and order in the list it opens), and how\n * its origin is labelled. Kept React-free so the render tests stay thin and\n * hosts that draw their own rows (`useAthenaSessionAttention`) can reuse the\n * same rules.\n */\nimport type { ConversationPullRequest } from '@athenaintel/agent-runtime';\nimport type { AthenaStatewireThreadSummary } from './statewire-thread-list';\n\nexport type SessionListGroup = 'Needs you' | 'Working' | 'Pinned' | 'Recent';\n\n/** Display order; a session appears once, in the FIRST group that applies. */\nexport const SESSION_LIST_GROUPS: readonly SessionListGroup[] = [\n 'Needs you',\n 'Working',\n 'Pinned',\n 'Recent',\n];\n\nexport function sessionListGroupOf(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'isFavorited'>,\n): SessionListGroup {\n if (thread.attention === 'needs_you') return 'Needs you';\n if (thread.attention === 'working') return 'Working';\n if (thread.isFavorited) return 'Pinned';\n return 'Recent';\n}\n\nexport interface SessionListSection {\n /** `null` for the single ungrouped section. */\n group: SessionListGroup | null;\n items: AthenaStatewireThreadSummary[];\n}\n\n/** Sections in display order; empty groups are omitted and row order is kept within a group. */\nexport function groupSessions(\n threads: readonly AthenaStatewireThreadSummary[],\n grouped: boolean,\n): SessionListSection[] {\n if (!grouped) return threads.length > 0 ? [{ group: null, items: [...threads] }] : [];\n const byGroup = new Map<SessionListGroup, AthenaStatewireThreadSummary[]>();\n for (const thread of threads) {\n const group = sessionListGroupOf(thread);\n const items = byGroup.get(group) ?? [];\n items.push(thread);\n byGroup.set(group, items);\n }\n return SESSION_LIST_GROUPS.flatMap((group) => {\n const items = byGroup.get(group);\n return items ? [{ group, items }] : [];\n });\n}\n\n/**\n * The status indicator at the head of a row: `needs_you` an orange dot,\n * `working` a spinner, `unread` (and not needs-you) a blue dot, `watching` a\n * clock, otherwise none. One indicator per row, in that precedence.\n */\nexport type SessionIndicator = 'needs_you' | 'working' | 'unread' | 'watching' | null;\n\nexport function sessionIndicator(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'isUnread'>,\n): SessionIndicator {\n if (thread.attention === 'needs_you') return 'needs_you';\n if (thread.attention === 'working') return 'working';\n if (thread.isUnread) return 'unread';\n if (thread.attention === 'watching') return 'watching';\n return null;\n}\n\nexport type SessionLineTone = 'needs_you' | 'ready' | 'error' | 'muted';\n\nexport interface SessionSecondaryLine {\n kind: 'next_action' | 'updated';\n text: string;\n tone: SessionLineTone;\n}\n\n/** \"5m ago\" / \"3h ago\" / \"2d ago\"; a locale date past a week; empty for an unparseable value. */\nexport function relativeTime(value: string, now: number = Date.now()): string {\n const timestamp = Date.parse(value);\n if (!Number.isFinite(timestamp)) return '';\n const minutes = Math.floor(Math.max(0, now - timestamp) / 60_000);\n if (minutes < 1) return 'just now';\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n return days < 7 ? `${days}d ago` : new Date(timestamp).toLocaleDateString();\n}\n\n/**\n * The row's second line: the server's next action coloured by attention\n * (needs-you orange, ready green, error red), else the muted relative time\n * of the last update.\n */\nexport function sessionSecondaryLine(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'nextAction' | 'updatedAt'>,\n now: number = Date.now(),\n): SessionSecondaryLine {\n const nextAction = thread.nextAction?.trim();\n if (nextAction) {\n const tone: SessionLineTone =\n thread.attention === 'needs_you'\n ? 'needs_you'\n : thread.attention === 'ready'\n ? 'ready'\n : thread.attention === 'error'\n ? 'error'\n : 'muted';\n return { kind: 'next_action', text: nextAction, tone };\n }\n return { kind: 'updated', text: relativeTime(thread.updatedAt, now), tone: 'muted' };\n}\n\nexport type PullRequestChipState = 'open' | 'draft' | 'merged' | 'closed';\n\nexport function pullRequestChipState(\n pullRequest: Pick<ConversationPullRequest, 'state' | 'isDraft'>,\n): PullRequestChipState {\n if (pullRequest.state === 'MERGED') return 'merged';\n if (pullRequest.state === 'CLOSED') return 'closed';\n return pullRequest.isDraft ? 'draft' : 'open';\n}\n\nexport interface PullRequestChip {\n /** State of the primary pull request (the one the chip links to). */\n state: PullRequestChipState;\n count: number;\n /** `#123` for one pull request, `3 PRs` for several. */\n label: string;\n url: string;\n title: string;\n checksState: ConversationPullRequest['checksState'];\n reviewDecision: string | null;\n}\n\nfunction pullRequestRank(pullRequest: ConversationPullRequest): number {\n return pullRequest.state === 'OPEN' ? (pullRequest.isDraft ? 1 : 0) : 2;\n}\n\nfunction updatedAtMs(pullRequest: ConversationPullRequest): number {\n const timestamp = Date.parse(pullRequest.updatedAt);\n return Number.isFinite(timestamp) ? timestamp : 0;\n}\n\n/**\n * A session's pull requests in the order its chip and its pull-request list\n * show them: the ones still in flight first (open before draft), then the\n * most recently updated. The first is the one the chip stands for.\n */\nexport function orderedPullRequests(\n pullRequests: readonly ConversationPullRequest[] | null | undefined,\n): ConversationPullRequest[] {\n if (!pullRequests) return [];\n return [...pullRequests].sort(\n (a, b) => pullRequestRank(a) - pullRequestRank(b) || updatedAtMs(b) - updatedAtMs(a),\n );\n}\n\n/** One chip per row, or `null` when the session opened no pull request (or none is known). */\nexport function pullRequestChip(\n pullRequests: readonly ConversationPullRequest[] | null | undefined,\n): PullRequestChip | null {\n if (!pullRequests || pullRequests.length === 0) return null;\n const primary = orderedPullRequests(pullRequests)[0];\n if (!primary) return null;\n return {\n state: pullRequestChipState(primary),\n count: pullRequests.length,\n label: pullRequests.length === 1 ? `#${primary.number}` : `${pullRequests.length} PRs`,\n url: primary.url,\n title: primary.title,\n checksState: primary.checksState,\n reviewDecision: primary.reviewDecision,\n };\n}\n\nconst CHANNEL_LABELS: Record<string, string> = {\n web: 'Athena',\n olympus: 'Athena',\n github: 'GitHub',\n api: 'API',\n email: 'Email',\n agent_email: 'Email',\n slack: 'Slack',\n sms: 'SMS',\n teams: 'Teams',\n task: 'Task',\n orchestration: 'Orchestration',\n computer: 'Computer',\n voice: 'Voice',\n mobile: 'Mobile',\n};\n\nfunction humanize(value: string): string {\n const words = value.replace(/^athena[-_]/, '').replace(/[-_.]+/g, ' ').trim();\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : '';\n}\n\n/**\n * The origin badge: the client app when it is a recognizable surface\n * (Chrome, an Office host, mobile, a computer asset), else the start channel,\n * else a non-manual trigger; `null` when nothing is known.\n */\nexport function sessionOriginLabel(\n thread: Pick<AthenaStatewireThreadSummary, 'startChannel' | 'appId' | 'triggerType'>,\n): string | null {\n const appId = thread.appId?.trim().toLowerCase() ?? '';\n if (appId) {\n if (appId.includes('chrome')) return 'Chrome';\n if (appId.includes('powerpoint')) return 'PowerPoint';\n if (appId.includes('word')) return 'Word';\n if (appId.includes('excel')) return 'Excel';\n if (appId.includes('office')) return 'Office';\n if (appId.includes('mobile')) return 'Mobile';\n if (appId.startsWith('asset_')) return 'Computer';\n if (CHANNEL_LABELS[appId]) return CHANNEL_LABELS[appId];\n }\n const channel = thread.startChannel?.trim().toLowerCase() ?? '';\n if (channel) return CHANNEL_LABELS[channel] ?? humanize(channel);\n if (appId) return humanize(appId) || null;\n const trigger = thread.triggerType?.trim().toLowerCase() ?? '';\n if (trigger && trigger !== 'manual') return humanize(trigger) || null;\n return null;\n}\n"],"names":[],"mappings":";;AAcO,MAAM,sBAAmD;AAAA,EAC9D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,mBACd,QACkB;AAClB,MAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,MAAI,OAAO,cAAc,UAAW,QAAO;AAC3C,MAAI,OAAO,YAAa,QAAO;AAC/B,SAAO;AACT;AASO,SAAS,cACd,SACA,SACsB;AACtB,MAAI,CAAC,QAAS,QAAO,QAAQ,SAAS,IAAI,CAAC,EAAE,OAAO,MAAM,OAAO,CAAC,GAAG,OAAO,EAAA,CAAG,IAAI,CAAA;AACnF,QAAM,8BAAc,IAAA;AACpB,aAAW,UAAU,SAAS;AAC5B,UAAM,QAAQ,mBAAmB,MAAM;AACvC,UAAM,QAAQ,QAAQ,IAAI,KAAK,KAAK,CAAA;AACpC,UAAM,KAAK,MAAM;AACjB,YAAQ,IAAI,OAAO,KAAK;AAAA,EAC1B;AACA,SAAO,oBAAoB,QAAQ,CAAC,UAAU;AAC5C,UAAM,QAAQ,QAAQ,IAAI,KAAK;AAC/B,WAAO,QAAQ,CAAC,EAAE,OAAO,MAAA,CAAO,IAAI,CAAA;AAAA,EACtC,CAAC;AACH;AASO,SAAS,iBACd,QACkB;AAClB,MAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,MAAI,OAAO,cAAc,UAAW,QAAO;AAC3C,MAAI,OAAO,SAAU,QAAO;AAC5B,MAAI,OAAO,cAAc,WAAY,QAAO;AAC5C,SAAO;AACT;AAWO,SAAS,aAAa,OAAe,MAAc,KAAK,OAAe;AAC5E,QAAM,YAAY,KAAK,MAAM,KAAK;AAClC,MAAI,CAAC,OAAO,SAAS,SAAS,EAAG,QAAO;AACxC,QAAM,UAAU,KAAK,MAAM,KAAK,IAAI,GAAG,MAAM,SAAS,IAAI,GAAM;AAChE,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,GAAI,QAAO,GAAG,OAAO;AACnC,QAAM,QAAQ,KAAK,MAAM,UAAU,EAAE;AACrC,MAAI,QAAQ,GAAI,QAAO,GAAG,KAAK;AAC/B,QAAM,OAAO,KAAK,MAAM,QAAQ,EAAE;AAClC,SAAO,OAAO,IAAI,GAAG,IAAI,UAAU,IAAI,KAAK,SAAS,EAAE,mBAAA;AACzD;AAOO,SAAS,qBACd,QACA,MAAc,KAAK,OACG;;AACtB,QAAM,cAAa,YAAO,eAAP,mBAAmB;AACtC,MAAI,YAAY;AACd,UAAM,OACJ,OAAO,cAAc,cACjB,cACA,OAAO,cAAc,UACnB,UACA,OAAO,cAAc,UACnB,UACA;AACV,WAAO,EAAE,MAAM,eAAe,MAAM,YAAY,KAAA;AAAA,EAClD;AACA,SAAO,EAAE,MAAM,WAAW,MAAM,aAAa,OAAO,WAAW,GAAG,GAAG,MAAM,QAAA;AAC7E;AAIO,SAAS,qBACd,aACsB;AACtB,MAAI,YAAY,UAAU,SAAU,QAAO;AAC3C,MAAI,YAAY,UAAU,SAAU,QAAO;AAC3C,SAAO,YAAY,UAAU,UAAU;AACzC;AAcA,SAAS,gBAAgB,aAA8C;AACrE,SAAO,YAAY,UAAU,SAAU,YAAY,UAAU,IAAI,IAAK;AACxE;AAEA,SAAS,YAAY,aAA8C;AACjE,QAAM,YAAY,KAAK,MAAM,YAAY,SAAS;AAClD,SAAO,OAAO,SAAS,SAAS,IAAI,YAAY;AAClD;AAOO,SAAS,oBACd,cAC2B;AAC3B,MAAI,CAAC,aAAc,QAAO,CAAA;AAC1B,SAAO,CAAC,GAAG,YAAY,EAAE;AAAA,IACvB,CAAC,GAAG,MAAM,gBAAgB,CAAC,IAAI,gBAAgB,CAAC,KAAK,YAAY,CAAC,IAAI,YAAY,CAAC;AAAA,EAAA;AAEvF;AAGO,SAAS,gBACd,cACwB;AACxB,MAAI,CAAC,gBAAgB,aAAa,WAAW,EAAG,QAAO;AACvD,QAAM,UAAU,oBAAoB,YAAY,EAAE,CAAC;AACnD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO;AAAA,IACL,OAAO,qBAAqB,OAAO;AAAA,IACnC,OAAO,aAAa;AAAA,IACpB,OAAO,aAAa,WAAW,IAAI,IAAI,QAAQ,MAAM,KAAK,GAAG,aAAa,MAAM;AAAA,IAChF,KAAK,QAAQ;AAAA,IACb,OAAO,QAAQ;AAAA,IACf,aAAa,QAAQ;AAAA,IACrB,gBAAgB,QAAQ;AAAA,EAAA;AAE5B;AAEA,MAAM,iBAAyC;AAAA,EAC7C,KAAK;AAAA,EACL,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,OAAO;AAAA,EACP,aAAa;AAAA,EACb,OAAO;AAAA,EACP,KAAK;AAAA,EACL,OAAO;AAAA,EACP,MAAM;AAAA,EACN,eAAe;AAAA,EACf,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AACV;AAEA,SAAS,SAAS,OAAuB;AACvC,QAAM,QAAQ,MAAM,QAAQ,eAAe,EAAE,EAAE,QAAQ,WAAW,GAAG,EAAE,KAAA;AACvE,SAAO,QAAQ,MAAM,OAAO,CAAC,EAAE,gBAAgB,MAAM,MAAM,CAAC,IAAI;AAClE;AAOO,SAAS,mBACd,QACe;;AACf,QAAM,UAAQ,YAAO,UAAP,mBAAc,OAAO,kBAAiB;AACpD,MAAI,OAAO;AACT,QAAI,MAAM,SAAS,QAAQ,EAAG,QAAO;AACrC,QAAI,MAAM,SAAS,YAAY,EAAG,QAAO;AACzC,QAAI,MAAM,SAAS,MAAM,EAAG,QAAO;AACnC,QAAI,MAAM,SAAS,OAAO,EAAG,QAAO;AACpC,QAAI,MAAM,SAAS,QAAQ,EAAG,QAAO;AACrC,QAAI,MAAM,SAAS,QAAQ,EAAG,QAAO;AACrC,QAAI,MAAM,WAAW,QAAQ,EAAG,QAAO;AACvC,QAAI,eAAe,KAAK,EAAG,QAAO,eAAe,KAAK;AAAA,EACxD;AACA,QAAM,YAAU,YAAO,iBAAP,mBAAqB,OAAO,kBAAiB;AAC7D,MAAI,QAAS,QAAO,eAAe,OAAO,KAAK,SAAS,OAAO;AAC/D,MAAI,MAAO,QAAO,SAAS,KAAK,KAAK;AACrC,QAAM,YAAU,YAAO,gBAAP,mBAAoB,OAAO,kBAAiB;AAC5D,MAAI,WAAW,YAAY,SAAU,QAAO,SAAS,OAAO,KAAK;AACjE,SAAO;AACT;;;;;;;;;;;"}
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Pure derivations behind `SessionList`: which group a session lands in,
3
3
  * which status indicator it shows, what its second line says, how its pull
4
- * requests collapse into one chip, and how its origin is labelled. Kept
5
- * React-free so the render tests stay thin and hosts that draw their own rows
6
- * (`useAthenaSessionAttention`) can reuse the same rules.
4
+ * requests collapse into one chip (and order in the list it opens), and how
5
+ * its origin is labelled. Kept React-free so the render tests stay thin and
6
+ * hosts that draw their own rows (`useAthenaSessionAttention`) can reuse the
7
+ * same rules.
7
8
  */
8
9
  import type { ConversationPullRequest } from '@athenaintel/agent-runtime';
9
10
  import type { AthenaStatewireThreadSummary } from './statewire-thread-list';
@@ -52,6 +53,12 @@ export interface PullRequestChip {
52
53
  checksState: ConversationPullRequest['checksState'];
53
54
  reviewDecision: string | null;
54
55
  }
56
+ /**
57
+ * A session's pull requests in the order its chip and its pull-request list
58
+ * show them: the ones still in flight first (open before draft), then the
59
+ * most recently updated. The first is the one the chip stands for.
60
+ */
61
+ export declare function orderedPullRequests(pullRequests: readonly ConversationPullRequest[] | null | undefined): ConversationPullRequest[];
55
62
  /** One chip per row, or `null` when the session opened no pull request (or none is known). */
56
63
  export declare function pullRequestChip(pullRequests: readonly ConversationPullRequest[] | null | undefined): PullRequestChip | null;
57
64
  /**
@@ -56,15 +56,22 @@ function pullRequestChipState(pullRequest) {
56
56
  if (pullRequest.state === "CLOSED") return "closed";
57
57
  return pullRequest.isDraft ? "draft" : "open";
58
58
  }
59
- function primaryPullRequest(pullRequests) {
60
- const rank = (pullRequest) => pullRequest.state === "OPEN" ? pullRequest.isDraft ? 1 : 0 : 2;
59
+ function pullRequestRank(pullRequest) {
60
+ return pullRequest.state === "OPEN" ? pullRequest.isDraft ? 1 : 0 : 2;
61
+ }
62
+ function updatedAtMs(pullRequest) {
63
+ const timestamp = Date.parse(pullRequest.updatedAt);
64
+ return Number.isFinite(timestamp) ? timestamp : 0;
65
+ }
66
+ function orderedPullRequests(pullRequests) {
67
+ if (!pullRequests) return [];
61
68
  return [...pullRequests].sort(
62
- (a, b) => rank(a) - rank(b) || Date.parse(b.updatedAt) - Date.parse(a.updatedAt)
63
- )[0];
69
+ (a, b) => pullRequestRank(a) - pullRequestRank(b) || updatedAtMs(b) - updatedAtMs(a)
70
+ );
64
71
  }
65
72
  function pullRequestChip(pullRequests) {
66
73
  if (!pullRequests || pullRequests.length === 0) return null;
67
- const primary = primaryPullRequest(pullRequests);
74
+ const primary = orderedPullRequests(pullRequests)[0];
68
75
  if (!primary) return null;
69
76
  return {
70
77
  state: pullRequestChipState(primary),
@@ -119,6 +126,7 @@ function sessionOriginLabel(thread) {
119
126
  export {
120
127
  SESSION_LIST_GROUPS,
121
128
  groupSessions,
129
+ orderedPullRequests,
122
130
  pullRequestChip,
123
131
  pullRequestChipState,
124
132
  relativeTime,
@@ -1 +1 @@
1
- {"version":3,"file":"session-list-model.js","sources":["../../src/threads/session-list-model.ts"],"sourcesContent":["/**\n * Pure derivations behind `SessionList`: which group a session lands in,\n * which status indicator it shows, what its second line says, how its pull\n * requests collapse into one chip, and how its origin is labelled. Kept\n * React-free so the render tests stay thin and hosts that draw their own rows\n * (`useAthenaSessionAttention`) can reuse the same rules.\n */\nimport type { ConversationPullRequest } from '@athenaintel/agent-runtime';\nimport type { AthenaStatewireThreadSummary } from './statewire-thread-list';\n\nexport type SessionListGroup = 'Needs you' | 'Working' | 'Pinned' | 'Recent';\n\n/** Display order; a session appears once, in the FIRST group that applies. */\nexport const SESSION_LIST_GROUPS: readonly SessionListGroup[] = [\n 'Needs you',\n 'Working',\n 'Pinned',\n 'Recent',\n];\n\nexport function sessionListGroupOf(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'isFavorited'>,\n): SessionListGroup {\n if (thread.attention === 'needs_you') return 'Needs you';\n if (thread.attention === 'working') return 'Working';\n if (thread.isFavorited) return 'Pinned';\n return 'Recent';\n}\n\nexport interface SessionListSection {\n /** `null` for the single ungrouped section. */\n group: SessionListGroup | null;\n items: AthenaStatewireThreadSummary[];\n}\n\n/** Sections in display order; empty groups are omitted and row order is kept within a group. */\nexport function groupSessions(\n threads: readonly AthenaStatewireThreadSummary[],\n grouped: boolean,\n): SessionListSection[] {\n if (!grouped) return threads.length > 0 ? [{ group: null, items: [...threads] }] : [];\n const byGroup = new Map<SessionListGroup, AthenaStatewireThreadSummary[]>();\n for (const thread of threads) {\n const group = sessionListGroupOf(thread);\n const items = byGroup.get(group) ?? [];\n items.push(thread);\n byGroup.set(group, items);\n }\n return SESSION_LIST_GROUPS.flatMap((group) => {\n const items = byGroup.get(group);\n return items ? [{ group, items }] : [];\n });\n}\n\n/**\n * The status indicator at the head of a row: `needs_you` an orange dot,\n * `working` a spinner, `unread` (and not needs-you) a blue dot, `watching` a\n * clock, otherwise none. One indicator per row, in that precedence.\n */\nexport type SessionIndicator = 'needs_you' | 'working' | 'unread' | 'watching' | null;\n\nexport function sessionIndicator(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'isUnread'>,\n): SessionIndicator {\n if (thread.attention === 'needs_you') return 'needs_you';\n if (thread.attention === 'working') return 'working';\n if (thread.isUnread) return 'unread';\n if (thread.attention === 'watching') return 'watching';\n return null;\n}\n\nexport type SessionLineTone = 'needs_you' | 'ready' | 'error' | 'muted';\n\nexport interface SessionSecondaryLine {\n kind: 'next_action' | 'updated';\n text: string;\n tone: SessionLineTone;\n}\n\n/** \"5m ago\" / \"3h ago\" / \"2d ago\"; a locale date past a week; empty for an unparseable value. */\nexport function relativeTime(value: string, now: number = Date.now()): string {\n const timestamp = Date.parse(value);\n if (!Number.isFinite(timestamp)) return '';\n const minutes = Math.floor(Math.max(0, now - timestamp) / 60_000);\n if (minutes < 1) return 'just now';\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n return days < 7 ? `${days}d ago` : new Date(timestamp).toLocaleDateString();\n}\n\n/**\n * The row's second line: the server's next action coloured by attention\n * (needs-you orange, ready green, error red), else the muted relative time\n * of the last update.\n */\nexport function sessionSecondaryLine(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'nextAction' | 'updatedAt'>,\n now: number = Date.now(),\n): SessionSecondaryLine {\n const nextAction = thread.nextAction?.trim();\n if (nextAction) {\n const tone: SessionLineTone =\n thread.attention === 'needs_you'\n ? 'needs_you'\n : thread.attention === 'ready'\n ? 'ready'\n : thread.attention === 'error'\n ? 'error'\n : 'muted';\n return { kind: 'next_action', text: nextAction, tone };\n }\n return { kind: 'updated', text: relativeTime(thread.updatedAt, now), tone: 'muted' };\n}\n\nexport type PullRequestChipState = 'open' | 'draft' | 'merged' | 'closed';\n\nexport function pullRequestChipState(\n pullRequest: Pick<ConversationPullRequest, 'state' | 'isDraft'>,\n): PullRequestChipState {\n if (pullRequest.state === 'MERGED') return 'merged';\n if (pullRequest.state === 'CLOSED') return 'closed';\n return pullRequest.isDraft ? 'draft' : 'open';\n}\n\nexport interface PullRequestChip {\n /** State of the primary pull request (the one the chip links to). */\n state: PullRequestChipState;\n count: number;\n /** `#123` for one pull request, `3 PRs` for several. */\n label: string;\n url: string;\n title: string;\n checksState: ConversationPullRequest['checksState'];\n reviewDecision: string | null;\n}\n\n/**\n * The pull request a chip stands for when a session opened several: the one\n * still in flight (open before draft), then the most recently updated.\n */\nfunction primaryPullRequest(\n pullRequests: readonly ConversationPullRequest[],\n): ConversationPullRequest | undefined {\n const rank = (pullRequest: ConversationPullRequest): number =>\n pullRequest.state === 'OPEN' ? (pullRequest.isDraft ? 1 : 0) : 2;\n return [...pullRequests].sort(\n (a, b) => rank(a) - rank(b) || Date.parse(b.updatedAt) - Date.parse(a.updatedAt),\n )[0];\n}\n\n/** One chip per row, or `null` when the session opened no pull request (or none is known). */\nexport function pullRequestChip(\n pullRequests: readonly ConversationPullRequest[] | null | undefined,\n): PullRequestChip | null {\n if (!pullRequests || pullRequests.length === 0) return null;\n const primary = primaryPullRequest(pullRequests);\n if (!primary) return null;\n return {\n state: pullRequestChipState(primary),\n count: pullRequests.length,\n label: pullRequests.length === 1 ? `#${primary.number}` : `${pullRequests.length} PRs`,\n url: primary.url,\n title: primary.title,\n checksState: primary.checksState,\n reviewDecision: primary.reviewDecision,\n };\n}\n\nconst CHANNEL_LABELS: Record<string, string> = {\n web: 'Athena',\n olympus: 'Athena',\n github: 'GitHub',\n api: 'API',\n email: 'Email',\n agent_email: 'Email',\n slack: 'Slack',\n sms: 'SMS',\n teams: 'Teams',\n task: 'Task',\n orchestration: 'Orchestration',\n computer: 'Computer',\n voice: 'Voice',\n mobile: 'Mobile',\n};\n\nfunction humanize(value: string): string {\n const words = value.replace(/^athena[-_]/, '').replace(/[-_.]+/g, ' ').trim();\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : '';\n}\n\n/**\n * The origin badge: the client app when it is a recognizable surface\n * (Chrome, an Office host, mobile, a computer asset), else the start channel,\n * else a non-manual trigger; `null` when nothing is known.\n */\nexport function sessionOriginLabel(\n thread: Pick<AthenaStatewireThreadSummary, 'startChannel' | 'appId' | 'triggerType'>,\n): string | null {\n const appId = thread.appId?.trim().toLowerCase() ?? '';\n if (appId) {\n if (appId.includes('chrome')) return 'Chrome';\n if (appId.includes('powerpoint')) return 'PowerPoint';\n if (appId.includes('word')) return 'Word';\n if (appId.includes('excel')) return 'Excel';\n if (appId.includes('office')) return 'Office';\n if (appId.includes('mobile')) return 'Mobile';\n if (appId.startsWith('asset_')) return 'Computer';\n if (CHANNEL_LABELS[appId]) return CHANNEL_LABELS[appId];\n }\n const channel = thread.startChannel?.trim().toLowerCase() ?? '';\n if (channel) return CHANNEL_LABELS[channel] ?? humanize(channel);\n if (appId) return humanize(appId) || null;\n const trigger = thread.triggerType?.trim().toLowerCase() ?? '';\n if (trigger && trigger !== 'manual') return humanize(trigger) || null;\n return null;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-list-model.js","sources":["../../src/threads/session-list-model.ts"],"sourcesContent":["/**\n * Pure derivations behind `SessionList`: which group a session lands in,\n * which status indicator it shows, what its second line says, how its pull\n * requests collapse into one chip (and order in the list it opens), and how\n * its origin is labelled. Kept React-free so the render tests stay thin and\n * hosts that draw their own rows (`useAthenaSessionAttention`) can reuse the\n * same rules.\n */\nimport type { ConversationPullRequest } from '@athenaintel/agent-runtime';\nimport type { AthenaStatewireThreadSummary } from './statewire-thread-list';\n\nexport type SessionListGroup = 'Needs you' | 'Working' | 'Pinned' | 'Recent';\n\n/** Display order; a session appears once, in the FIRST group that applies. */\nexport const SESSION_LIST_GROUPS: readonly SessionListGroup[] = [\n 'Needs you',\n 'Working',\n 'Pinned',\n 'Recent',\n];\n\nexport function sessionListGroupOf(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'isFavorited'>,\n): SessionListGroup {\n if (thread.attention === 'needs_you') return 'Needs you';\n if (thread.attention === 'working') return 'Working';\n if (thread.isFavorited) return 'Pinned';\n return 'Recent';\n}\n\nexport interface SessionListSection {\n /** `null` for the single ungrouped section. */\n group: SessionListGroup | null;\n items: AthenaStatewireThreadSummary[];\n}\n\n/** Sections in display order; empty groups are omitted and row order is kept within a group. */\nexport function groupSessions(\n threads: readonly AthenaStatewireThreadSummary[],\n grouped: boolean,\n): SessionListSection[] {\n if (!grouped) return threads.length > 0 ? [{ group: null, items: [...threads] }] : [];\n const byGroup = new Map<SessionListGroup, AthenaStatewireThreadSummary[]>();\n for (const thread of threads) {\n const group = sessionListGroupOf(thread);\n const items = byGroup.get(group) ?? [];\n items.push(thread);\n byGroup.set(group, items);\n }\n return SESSION_LIST_GROUPS.flatMap((group) => {\n const items = byGroup.get(group);\n return items ? [{ group, items }] : [];\n });\n}\n\n/**\n * The status indicator at the head of a row: `needs_you` an orange dot,\n * `working` a spinner, `unread` (and not needs-you) a blue dot, `watching` a\n * clock, otherwise none. One indicator per row, in that precedence.\n */\nexport type SessionIndicator = 'needs_you' | 'working' | 'unread' | 'watching' | null;\n\nexport function sessionIndicator(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'isUnread'>,\n): SessionIndicator {\n if (thread.attention === 'needs_you') return 'needs_you';\n if (thread.attention === 'working') return 'working';\n if (thread.isUnread) return 'unread';\n if (thread.attention === 'watching') return 'watching';\n return null;\n}\n\nexport type SessionLineTone = 'needs_you' | 'ready' | 'error' | 'muted';\n\nexport interface SessionSecondaryLine {\n kind: 'next_action' | 'updated';\n text: string;\n tone: SessionLineTone;\n}\n\n/** \"5m ago\" / \"3h ago\" / \"2d ago\"; a locale date past a week; empty for an unparseable value. */\nexport function relativeTime(value: string, now: number = Date.now()): string {\n const timestamp = Date.parse(value);\n if (!Number.isFinite(timestamp)) return '';\n const minutes = Math.floor(Math.max(0, now - timestamp) / 60_000);\n if (minutes < 1) return 'just now';\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n return days < 7 ? `${days}d ago` : new Date(timestamp).toLocaleDateString();\n}\n\n/**\n * The row's second line: the server's next action coloured by attention\n * (needs-you orange, ready green, error red), else the muted relative time\n * of the last update.\n */\nexport function sessionSecondaryLine(\n thread: Pick<AthenaStatewireThreadSummary, 'attention' | 'nextAction' | 'updatedAt'>,\n now: number = Date.now(),\n): SessionSecondaryLine {\n const nextAction = thread.nextAction?.trim();\n if (nextAction) {\n const tone: SessionLineTone =\n thread.attention === 'needs_you'\n ? 'needs_you'\n : thread.attention === 'ready'\n ? 'ready'\n : thread.attention === 'error'\n ? 'error'\n : 'muted';\n return { kind: 'next_action', text: nextAction, tone };\n }\n return { kind: 'updated', text: relativeTime(thread.updatedAt, now), tone: 'muted' };\n}\n\nexport type PullRequestChipState = 'open' | 'draft' | 'merged' | 'closed';\n\nexport function pullRequestChipState(\n pullRequest: Pick<ConversationPullRequest, 'state' | 'isDraft'>,\n): PullRequestChipState {\n if (pullRequest.state === 'MERGED') return 'merged';\n if (pullRequest.state === 'CLOSED') return 'closed';\n return pullRequest.isDraft ? 'draft' : 'open';\n}\n\nexport interface PullRequestChip {\n /** State of the primary pull request (the one the chip links to). */\n state: PullRequestChipState;\n count: number;\n /** `#123` for one pull request, `3 PRs` for several. */\n label: string;\n url: string;\n title: string;\n checksState: ConversationPullRequest['checksState'];\n reviewDecision: string | null;\n}\n\nfunction pullRequestRank(pullRequest: ConversationPullRequest): number {\n return pullRequest.state === 'OPEN' ? (pullRequest.isDraft ? 1 : 0) : 2;\n}\n\nfunction updatedAtMs(pullRequest: ConversationPullRequest): number {\n const timestamp = Date.parse(pullRequest.updatedAt);\n return Number.isFinite(timestamp) ? timestamp : 0;\n}\n\n/**\n * A session's pull requests in the order its chip and its pull-request list\n * show them: the ones still in flight first (open before draft), then the\n * most recently updated. The first is the one the chip stands for.\n */\nexport function orderedPullRequests(\n pullRequests: readonly ConversationPullRequest[] | null | undefined,\n): ConversationPullRequest[] {\n if (!pullRequests) return [];\n return [...pullRequests].sort(\n (a, b) => pullRequestRank(a) - pullRequestRank(b) || updatedAtMs(b) - updatedAtMs(a),\n );\n}\n\n/** One chip per row, or `null` when the session opened no pull request (or none is known). */\nexport function pullRequestChip(\n pullRequests: readonly ConversationPullRequest[] | null | undefined,\n): PullRequestChip | null {\n if (!pullRequests || pullRequests.length === 0) return null;\n const primary = orderedPullRequests(pullRequests)[0];\n if (!primary) return null;\n return {\n state: pullRequestChipState(primary),\n count: pullRequests.length,\n label: pullRequests.length === 1 ? `#${primary.number}` : `${pullRequests.length} PRs`,\n url: primary.url,\n title: primary.title,\n checksState: primary.checksState,\n reviewDecision: primary.reviewDecision,\n };\n}\n\nconst CHANNEL_LABELS: Record<string, string> = {\n web: 'Athena',\n olympus: 'Athena',\n github: 'GitHub',\n api: 'API',\n email: 'Email',\n agent_email: 'Email',\n slack: 'Slack',\n sms: 'SMS',\n teams: 'Teams',\n task: 'Task',\n orchestration: 'Orchestration',\n computer: 'Computer',\n voice: 'Voice',\n mobile: 'Mobile',\n};\n\nfunction humanize(value: string): string {\n const words = value.replace(/^athena[-_]/, '').replace(/[-_.]+/g, ' ').trim();\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : '';\n}\n\n/**\n * The origin badge: the client app when it is a recognizable surface\n * (Chrome, an Office host, mobile, a computer asset), else the start channel,\n * else a non-manual trigger; `null` when nothing is known.\n */\nexport function sessionOriginLabel(\n thread: Pick<AthenaStatewireThreadSummary, 'startChannel' | 'appId' | 'triggerType'>,\n): string | null {\n const appId = thread.appId?.trim().toLowerCase() ?? '';\n if (appId) {\n if (appId.includes('chrome')) return 'Chrome';\n if (appId.includes('powerpoint')) return 'PowerPoint';\n if (appId.includes('word')) return 'Word';\n if (appId.includes('excel')) return 'Excel';\n if (appId.includes('office')) return 'Office';\n if (appId.includes('mobile')) return 'Mobile';\n if (appId.startsWith('asset_')) return 'Computer';\n if (CHANNEL_LABELS[appId]) return CHANNEL_LABELS[appId];\n }\n const channel = thread.startChannel?.trim().toLowerCase() ?? '';\n if (channel) return CHANNEL_LABELS[channel] ?? humanize(channel);\n if (appId) return humanize(appId) || null;\n const trigger = thread.triggerType?.trim().toLowerCase() ?? '';\n if (trigger && trigger !== 'manual') return humanize(trigger) || null;\n return null;\n}\n"],"names":[],"mappings":"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"}
@@ -13,6 +13,8 @@ const mintThreadId = require("./mint-thread-id.cjs");
13
13
  const threadIdContext = require("./thread-id-context.cjs");
14
14
  const threadPrefetcher = require("./thread-prefetcher.cjs");
15
15
  const title = require("./title.cjs");
16
+ const authRefusalRestart = require("../runtime/auth-refusal-restart.cjs");
17
+ const credentialRenewal = require("../runtime/credential-renewal.cjs");
16
18
  const useThreadArchive = require("./use-thread-archive.cjs");
17
19
  const useThreadFavorite = require("./use-thread-favorite.cjs");
18
20
  const useThreadRename = require("./use-thread-rename.cjs");
@@ -74,11 +76,11 @@ function StatewireThreadListState({
74
76
  const principal = ((_a = authContext == null ? void 0 : authContext.user) == null ? void 0 : _a.userId) ?? "";
75
77
  const hasAuth = !!(apiKey || token || getToken);
76
78
  const getAuth = react.useCallback(
77
- async () => ({
79
+ async (options) => ({
78
80
  apiKey: apiKeyRef.current,
79
81
  token: await statewireAuthHeaders.resolveStatewireAuthToken(
80
82
  { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },
81
- { staleAuth: false }
83
+ { staleAuth: (options == null ? void 0 : options.force) ?? false }
82
84
  )
83
85
  }),
84
86
  []
@@ -124,17 +126,7 @@ function StatewireThreadListState({
124
126
  // Keyed by the non-secret principal and credential generation, never the
125
127
  // credential itself.
126
128
  queryKey: ["athena-sdk", "account-identity", backendUrl, principal, credentialGeneration],
127
- queryFn: async ({ signal }) => api.fetchAccountIdentity(
128
- backendUrl,
129
- {
130
- apiKey: apiKeyRef.current,
131
- token: await statewireAuthHeaders.resolveStatewireAuthToken(
132
- { apiKey: apiKeyRef.current, token: tokenRef.current, getToken: getTokenRef.current },
133
- { staleAuth: false }
134
- )
135
- },
136
- { signal }
137
- ),
129
+ queryFn: ({ signal }) => credentialRenewal.withCredentialRenewal(getAuth, (auth) => api.fetchAccountIdentity(backendUrl, auth, { signal })),
138
130
  enabled: identityEnabled,
139
131
  staleTime: Number.POSITIVE_INFINITY
140
132
  });
@@ -148,6 +140,10 @@ function StatewireThreadListState({
148
140
  headers: indexHeaders,
149
141
  enabled: indexEnabled
150
142
  });
143
+ authRefusalRestart.useRestartAfterAuthRefusal({
144
+ connection: index.connection,
145
+ reconnect: index.connection.reconnect
146
+ });
151
147
  const indexByThread = react.useMemo(() => {
152
148
  const entries = /* @__PURE__ */ new Map();
153
149
  for (const entry of index.entries ?? []) entries.set(entry.thread_id, entry);
@@ -263,7 +259,10 @@ function StatewireThreadListState({
263
259
  const scope = prefetchScope;
264
260
  return threadPrefetcher.createThreadPrefetcher({
265
261
  cache: snapshotCache,
266
- fetchState: async (threadId) => api.getThreadState(backendUrl, await getAuth(), threadId, { skipCache: false }),
262
+ fetchState: async (threadId) => credentialRenewal.withCredentialRenewal(
263
+ getAuth,
264
+ (auth) => api.getThreadState(backendUrl, auth, threadId, { skipCache: false })
265
+ ),
267
266
  isCurrent: () => prefetchScopeRef.current === scope
268
267
  });
269
268
  }, [snapshotCache, prefetchScope, backendUrl, getAuth]);