@athenaintel/react 0.41.2 → 0.41.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +6 -0
  2. package/dist/agent-runtime/src/durable-thread-snapshot-cache.cjs +158 -0
  3. package/dist/agent-runtime/src/durable-thread-snapshot-cache.cjs.map +1 -0
  4. package/dist/agent-runtime/src/durable-thread-snapshot-cache.js +158 -0
  5. package/dist/agent-runtime/src/durable-thread-snapshot-cache.js.map +1 -0
  6. package/dist/agent-runtime/src/thread-snapshot-mirror.cjs +400 -0
  7. package/dist/agent-runtime/src/thread-snapshot-mirror.cjs.map +1 -0
  8. package/dist/agent-runtime/src/thread-snapshot-mirror.js +400 -0
  9. package/dist/agent-runtime/src/thread-snapshot-mirror.js.map +1 -0
  10. package/dist/chat/AthenaChat.cjs +5 -1
  11. package/dist/chat/AthenaChat.cjs.map +1 -1
  12. package/dist/chat/AthenaChat.js +5 -1
  13. package/dist/chat/AthenaChat.js.map +1 -1
  14. package/dist/chat/super-grouping/activity-timeline.cjs +4 -3
  15. package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
  16. package/dist/chat/super-grouping/activity-timeline.d.ts +7 -1
  17. package/dist/chat/super-grouping/activity-timeline.js +4 -3
  18. package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
  19. package/dist/index.cjs +5 -0
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +5 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/offline/offline-history.cjs +77 -0
  25. package/dist/offline/offline-history.cjs.map +1 -0
  26. package/dist/offline/offline-history.d.ts +60 -0
  27. package/dist/offline/offline-history.js +77 -0
  28. package/dist/offline/offline-history.js.map +1 -0
  29. package/dist/provider/AthenaProvider.cjs +46 -3
  30. package/dist/provider/AthenaProvider.cjs.map +1 -1
  31. package/dist/provider/AthenaProvider.d.ts +8 -1
  32. package/dist/provider/AthenaProvider.js +46 -3
  33. package/dist/provider/AthenaProvider.js.map +1 -1
  34. package/dist/react/package.json.cjs +1 -1
  35. package/dist/react/package.json.js +1 -1
  36. package/dist/threads/offline-session-list.cjs +75 -0
  37. package/dist/threads/offline-session-list.cjs.map +1 -0
  38. package/dist/threads/offline-session-list.d.ts +27 -0
  39. package/dist/threads/offline-session-list.js +75 -0
  40. package/dist/threads/offline-session-list.js.map +1 -0
  41. package/dist/threads/statewire-thread-list.cjs +63 -4
  42. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  43. package/dist/threads/statewire-thread-list.d.ts +21 -2
  44. package/dist/threads/statewire-thread-list.js +64 -5
  45. package/dist/threads/statewire-thread-list.js.map +1 -1
  46. package/package.json +1 -1
