@athenaintel/react 0.57.1 → 0.57.2
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 +19 -0
- package/dist/chat/AthenaChat.cjs +47 -2
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +47 -2
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +7 -0
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.js +9 -2
- 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/runtime/statewire-run-config.cjs +15 -0
- package/dist/runtime/statewire-run-config.cjs.map +1 -1
- package/dist/runtime/statewire-run-config.d.ts +13 -0
- package/dist/runtime/statewire-run-config.js +15 -0
- package/dist/runtime/statewire-run-config.js.map +1 -1
- package/dist/threads/SessionList.cjs +22 -19
- package/dist/threads/SessionList.cjs.map +1 -1
- package/dist/threads/SessionList.js +22 -19
- package/dist/threads/SessionList.js.map +1 -1
- package/dist/threads/live-session-rows.cjs +207 -0
- package/dist/threads/live-session-rows.cjs.map +1 -0
- package/dist/threads/live-session-rows.d.ts +175 -0
- package/dist/threads/live-session-rows.js +207 -0
- package/dist/threads/live-session-rows.js.map +1 -0
- package/dist/threads/statewire-thread-list.cjs +173 -20
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.d.ts +44 -1
- package/dist/threads/statewire-thread-list.js +174 -21
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const conversationList = require("../agent-runtime/src/conversation-list.cjs");
|
|
4
|
+
const AGENT_HOME_EXCLUDED_START_CHANNELS = /* @__PURE__ */ new Set([
|
|
5
|
+
"task",
|
|
6
|
+
"agent_orchestration"
|
|
7
|
+
]);
|
|
8
|
+
const LISTED_ASSET_TYPE = "session";
|
|
9
|
+
function isAgentHomeIndexEntry(entry, homeAgentId) {
|
|
10
|
+
if (!homeAgentId || entry.collab_agent_id !== homeAgentId) return false;
|
|
11
|
+
return !AGENT_HOME_EXCLUDED_START_CHANNELS.has(entry.start_channel ?? "");
|
|
12
|
+
}
|
|
13
|
+
function inSessionListScope(entry, scope) {
|
|
14
|
+
const appIds = [...scope.appId ? [scope.appId] : [], ...scope.additionalAppIds];
|
|
15
|
+
const agentId = scope.homeAgentApplied ? scope.homeAgentId : void 0;
|
|
16
|
+
if (appIds.length === 0) return agentId ? isAgentHomeIndexEntry(entry, agentId) : true;
|
|
17
|
+
return appIds.includes(entry.app_id ?? "") || isAgentHomeIndexEntry(entry, agentId);
|
|
18
|
+
}
|
|
19
|
+
function timestampMs(value) {
|
|
20
|
+
const parsed = Date.parse(value);
|
|
21
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
22
|
+
}
|
|
23
|
+
function withIndexedSessions({
|
|
24
|
+
conversations,
|
|
25
|
+
loadedConversations,
|
|
26
|
+
entries,
|
|
27
|
+
scope,
|
|
28
|
+
searching,
|
|
29
|
+
hasResolvedFirstPage,
|
|
30
|
+
hasNextPage
|
|
31
|
+
}) {
|
|
32
|
+
if (!entries || entries.length === 0 || searching || !hasResolvedFirstPage) return conversations;
|
|
33
|
+
const listed = new Set(conversations.map((conversation) => conversation.threadId));
|
|
34
|
+
let windowTailMs = null;
|
|
35
|
+
if (hasNextPage) {
|
|
36
|
+
for (const conversation of loadedConversations) {
|
|
37
|
+
const ms = timestampMs(conversation.updatedAt);
|
|
38
|
+
if (windowTailMs === null || ms < windowTailMs) windowTailMs = ms;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const added = [];
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (listed.has(entry.thread_id)) continue;
|
|
44
|
+
if (entry.asset_type !== LISTED_ASSET_TYPE) continue;
|
|
45
|
+
if (!inSessionListScope(entry, scope)) continue;
|
|
46
|
+
if (windowTailMs !== null && timestampMs(entry.updated_at) < windowTailMs) continue;
|
|
47
|
+
listed.add(entry.thread_id);
|
|
48
|
+
added.push(conversationList.summaryFromConversationIndexEntry(entry));
|
|
49
|
+
}
|
|
50
|
+
return added.length === 0 ? conversations : [...conversations, ...added];
|
|
51
|
+
}
|
|
52
|
+
const LOCAL_SEND_TTL_MS = 6e4;
|
|
53
|
+
const PENDING_TITLE_MAX_LENGTH = 200;
|
|
54
|
+
function pendingSessionTitle(text) {
|
|
55
|
+
const line = (text ?? "").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim();
|
|
56
|
+
if (!line) return null;
|
|
57
|
+
return line.length <= PENDING_TITLE_MAX_LENGTH ? line : `${line.slice(0, PENDING_TITLE_MAX_LENGTH - 1).trimEnd()}…`;
|
|
58
|
+
}
|
|
59
|
+
function localSessionSend({
|
|
60
|
+
threadId,
|
|
61
|
+
text,
|
|
62
|
+
sentAtMs,
|
|
63
|
+
newChat,
|
|
64
|
+
startChannel,
|
|
65
|
+
serverConversations
|
|
66
|
+
}) {
|
|
67
|
+
let newestMs = 0;
|
|
68
|
+
let baselineUpdatedAt = null;
|
|
69
|
+
for (const conversation of serverConversations) {
|
|
70
|
+
const ms = timestampMs(conversation.updatedAt);
|
|
71
|
+
if (ms > newestMs) newestMs = ms;
|
|
72
|
+
if (conversation.threadId === threadId) baselineUpdatedAt = conversation.updatedAt;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
threadId,
|
|
76
|
+
sentAtMs,
|
|
77
|
+
sortAt: new Date(Math.max(sentAtMs, newestMs + 1)).toISOString(),
|
|
78
|
+
baselineUpdatedAt,
|
|
79
|
+
newChat: newChat && baselineUpdatedAt === null,
|
|
80
|
+
title: pendingSessionTitle(text),
|
|
81
|
+
startChannel
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function isLocalSendReported(send, serverRow) {
|
|
85
|
+
if (!serverRow) return false;
|
|
86
|
+
return send.baselineUpdatedAt === null || serverRow.updatedAt !== send.baselineUpdatedAt;
|
|
87
|
+
}
|
|
88
|
+
function withoutLocalSends(sends, threadIds) {
|
|
89
|
+
if (!threadIds.some((threadId) => sends.has(threadId))) return sends;
|
|
90
|
+
const next = new Map(sends);
|
|
91
|
+
for (const threadId of threadIds) next.delete(threadId);
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
function settledLocalSends({
|
|
95
|
+
sends,
|
|
96
|
+
serverConversations,
|
|
97
|
+
nowMs,
|
|
98
|
+
ttlMs = LOCAL_SEND_TTL_MS
|
|
99
|
+
}) {
|
|
100
|
+
if (sends.size === 0) return [];
|
|
101
|
+
const byThread = new Map(serverConversations.map((row) => [row.threadId, row]));
|
|
102
|
+
const settled = [];
|
|
103
|
+
for (const send of sends.values()) {
|
|
104
|
+
if (nowMs - send.sentAtMs >= ttlMs || isLocalSendReported(send, byThread.get(send.threadId))) {
|
|
105
|
+
settled.push(send.threadId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return settled;
|
|
109
|
+
}
|
|
110
|
+
function withSentMessage(row, send) {
|
|
111
|
+
return {
|
|
112
|
+
...row,
|
|
113
|
+
updatedAt: timestampMs(row.updatedAt) >= timestampMs(send.sortAt) ? row.updatedAt : send.sortAt,
|
|
114
|
+
state: "running",
|
|
115
|
+
sessionStatus: "active",
|
|
116
|
+
runStatus: "running",
|
|
117
|
+
attention: "working",
|
|
118
|
+
nextAction: null,
|
|
119
|
+
attentionSource: null,
|
|
120
|
+
isUnread: false
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function pendingConversation(send, appId) {
|
|
124
|
+
const sentAt = new Date(send.sentAtMs).toISOString();
|
|
125
|
+
return {
|
|
126
|
+
threadId: send.threadId,
|
|
127
|
+
title: send.title ?? "",
|
|
128
|
+
updatedAt: send.sortAt,
|
|
129
|
+
createdAt: sentAt,
|
|
130
|
+
state: "running",
|
|
131
|
+
runtime: null,
|
|
132
|
+
// What the server stamps on the session's first run, when that is known here: the row's
|
|
133
|
+
// origin badge reads as the server's row will, never as a label made from the app id.
|
|
134
|
+
sourceChannel: send.startChannel,
|
|
135
|
+
assetType: LISTED_ASSET_TYPE,
|
|
136
|
+
assetId: null,
|
|
137
|
+
appId: appId ?? null,
|
|
138
|
+
triggerType: null,
|
|
139
|
+
preview: send.title,
|
|
140
|
+
messageCount: null,
|
|
141
|
+
isUnread: false,
|
|
142
|
+
sessionStatus: "active",
|
|
143
|
+
runStatus: "running",
|
|
144
|
+
attention: "working",
|
|
145
|
+
nextAction: null,
|
|
146
|
+
attentionSource: null,
|
|
147
|
+
isFavorited: false,
|
|
148
|
+
createdByMe: true
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function withLocalSends({
|
|
152
|
+
conversations,
|
|
153
|
+
sends,
|
|
154
|
+
appId,
|
|
155
|
+
showPendingSessions
|
|
156
|
+
}) {
|
|
157
|
+
if (sends.size === 0) return { conversations, pendingThreadIds: EMPTY_IDS };
|
|
158
|
+
const listed = /* @__PURE__ */ new Set();
|
|
159
|
+
let changed = false;
|
|
160
|
+
const rows = conversations.map((row) => {
|
|
161
|
+
listed.add(row.threadId);
|
|
162
|
+
const send = sends.get(row.threadId);
|
|
163
|
+
if (!send || isLocalSendReported(send, row)) return row;
|
|
164
|
+
changed = true;
|
|
165
|
+
return withSentMessage(row, send);
|
|
166
|
+
});
|
|
167
|
+
const pendingThreadIds = /* @__PURE__ */ new Set();
|
|
168
|
+
if (showPendingSessions) {
|
|
169
|
+
for (const send of sends.values()) {
|
|
170
|
+
if (!send.newChat || listed.has(send.threadId)) continue;
|
|
171
|
+
pendingThreadIds.add(send.threadId);
|
|
172
|
+
rows.push(pendingConversation(send, appId));
|
|
173
|
+
changed = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
conversations: changed ? rows : conversations,
|
|
178
|
+
pendingThreadIds: pendingThreadIds.size === 0 ? EMPTY_IDS : pendingThreadIds
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
182
|
+
function sortNewestFirst(rows) {
|
|
183
|
+
var _a, _b;
|
|
184
|
+
const keyed = rows.map((row, index) => ({ row, index, ms: timestampMs(row.updatedAt) }));
|
|
185
|
+
let ordered = true;
|
|
186
|
+
for (let i = 1; i < keyed.length; i++) {
|
|
187
|
+
if ((((_a = keyed[i - 1]) == null ? void 0 : _a.ms) ?? 0) < (((_b = keyed[i]) == null ? void 0 : _b.ms) ?? 0)) {
|
|
188
|
+
ordered = false;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (ordered) return rows;
|
|
193
|
+
keyed.sort((a, b) => b.ms - a.ms || a.index - b.index);
|
|
194
|
+
return keyed.map(({ row }) => row);
|
|
195
|
+
}
|
|
196
|
+
exports.LOCAL_SEND_TTL_MS = LOCAL_SEND_TTL_MS;
|
|
197
|
+
exports.inSessionListScope = inSessionListScope;
|
|
198
|
+
exports.isAgentHomeIndexEntry = isAgentHomeIndexEntry;
|
|
199
|
+
exports.isLocalSendReported = isLocalSendReported;
|
|
200
|
+
exports.localSessionSend = localSessionSend;
|
|
201
|
+
exports.pendingSessionTitle = pendingSessionTitle;
|
|
202
|
+
exports.settledLocalSends = settledLocalSends;
|
|
203
|
+
exports.sortNewestFirst = sortNewestFirst;
|
|
204
|
+
exports.withIndexedSessions = withIndexedSessions;
|
|
205
|
+
exports.withLocalSends = withLocalSends;
|
|
206
|
+
exports.withoutLocalSends = withoutLocalSends;
|
|
207
|
+
//# sourceMappingURL=live-session-rows.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-session-rows.cjs","sources":["../../src/threads/live-session-rows.ts"],"sourcesContent":["/**\n * The rows the session list shows between one server answer and the next.\n *\n * The list's rows come from the fetched REST pages, and the signed-in user's\n * live conversation index updates them in place. Two gaps are filled here, so\n * the list follows what just happened without waiting for a re-read:\n *\n * - **Sessions the index pushed that the page does not list yet.** A session\n * just started (in this tab or anywhere else) reaches the index in about a\n * second; the re-read the list runs for it takes several more (measured on\n * staging 2026-11-06: push at +1.8 s, row drawn at +5.5 s). The pushed entry\n * is drawn at once, while the re-read still fills in what the index never\n * carries (pull requests, who started it).\n * - **A message this tab just sent.** The session moves to the top as working\n * the moment it is sent (a chat the server does not know yet gets a pending\n * row titled with the message), until the server reports the send.\n *\n * Rows are kept newest first by `updatedAt`, the server's own order, so a\n * session that just became active moves up instead of staying where its last\n * fetched copy was.\n *\n * Pure functions over the shared contract rows: the provider composes them,\n * and they are tested on their own.\n */\nimport {\n type ConversationIndexEntry,\n type ConversationSummary,\n summaryFromConversationIndexEntry,\n} from '@athenaintel/agent-runtime';\n\n/**\n * Start channels of an agent's sessions that the server leaves out when it lists the conversations\n * with a home agent (`home_agent_id`): the agent's tasks and the sessions an orchestrating agent\n * starts.\n */\nconst AGENT_HOME_EXCLUDED_START_CHANNELS: ReadonlySet<string> = new Set([\n 'task',\n 'agent_orchestration',\n]);\n\n/** The REST thread list only lists this asset type (`sdk_thread_list.py`); the index carries more. */\nconst LISTED_ASSET_TYPE = 'session';\n\n/**\n * Whether a pushed index entry is one of the conversations with `homeAgentId`'s agent that the\n * server lists beside the app scope: the agent's, and not one of its tasks or orchestration\n * sessions. An entry from a pod that predates `collab_agent_id` is not.\n */\nexport function isAgentHomeIndexEntry(\n entry: { collab_agent_id?: string | null; start_channel?: string | null },\n homeAgentId: string | undefined,\n): boolean {\n if (!homeAgentId || entry.collab_agent_id !== homeAgentId) return false;\n return !AGENT_HOME_EXCLUDED_START_CHANNELS.has(entry.start_channel ?? '');\n}\n\n/** Which sessions a list shows: its app scope, as the server applies it to every read. */\nexport interface SessionListScope {\n /** The provider's `appId`; the app's own sessions. */\n appId: string | undefined;\n /** Other apps whose sessions the list shows too (a union with `appId`). */\n additionalAppIds: readonly string[];\n /** The collab agent whose conversations the list also asks for (`home_agent_id`). */\n homeAgentId: string | undefined;\n /**\n * Whether the list's latest first page said the server widened it to the home agent's\n * conversations. A server that did not never lists them.\n */\n homeAgentApplied: boolean;\n}\n\n/**\n * Whether the server lists a session with these stamps in a list of this scope — the app filter\n * and the home-agent union exactly as `sdk_thread_list.py` applies them. With no app ids the list\n * holds every app's sessions, unless it was widened to an agent, which then makes it that agent's.\n */\nexport function inSessionListScope(\n entry: { app_id: string | null; collab_agent_id?: string | null; start_channel?: string | null },\n scope: SessionListScope,\n): boolean {\n const appIds = [...(scope.appId ? [scope.appId] : []), ...scope.additionalAppIds];\n const agentId = scope.homeAgentApplied ? scope.homeAgentId : undefined;\n if (appIds.length === 0) return agentId ? isAgentHomeIndexEntry(entry, agentId) : true;\n return appIds.includes(entry.app_id ?? '') || isAgentHomeIndexEntry(entry, agentId);\n}\n\nfunction timestampMs(value: string): number {\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\n/**\n * The listed rows plus every session the index pushed that the list should show and does not\n * list yet: in the list's scope, a listed asset type, and — while older pages remain unread — no\n * older than the oldest loaded row, so a row is never drawn below sessions that are not loaded\n * yet. Nothing is added while a search is active (the server matches it; the index cannot) or\n * before the first page has answered (the page decides what the list starts with). Identity is\n * kept when nothing is added. The rows are not re-sorted here ({@link sortNewestFirst}).\n */\nexport function withIndexedSessions({\n conversations,\n loadedConversations,\n entries,\n scope,\n searching,\n hasResolvedFirstPage,\n hasNextPage,\n}: {\n /** The rows the list shows (fetched pages, pinned rows, folded with the index). */\n conversations: readonly ConversationSummary[];\n /** The fetched pages' rows alone: the window older pages continue below. */\n loadedConversations: readonly ConversationSummary[];\n entries: readonly ConversationIndexEntry[] | null;\n scope: SessionListScope;\n searching: boolean;\n hasResolvedFirstPage: boolean;\n hasNextPage: boolean;\n}): readonly ConversationSummary[] {\n if (!entries || entries.length === 0 || searching || !hasResolvedFirstPage) return conversations;\n const listed = new Set(conversations.map((conversation) => conversation.threadId));\n let windowTailMs: number | null = null;\n if (hasNextPage) {\n for (const conversation of loadedConversations) {\n const ms = timestampMs(conversation.updatedAt);\n if (windowTailMs === null || ms < windowTailMs) windowTailMs = ms;\n }\n }\n const added: ConversationSummary[] = [];\n for (const entry of entries) {\n if (listed.has(entry.thread_id)) continue;\n if (entry.asset_type !== LISTED_ASSET_TYPE) continue;\n if (!inSessionListScope(entry, scope)) continue;\n if (windowTailMs !== null && timestampMs(entry.updated_at) < windowTailMs) continue;\n listed.add(entry.thread_id);\n added.push(summaryFromConversationIndexEntry(entry));\n }\n return added.length === 0 ? conversations : [...conversations, ...added];\n}\n\n/** How long a sent message holds its session's row before the server's own copy must take over. */\nexport const LOCAL_SEND_TTL_MS = 60_000;\n\n/** The longest provisional title kept from a sent message; the server's own title replaces it. */\nconst PENDING_TITLE_MAX_LENGTH = 200;\n\n/**\n * A message this tab sent that the server has not reported yet. Held by the provider; it\n * shapes the session's row until the server's copy of the session reflects the send.\n */\nexport interface LocalSessionSend {\n threadId: string;\n /** When this tab sent it (the client's clock, ms since the epoch). */\n sentAtMs: number;\n /**\n * The instant the row sorts at until the server reports the send: the send time, and never\n * older than the newest row the list showed when it was sent, so the session leads the list\n * even on a client whose clock runs behind the server's.\n */\n sortAt: string;\n /**\n * The session's `updatedAt` as the list showed it when the message was sent; `null` when the\n * list did not show the session (a new chat, or an older session the loaded pages do not\n * reach). The send is reported once the server's row differs.\n */\n baselineUpdatedAt: string | null;\n /**\n * Whether the session was a chat this tab started that the server had not listed yet. Only\n * such a send gets a pending row: a session missing from the loaded pages may well exist.\n */\n newChat: boolean;\n /** The message as one line, the pending title of a chat the server does not know yet. */\n title: string | null;\n /**\n * The provenance the session's first run stamps, when it is known here (the SDK's own `web`,\n * or a host's `extraRunConfig.start_channel`); `null` when the backend decides it (a collab\n * channel stamps its own kind). A new chat's pending row shows it as its origin.\n */\n startChannel: string | null;\n}\n\n/**\n * A sent message as the one-line provisional title of its session: markdown links reduced to\n * their text (a mention reads as its name), whitespace folded, and long messages cut. `null`\n * when nothing is left (an attachment sent alone).\n */\nexport function pendingSessionTitle(text: string | null | undefined): string | null {\n const line = (text ?? '')\n .replace(/!?\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\s+/g, ' ')\n .trim();\n if (!line) return null;\n return line.length <= PENDING_TITLE_MAX_LENGTH\n ? line\n : `${line.slice(0, PENDING_TITLE_MAX_LENGTH - 1).trimEnd()}…`;\n}\n\n/**\n * Record a send against the rows the server last reported: the baseline the server must move\n * past, and the instant the row sorts at until it does.\n */\nexport function localSessionSend({\n threadId,\n text,\n sentAtMs,\n newChat,\n startChannel,\n serverConversations,\n}: {\n threadId: string;\n text: string | null | undefined;\n sentAtMs: number;\n /** The session was a chat this tab started that the server had not listed yet. */\n newChat: boolean;\n /** The `start_channel` the send stamps (`effectiveStatewireStartChannel`); `null` if unknown. */\n startChannel: string | null;\n /** The server's rows as the list shows them (fetched pages and pushed index entries). */\n serverConversations: readonly ConversationSummary[];\n}): LocalSessionSend {\n let newestMs = 0;\n let baselineUpdatedAt: string | null = null;\n for (const conversation of serverConversations) {\n const ms = timestampMs(conversation.updatedAt);\n if (ms > newestMs) newestMs = ms;\n if (conversation.threadId === threadId) baselineUpdatedAt = conversation.updatedAt;\n }\n return {\n threadId,\n sentAtMs,\n sortAt: new Date(Math.max(sentAtMs, newestMs + 1)).toISOString(),\n baselineUpdatedAt,\n newChat: newChat && baselineUpdatedAt === null,\n title: pendingSessionTitle(text),\n startChannel,\n };\n}\n\n/**\n * Whether the server's copy of the session reflects the send: a session the list did not show\n * is reported once the server lists it at all; a listed session once its `updatedAt` moved.\n */\nexport function isLocalSendReported(\n send: LocalSessionSend,\n serverRow: ConversationSummary | undefined,\n): boolean {\n if (!serverRow) return false;\n return send.baselineUpdatedAt === null || serverRow.updatedAt !== send.baselineUpdatedAt;\n}\n\n/** The sends without `threadIds`; the same map when it holds none of them. */\nexport function withoutLocalSends(\n sends: ReadonlyMap<string, LocalSessionSend>,\n threadIds: readonly string[],\n): ReadonlyMap<string, LocalSessionSend> {\n if (!threadIds.some((threadId) => sends.has(threadId))) return sends;\n const next = new Map(sends);\n for (const threadId of threadIds) next.delete(threadId);\n return next;\n}\n\n/** The sends to forget: reported by the server, or older than {@link LOCAL_SEND_TTL_MS}. */\nexport function settledLocalSends({\n sends,\n serverConversations,\n nowMs,\n ttlMs = LOCAL_SEND_TTL_MS,\n}: {\n sends: ReadonlyMap<string, LocalSessionSend>;\n serverConversations: readonly ConversationSummary[];\n nowMs: number;\n ttlMs?: number;\n}): string[] {\n if (sends.size === 0) return [];\n const byThread = new Map(serverConversations.map((row) => [row.threadId, row]));\n const settled: string[] = [];\n for (const send of sends.values()) {\n if (nowMs - send.sentAtMs >= ttlMs || isLocalSendReported(send, byThread.get(send.threadId))) {\n settled.push(send.threadId);\n }\n }\n return settled;\n}\n\n/** The session as working, sorted at the send, until the server reports the send. */\nfunction withSentMessage(row: ConversationSummary, send: LocalSessionSend): ConversationSummary {\n return {\n ...row,\n updatedAt: timestampMs(row.updatedAt) >= timestampMs(send.sortAt) ? row.updatedAt : send.sortAt,\n state: 'running',\n sessionStatus: 'active',\n runStatus: 'running',\n attention: 'working',\n nextAction: null,\n attentionSource: null,\n isUnread: false,\n };\n}\n\n/** The pending row of a chat the server does not know yet, as the server will first list it. */\nfunction pendingConversation(send: LocalSessionSend, appId: string | undefined): ConversationSummary {\n const sentAt = new Date(send.sentAtMs).toISOString();\n return {\n threadId: send.threadId,\n title: send.title ?? '',\n updatedAt: send.sortAt,\n createdAt: sentAt,\n state: 'running',\n runtime: null,\n // What the server stamps on the session's first run, when that is known here: the row's\n // origin badge reads as the server's row will, never as a label made from the app id.\n sourceChannel: send.startChannel,\n assetType: LISTED_ASSET_TYPE,\n assetId: null,\n appId: appId ?? null,\n triggerType: null,\n preview: send.title,\n messageCount: null,\n isUnread: false,\n sessionStatus: 'active',\n runStatus: 'running',\n attention: 'working',\n nextAction: null,\n attentionSource: null,\n isFavorited: false,\n createdByMe: true,\n };\n}\n\n/**\n * The rows with this tab's unreported sends applied: a listed session the server has not\n * reported the send for shows as working and sorts at the send; a new chat this tab started\n * gets a pending row, if the list's scope holds this app's new sessions and no search is\n * active. A session the loaded pages do not reach gets nothing here: it exists, and the\n * server's push (or re-read) brings it to the top. Returns the rows and the ids of the pending\n * rows (which the server does not know, so writes cannot be offered on them). Identity is kept\n * when no send applies.\n */\nexport function withLocalSends({\n conversations,\n sends,\n appId,\n showPendingSessions,\n}: {\n conversations: readonly ConversationSummary[];\n sends: ReadonlyMap<string, LocalSessionSend>;\n /** The app new sessions are tagged with (the provider's `appId`). */\n appId: string | undefined;\n /** Whether a new chat's pending row belongs in this list (its scope and search allow it). */\n showPendingSessions: boolean;\n}): { conversations: readonly ConversationSummary[]; pendingThreadIds: ReadonlySet<string> } {\n if (sends.size === 0) return { conversations, pendingThreadIds: EMPTY_IDS };\n const listed = new Set<string>();\n let changed = false;\n const rows = conversations.map((row) => {\n listed.add(row.threadId);\n const send = sends.get(row.threadId);\n if (!send || isLocalSendReported(send, row)) return row;\n changed = true;\n return withSentMessage(row, send);\n });\n const pendingThreadIds = new Set<string>();\n if (showPendingSessions) {\n for (const send of sends.values()) {\n if (!send.newChat || listed.has(send.threadId)) continue;\n pendingThreadIds.add(send.threadId);\n rows.push(pendingConversation(send, appId));\n changed = true;\n }\n }\n return {\n conversations: changed ? rows : conversations,\n pendingThreadIds: pendingThreadIds.size === 0 ? EMPTY_IDS : pendingThreadIds,\n };\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\n/**\n * The rows newest first by `updatedAt` (the order the server lists them in), keeping the given\n * order between rows of the same instant. Identity is kept when the rows are already in order.\n */\nexport function sortNewestFirst<T extends { updatedAt: string }>(rows: readonly T[]): readonly T[] {\n const keyed = rows.map((row, index) => ({ row, index, ms: timestampMs(row.updatedAt) }));\n let ordered = true;\n for (let i = 1; i < keyed.length; i++) {\n if ((keyed[i - 1]?.ms ?? 0) < (keyed[i]?.ms ?? 0)) {\n ordered = false;\n break;\n }\n }\n if (ordered) return rows;\n keyed.sort((a, b) => b.ms - a.ms || a.index - b.index);\n return keyed.map(({ row }) => row);\n}\n"],"names":["summaryFromConversationIndexEntry"],"mappings":"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"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rows the session list shows between one server answer and the next.
|
|
3
|
+
*
|
|
4
|
+
* The list's rows come from the fetched REST pages, and the signed-in user's
|
|
5
|
+
* live conversation index updates them in place. Two gaps are filled here, so
|
|
6
|
+
* the list follows what just happened without waiting for a re-read:
|
|
7
|
+
*
|
|
8
|
+
* - **Sessions the index pushed that the page does not list yet.** A session
|
|
9
|
+
* just started (in this tab or anywhere else) reaches the index in about a
|
|
10
|
+
* second; the re-read the list runs for it takes several more (measured on
|
|
11
|
+
* staging 2026-11-06: push at +1.8 s, row drawn at +5.5 s). The pushed entry
|
|
12
|
+
* is drawn at once, while the re-read still fills in what the index never
|
|
13
|
+
* carries (pull requests, who started it).
|
|
14
|
+
* - **A message this tab just sent.** The session moves to the top as working
|
|
15
|
+
* the moment it is sent (a chat the server does not know yet gets a pending
|
|
16
|
+
* row titled with the message), until the server reports the send.
|
|
17
|
+
*
|
|
18
|
+
* Rows are kept newest first by `updatedAt`, the server's own order, so a
|
|
19
|
+
* session that just became active moves up instead of staying where its last
|
|
20
|
+
* fetched copy was.
|
|
21
|
+
*
|
|
22
|
+
* Pure functions over the shared contract rows: the provider composes them,
|
|
23
|
+
* and they are tested on their own.
|
|
24
|
+
*/
|
|
25
|
+
import { type ConversationIndexEntry, type ConversationSummary } from '../agent-runtime/src/index';
|
|
26
|
+
/**
|
|
27
|
+
* Whether a pushed index entry is one of the conversations with `homeAgentId`'s agent that the
|
|
28
|
+
* server lists beside the app scope: the agent's, and not one of its tasks or orchestration
|
|
29
|
+
* sessions. An entry from a pod that predates `collab_agent_id` is not.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isAgentHomeIndexEntry(entry: {
|
|
32
|
+
collab_agent_id?: string | null;
|
|
33
|
+
start_channel?: string | null;
|
|
34
|
+
}, homeAgentId: string | undefined): boolean;
|
|
35
|
+
/** Which sessions a list shows: its app scope, as the server applies it to every read. */
|
|
36
|
+
export interface SessionListScope {
|
|
37
|
+
/** The provider's `appId`; the app's own sessions. */
|
|
38
|
+
appId: string | undefined;
|
|
39
|
+
/** Other apps whose sessions the list shows too (a union with `appId`). */
|
|
40
|
+
additionalAppIds: readonly string[];
|
|
41
|
+
/** The collab agent whose conversations the list also asks for (`home_agent_id`). */
|
|
42
|
+
homeAgentId: string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the list's latest first page said the server widened it to the home agent's
|
|
45
|
+
* conversations. A server that did not never lists them.
|
|
46
|
+
*/
|
|
47
|
+
homeAgentApplied: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether the server lists a session with these stamps in a list of this scope — the app filter
|
|
51
|
+
* and the home-agent union exactly as `sdk_thread_list.py` applies them. With no app ids the list
|
|
52
|
+
* holds every app's sessions, unless it was widened to an agent, which then makes it that agent's.
|
|
53
|
+
*/
|
|
54
|
+
export declare function inSessionListScope(entry: {
|
|
55
|
+
app_id: string | null;
|
|
56
|
+
collab_agent_id?: string | null;
|
|
57
|
+
start_channel?: string | null;
|
|
58
|
+
}, scope: SessionListScope): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The listed rows plus every session the index pushed that the list should show and does not
|
|
61
|
+
* list yet: in the list's scope, a listed asset type, and — while older pages remain unread — no
|
|
62
|
+
* older than the oldest loaded row, so a row is never drawn below sessions that are not loaded
|
|
63
|
+
* yet. Nothing is added while a search is active (the server matches it; the index cannot) or
|
|
64
|
+
* before the first page has answered (the page decides what the list starts with). Identity is
|
|
65
|
+
* kept when nothing is added. The rows are not re-sorted here ({@link sortNewestFirst}).
|
|
66
|
+
*/
|
|
67
|
+
export declare function withIndexedSessions({ conversations, loadedConversations, entries, scope, searching, hasResolvedFirstPage, hasNextPage, }: {
|
|
68
|
+
/** The rows the list shows (fetched pages, pinned rows, folded with the index). */
|
|
69
|
+
conversations: readonly ConversationSummary[];
|
|
70
|
+
/** The fetched pages' rows alone: the window older pages continue below. */
|
|
71
|
+
loadedConversations: readonly ConversationSummary[];
|
|
72
|
+
entries: readonly ConversationIndexEntry[] | null;
|
|
73
|
+
scope: SessionListScope;
|
|
74
|
+
searching: boolean;
|
|
75
|
+
hasResolvedFirstPage: boolean;
|
|
76
|
+
hasNextPage: boolean;
|
|
77
|
+
}): readonly ConversationSummary[];
|
|
78
|
+
/** How long a sent message holds its session's row before the server's own copy must take over. */
|
|
79
|
+
export declare const LOCAL_SEND_TTL_MS = 60000;
|
|
80
|
+
/**
|
|
81
|
+
* A message this tab sent that the server has not reported yet. Held by the provider; it
|
|
82
|
+
* shapes the session's row until the server's copy of the session reflects the send.
|
|
83
|
+
*/
|
|
84
|
+
export interface LocalSessionSend {
|
|
85
|
+
threadId: string;
|
|
86
|
+
/** When this tab sent it (the client's clock, ms since the epoch). */
|
|
87
|
+
sentAtMs: number;
|
|
88
|
+
/**
|
|
89
|
+
* The instant the row sorts at until the server reports the send: the send time, and never
|
|
90
|
+
* older than the newest row the list showed when it was sent, so the session leads the list
|
|
91
|
+
* even on a client whose clock runs behind the server's.
|
|
92
|
+
*/
|
|
93
|
+
sortAt: string;
|
|
94
|
+
/**
|
|
95
|
+
* The session's `updatedAt` as the list showed it when the message was sent; `null` when the
|
|
96
|
+
* list did not show the session (a new chat, or an older session the loaded pages do not
|
|
97
|
+
* reach). The send is reported once the server's row differs.
|
|
98
|
+
*/
|
|
99
|
+
baselineUpdatedAt: string | null;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the session was a chat this tab started that the server had not listed yet. Only
|
|
102
|
+
* such a send gets a pending row: a session missing from the loaded pages may well exist.
|
|
103
|
+
*/
|
|
104
|
+
newChat: boolean;
|
|
105
|
+
/** The message as one line, the pending title of a chat the server does not know yet. */
|
|
106
|
+
title: string | null;
|
|
107
|
+
/**
|
|
108
|
+
* The provenance the session's first run stamps, when it is known here (the SDK's own `web`,
|
|
109
|
+
* or a host's `extraRunConfig.start_channel`); `null` when the backend decides it (a collab
|
|
110
|
+
* channel stamps its own kind). A new chat's pending row shows it as its origin.
|
|
111
|
+
*/
|
|
112
|
+
startChannel: string | null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A sent message as the one-line provisional title of its session: markdown links reduced to
|
|
116
|
+
* their text (a mention reads as its name), whitespace folded, and long messages cut. `null`
|
|
117
|
+
* when nothing is left (an attachment sent alone).
|
|
118
|
+
*/
|
|
119
|
+
export declare function pendingSessionTitle(text: string | null | undefined): string | null;
|
|
120
|
+
/**
|
|
121
|
+
* Record a send against the rows the server last reported: the baseline the server must move
|
|
122
|
+
* past, and the instant the row sorts at until it does.
|
|
123
|
+
*/
|
|
124
|
+
export declare function localSessionSend({ threadId, text, sentAtMs, newChat, startChannel, serverConversations, }: {
|
|
125
|
+
threadId: string;
|
|
126
|
+
text: string | null | undefined;
|
|
127
|
+
sentAtMs: number;
|
|
128
|
+
/** The session was a chat this tab started that the server had not listed yet. */
|
|
129
|
+
newChat: boolean;
|
|
130
|
+
/** The `start_channel` the send stamps (`effectiveStatewireStartChannel`); `null` if unknown. */
|
|
131
|
+
startChannel: string | null;
|
|
132
|
+
/** The server's rows as the list shows them (fetched pages and pushed index entries). */
|
|
133
|
+
serverConversations: readonly ConversationSummary[];
|
|
134
|
+
}): LocalSessionSend;
|
|
135
|
+
/**
|
|
136
|
+
* Whether the server's copy of the session reflects the send: a session the list did not show
|
|
137
|
+
* is reported once the server lists it at all; a listed session once its `updatedAt` moved.
|
|
138
|
+
*/
|
|
139
|
+
export declare function isLocalSendReported(send: LocalSessionSend, serverRow: ConversationSummary | undefined): boolean;
|
|
140
|
+
/** The sends without `threadIds`; the same map when it holds none of them. */
|
|
141
|
+
export declare function withoutLocalSends(sends: ReadonlyMap<string, LocalSessionSend>, threadIds: readonly string[]): ReadonlyMap<string, LocalSessionSend>;
|
|
142
|
+
/** The sends to forget: reported by the server, or older than {@link LOCAL_SEND_TTL_MS}. */
|
|
143
|
+
export declare function settledLocalSends({ sends, serverConversations, nowMs, ttlMs, }: {
|
|
144
|
+
sends: ReadonlyMap<string, LocalSessionSend>;
|
|
145
|
+
serverConversations: readonly ConversationSummary[];
|
|
146
|
+
nowMs: number;
|
|
147
|
+
ttlMs?: number;
|
|
148
|
+
}): string[];
|
|
149
|
+
/**
|
|
150
|
+
* The rows with this tab's unreported sends applied: a listed session the server has not
|
|
151
|
+
* reported the send for shows as working and sorts at the send; a new chat this tab started
|
|
152
|
+
* gets a pending row, if the list's scope holds this app's new sessions and no search is
|
|
153
|
+
* active. A session the loaded pages do not reach gets nothing here: it exists, and the
|
|
154
|
+
* server's push (or re-read) brings it to the top. Returns the rows and the ids of the pending
|
|
155
|
+
* rows (which the server does not know, so writes cannot be offered on them). Identity is kept
|
|
156
|
+
* when no send applies.
|
|
157
|
+
*/
|
|
158
|
+
export declare function withLocalSends({ conversations, sends, appId, showPendingSessions, }: {
|
|
159
|
+
conversations: readonly ConversationSummary[];
|
|
160
|
+
sends: ReadonlyMap<string, LocalSessionSend>;
|
|
161
|
+
/** The app new sessions are tagged with (the provider's `appId`). */
|
|
162
|
+
appId: string | undefined;
|
|
163
|
+
/** Whether a new chat's pending row belongs in this list (its scope and search allow it). */
|
|
164
|
+
showPendingSessions: boolean;
|
|
165
|
+
}): {
|
|
166
|
+
conversations: readonly ConversationSummary[];
|
|
167
|
+
pendingThreadIds: ReadonlySet<string>;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* The rows newest first by `updatedAt` (the order the server lists them in), keeping the given
|
|
171
|
+
* order between rows of the same instant. Identity is kept when the rows are already in order.
|
|
172
|
+
*/
|
|
173
|
+
export declare function sortNewestFirst<T extends {
|
|
174
|
+
updatedAt: string;
|
|
175
|
+
}>(rows: readonly T[]): readonly T[];
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { summaryFromConversationIndexEntry } from "../agent-runtime/src/conversation-list.js";
|
|
2
|
+
const AGENT_HOME_EXCLUDED_START_CHANNELS = /* @__PURE__ */ new Set([
|
|
3
|
+
"task",
|
|
4
|
+
"agent_orchestration"
|
|
5
|
+
]);
|
|
6
|
+
const LISTED_ASSET_TYPE = "session";
|
|
7
|
+
function isAgentHomeIndexEntry(entry, homeAgentId) {
|
|
8
|
+
if (!homeAgentId || entry.collab_agent_id !== homeAgentId) return false;
|
|
9
|
+
return !AGENT_HOME_EXCLUDED_START_CHANNELS.has(entry.start_channel ?? "");
|
|
10
|
+
}
|
|
11
|
+
function inSessionListScope(entry, scope) {
|
|
12
|
+
const appIds = [...scope.appId ? [scope.appId] : [], ...scope.additionalAppIds];
|
|
13
|
+
const agentId = scope.homeAgentApplied ? scope.homeAgentId : void 0;
|
|
14
|
+
if (appIds.length === 0) return agentId ? isAgentHomeIndexEntry(entry, agentId) : true;
|
|
15
|
+
return appIds.includes(entry.app_id ?? "") || isAgentHomeIndexEntry(entry, agentId);
|
|
16
|
+
}
|
|
17
|
+
function timestampMs(value) {
|
|
18
|
+
const parsed = Date.parse(value);
|
|
19
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
20
|
+
}
|
|
21
|
+
function withIndexedSessions({
|
|
22
|
+
conversations,
|
|
23
|
+
loadedConversations,
|
|
24
|
+
entries,
|
|
25
|
+
scope,
|
|
26
|
+
searching,
|
|
27
|
+
hasResolvedFirstPage,
|
|
28
|
+
hasNextPage
|
|
29
|
+
}) {
|
|
30
|
+
if (!entries || entries.length === 0 || searching || !hasResolvedFirstPage) return conversations;
|
|
31
|
+
const listed = new Set(conversations.map((conversation) => conversation.threadId));
|
|
32
|
+
let windowTailMs = null;
|
|
33
|
+
if (hasNextPage) {
|
|
34
|
+
for (const conversation of loadedConversations) {
|
|
35
|
+
const ms = timestampMs(conversation.updatedAt);
|
|
36
|
+
if (windowTailMs === null || ms < windowTailMs) windowTailMs = ms;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const added = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (listed.has(entry.thread_id)) continue;
|
|
42
|
+
if (entry.asset_type !== LISTED_ASSET_TYPE) continue;
|
|
43
|
+
if (!inSessionListScope(entry, scope)) continue;
|
|
44
|
+
if (windowTailMs !== null && timestampMs(entry.updated_at) < windowTailMs) continue;
|
|
45
|
+
listed.add(entry.thread_id);
|
|
46
|
+
added.push(summaryFromConversationIndexEntry(entry));
|
|
47
|
+
}
|
|
48
|
+
return added.length === 0 ? conversations : [...conversations, ...added];
|
|
49
|
+
}
|
|
50
|
+
const LOCAL_SEND_TTL_MS = 6e4;
|
|
51
|
+
const PENDING_TITLE_MAX_LENGTH = 200;
|
|
52
|
+
function pendingSessionTitle(text) {
|
|
53
|
+
const line = (text ?? "").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim();
|
|
54
|
+
if (!line) return null;
|
|
55
|
+
return line.length <= PENDING_TITLE_MAX_LENGTH ? line : `${line.slice(0, PENDING_TITLE_MAX_LENGTH - 1).trimEnd()}…`;
|
|
56
|
+
}
|
|
57
|
+
function localSessionSend({
|
|
58
|
+
threadId,
|
|
59
|
+
text,
|
|
60
|
+
sentAtMs,
|
|
61
|
+
newChat,
|
|
62
|
+
startChannel,
|
|
63
|
+
serverConversations
|
|
64
|
+
}) {
|
|
65
|
+
let newestMs = 0;
|
|
66
|
+
let baselineUpdatedAt = null;
|
|
67
|
+
for (const conversation of serverConversations) {
|
|
68
|
+
const ms = timestampMs(conversation.updatedAt);
|
|
69
|
+
if (ms > newestMs) newestMs = ms;
|
|
70
|
+
if (conversation.threadId === threadId) baselineUpdatedAt = conversation.updatedAt;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
threadId,
|
|
74
|
+
sentAtMs,
|
|
75
|
+
sortAt: new Date(Math.max(sentAtMs, newestMs + 1)).toISOString(),
|
|
76
|
+
baselineUpdatedAt,
|
|
77
|
+
newChat: newChat && baselineUpdatedAt === null,
|
|
78
|
+
title: pendingSessionTitle(text),
|
|
79
|
+
startChannel
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function isLocalSendReported(send, serverRow) {
|
|
83
|
+
if (!serverRow) return false;
|
|
84
|
+
return send.baselineUpdatedAt === null || serverRow.updatedAt !== send.baselineUpdatedAt;
|
|
85
|
+
}
|
|
86
|
+
function withoutLocalSends(sends, threadIds) {
|
|
87
|
+
if (!threadIds.some((threadId) => sends.has(threadId))) return sends;
|
|
88
|
+
const next = new Map(sends);
|
|
89
|
+
for (const threadId of threadIds) next.delete(threadId);
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
function settledLocalSends({
|
|
93
|
+
sends,
|
|
94
|
+
serverConversations,
|
|
95
|
+
nowMs,
|
|
96
|
+
ttlMs = LOCAL_SEND_TTL_MS
|
|
97
|
+
}) {
|
|
98
|
+
if (sends.size === 0) return [];
|
|
99
|
+
const byThread = new Map(serverConversations.map((row) => [row.threadId, row]));
|
|
100
|
+
const settled = [];
|
|
101
|
+
for (const send of sends.values()) {
|
|
102
|
+
if (nowMs - send.sentAtMs >= ttlMs || isLocalSendReported(send, byThread.get(send.threadId))) {
|
|
103
|
+
settled.push(send.threadId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return settled;
|
|
107
|
+
}
|
|
108
|
+
function withSentMessage(row, send) {
|
|
109
|
+
return {
|
|
110
|
+
...row,
|
|
111
|
+
updatedAt: timestampMs(row.updatedAt) >= timestampMs(send.sortAt) ? row.updatedAt : send.sortAt,
|
|
112
|
+
state: "running",
|
|
113
|
+
sessionStatus: "active",
|
|
114
|
+
runStatus: "running",
|
|
115
|
+
attention: "working",
|
|
116
|
+
nextAction: null,
|
|
117
|
+
attentionSource: null,
|
|
118
|
+
isUnread: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function pendingConversation(send, appId) {
|
|
122
|
+
const sentAt = new Date(send.sentAtMs).toISOString();
|
|
123
|
+
return {
|
|
124
|
+
threadId: send.threadId,
|
|
125
|
+
title: send.title ?? "",
|
|
126
|
+
updatedAt: send.sortAt,
|
|
127
|
+
createdAt: sentAt,
|
|
128
|
+
state: "running",
|
|
129
|
+
runtime: null,
|
|
130
|
+
// What the server stamps on the session's first run, when that is known here: the row's
|
|
131
|
+
// origin badge reads as the server's row will, never as a label made from the app id.
|
|
132
|
+
sourceChannel: send.startChannel,
|
|
133
|
+
assetType: LISTED_ASSET_TYPE,
|
|
134
|
+
assetId: null,
|
|
135
|
+
appId: appId ?? null,
|
|
136
|
+
triggerType: null,
|
|
137
|
+
preview: send.title,
|
|
138
|
+
messageCount: null,
|
|
139
|
+
isUnread: false,
|
|
140
|
+
sessionStatus: "active",
|
|
141
|
+
runStatus: "running",
|
|
142
|
+
attention: "working",
|
|
143
|
+
nextAction: null,
|
|
144
|
+
attentionSource: null,
|
|
145
|
+
isFavorited: false,
|
|
146
|
+
createdByMe: true
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function withLocalSends({
|
|
150
|
+
conversations,
|
|
151
|
+
sends,
|
|
152
|
+
appId,
|
|
153
|
+
showPendingSessions
|
|
154
|
+
}) {
|
|
155
|
+
if (sends.size === 0) return { conversations, pendingThreadIds: EMPTY_IDS };
|
|
156
|
+
const listed = /* @__PURE__ */ new Set();
|
|
157
|
+
let changed = false;
|
|
158
|
+
const rows = conversations.map((row) => {
|
|
159
|
+
listed.add(row.threadId);
|
|
160
|
+
const send = sends.get(row.threadId);
|
|
161
|
+
if (!send || isLocalSendReported(send, row)) return row;
|
|
162
|
+
changed = true;
|
|
163
|
+
return withSentMessage(row, send);
|
|
164
|
+
});
|
|
165
|
+
const pendingThreadIds = /* @__PURE__ */ new Set();
|
|
166
|
+
if (showPendingSessions) {
|
|
167
|
+
for (const send of sends.values()) {
|
|
168
|
+
if (!send.newChat || listed.has(send.threadId)) continue;
|
|
169
|
+
pendingThreadIds.add(send.threadId);
|
|
170
|
+
rows.push(pendingConversation(send, appId));
|
|
171
|
+
changed = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
conversations: changed ? rows : conversations,
|
|
176
|
+
pendingThreadIds: pendingThreadIds.size === 0 ? EMPTY_IDS : pendingThreadIds
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
180
|
+
function sortNewestFirst(rows) {
|
|
181
|
+
var _a, _b;
|
|
182
|
+
const keyed = rows.map((row, index) => ({ row, index, ms: timestampMs(row.updatedAt) }));
|
|
183
|
+
let ordered = true;
|
|
184
|
+
for (let i = 1; i < keyed.length; i++) {
|
|
185
|
+
if ((((_a = keyed[i - 1]) == null ? void 0 : _a.ms) ?? 0) < (((_b = keyed[i]) == null ? void 0 : _b.ms) ?? 0)) {
|
|
186
|
+
ordered = false;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (ordered) return rows;
|
|
191
|
+
keyed.sort((a, b) => b.ms - a.ms || a.index - b.index);
|
|
192
|
+
return keyed.map(({ row }) => row);
|
|
193
|
+
}
|
|
194
|
+
export {
|
|
195
|
+
LOCAL_SEND_TTL_MS,
|
|
196
|
+
inSessionListScope,
|
|
197
|
+
isAgentHomeIndexEntry,
|
|
198
|
+
isLocalSendReported,
|
|
199
|
+
localSessionSend,
|
|
200
|
+
pendingSessionTitle,
|
|
201
|
+
settledLocalSends,
|
|
202
|
+
sortNewestFirst,
|
|
203
|
+
withIndexedSessions,
|
|
204
|
+
withLocalSends,
|
|
205
|
+
withoutLocalSends
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=live-session-rows.js.map
|