@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.
- package/README.md +6 -0
- package/dist/agent-runtime/src/durable-thread-snapshot-cache.cjs +158 -0
- package/dist/agent-runtime/src/durable-thread-snapshot-cache.cjs.map +1 -0
- package/dist/agent-runtime/src/durable-thread-snapshot-cache.js +158 -0
- package/dist/agent-runtime/src/durable-thread-snapshot-cache.js.map +1 -0
- package/dist/agent-runtime/src/thread-snapshot-mirror.cjs +400 -0
- package/dist/agent-runtime/src/thread-snapshot-mirror.cjs.map +1 -0
- package/dist/agent-runtime/src/thread-snapshot-mirror.js +400 -0
- package/dist/agent-runtime/src/thread-snapshot-mirror.js.map +1 -0
- package/dist/chat/AthenaChat.cjs +5 -1
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +5 -1
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +4 -3
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.d.ts +7 -1
- package/dist/chat/super-grouping/activity-timeline.js +4 -3
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/offline/offline-history.cjs +77 -0
- package/dist/offline/offline-history.cjs.map +1 -0
- package/dist/offline/offline-history.d.ts +60 -0
- package/dist/offline/offline-history.js +77 -0
- package/dist/offline/offline-history.js.map +1 -0
- package/dist/provider/AthenaProvider.cjs +46 -3
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.d.ts +8 -1
- package/dist/provider/AthenaProvider.js +46 -3
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/threads/offline-session-list.cjs +75 -0
- package/dist/threads/offline-session-list.cjs.map +1 -0
- package/dist/threads/offline-session-list.d.ts +27 -0
- package/dist/threads/offline-session-list.js +75 -0
- package/dist/threads/offline-session-list.js.map +1 -0
- package/dist/threads/statewire-thread-list.cjs +63 -4
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.d.ts +21 -2
- package/dist/threads/statewire-thread-list.js +64 -5
- package/dist/threads/statewire-thread-list.js.map +1 -1
- 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
|
-
/**
|
|
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
|
|
121
|
-
|
|
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":";;;AAuEA,MAAM,+BAAoC,IAAA;AAWnC,SAAS,2BAA2B,MAA+C;AACxF,QAAM,WAAoB,KAAK;AAC/B,MAAI,OAAO,aAAa,YAAY,aAAa,KAAM,QAAO;AAC9D,QAAM,WAAqB,SACxB;AACH,MAAI,OAAO,aAAa,YAAY,aAAa,KAAM,QAAO;AAC9D,SAAQ,SAAqC,cAAc;AAC7D;AAGA,SAAS,YAAY,MAA6B;AAChD,SAAO,2BAA2B,IAAI,IAAI,SAAY,KAAK;AAC7D;AAEA,SAAS,eACP,MACA,eACuC;AACvC,MAAI,YAAY,IAAI,MAAM,OAAW,QAAO,EAAE,MAAM,WAAA;AACpD,OAAI,+CAAe,UAAS,cAAc;AACxC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ,cAAc,WAAW,eAAe,UAAU,cAAc;AAAA,MACxE,OAAO,cAAc;AAAA,IAAA;AAAA,EAEzB;AACA,OAAI,+CAAe,UAAS,WAAY,QAAO,EAAE,MAAM,WAAA;AACvD,OAAI,+CAAe,UAAS,mBAAmB;AAC7C,WAAO,EAAE,MAAM,mBAAmB,QAAQ,cAAc,OAAA;AAAA,EAC1D;AACA,OAAI,+CAAe,UAAS,UAAW,QAAO,EAAE,MAAM,UAAA;AACtD,SAAO;AACT;AAGO,SAAS,qBACd,MACA,eACA,EAAE,eAAe,UAAU,mBAAmB,SAAA,IAAsC,IACjE;;AACnB,QAAI,UAAK,aAAL,mBAAe,cAAa,SAAS,uBAAuB,KAAK,MAAM,MAAM,MAAM;AACrF,WAAO;AAAA,EACT;AACA,QAAI,UAAK,aAAL,mBAAe,gBAAe,iBAAe,UAAK,aAAL,mBAAe,gBAAe,WAAW;AACxF,WAAO;AAAA,EACT;AACA,QAAM,SAAS,YAAY,IAAI;AAC/B,QAAM,aAAa,WAAW;AAC9B,QAAM,kBACJ,kBAAkB,UAClB,cAAc,SAAS,aACvB,cAAc,SAAS;AACzB,MACE,iBACC,UAAK,cAAL,mBAAgB,UAAS,WACvB,KAAK,aAAa,UACjB,KAAK,SAAS,aAAa,UAC3B,KAAK,SAAS,eAAe,UAC9B,aAAa,IAAI,KAAK,QAAQ,KAAK,kBACtC;AACA,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB,mBAAmB;AAAA,IACxC,QAAQ,eAAe,MAAM,aAAa;AAAA,IAC1C;AAAA,IACA,SAAS,KAAK;AAAA,IACd,oBAAoB,iBAAiB,IAAI,KAAK,QAAQ;AAAA,EAAA,CACvD;AACD,MAAI,eAAe,UAAU,kBAAmB,QAAO;AACvD,SAAO,eAAe;AACxB;AAUO,SAAS,sBACd,SACA,eACA,UAAmC,CAAA,GACnB;AAChB,MAAI,EAAC,mCAAS,QAAQ,QAAO,CAAA;AAE7B,QAAM,gBAAgB,QAAQ,SAAS;AACvC,QAAM,WAA2B,CAAA;AACjC,WAAS,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;AACtD,UAAM,OAAO,QAAQ,KAAK;AAC1B,QAAI,KAAK,SAAS,aAAa;AAC7B,YAAM,SAAS,YAAY,IAAI;AAC/B,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,IAAI,KAAK;AAAA,QACT,WAAW;AAAA,QACX,MAAM,KAAK;AAAA,QACX,GAAI,WAAW,SAAY,EAAE,OAAA,IAAW,CAAA;AAAA,QACxC,UAAU,KAAK;AAAA,QACf,OAAO,qBAAqB,MAAM,eAAe,OAAO;AAAA,MAAA,CACzD;AACD;AAAA,IACF;AACA,QAAI,KAAK,SAAS,YAAa;AAE/B,UAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AACzD,UAAM,kBAAiB,+CAAe,UAAS,aAAa,UAAU;AACtE,QAAI,CAAC,kBAAkB,KAAK,KAAA,MAAW,GAAI;AAC3C,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,IAAI,aAAa,KAAK;AAAA,MACtB,WAAW;AAAA,MACX;AAAA,MACA,OAAO,iBAAiB,YAAY;AAAA,IAAA,CACrC;AAAA,EACH;AACA,SAAO;AACT;AAGO,SAAS,mBAAmB,UAA2C;AAC5E,SAAO,SAAS,OAAO,CAAC,OAAO,SAAU,KAAK,SAAS,SAAS,QAAQ,IAAI,OAAQ,CAAC;AACvF;AAOO,SAAS,qBACd,QACA,UACA,EAAE,kBAAkB,MAAA,IAAyC,IAC9C;AACf,MACE,oBACA,iCAAQ,UAAS,qBACjB,SAAS,KAAK,CAAC,SAAS,KAAK,SAAS,UAAU,KAAK,UAAU,SAAS,GACxE;AACA,WAAO;AAAA,EACT;AACA,OAAI,iCAAQ,UAAS,UAAW,QAAO;AACvC,OAAI,iCAAQ,UAAS,aAAc,QAAO;AAC1C,SAAO;AACT;AAOO,SAAS,kBAAkB,MAAc,OAAuC;;AACrF,MAAI,UAAU,UAAW,QAAO;AAChC,QAAM,WAAU,yCAA8B,KAAK,IAAI,MAAvC,mBAA2C,OAA3C,mBAA+C;AAC/D,SAAO,UAAU,QAAQ,QAAQ,UAAU,EAAE,IAAI;AACnD;AASO,SAAS,qBACd,MACA,wBACA,aACQ;AACR,MAAI,KAAK,SAAS,YAAa,QAAO,kBAAkB,KAAK,MAAM,KAAK,KAAK;AAC7E,SAAO,sBAAsB,MAAM,wBAAwB,WAAW,EAAE;AAC1E;AAGO,SAAS,sBACd,MACA,wBACA,aACiB;AACjB,SAAO,kBAAkB;AAAA,IACvB,UAAU,KAAK;AAAA,IACf,MAAM,KAAK;AAAA,IACX,QAAQ,KAAK;AAAA,IACb,QAAQ,KAAK;AAAA,IACb,GAAI,cAAc,EAAE,YAAA,IAAgB,CAAA;AAAA,IACpC,GAAI,yBAAyB,EAAE,aAAa,uBAAuB,KAAK,QAAQ,MAAM,CAAA;AAAA,EAAC,CACxF;AACH;AAGO,SAAS,mBAAmB,UAA2C;AAC5E,QAAM,YAAY,mBAAmB,QAAQ;AAC7C,QAAM,WAAW,SAAS;AAAA,IACxB,CAAC,SAAS,KAAK,SAAS,WAAW,KAAK,UAAU,WAAW,KAAK,UAAU;AAAA,EAAA;AAE9E,MAAI,SAAU,QAAO;AACrB,MAAI,SAAS,KAAK,CAAC,SAAS,KAAK,SAAS,UAAU,KAAK,UAAU,WAAW,GAAG;AAC/E,WAAO;AAAA,EACT;AACA,MAAI,SAAS,KAAK,CAAC,SAAS,KAAK,SAAS,UAAU,KAAK,UAAU,QAAQ,GAAG;AAC5E,WAAO;AAAA,EACT;AACA,MAAI,YAAY,EAAG,QAAO,QAAQ,SAAS,IAAI,cAAc,IAAI,SAAS,OAAO;AACjF,SAAO;AACT;AAWA,SAAS,iBACP,MACA,OACA,eACS;AACT,UAAO,6BAAM,UAAS,WAAW,QAAQ,iBAAiB,qBAAqB,IAAI;AACrF;AAQO,SAAS,aAAa,SAA8D;AACzF,QAAM,QAAQ,kBAAkB,OAAO;AACvC,SAAO,MAAM,SAAS,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,KAAK,MAAM,IAAI;AAC1E;AAeO,SAAS,kBAAkB,SAAmE;AACnG,MAAI,EAAC,mCAAS,QAAQ,QAAO,CAAA;AAE7B,QAAM,gBAAgB,QAAQ,cAAc,CAAC,UAAS,6BAAM,UAAS,WAAW;AAChF,QAAM,QAA4B,CAAA;AAClC,UAAQ,QAAQ,CAAC,MAAM,UAAU;AAC/B,QAAI,KAAK,QAAQ,iBAAiB,MAAM,OAAO,aAAa,EAAG,OAAM,KAAK,EAAE,OAAO,MAAM,KAAK,MAAM;AAAA,EACtG,CAAC;AACD,SAAO;AACT;AAOO,SAAS,2BAA2B,UAA8C;AACvF,SAAO,CAAC,UAAU;AAChB,UAAM,gBAAgB,MAAM,cAAc,CAAC,UAAS,6BAAM,UAAS,WAAW;AAC9E,WAAO,SAAS,KAAK,EAClB,IAAI,CAAC,WAAW;AAAA,MACf,GAAG;AAAA,MACH,SAAS,MAAM,QAAQ,OAAO,CAAC,UAAU,CAAC,iBAAiB,MAAM,KAAK,GAAG,OAAO,aAAa,CAAC;AAAA,IAAA,EAC9F,EACD,OAAO,CAAC,UAAU,MAAM,QAAQ,SAAS,CAAC;AAAA,EAC/C;AACF;AAOO,SAAS,uBACd,UACA,SACkB;AAClB,SAAO,CAAC,UACN,SAAS,KAAK,EACX,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,SAAS,MAAM,QAAQ,OAAO,CAAC,UAAU;AACvC,YAAM,OAAO,MAAM,KAAK;AACxB,aAAO,SAAS,UAAa,CAAC,QAAQ,IAAI;AAAA,IAC5C,CAAC;AAAA,EAAA,EACD,EACD,OAAO,CAAC,UAAU,MAAM,QAAQ,SAAS,CAAC;AACjD;"}
|
|
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;
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
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
|
}
|