@@ -103,7 +103,13 @@ export declare function getActivityItemLabel(item: ActivityItem, resolveToolDisp
103
103
  export declare function summarizeActivityTool(item: Extract<ActivityItem, {
104
104
  kind: 'tool';
105
105
  }>, resolveToolDisplayName?: (toolName: string) => string | undefined, assetTitles?: ReadonlyMap<string, string>): ToolCallSummary;
106
- /** The collapsed-card summary of a finished turn. */
106
+ /**
107
+ * The collapsed-card summary of a finished turn. "Completed with issues" means
108
+ * the turn ended on a problem: a call the person refused, or a failed call
109
+ * that no later call completed after. A failed command the agent then got past
110
+ * (a search that found nothing, a retry that worked) is ordinary work, so a
111
+ * long turn that recovered from it reads as the tools it used.
112
+ */
107
113
  export declare function getActivitySummary(timeline: readonly ActivityItem[]): string;
108
114
  /** The part shape the final-text read needs; any message role's parts satisfy it. */
109
115
  export type FinalTextPart = {
@@ -117,9 +117,10 @@ function summarizeActivityTool(item, resolveToolDisplayName, assetTitles) {
117
117
  }
118
118
  function getActivitySummary(timeline) {
119
119
  const toolCount = countActivityTools(timeline);
120
- const hasIssue = timeline.some(
121
- (item) => item.kind === "tool" && (item.state === "error" || item.state === "rejected")
122
- );
120
+ const tools = timeline.filter((item) => item.kind === "tool");
121
+ const lastFailure = tools.findLastIndex((item) => item.state === "error");
122
+ const endedOnFailure = lastFailure >= 0 && !tools.slice(lastFailure + 1).some((item) => item.state === "complete");
123
+ const hasIssue = endedOnFailure || tools.some((item) => item.state === "rejected");
123
124
  if (hasIssue) return "Completed with issues";
124
125
  if (timeline.some((item) => item.kind === "tool" && item.state === "cancelled")) {
125
126
  return "Action cancelled";
@@ -1 +1 @@
1
- {"version":3,"file":"activity-timeline.js","sources":["../../../src/chat/super-grouping/activity-timeline.ts"],"sourcesContent":["/**\n * The assistant turn's activity timeline — the one derivation of \"what did\n * this turn do, and how did each step end\" behind the collapsed view\n * (previously called the conversational view) on every surface: the SDK's\n * `AthenaChat`, Olympus's Spaces chat, and the Chrome extension. Pure\n * functions over assistant-ui message parts; `SuperGroupingCard` renders them.\n *\n * Step states come from the shared classifier (`classifyToolResult`) plus the\n * approval outcomes a part carries (`approval`, `interrupt`, a rejection\n * result), so a call that reads as failed, paused, or rejected on one surface\n * cannot read as a green check on another. Copy comes from the shared tool\n * summary registry (`summarizeToolCall`), itself built on the display\n * registry.\n */\nimport type {\n MessageStatus,\n ThreadAssistantMessagePart,\n ToolCallMessagePartStatus,\n} from '@assistant-ui/react';\nimport {\n classifyToolResult,\n isSpokenTurnTextPart,\n readRejectedToolResult,\n type ToolStatusLabelState,\n} from '@athenaintel/agent-runtime';\nimport { summarizeToolCall, type ToolCallSummary } from '../../tools/tool-summary';\nimport type { GroupablePart, GroupingFunction } from './grouping-function';\n\nexport type ActivityToolState = ToolStatusLabelState;\n\nexport type ActivityItem =\n | {\n kind: 'reasoning';\n id?: string;\n /** Position of the part in the message's content. */\n partIndex?: number;\n text: string;\n state: 'running' | 'complete';\n }\n | {\n kind: 'tool';\n id?: string;\n /** Position of the part in the message's content. */\n partIndex?: number;\n toolName: string;\n args?: ToolCallPart['args'];\n /** The tool's real result; absent while it runs or holds only the streaming placeholder. */\n result?: unknown;\n state: ActivityToolState;\n };\n\n/** The turn as a whole: still working, parked on the person, ended in error, or done. */\nexport type ActivityPhase = 'running' | 'waiting' | 'complete' | 'error';\n\ntype ToolCallPart = Extract<ThreadAssistantMessagePart, { type: 'tool-call' }>;\n\nexport interface ActivityTimelineOptions {\n /**\n * Tools whose unresolved call means the turn is waiting on the person even\n * without an interrupt or approval on the part (the Chrome extension's\n * device plan approval, which the tool UI itself collects).\n */\n waitingTools?: ReadonlySet<string>;\n /**\n * Tools whose result is an application payload relayed verbatim (the Chrome\n * extension's page JavaScript), so a `success: false` flag counts only with\n * an error detail — see `classifyToolResult`'s `passThroughPayload`.\n */\n passThroughTools?: ReadonlySet<string>;\n}\n\nconst NO_TOOLS: ReadonlySet<string> = new Set();\n\n/**\n * Whether a tool-call part still holds the backend's streaming placeholder\n * result rather than the tool's real output. Long-running tools (task\n * subagents, studio and Python execution, deep research) append a ToolMessage\n * the moment they start so their card can fill in live; it carries\n * `additional_kwargs.streaming`, which the runtime converter lifts into\n * `artifact._tool_message_metadata`. The real ToolMessage that replaces it\n * has no such flag, so a defined `result` alone does not mean the tool is done.\n */\nexport function isStreamingToolPlaceholder(part: Pick<ToolCallPart, 'artifact'>): boolean {\n const artifact: unknown = part.artifact;\n if (typeof artifact !== 'object' || artifact === null) return false;\n const metadata: unknown = (artifact as { _tool_message_metadata?: unknown })\n ._tool_message_metadata;\n if (typeof metadata !== 'object' || metadata === null) return false;\n return (metadata as { streaming?: unknown }).streaming === true;\n}\n\n/** The tool's real result: undefined while it has none or only the streaming placeholder. */\nfunction finalResult(part: ToolCallPart): unknown {\n return isStreamingToolPlaceholder(part) ? undefined : part.result;\n}\n\nfunction toolCallStatus(\n part: ToolCallPart,\n messageStatus: MessageStatus | undefined,\n): ToolCallMessagePartStatus | undefined {\n if (finalResult(part) !== undefined) return { type: 'complete' };\n if (messageStatus?.type === 'incomplete') {\n return {\n type: 'incomplete',\n reason: messageStatus.reason === 'tool-calls' ? 'other' : messageStatus.reason,\n error: messageStatus.error,\n };\n }\n if (messageStatus?.type === 'complete') return { type: 'complete' };\n if (messageStatus?.type === 'requires-action') {\n return { type: 'requires-action', reason: messageStatus.reason };\n }\n if (messageStatus?.type === 'running') return { type: 'running' };\n return undefined;\n}\n\n/** How one tool call ended (or is going), reading the approval fields before the result. */\nexport function getActivityToolState(\n part: ToolCallPart,\n messageStatus: MessageStatus | undefined,\n { waitingTools = NO_TOOLS, passThroughTools = NO_TOOLS }: ActivityTimelineOptions = {},\n): ActivityToolState {\n if (part.approval?.approved === false || readRejectedToolResult(part.result) !== null) {\n return 'rejected';\n }\n if (part.approval?.resolution === 'cancelled' || part.approval?.resolution === 'expired') {\n return 'cancelled';\n }\n const result = finalResult(part);\n const unresolved = result === undefined;\n const messageIsActive =\n messageStatus === undefined ||\n messageStatus.type === 'running' ||\n messageStatus.type === 'requires-action';\n if (\n unresolved &&\n (part.interrupt?.type === 'human' ||\n (part.approval !== undefined &&\n part.approval.approved === undefined &&\n part.approval.resolution === undefined) ||\n (waitingTools.has(part.toolName) && messageIsActive))\n ) {\n return 'waiting';\n }\n\n const classification = classifyToolResult({\n status: toolCallStatus(part, messageStatus),\n result,\n isError: part.isError,\n passThroughPayload: passThroughTools.has(part.toolName),\n });\n if (classification.state === 'requires-action') return 'waiting';\n return classification.state;\n}\n\n/**\n * The compact reasoning/tool timeline in the order the parts streamed.\n *\n * A reasoning part is still streaming only while it is the final part of a\n * running message — once any later part appears, the thought has settled.\n * Settled reasoning parts with no text are dropped so the list never shows an\n * empty thinking row.\n */\nexport function buildActivityTimeline(\n content: readonly ThreadAssistantMessagePart[] | undefined,\n messageStatus: MessageStatus | undefined,\n options: ActivityTimelineOptions = {},\n): ActivityItem[] {\n if (!content?.length) return [];\n\n const lastPartIndex = content.length - 1;\n const timeline: ActivityItem[] = [];\n for (let index = 0; index < content.length; index += 1) {\n const part = content[index];\n if (part.type === 'tool-call') {\n const result = finalResult(part);\n timeline.push({\n kind: 'tool',\n id: part.toolCallId,\n partIndex: index,\n args: part.args,\n ...(result !== undefined ? { result } : {}),\n toolName: part.toolName,\n state: getActivityToolState(part, messageStatus, options),\n });\n continue;\n }\n if (part.type !== 'reasoning') continue;\n\n const text = typeof part.text === 'string' ? part.text : '';\n const stillStreaming = messageStatus?.type === 'running' && index === lastPartIndex;\n if (!stillStreaming && text.trim() === '') continue;\n timeline.push({\n kind: 'reasoning',\n id: `reasoning-${index}`,\n partIndex: index,\n text,\n state: stillStreaming ? 'running' : 'complete',\n });\n }\n return timeline;\n}\n\n/** Number of tool calls in the timeline. */\nexport function countActivityTools(timeline: readonly ActivityItem[]): number {\n return timeline.reduce((count, item) => (item.kind === 'tool' ? count + 1 : count), 0);\n}\n\n/**\n * The turn's phase. Waiting on the person wins over everything — a\n * `requires-action` status, a waiting tool, or a host's own `waitingForInput`\n * assertion (no in-repo host passes one) — then the message's own status.\n */\nexport function resolveActivityPhase(\n status: MessageStatus | undefined,\n timeline: readonly ActivityItem[],\n { waitingForInput = false }: { waitingForInput?: boolean } = {},\n): ActivityPhase {\n if (\n waitingForInput ||\n status?.type === 'requires-action' ||\n timeline.some((item) => item.kind === 'tool' && item.state === 'waiting')\n ) {\n return 'waiting';\n }\n if (status?.type === 'running') return 'running';\n if (status?.type === 'incomplete') return 'error';\n return 'complete';\n}\n\n/**\n * A settled thought's row: the heading a reasoning summary opens with\n * (\"**Checking the workstation**\" → \"Checking the workstation\"), else the\n * generic wording. The thought itself stays in the run's details.\n */\nexport function getReasoningLabel(text: string, state: 'running' | 'complete'): string {\n if (state === 'running') return 'Thinking';\n const heading = /^\\s*\\*\\*([^*\\n]{3,80}?)\\*\\*/.exec(text)?.[1]?.trim();\n return heading ? heading.replace(/[.:]+$/, '') : 'Thought through the next step';\n}\n\n/**\n * The status-row copy for one step: the call's own summary from the shared\n * summary registry (`summarizeToolCall`: \"Ran git status\", \"Read config.ts\").\n * `resolveToolDisplayName` lets a host with a richer tool catalog name tools\n * neither registry has an entry for; `assetTitles` names the machine or\n * document a call targeted by id when an earlier result in the turn did.\n */\nexport function getActivityItemLabel(\n item: ActivityItem,\n resolveToolDisplayName?: (toolName: string) => string | undefined,\n assetTitles?: ReadonlyMap<string, string>,\n): string {\n if (item.kind === 'reasoning') return getReasoningLabel(item.text, item.state);\n return summarizeActivityTool(item, resolveToolDisplayName, assetTitles).label;\n}\n\n/** The summary of one tool step (see {@link getActivityItemLabel}). */\nexport function summarizeActivityTool(\n item: Extract<ActivityItem, { kind: 'tool' }>,\n resolveToolDisplayName?: (toolName: string) => string | undefined,\n assetTitles?: ReadonlyMap<string, string>,\n): ToolCallSummary {\n return summarizeToolCall({\n toolName: item.toolName,\n args: item.args,\n result: item.result,\n status: item.state,\n ...(assetTitles ? { assetTitles } : {}),\n ...(resolveToolDisplayName ? { displayName: resolveToolDisplayName(item.toolName) } : {}),\n });\n}\n\n/** The collapsed-card summary of a finished turn. */\nexport function getActivitySummary(timeline: readonly ActivityItem[]): string {\n const toolCount = countActivityTools(timeline);\n const hasIssue = timeline.some(\n (item) => item.kind === 'tool' && (item.state === 'error' || item.state === 'rejected'),\n );\n if (hasIssue) return 'Completed with issues';\n if (timeline.some((item) => item.kind === 'tool' && item.state === 'cancelled')) {\n return 'Action cancelled';\n }\n if (timeline.some((item) => item.kind === 'tool' && item.state === 'paused')) {\n return 'Paused during tool use';\n }\n if (toolCount > 0) return `Used ${toolCount} ${toolCount === 1 ? 'tool' : 'tools'}`;\n return 'Reasoning complete';\n}\n\n/** The part shape the final-text read needs; any message role's parts satisfy it. */\nexport type FinalTextPart = { readonly type: string; readonly text?: string };\n\n/**\n * Whether a text part shows below the card instead of in the details panel:\n * it comes after the final tool call, or it holds a spoken voice turn's words\n * (a live turn heard during a delegated run lands between the run's tool\n * calls, see `isSpokenTurnTextPart`; it is the conversation, not narration).\n */\nfunction isAnswerTextPart(\n part: { readonly type: string } | undefined,\n index: number,\n lastToolIndex: number,\n): boolean {\n return part?.type === 'text' && (index > lastToolIndex || isSpokenTurnTextPart(part));\n}\n\n/**\n * The visible answer of a grouped turn: the text after the final tool call\n * (all of it when the turn never called a tool), plus the words of every\n * spoken voice turn in the message, in order. Other intermediate text (before\n * or between tool calls) stays available in the details panel.\n */\nexport function getFinalText(content: readonly FinalTextPart[] | undefined): string | null {\n const parts = getFinalTextParts(content);\n return parts.length > 0 ? parts.map((part) => part.text).join('\\n\\n') : null;\n}\n\n/** One text part of the visible answer, with its index in the message content. */\nexport interface FinalTextSegment {\n index: number;\n text: string;\n}\n\n/**\n * The parts `getFinalText` joins, each kept separate with its content index.\n * A spoken turn and the run's own reply can stream at the same time, so the\n * earlier part may grow while a later one exists: rendered as one joined\n * string, that text changes in the middle rather than at its end, and the\n * streaming reveal restarts from an empty bubble on every word.\n */\nexport function getFinalTextParts(content: readonly FinalTextPart[] | undefined): FinalTextSegment[] {\n if (!content?.length) return [];\n\n const lastToolIndex = content.findLastIndex((part) => part?.type === 'tool-call');\n const parts: FinalTextSegment[] = [];\n content.forEach((part, index) => {\n if (part.text && isAnswerTextPart(part, index, lastToolIndex)) parts.push({ index, text: part.text });\n });\n return parts;\n}\n\n/**\n * Keep the answer out of the details panel: it renders below the card\n * (`SuperGroupingFinalText`), so the details grouping drops every text part\n * `getFinalText` shows and any group that becomes empty.\n */\nexport function excludeFinalTextFromGroups(grouping: GroupingFunction): GroupingFunction {\n return (parts) => {\n const lastToolIndex = parts.findLastIndex((part) => part?.type === 'tool-call');\n return grouping(parts)\n .map((group) => ({\n ...group,\n indices: group.indices.filter((index) => !isAnswerTextPart(parts[index], index, lastToolIndex)),\n }))\n .filter((group) => group.indices.length > 0);\n };\n}\n\n/**\n * Drop the parts `exclude` matches from every group, and any group left\n * empty: `SuperGroupingCard` renders inline tool calls outside the details,\n * so they must not render a second time inside it.\n */\nexport function excludePartsFromGroups(\n grouping: GroupingFunction,\n exclude: (part: GroupablePart) => boolean,\n): GroupingFunction {\n return (parts) =>\n grouping(parts)\n .map((group) => ({\n ...group,\n indices: group.indices.filter((index) => {\n const part = parts[index];\n return part === undefined || !exclude(part);\n }),\n }))\n .filter((group) => group.indices.length > 0);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"activity-timeline.js","sources":["../../../src/chat/super-grouping/activity-timeline.ts"],"sourcesContent":["/**\n * The assistant turn's activity timeline — the one derivation of \"what did\n * this turn do, and how did each step end\" behind the collapsed view\n * (previously called the conversational view) on every surface: the SDK's\n * `AthenaChat`, Olympus's Spaces chat, and the Chrome extension. Pure\n * functions over assistant-ui message parts; `SuperGroupingCard` renders them.\n *\n * Step states come from the shared classifier (`classifyToolResult`) plus the\n * approval outcomes a part carries (`approval`, `interrupt`, a rejection\n * result), so a call that reads as failed, paused, or rejected on one surface\n * cannot read as a green check on another. Copy comes from the shared tool\n * summary registry (`summarizeToolCall`), itself built on the display\n * registry.\n */\nimport type {\n MessageStatus,\n ThreadAssistantMessagePart,\n ToolCallMessagePartStatus,\n} from '@assistant-ui/react';\nimport {\n classifyToolResult,\n isSpokenTurnTextPart,\n readRejectedToolResult,\n type ToolStatusLabelState,\n} from '@athenaintel/agent-runtime';\nimport { summarizeToolCall, type ToolCallSummary } from '../../tools/tool-summary';\nimport type { GroupablePart, GroupingFunction } from './grouping-function';\n\nexport type ActivityToolState = ToolStatusLabelState;\n\nexport type ActivityItem =\n | {\n kind: 'reasoning';\n id?: string;\n /** Position of the part in the message's content. */\n partIndex?: number;\n text: string;\n state: 'running' | 'complete';\n }\n | {\n kind: 'tool';\n id?: string;\n /** Position of the part in the message's content. */\n partIndex?: number;\n toolName: string;\n args?: ToolCallPart['args'];\n /** The tool's real result; absent while it runs or holds only the streaming placeholder. */\n result?: unknown;\n state: ActivityToolState;\n };\n\n/** The turn as a whole: still working, parked on the person, ended in error, or done. */\nexport type ActivityPhase = 'running' | 'waiting' | 'complete' | 'error';\n\ntype ToolCallPart = Extract<ThreadAssistantMessagePart, { type: 'tool-call' }>;\n\nexport interface ActivityTimelineOptions {\n /**\n * Tools whose unresolved call means the turn is waiting on the person even\n * without an interrupt or approval on the part (the Chrome extension's\n * device plan approval, which the tool UI itself collects).\n */\n waitingTools?: ReadonlySet<string>;\n /**\n * Tools whose result is an application payload relayed verbatim (the Chrome\n * extension's page JavaScript), so a `success: false` flag counts only with\n * an error detail — see `classifyToolResult`'s `passThroughPayload`.\n */\n passThroughTools?: ReadonlySet<string>;\n}\n\nconst NO_TOOLS: ReadonlySet<string> = new Set();\n\n/**\n * Whether a tool-call part still holds the backend's streaming placeholder\n * result rather than the tool's real output. Long-running tools (task\n * subagents, studio and Python execution, deep research) append a ToolMessage\n * the moment they start so their card can fill in live; it carries\n * `additional_kwargs.streaming`, which the runtime converter lifts into\n * `artifact._tool_message_metadata`. The real ToolMessage that replaces it\n * has no such flag, so a defined `result` alone does not mean the tool is done.\n */\nexport function isStreamingToolPlaceholder(part: Pick<ToolCallPart, 'artifact'>): boolean {\n const artifact: unknown = part.artifact;\n if (typeof artifact !== 'object' || artifact === null) return false;\n const metadata: unknown = (artifact as { _tool_message_metadata?: unknown })\n ._tool_message_metadata;\n if (typeof metadata !== 'object' || metadata === null) return false;\n return (metadata as { streaming?: unknown }).streaming === true;\n}\n\n/** The tool's real result: undefined while it has none or only the streaming placeholder. */\nfunction finalResult(part: ToolCallPart): unknown {\n return isStreamingToolPlaceholder(part) ? undefined : part.result;\n}\n\nfunction toolCallStatus(\n part: ToolCallPart,\n messageStatus: MessageStatus | undefined,\n): ToolCallMessagePartStatus | undefined {\n if (finalResult(part) !== undefined) return { type: 'complete' };\n if (messageStatus?.type === 'incomplete') {\n return {\n type: 'incomplete',\n reason: messageStatus.reason === 'tool-calls' ? 'other' : messageStatus.reason,\n error: messageStatus.error,\n };\n }\n if (messageStatus?.type === 'complete') return { type: 'complete' };\n if (messageStatus?.type === 'requires-action') {\n return { type: 'requires-action', reason: messageStatus.reason };\n }\n if (messageStatus?.type === 'running') return { type: 'running' };\n return undefined;\n}\n\n/** How one tool call ended (or is going), reading the approval fields before the result. */\nexport function getActivityToolState(\n part: ToolCallPart,\n messageStatus: MessageStatus | undefined,\n { waitingTools = NO_TOOLS, passThroughTools = NO_TOOLS }: ActivityTimelineOptions = {},\n): ActivityToolState {\n if (part.approval?.approved === false || readRejectedToolResult(part.result) !== null) {\n return 'rejected';\n }\n if (part.approval?.resolution === 'cancelled' || part.approval?.resolution === 'expired') {\n return 'cancelled';\n }\n const result = finalResult(part);\n const unresolved = result === undefined;\n const messageIsActive =\n messageStatus === undefined ||\n messageStatus.type === 'running' ||\n messageStatus.type === 'requires-action';\n if (\n unresolved &&\n (part.interrupt?.type === 'human' ||\n (part.approval !== undefined &&\n part.approval.approved === undefined &&\n part.approval.resolution === undefined) ||\n (waitingTools.has(part.toolName) && messageIsActive))\n ) {\n return 'waiting';\n }\n\n const classification = classifyToolResult({\n status: toolCallStatus(part, messageStatus),\n result,\n isError: part.isError,\n passThroughPayload: passThroughTools.has(part.toolName),\n });\n if (classification.state === 'requires-action') return 'waiting';\n return classification.state;\n}\n\n/**\n * The compact reasoning/tool timeline in the order the parts streamed.\n *\n * A reasoning part is still streaming only while it is the final part of a\n * running message — once any later part appears, the thought has settled.\n * Settled reasoning parts with no text are dropped so the list never shows an\n * empty thinking row.\n */\nexport function buildActivityTimeline(\n content: readonly ThreadAssistantMessagePart[] | undefined,\n messageStatus: MessageStatus | undefined,\n options: ActivityTimelineOptions = {},\n): ActivityItem[] {\n if (!content?.length) return [];\n\n const lastPartIndex = content.length - 1;\n const timeline: ActivityItem[] = [];\n for (let index = 0; index < content.length; index += 1) {\n const part = content[index];\n if (part.type === 'tool-call') {\n const result = finalResult(part);\n timeline.push({\n kind: 'tool',\n id: part.toolCallId,\n partIndex: index,\n args: part.args,\n ...(result !== undefined ? { result } : {}),\n toolName: part.toolName,\n state: getActivityToolState(part, messageStatus, options),\n });\n continue;\n }\n if (part.type !== 'reasoning') continue;\n\n const text = typeof part.text === 'string' ? part.text : '';\n const stillStreaming = messageStatus?.type === 'running' && index === lastPartIndex;\n if (!stillStreaming && text.trim() === '') continue;\n timeline.push({\n kind: 'reasoning',\n id: `reasoning-${index}`,\n partIndex: index,\n text,\n state: stillStreaming ? 'running' : 'complete',\n });\n }\n return timeline;\n}\n\n/** Number of tool calls in the timeline. */\nexport function countActivityTools(timeline: readonly ActivityItem[]): number {\n return timeline.reduce((count, item) => (item.kind === 'tool' ? count + 1 : count), 0);\n}\n\n/**\n * The turn's phase. Waiting on the person wins over everything — a\n * `requires-action` status, a waiting tool, or a host's own `waitingForInput`\n * assertion (no in-repo host passes one) — then the message's own status.\n */\nexport function resolveActivityPhase(\n status: MessageStatus | undefined,\n timeline: readonly ActivityItem[],\n { waitingForInput = false }: { waitingForInput?: boolean } = {},\n): ActivityPhase {\n if (\n waitingForInput ||\n status?.type === 'requires-action' ||\n timeline.some((item) => item.kind === 'tool' && item.state === 'waiting')\n ) {\n return 'waiting';\n }\n if (status?.type === 'running') return 'running';\n if (status?.type === 'incomplete') return 'error';\n return 'complete';\n}\n\n/**\n * A settled thought's row: the heading a reasoning summary opens with\n * (\"**Checking the workstation**\" → \"Checking the workstation\"), else the\n * generic wording. The thought itself stays in the run's details.\n */\nexport function getReasoningLabel(text: string, state: 'running' | 'complete'): string {\n if (state === 'running') return 'Thinking';\n const heading = /^\\s*\\*\\*([^*\\n]{3,80}?)\\*\\*/.exec(text)?.[1]?.trim();\n return heading ? heading.replace(/[.:]+$/, '') : 'Thought through the next step';\n}\n\n/**\n * The status-row copy for one step: the call's own summary from the shared\n * summary registry (`summarizeToolCall`: \"Ran git status\", \"Read config.ts\").\n * `resolveToolDisplayName` lets a host with a richer tool catalog name tools\n * neither registry has an entry for; `assetTitles` names the machine or\n * document a call targeted by id when an earlier result in the turn did.\n */\nexport function getActivityItemLabel(\n item: ActivityItem,\n resolveToolDisplayName?: (toolName: string) => string | undefined,\n assetTitles?: ReadonlyMap<string, string>,\n): string {\n if (item.kind === 'reasoning') return getReasoningLabel(item.text, item.state);\n return summarizeActivityTool(item, resolveToolDisplayName, assetTitles).label;\n}\n\n/** The summary of one tool step (see {@link getActivityItemLabel}). */\nexport function summarizeActivityTool(\n item: Extract<ActivityItem, { kind: 'tool' }>,\n resolveToolDisplayName?: (toolName: string) => string | undefined,\n assetTitles?: ReadonlyMap<string, string>,\n): ToolCallSummary {\n return summarizeToolCall({\n toolName: item.toolName,\n args: item.args,\n result: item.result,\n status: item.state,\n ...(assetTitles ? { assetTitles } : {}),\n ...(resolveToolDisplayName ? { displayName: resolveToolDisplayName(item.toolName) } : {}),\n });\n}\n\n/**\n * The collapsed-card summary of a finished turn. \"Completed with issues\" means\n * the turn ended on a problem: a call the person refused, or a failed call\n * that no later call completed after. A failed command the agent then got past\n * (a search that found nothing, a retry that worked) is ordinary work, so a\n * long turn that recovered from it reads as the tools it used.\n */\nexport function getActivitySummary(timeline: readonly ActivityItem[]): string {\n const toolCount = countActivityTools(timeline);\n const tools = timeline.filter((item) => item.kind === 'tool');\n const lastFailure = tools.findLastIndex((item) => item.state === 'error');\n const endedOnFailure =\n lastFailure >= 0 && !tools.slice(lastFailure + 1).some((item) => item.state === 'complete');\n const hasIssue = endedOnFailure || tools.some((item) => item.state === 'rejected');\n if (hasIssue) return 'Completed with issues';\n if (timeline.some((item) => item.kind === 'tool' && item.state === 'cancelled')) {\n return 'Action cancelled';\n }\n if (timeline.some((item) => item.kind === 'tool' && item.state === 'paused')) {\n return 'Paused during tool use';\n }\n if (toolCount > 0) return `Used ${toolCount} ${toolCount === 1 ? 'tool' : 'tools'}`;\n return 'Reasoning complete';\n}\n\n/** The part shape the final-text read needs; any message role's parts satisfy it. */\nexport type FinalTextPart = { readonly type: string; readonly text?: string };\n\n/**\n * Whether a text part shows below the card instead of in the details panel:\n * it comes after the final tool call, or it holds a spoken voice turn's words\n * (a live turn heard during a delegated run lands between the run's tool\n * calls, see `isSpokenTurnTextPart`; it is the conversation, not narration).\n */\nfunction isAnswerTextPart(\n part: { readonly type: string } | undefined,\n index: number,\n lastToolIndex: number,\n): boolean {\n return part?.type === 'text' && (index > lastToolIndex || isSpokenTurnTextPart(part));\n}\n\n/**\n * The visible answer of a grouped turn: the text after the final tool call\n * (all of it when the turn never called a tool), plus the words of every\n * spoken voice turn in the message, in order. Other intermediate text (before\n * or between tool calls) stays available in the details panel.\n */\nexport function getFinalText(content: readonly FinalTextPart[] | undefined): string | null {\n const parts = getFinalTextParts(content);\n return parts.length > 0 ? parts.map((part) => part.text).join('\\n\\n') : null;\n}\n\n/** One text part of the visible answer, with its index in the message content. */\nexport interface FinalTextSegment {\n index: number;\n text: string;\n}\n\n/**\n * The parts `getFinalText` joins, each kept separate with its content index.\n * A spoken turn and the run's own reply can stream at the same time, so the\n * earlier part may grow while a later one exists: rendered as one joined\n * string, that text changes in the middle rather than at its end, and the\n * streaming reveal restarts from an empty bubble on every word.\n */\nexport function getFinalTextParts(content: readonly FinalTextPart[] | undefined): FinalTextSegment[] {\n if (!content?.length) return [];\n\n const lastToolIndex = content.findLastIndex((part) => part?.type === 'tool-call');\n const parts: FinalTextSegment[] = [];\n content.forEach((part, index) => {\n if (part.text && isAnswerTextPart(part, index, lastToolIndex)) parts.push({ index, text: part.text });\n });\n return parts;\n}\n\n/**\n * Keep the answer out of the details panel: it renders below the card\n * (`SuperGroupingFinalText`), so the details grouping drops every text part\n * `getFinalText` shows and any group that becomes empty.\n */\nexport function excludeFinalTextFromGroups(grouping: GroupingFunction): GroupingFunction {\n return (parts) => {\n const lastToolIndex = parts.findLastIndex((part) => part?.type === 'tool-call');\n return grouping(parts)\n .map((group) => ({\n ...group,\n indices: group.indices.filter((index) => !isAnswerTextPart(parts[index], index, lastToolIndex)),\n }))\n .filter((group) => group.indices.length > 0);\n };\n}\n\n/**\n * Drop the parts `exclude` matches from every group, and any group left\n * empty: `SuperGroupingCard` renders inline tool calls outside the details,\n * so they must not render a second time inside it.\n */\nexport function excludePartsFromGroups(\n grouping: GroupingFunction,\n exclude: (part: GroupablePart) => boolean,\n): GroupingFunction {\n return (parts) =>\n grouping(parts)\n .map((group) => ({\n ...group,\n indices: group.indices.filter((index) => {\n const part = parts[index];\n return part === undefined || !exclude(part);\n }),\n }))\n .filter((group) => group.indices.length > 0);\n}\n"],"names":[],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -38,6 +38,7 @@ const statewireLifecycle = require("./runtime/statewire-lifecycle.cjs");
38
38
  const statewireThreadList = require("./threads/statewire-thread-list.cjs");
39
39
  const WaitingOnYouBanner = require("./chat/WaitingOnYouBanner.cjs");
40
40
  const wakeMarker = require("./chat-ui/messages/wake-marker.cjs");
41
+ const offlineHistory = require("./offline/offline-history.cjs");
41
42
  const tiptapText = require("./chat/tiptap-text.cjs");
42
43
  const athenaCitationLinks = require("./chat/athena-citation-links.cjs");
43
44
  const tooltipIconButton = require("./chat/tooltip-icon-button.cjs");
@@ -219,6 +220,10 @@ exports.WaitingOnYouBanner = WaitingOnYouBanner.WaitingOnYouBanner;
219
220
  exports.WaitingOnYouBannerView = WaitingOnYouBanner.WaitingOnYouBannerView;
220
221
  exports.WakeMarkerCard = wakeMarker.WakeMarkerCard;
221
222
  exports.WakeMarkerMessage = wakeMarker.WakeMarkerMessage;
223
+ exports.OFFLINE_SNAPSHOT_READ_WAIT_MS = offlineHistory.OFFLINE_SNAPSHOT_READ_WAIT_MS;
224
+ exports.OfflineSnapshotBanner = offlineHistory.OfflineSnapshotBanner;
225
+ exports.formatOfflineSnapshotTime = offlineHistory.formatOfflineSnapshotTime;
226
+ exports.useAthenaOfflineSnapshotAt = offlineHistory.useAthenaOfflineSnapshotAt;
222
227
  exports.TiptapText = tiptapText.TiptapText;
223
228
  exports.isAthenaCitationUrl = athenaCitationLinks.isAthenaCitationUrl;
224
229
  exports.parseAthenaCitationLink = athenaCitationLinks.parseAthenaCitationLink;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.d.ts CHANGED
@@ -51,6 +51,8 @@ export { WaitingOnYouBanner, WaitingOnYouBannerView } from './chat/WaitingOnYouB
51
51
  export type { WaitingOnYouBannerProps, WaitingOnYouBannerViewProps, } from './chat/WaitingOnYouBanner';
52
52
  export { WakeMarkerCard, WakeMarkerMessage } from './chat-ui/messages/wake-marker';
53
53
  export type { WakeMarkerCardProps } from './chat-ui/messages/wake-marker';
54
+ export { formatOfflineSnapshotTime, OFFLINE_SNAPSHOT_READ_WAIT_MS, OfflineSnapshotBanner, useAthenaOfflineSnapshotAt, } from './offline/offline-history';
55
+ export type { AthenaOfflineHistory, OfflineSnapshotBannerProps, } from './offline/offline-history';
54
56
  export { asHitlApproval, hasStatewireThreadExtras, HITL_PENDING_RESUME_GRACE_MS, isPendingInterrupt, readInterruptMessage, } from './chat/statewire-approval';
55
57
  export type { HitlApprovalPayload } from './chat/statewire-approval';
56
58
  export { TiptapText } from './chat/tiptap-text';
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ import { athenaStatewireErrorNotice } from "./runtime/statewire-lifecycle.js";
36
36
  import { threadSummaryFromConversation, useAthenaStatewireThreadList, useRefreshThreadList } from "./threads/statewire-thread-list.js";
37
37
  import { WaitingOnYouBanner, WaitingOnYouBannerView } from "./chat/WaitingOnYouBanner.js";
38
38
  import { WakeMarkerCard, WakeMarkerMessage } from "./chat-ui/messages/wake-marker.js";
39
+ import { OFFLINE_SNAPSHOT_READ_WAIT_MS, OfflineSnapshotBanner, formatOfflineSnapshotTime, useAthenaOfflineSnapshotAt } from "./offline/offline-history.js";
39
40
  import { TiptapText } from "./chat/tiptap-text.js";
40
41
  import { isAthenaCitationUrl, parseAthenaCitationLink, useAthenaCitationLinkHandler, useAthenaLinkClickHandler } from "./chat/athena-citation-links.js";
41
42
  import { TooltipIconButton } from "./chat/tooltip-icon-button.js";
@@ -206,6 +207,8 @@ export {
206
207
  MODEL_PROVIDER_LABELS,
207
208
  NestedPtcComponentsProvider,
208
209
  NestedPtcMessages,
210
+ OFFLINE_SNAPSHOT_READ_WAIT_MS,
211
+ OfflineSnapshotBanner,
209
212
  OpenAssetToolUI,
210
213
  OpenBrowserToolUI,
211
214
  OwnerDocumentProvider,
@@ -366,6 +369,7 @@ export {
366
369
  finalLiveDesktopCloseNotice,
367
370
  formatA1Cell,
368
371
  formatA1Range,
372
+ formatOfflineSnapshotTime,
369
373
  formatRecordingClock,
370
374
  formatTestRecordingCounts,
371
375
  formatToolName,
@@ -488,6 +492,7 @@ export {
488
492
  useAthenaInboxUnreadCount,
489
493
  useAthenaLinkClickHandler,
490
494
  useAthenaModelCatalog,
495
+ useAthenaOfflineSnapshotAt,
491
496
  useAthenaRepoSearch,
492
497
  useAthenaRunLive,
493
498
  useAthenaSessionAttention,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const lucideReact = require("lucide-react");
5
+ const react = require("react");
6
+ const threadIdContext = require("../threads/thread-id-context.cjs");
7
+ const cnfast = require("cnfast");
8
+ const OFFLINE_SNAPSHOT_READ_WAIT_MS = 1500;
9
+ function useKeptSnapshotsGate({
10
+ cache,
11
+ ownerId,
12
+ isOffline,
13
+ waitMs = OFFLINE_SNAPSHOT_READ_WAIT_MS
14
+ }) {
15
+ const [readFor, setReadFor] = react.useState(void 0);
16
+ react.useEffect(() => {
17
+ if (!cache) return;
18
+ let live = true;
19
+ const settle = () => {
20
+ if (live) setReadFor(ownerId);
21
+ };
22
+ const timer = setTimeout(settle, waitMs);
23
+ const done = () => {
24
+ clearTimeout(timer);
25
+ settle();
26
+ };
27
+ void cache.setOwner(ownerId).then(done, done);
28
+ return () => {
29
+ live = false;
30
+ clearTimeout(timer);
31
+ };
32
+ }, [cache, ownerId, waitMs]);
33
+ const [mountedOffline] = react.useState(() => cache !== null && isOffline());
34
+ return mountedOffline && ownerId !== null && readFor !== ownerId;
35
+ }
36
+ const AthenaOfflineSnapshotsContext = react.createContext(null);
37
+ const noSubscription = () => () => void 0;
38
+ const serverSnapshot = () => null;
39
+ function useAthenaOfflineSnapshotAt(threadId) {
40
+ const snapshots = react.useContext(AthenaOfflineSnapshotsContext);
41
+ return react.useSyncExternalStore(
42
+ snapshots ? (listener) => snapshots.subscribeOfflineSeeds(listener) : noSubscription,
43
+ () => snapshots && threadId ? snapshots.offlineSeedAt(threadId) : null,
44
+ serverSnapshot
45
+ );
46
+ }
47
+ function formatOfflineSnapshotTime(at) {
48
+ return new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(at);
49
+ }
50
+ const OfflineSnapshotBanner = ({ className }) => {
51
+ const at = useAthenaOfflineSnapshotAt(threadIdContext.useAthenaThreadId());
52
+ if (at === null) return null;
53
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsxs(
54
+ "div",
55
+ {
56
+ role: "status",
57
+ className: cnfast.cn(
58
+ "mx-auto flex w-full max-w-(--thread-max-width) items-center gap-2 rounded-md border px-3 py-2 text-xs",
59
+ "border-border bg-muted text-muted-foreground"
60
+ ),
61
+ children: [
62
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CloudOffIcon, { className: "size-3.5 shrink-0", "aria-hidden": true }),
63
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
64
+ "Offline · this conversation as of ",
65
+ formatOfflineSnapshotTime(at)
66
+ ] })
67
+ ]
68
+ }
69
+ ) });
70
+ };
71
+ exports.AthenaOfflineSnapshotsContext = AthenaOfflineSnapshotsContext;
72
+ exports.OFFLINE_SNAPSHOT_READ_WAIT_MS = OFFLINE_SNAPSHOT_READ_WAIT_MS;
73
+ exports.OfflineSnapshotBanner = OfflineSnapshotBanner;
74
+ exports.formatOfflineSnapshotTime = formatOfflineSnapshotTime;
75
+ exports.useAthenaOfflineSnapshotAt = useAthenaOfflineSnapshotAt;
76
+ exports.useKeptSnapshotsGate = useKeptSnapshotsGate;
77
+ //# sourceMappingURL=offline-history.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-history.cjs","sources":["../../src/offline/offline-history.tsx"],"sourcesContent":["import type { DurableThreadSnapshotCache } from '@athenaintel/agent-runtime';\nimport { CloudOffIcon } from 'lucide-react';\nimport {\n createContext,\n type FC,\n useContext,\n useEffect,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport { cn } from '../lib/utils';\nimport { useAthenaThreadId } from '../threads/thread-id-context';\n\n/**\n * Opt-in: keep the chat's recent conversations, and the session list, on this device, sealed with\n * the host's key, so a reload paints them at once and they can be read while Athena can't be\n * reached (`AthenaProvider`'s `offlineHistory`). Without it nothing is stored, as before.\n *\n * - `ownerId` names whose they are (the signed-in person). Changing it drops what the previous\n * owner kept, in memory and on the device (the session list's cached pages too); nothing kept\n * under one owner is ever shown to another. Passing no `offlineHistory` any more (a deployment\n * that now keeps nothing on devices) drops it all the same.\n * - `getKey` returns the AES-GCM key that seals them, for that owner, or `null` to keep nothing\n * (no key, a deployment that keeps nothing on devices). Midas's comes from its main process,\n * kept with the OS keychain and handed to the page only for the signed-in person.\n * - `isOffline` says Athena can't be reached (the browser's own offline when omitted): then a\n * conversation opens on its last resting transcript this device holds, up to 30 days old, marked\n * \"as of\" when it was captured; a chat that mounts offline waits (at most\n * {@link OFFLINE_SNAPSHOT_READ_WAIT_MS}) for what the device kept to be read.\n */\nexport interface AthenaOfflineHistory {\n ownerId: string;\n getKey: () => Promise<CryptoKey | null>;\n isOffline?: () => boolean;\n}\n\n/**\n * How long a chat that mounts offline waits for what this device kept before it mounts without:\n * the host's key and IndexedDB answer in milliseconds unless something is stuck.\n */\nexport const OFFLINE_SNAPSHOT_READ_WAIT_MS = 1_500;\n\n/**\n * Binds `cache` to `ownerId`, and says whether a chat that mounted offline still waits for what\n * this device kept for that owner to be read (at most `waitMs`). The runtime takes a thread's seed\n * once, as it mounts, and offline that seed is all a thread can show; online it never waits, the\n * attach delivers the thread.\n */\nexport function useKeptSnapshotsGate({\n cache,\n ownerId,\n isOffline,\n waitMs = OFFLINE_SNAPSHOT_READ_WAIT_MS,\n}: {\n cache: Pick<DurableThreadSnapshotCache, 'setOwner'> | null;\n ownerId: string | null;\n isOffline: () => boolean;\n waitMs?: number;\n}): boolean {\n const [readFor, setReadFor] = useState<string | null | undefined>(undefined);\n useEffect(() => {\n if (!cache) return;\n let live = true;\n const settle = () => {\n if (live) setReadFor(ownerId);\n };\n const timer = setTimeout(settle, waitMs);\n const done = () => {\n clearTimeout(timer);\n settle();\n };\n void cache.setOwner(ownerId).then(done, done);\n return () => {\n live = false;\n clearTimeout(timer);\n };\n }, [cache, ownerId, waitMs]);\n const [mountedOffline] = useState(() => cache !== null && isOffline());\n return mountedOffline && ownerId !== null && readFor !== ownerId;\n}\n\n/** The provider's offline snapshots: when a thread on screen was captured, if it opened offline. */\nexport type AthenaOfflineSnapshots = Pick<\n DurableThreadSnapshotCache,\n 'offlineSeedAt' | 'subscribeOfflineSeeds'\n>;\n\nexport const AthenaOfflineSnapshotsContext = createContext<AthenaOfflineSnapshots | null>(null);\n\nconst noSubscription = (): (() => void) => () => undefined;\nconst serverSnapshot = (): number | null => null;\n\n/**\n * When the transcript `threadId` shows was captured, if it opened from this device while Athena\n * could not be reached; `null` once the thread's own state arrives, outside an `offlineHistory`\n * provider, or for a new chat.\n */\nexport function useAthenaOfflineSnapshotAt(threadId: string | undefined): number | null {\n const snapshots = useContext(AthenaOfflineSnapshotsContext);\n return useSyncExternalStore(\n snapshots ? (listener) => snapshots.subscribeOfflineSeeds(listener) : noSubscription,\n () => (snapshots && threadId ? snapshots.offlineSeedAt(threadId) : null),\n serverSnapshot,\n );\n}\n\nexport function formatOfflineSnapshotTime(at: number): string {\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(at);\n}\n\nexport interface OfflineSnapshotBannerProps {\n /** Applied to the outer wrapper (host padding); the banner itself is width-capped inside it. */\n className?: string;\n}\n\n/**\n * Above a conversation that opened from this device while Athena can't be reached: what is shown\n * is the conversation as it was then, not live. Renders nothing otherwise.\n */\nexport const OfflineSnapshotBanner: FC<OfflineSnapshotBannerProps> = ({ className }) => {\n const at = useAthenaOfflineSnapshotAt(useAthenaThreadId());\n if (at === null) return null;\n return (\n <div className={className}>\n <div\n role=\"status\"\n className={cn(\n 'mx-auto flex w-full max-w-(--thread-max-width) items-center gap-2 rounded-md border px-3 py-2 text-xs',\n 'border-border bg-muted text-muted-foreground',\n )}\n >\n <CloudOffIcon className=\"size-3.5 shrink-0\" aria-hidden />\n <span>Offline · this conversation as of {formatOfflineSnapshotTime(at)}</span>\n </div>\n </div>\n );\n};\n"],"names":["useState","useEffect","createContext","useContext","useSyncExternalStore","useAthenaThreadId","jsx","jsxs","cn","CloudOffIcon"],"mappings":";;;;;;;AAwCO,MAAM,gCAAgC;AAQtC,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AACX,GAKY;AACV,QAAM,CAAC,SAAS,UAAU,IAAIA,MAAAA,SAAoC,MAAS;AAC3EC,QAAAA,UAAU,MAAM;AACd,QAAI,CAAC,MAAO;AACZ,QAAI,OAAO;AACX,UAAM,SAAS,MAAM;AACnB,UAAI,iBAAiB,OAAO;AAAA,IAC9B;AACA,UAAM,QAAQ,WAAW,QAAQ,MAAM;AACvC,UAAM,OAAO,MAAM;AACjB,mBAAa,KAAK;AAClB,aAAA;AAAA,IACF;AACA,SAAK,MAAM,SAAS,OAAO,EAAE,KAAK,MAAM,IAAI;AAC5C,WAAO,MAAM;AACX,aAAO;AACP,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,MAAM,CAAC;AAC3B,QAAM,CAAC,cAAc,IAAID,MAAAA,SAAS,MAAM,UAAU,QAAQ,WAAW;AACrE,SAAO,kBAAkB,YAAY,QAAQ,YAAY;AAC3D;AAQO,MAAM,gCAAgCE,MAAAA,cAA6C,IAAI;AAE9F,MAAM,iBAAiB,MAAoB,MAAM;AACjD,MAAM,iBAAiB,MAAqB;AAOrC,SAAS,2BAA2B,UAA6C;AACtF,QAAM,YAAYC,MAAAA,WAAW,6BAA6B;AAC1D,SAAOC,MAAAA;AAAAA,IACL,YAAY,CAAC,aAAa,UAAU,sBAAsB,QAAQ,IAAI;AAAA,IACtE,MAAO,aAAa,WAAW,UAAU,cAAc,QAAQ,IAAI;AAAA,IACnE;AAAA,EAAA;AAEJ;AAEO,SAAS,0BAA0B,IAAoB;AAC5D,SAAO,IAAI,KAAK,eAAe,QAAW,EAAE,WAAW,UAAU,WAAW,QAAA,CAAS,EAAE,OAAO,EAAE;AAClG;AAWO,MAAM,wBAAwD,CAAC,EAAE,gBAAgB;AACtF,QAAM,KAAK,2BAA2BC,gBAAAA,mBAAmB;AACzD,MAAI,OAAO,KAAM,QAAO;AACxB,SACEC,2BAAAA,IAAC,SAAI,WACH,UAAAC,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAWC,OAAAA;AAAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAAF,2BAAAA,IAACG,YAAAA,cAAA,EAAa,WAAU,qBAAoB,eAAW,MAAC;AAAA,wCACvD,QAAA,EAAK,UAAA;AAAA,UAAA;AAAA,UAAmC,0BAA0B,EAAE;AAAA,QAAA,EAAA,CAAE;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAE3E;AAEJ;;;;;;;"}
@@ -0,0 +1,60 @@
1
+ import type { DurableThreadSnapshotCache } from '@athenaintel/agent-runtime';
2
+ import { type FC } from 'react';
3
+ /**
4
+ * Opt-in: keep the chat's recent conversations, and the session list, on this device, sealed with
5
+ * the host's key, so a reload paints them at once and they can be read while Athena can't be
6
+ * reached (`AthenaProvider`'s `offlineHistory`). Without it nothing is stored, as before.
7
+ *
8
+ * - `ownerId` names whose they are (the signed-in person). Changing it drops what the previous
9
+ * owner kept, in memory and on the device (the session list's cached pages too); nothing kept
10
+ * under one owner is ever shown to another. Passing no `offlineHistory` any more (a deployment
11
+ * that now keeps nothing on devices) drops it all the same.
12
+ * - `getKey` returns the AES-GCM key that seals them, for that owner, or `null` to keep nothing
13
+ * (no key, a deployment that keeps nothing on devices). Midas's comes from its main process,
14
+ * kept with the OS keychain and handed to the page only for the signed-in person.
15
+ * - `isOffline` says Athena can't be reached (the browser's own offline when omitted): then a
16
+ * conversation opens on its last resting transcript this device holds, up to 30 days old, marked
17
+ * "as of" when it was captured; a chat that mounts offline waits (at most
18
+ * {@link OFFLINE_SNAPSHOT_READ_WAIT_MS}) for what the device kept to be read.
19
+ */
20
+ export interface AthenaOfflineHistory {
21
+ ownerId: string;
22
+ getKey: () => Promise<CryptoKey | null>;
23
+ isOffline?: () => boolean;
24
+ }
25
+ /**
26
+ * How long a chat that mounts offline waits for what this device kept before it mounts without:
27
+ * the host's key and IndexedDB answer in milliseconds unless something is stuck.
28
+ */
29
+ export declare const OFFLINE_SNAPSHOT_READ_WAIT_MS = 1500;
30
+ /**
31
+ * Binds `cache` to `ownerId`, and says whether a chat that mounted offline still waits for what
32
+ * this device kept for that owner to be read (at most `waitMs`). The runtime takes a thread's seed
33
+ * once, as it mounts, and offline that seed is all a thread can show; online it never waits, the
34
+ * attach delivers the thread.
35
+ */
36
+ export declare function useKeptSnapshotsGate({ cache, ownerId, isOffline, waitMs, }: {
37
+ cache: Pick<DurableThreadSnapshotCache, 'setOwner'> | null;
38
+ ownerId: string | null;
39
+ isOffline: () => boolean;
40
+ waitMs?: number;
41
+ }): boolean;
42
+ /** The provider's offline snapshots: when a thread on screen was captured, if it opened offline. */
43
+ export type AthenaOfflineSnapshots = Pick<DurableThreadSnapshotCache, 'offlineSeedAt' | 'subscribeOfflineSeeds'>;
44
+ export declare const AthenaOfflineSnapshotsContext: import("react").Context<AthenaOfflineSnapshots | null>;
45
+ /**
46
+ * When the transcript `threadId` shows was captured, if it opened from this device while Athena
47
+ * could not be reached; `null` once the thread's own state arrives, outside an `offlineHistory`
48
+ * provider, or for a new chat.
49
+ */
50
+ export declare function useAthenaOfflineSnapshotAt(threadId: string | undefined): number | null;
51
+ export declare function formatOfflineSnapshotTime(at: number): string;
52
+ export interface OfflineSnapshotBannerProps {
53
+ /** Applied to the outer wrapper (host padding); the banner itself is width-capped inside it. */
54
+ className?: string;
55
+ }
56
+ /**
57
+ * Above a conversation that opened from this device while Athena can't be reached: what is shown
58
+ * is the conversation as it was then, not live. Renders nothing otherwise.
59
+ */
60
+ export declare const OfflineSnapshotBanner: FC<OfflineSnapshotBannerProps>;
@@ -0,0 +1,77 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { CloudOffIcon } from "lucide-react";
3
+ import { useContext, useSyncExternalStore, useState, useEffect, createContext } from "react";
4
+ import { useAthenaThreadId } from "../threads/thread-id-context.js";
5
+ import { cn } from "cnfast";
6
+ const OFFLINE_SNAPSHOT_READ_WAIT_MS = 1500;
7
+ function useKeptSnapshotsGate({
8
+ cache,
9
+ ownerId,
10
+ isOffline,
11
+ waitMs = OFFLINE_SNAPSHOT_READ_WAIT_MS
12
+ }) {
13
+ const [readFor, setReadFor] = useState(void 0);
14
+ useEffect(() => {
15
+ if (!cache) return;
16
+ let live = true;
17
+ const settle = () => {
18
+ if (live) setReadFor(ownerId);
19
+ };
20
+ const timer = setTimeout(settle, waitMs);
21
+ const done = () => {
22
+ clearTimeout(timer);
23
+ settle();
24
+ };
25
+ void cache.setOwner(ownerId).then(done, done);
26
+ return () => {
27
+ live = false;
28
+ clearTimeout(timer);
29
+ };
30
+ }, [cache, ownerId, waitMs]);
31
+ const [mountedOffline] = useState(() => cache !== null && isOffline());
32
+ return mountedOffline && ownerId !== null && readFor !== ownerId;
33
+ }
34
+ const AthenaOfflineSnapshotsContext = createContext(null);
35
+ const noSubscription = () => () => void 0;
36
+ const serverSnapshot = () => null;
37
+ function useAthenaOfflineSnapshotAt(threadId) {
38
+ const snapshots = useContext(AthenaOfflineSnapshotsContext);
39
+ return useSyncExternalStore(
40
+ snapshots ? (listener) => snapshots.subscribeOfflineSeeds(listener) : noSubscription,
41
+ () => snapshots && threadId ? snapshots.offlineSeedAt(threadId) : null,
42
+ serverSnapshot
43
+ );
44
+ }
45
+ function formatOfflineSnapshotTime(at) {
46
+ return new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(at);
47
+ }
48
+ const OfflineSnapshotBanner = ({ className }) => {
49
+ const at = useAthenaOfflineSnapshotAt(useAthenaThreadId());
50
+ if (at === null) return null;
51
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsxs(
52
+ "div",
53
+ {
54
+ role: "status",
55
+ className: cn(
56
+ "mx-auto flex w-full max-w-(--thread-max-width) items-center gap-2 rounded-md border px-3 py-2 text-xs",
57
+ "border-border bg-muted text-muted-foreground"
58
+ ),
59
+ children: [
60
+ /* @__PURE__ */ jsx(CloudOffIcon, { className: "size-3.5 shrink-0", "aria-hidden": true }),
61
+ /* @__PURE__ */ jsxs("span", { children: [
62
+ "Offline · this conversation as of ",
63
+ formatOfflineSnapshotTime(at)
64
+ ] })
65
+ ]
66
+ }
67
+ ) });
68
+ };
69
+ export {
70
+ AthenaOfflineSnapshotsContext,
71
+ OFFLINE_SNAPSHOT_READ_WAIT_MS,
72
+ OfflineSnapshotBanner,
73
+ formatOfflineSnapshotTime,
74
+ useAthenaOfflineSnapshotAt,
75
+ useKeptSnapshotsGate
76
+ };
77
+ //# sourceMappingURL=offline-history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-history.js","sources":["../../src/offline/offline-history.tsx"],"sourcesContent":["import type { DurableThreadSnapshotCache } from '@athenaintel/agent-runtime';\nimport { CloudOffIcon } from 'lucide-react';\nimport {\n createContext,\n type FC,\n useContext,\n useEffect,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport { cn } from '../lib/utils';\nimport { useAthenaThreadId } from '../threads/thread-id-context';\n\n/**\n * Opt-in: keep the chat's recent conversations, and the session list, on this device, sealed with\n * the host's key, so a reload paints them at once and they can be read while Athena can't be\n * reached (`AthenaProvider`'s `offlineHistory`). Without it nothing is stored, as before.\n *\n * - `ownerId` names whose they are (the signed-in person). Changing it drops what the previous\n * owner kept, in memory and on the device (the session list's cached pages too); nothing kept\n * under one owner is ever shown to another. Passing no `offlineHistory` any more (a deployment\n * that now keeps nothing on devices) drops it all the same.\n * - `getKey` returns the AES-GCM key that seals them, for that owner, or `null` to keep nothing\n * (no key, a deployment that keeps nothing on devices). Midas's comes from its main process,\n * kept with the OS keychain and handed to the page only for the signed-in person.\n * - `isOffline` says Athena can't be reached (the browser's own offline when omitted): then a\n * conversation opens on its last resting transcript this device holds, up to 30 days old, marked\n * \"as of\" when it was captured; a chat that mounts offline waits (at most\n * {@link OFFLINE_SNAPSHOT_READ_WAIT_MS}) for what the device kept to be read.\n */\nexport interface AthenaOfflineHistory {\n ownerId: string;\n getKey: () => Promise<CryptoKey | null>;\n isOffline?: () => boolean;\n}\n\n/**\n * How long a chat that mounts offline waits for what this device kept before it mounts without:\n * the host's key and IndexedDB answer in milliseconds unless something is stuck.\n */\nexport const OFFLINE_SNAPSHOT_READ_WAIT_MS = 1_500;\n\n/**\n * Binds `cache` to `ownerId`, and says whether a chat that mounted offline still waits for what\n * this device kept for that owner to be read (at most `waitMs`). The runtime takes a thread's seed\n * once, as it mounts, and offline that seed is all a thread can show; online it never waits, the\n * attach delivers the thread.\n */\nexport function useKeptSnapshotsGate({\n cache,\n ownerId,\n isOffline,\n waitMs = OFFLINE_SNAPSHOT_READ_WAIT_MS,\n}: {\n cache: Pick<DurableThreadSnapshotCache, 'setOwner'> | null;\n ownerId: string | null;\n isOffline: () => boolean;\n waitMs?: number;\n}): boolean {\n const [readFor, setReadFor] = useState<string | null | undefined>(undefined);\n useEffect(() => {\n if (!cache) return;\n let live = true;\n const settle = () => {\n if (live) setReadFor(ownerId);\n };\n const timer = setTimeout(settle, waitMs);\n const done = () => {\n clearTimeout(timer);\n settle();\n };\n void cache.setOwner(ownerId).then(done, done);\n return () => {\n live = false;\n clearTimeout(timer);\n };\n }, [cache, ownerId, waitMs]);\n const [mountedOffline] = useState(() => cache !== null && isOffline());\n return mountedOffline && ownerId !== null && readFor !== ownerId;\n}\n\n/** The provider's offline snapshots: when a thread on screen was captured, if it opened offline. */\nexport type AthenaOfflineSnapshots = Pick<\n DurableThreadSnapshotCache,\n 'offlineSeedAt' | 'subscribeOfflineSeeds'\n>;\n\nexport const AthenaOfflineSnapshotsContext = createContext<AthenaOfflineSnapshots | null>(null);\n\nconst noSubscription = (): (() => void) => () => undefined;\nconst serverSnapshot = (): number | null => null;\n\n/**\n * When the transcript `threadId` shows was captured, if it opened from this device while Athena\n * could not be reached; `null` once the thread's own state arrives, outside an `offlineHistory`\n * provider, or for a new chat.\n */\nexport function useAthenaOfflineSnapshotAt(threadId: string | undefined): number | null {\n const snapshots = useContext(AthenaOfflineSnapshotsContext);\n return useSyncExternalStore(\n snapshots ? (listener) => snapshots.subscribeOfflineSeeds(listener) : noSubscription,\n () => (snapshots && threadId ? snapshots.offlineSeedAt(threadId) : null),\n serverSnapshot,\n );\n}\n\nexport function formatOfflineSnapshotTime(at: number): string {\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(at);\n}\n\nexport interface OfflineSnapshotBannerProps {\n /** Applied to the outer wrapper (host padding); the banner itself is width-capped inside it. */\n className?: string;\n}\n\n/**\n * Above a conversation that opened from this device while Athena can't be reached: what is shown\n * is the conversation as it was then, not live. Renders nothing otherwise.\n */\nexport const OfflineSnapshotBanner: FC<OfflineSnapshotBannerProps> = ({ className }) => {\n const at = useAthenaOfflineSnapshotAt(useAthenaThreadId());\n if (at === null) return null;\n return (\n <div className={className}>\n <div\n role=\"status\"\n className={cn(\n 'mx-auto flex w-full max-w-(--thread-max-width) items-center gap-2 rounded-md border px-3 py-2 text-xs',\n 'border-border bg-muted text-muted-foreground',\n )}\n >\n <CloudOffIcon className=\"size-3.5 shrink-0\" aria-hidden />\n <span>Offline · this conversation as of {formatOfflineSnapshotTime(at)}</span>\n </div>\n </div>\n );\n};\n"],"names":[],"mappings":";;;;;AAwCO,MAAM,gCAAgC;AAQtC,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AACX,GAKY;AACV,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoC,MAAS;AAC3E,YAAU,MAAM;AACd,QAAI,CAAC,MAAO;AACZ,QAAI,OAAO;AACX,UAAM,SAAS,MAAM;AACnB,UAAI,iBAAiB,OAAO;AAAA,IAC9B;AACA,UAAM,QAAQ,WAAW,QAAQ,MAAM;AACvC,UAAM,OAAO,MAAM;AACjB,mBAAa,KAAK;AAClB,aAAA;AAAA,IACF;AACA,SAAK,MAAM,SAAS,OAAO,EAAE,KAAK,MAAM,IAAI;AAC5C,WAAO,MAAM;AACX,aAAO;AACP,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,MAAM,CAAC;AAC3B,QAAM,CAAC,cAAc,IAAI,SAAS,MAAM,UAAU,QAAQ,WAAW;AACrE,SAAO,kBAAkB,YAAY,QAAQ,YAAY;AAC3D;AAQO,MAAM,gCAAgC,cAA6C,IAAI;AAE9F,MAAM,iBAAiB,MAAoB,MAAM;AACjD,MAAM,iBAAiB,MAAqB;AAOrC,SAAS,2BAA2B,UAA6C;AACtF,QAAM,YAAY,WAAW,6BAA6B;AAC1D,SAAO;AAAA,IACL,YAAY,CAAC,aAAa,UAAU,sBAAsB,QAAQ,IAAI;AAAA,IACtE,MAAO,aAAa,WAAW,UAAU,cAAc,QAAQ,IAAI;AAAA,IACnE;AAAA,EAAA;AAEJ;AAEO,SAAS,0BAA0B,IAAoB;AAC5D,SAAO,IAAI,KAAK,eAAe,QAAW,EAAE,WAAW,UAAU,WAAW,QAAA,CAAS,EAAE,OAAO,EAAE;AAClG;AAWO,MAAM,wBAAwD,CAAC,EAAE,gBAAgB;AACtF,QAAM,KAAK,2BAA2B,mBAAmB;AACzD,MAAI,OAAO,KAAM,QAAO;AACxB,SACE,oBAAC,SAAI,WACH,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAA,oBAAC,cAAA,EAAa,WAAU,qBAAoB,eAAW,MAAC;AAAA,6BACvD,QAAA,EAAK,UAAA;AAAA,UAAA;AAAA,UAAmC,0BAA0B,EAAE;AAAA,QAAA,EAAA,CAAE;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAE3E;AAEJ;"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const react$1 = require("@assistant-ui/react");
5
5
  const react = require("react");
6
+ const offlineHistory = require("../offline/offline-history.cjs");
6
7
  const athenaUrls = require("../lib/athena-urls.cjs");
7
8
  const mint = require("../collab/mint.cjs");
8
9
  const auth = require("../runtime/auth.cjs");
@@ -36,6 +37,8 @@ const assetPanelStore = require("../assets/asset-panel-store.cjs");
36
37
  const index = require("../theme/index.cjs");
37
38
  const themeStyleContext = require("./theme-style-context.cjs");
38
39
  const PostHogProvider = require("./PostHogProvider.cjs");
40
+ const threadSnapshotMirror = require("../agent-runtime/src/thread-snapshot-mirror.cjs");
41
+ const durableThreadSnapshotCache = require("../agent-runtime/src/durable-thread-snapshot-cache.cjs");
39
42
  const threadSnapshotCache = require("../agent-runtime/src/thread-snapshot-cache.cjs");
40
43
  const toolOutputs = require("../agent-runtime/src/tool-outputs.cjs");
41
44
  const threadStatus = require("../agent-runtime/src/thread-status.cjs");
@@ -354,6 +357,7 @@ function AthenaStatewireWithThreadList({
354
357
  initialThreadId,
355
358
  includePullRequests,
356
359
  additionalListAppIds,
360
+ offlineList,
357
361
  ...standaloneProps
358
362
  }) {
359
363
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -368,6 +372,7 @@ function AthenaStatewireWithThreadList({
368
372
  additionalListAppIds,
369
373
  initialThreadId,
370
374
  includePullRequests,
375
+ offlineList,
371
376
  snapshotCache: standaloneProps.snapshotCache,
372
377
  children: /* @__PURE__ */ jsxRuntime.jsx(
373
378
  AthenaStatewireActiveThreadHost,
@@ -448,7 +453,8 @@ function AthenaProvider({
448
453
  posthog: posthogProp,
449
454
  debug,
450
455
  onDiagnostic,
451
- onError
456
+ onError,
457
+ offlineHistory: offlineHistory$1
452
458
  }) {
453
459
  var _a;
454
460
  const agentAssetId = statewireRunConfig.resolveAgentProp({ agent });
@@ -525,7 +531,43 @@ function AthenaProvider({
525
531
  });
526
532
  const authContext = react.useContext(AthenaAuthContext.AthenaAuthContext);
527
533
  const authProviderToken = (authContext == null ? void 0 : authContext.accessToken) ?? null;
528
- const [snapshotCache] = react.useState(() => threadSnapshotCache.createDeepAgentThreadSnapshotCache());
534
+ const offlineHistoryRef = react.useRef(offlineHistory$1);
535
+ offlineHistoryRef.current = offlineHistory$1;
536
+ const isOfflineNow = () => {
537
+ var _a2, _b;
538
+ return ((_b = (_a2 = offlineHistoryRef.current) == null ? void 0 : _a2.isOffline) == null ? void 0 : _b.call(_a2)) ?? (typeof navigator !== "undefined" && navigator.onLine === false);
539
+ };
540
+ const [offlineMirror] = react.useState(
541
+ () => offlineHistory$1 ? threadSnapshotMirror.createThreadSnapshotMirror({
542
+ getKey: (ownerId) => {
543
+ const host = offlineHistoryRef.current;
544
+ return host && host.ownerId === ownerId ? host.getKey() : Promise.resolve(null);
545
+ }
546
+ }) : null
547
+ );
548
+ const [durableSnapshotCache] = react.useState(
549
+ () => offlineMirror ? durableThreadSnapshotCache.createDurableThreadSnapshotCache({ mirror: offlineMirror, isOffline: isOfflineNow }) : null
550
+ );
551
+ const [snapshotCache] = react.useState(
552
+ () => durableSnapshotCache ?? threadSnapshotCache.createDeepAgentThreadSnapshotCache()
553
+ );
554
+ const offlineOwnerId = (offlineHistory$1 == null ? void 0 : offlineHistory$1.ownerId) ?? null;
555
+ const awaitingKeptSnapshots = offlineHistory.useKeptSnapshotsGate({
556
+ cache: durableSnapshotCache,
557
+ ownerId: offlineOwnerId,
558
+ isOffline: isOfflineNow
559
+ });
560
+ const offlineSnapshots = react.useMemo(
561
+ () => durableSnapshotCache ? {
562
+ offlineSeedAt: (threadId) => durableSnapshotCache.offlineSeedAt(threadId),
563
+ subscribeOfflineSeeds: (listener) => durableSnapshotCache.subscribeOfflineSeeds(listener)
564
+ } : null,
565
+ [durableSnapshotCache]
566
+ );
567
+ const offlineList = react.useMemo(
568
+ () => offlineMirror && offlineOwnerId ? { mirror: offlineMirror, ownerId: offlineOwnerId } : void 0,
569
+ [offlineMirror, offlineOwnerId]
570
+ );
529
571
  const [composerHandoff$1] = react.useState(composerHandoff.createComposerHandoffStore);
530
572
  const [toolOutputs$1] = react.useState(() => toolOutputs.createDeepAgentToolOutputStore());
531
573
  const [historyStore] = react.useState(statewireHistoryContext.createStatewireHistoryStore);
@@ -608,6 +650,7 @@ function AthenaProvider({
608
650
  initialThreadId: threadIdProp,
609
651
  includePullRequests: threadListIncludePullRequests,
610
652
  additionalListAppIds: threadListAdditionalAppIds,
653
+ offlineList,
611
654
  children
612
655
  }
613
656
  ) : (
@@ -629,7 +672,7 @@ function AthenaProvider({
629
672
  {
630
673
  className: themeStyleVars ? "athena-sdk athena-themed" : "athena-sdk",
631
674
  style: themeStyleVars,
632
- children: /* @__PURE__ */ jsxRuntime.jsx(PostHogProvider.PostHogProvider, { config: posthogConfig, children: /* @__PURE__ */ jsxRuntime.jsx(composerHandoff.ComposerHandoffContext.Provider, { value: composerHandoff$1, children: /* @__PURE__ */ jsxRuntime.jsx(themeStyleContext.AthenaThemeStyleContext.Provider, { value: themeStyleVars, children: /* @__PURE__ */ jsxRuntime.jsx(AthenaCredentialSourceContext.AthenaCredentialSourceContext.Provider, { value: credentials, children: /* @__PURE__ */ jsxRuntime.jsx(agentSelectionContext.AthenaAgentSelectionContext.Provider, { value: agentSelection, children: inner }) }) }) }) })
675
+ children: /* @__PURE__ */ jsxRuntime.jsx(PostHogProvider.PostHogProvider, { config: posthogConfig, children: /* @__PURE__ */ jsxRuntime.jsx(composerHandoff.ComposerHandoffContext.Provider, { value: composerHandoff$1, children: /* @__PURE__ */ jsxRuntime.jsx(themeStyleContext.AthenaThemeStyleContext.Provider, { value: themeStyleVars, children: /* @__PURE__ */ jsxRuntime.jsx(AthenaCredentialSourceContext.AthenaCredentialSourceContext.Provider, { value: credentials, children: /* @__PURE__ */ jsxRuntime.jsx(agentSelectionContext.AthenaAgentSelectionContext.Provider, { value: agentSelection, children: /* @__PURE__ */ jsxRuntime.jsx(offlineHistory.AthenaOfflineSnapshotsContext.Provider, { value: offlineSnapshots, children: awaitingKeptSnapshots ? null : inner }) }) }) }) }) })
633
676
  }
634
677
  );
635
678
  }