@beanx/cathygo-web-core 0.1.2 → 0.1.4
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 +8 -2
- package/dist/index.d.ts +42 -2
- package/dist/index.js +390 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,10 +21,16 @@ Host applications still own account routing, agent selection, transport setup,
|
|
|
21
21
|
selected session id, lightweight session lists, and product-specific shell UI.
|
|
22
22
|
They should not own a second session execution runtime.
|
|
23
23
|
|
|
24
|
+
## Session Host Helpers (`@0.1.4`)
|
|
25
|
+
|
|
26
|
+
- `buildSessionLoadedAction` / `conversationMessagesToChatMessages`
|
|
27
|
+
- `createSessionEventRouter`
|
|
28
|
+
- `selectCanSend`, `selectCanStop`, `selectActiveTurnId`, `selectSessionStatus`, `selectIsMutating`
|
|
29
|
+
|
|
24
30
|
## First vNext Deliverables
|
|
25
31
|
|
|
26
32
|
- Keep the current single-session `ChatState` reducer exported.
|
|
27
|
-
-
|
|
33
|
+
- Session host helpers and selectors (above).
|
|
28
34
|
- Add reducer tests for interrupt, replay, stop, and optimistic reconciliation.
|
|
29
35
|
- Prove the implementation in `cathygo-agent/web` before publishing
|
|
30
|
-
|
|
36
|
+
npm bumps on the `0.1.x` line (`0.2.0` reserved for breaking renames).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { Dispatch, RefObject } from 'react';
|
|
3
|
-
import { AgentActivity, SessionRuntimeSnapshot, LearningTurnEvent, ConversationSummary, GatewayModelStatus } from '@beanx/cathygo-protocol';
|
|
3
|
+
import { AgentActivity, SessionRuntimeSnapshot, LearningTurnEvent, ConversationSummary, GatewayModelStatus, ConversationDetail, ConversationMessage } from '@beanx/cathygo-protocol';
|
|
4
4
|
|
|
5
5
|
type AgentIdentityStatus = {
|
|
6
6
|
agent_id?: string | null;
|
|
@@ -73,6 +73,12 @@ type ChatAttachment = {
|
|
|
73
73
|
width?: number | null;
|
|
74
74
|
height?: number | null;
|
|
75
75
|
created_at?: string | null;
|
|
76
|
+
thumbnail?: {
|
|
77
|
+
mime_type: string;
|
|
78
|
+
data_base64: string;
|
|
79
|
+
width?: number | null;
|
|
80
|
+
height?: number | null;
|
|
81
|
+
} | null;
|
|
76
82
|
};
|
|
77
83
|
type ChatMessagePart = {
|
|
78
84
|
type: 'text';
|
|
@@ -88,6 +94,8 @@ type ChatMessage$1 = {
|
|
|
88
94
|
content: string;
|
|
89
95
|
parts?: ChatMessagePart[];
|
|
90
96
|
turnId?: string;
|
|
97
|
+
clientInputId?: string;
|
|
98
|
+
clientTurnId?: string;
|
|
91
99
|
status?: 'streaming' | 'done' | 'error';
|
|
92
100
|
};
|
|
93
101
|
type ChatState = {
|
|
@@ -143,6 +151,8 @@ type ChatAction = {
|
|
|
143
151
|
id: string;
|
|
144
152
|
content: string;
|
|
145
153
|
parts?: ChatMessagePart[];
|
|
154
|
+
clientInputId?: string;
|
|
155
|
+
clientTurnId?: string;
|
|
146
156
|
} | {
|
|
147
157
|
type: 'session.input.accepted';
|
|
148
158
|
sessionId: string;
|
|
@@ -384,4 +394,34 @@ declare function ChatView({ chat, draft, attachments, attachmentPolicy, composer
|
|
|
384
394
|
|
|
385
395
|
declare function modelStatusText(model: GatewayModelStatus): string;
|
|
386
396
|
|
|
387
|
-
|
|
397
|
+
type RunningTurnsBySession = Record<string, string>;
|
|
398
|
+
declare function eventSessionId(event: LearningTurnEvent): string | undefined;
|
|
399
|
+
declare function eventTurnId(event: LearningTurnEvent): string | undefined;
|
|
400
|
+
declare function shouldApplyEventToRuntime(chat: ChatState | undefined, event: LearningTurnEvent): boolean;
|
|
401
|
+
declare function shouldApplyEventToActiveSession(event: LearningTurnEvent, activeSessionId: string | undefined): boolean;
|
|
402
|
+
declare function isChatStateMutating(chat: ChatState | undefined): boolean;
|
|
403
|
+
declare function selectCanSend(chat: ChatState): boolean;
|
|
404
|
+
declare function selectCanStop(chat: ChatState): boolean;
|
|
405
|
+
declare function selectActiveTurnId(chat: ChatState): string | undefined;
|
|
406
|
+
declare function selectSessionStatus(chat: ChatState): string;
|
|
407
|
+
declare function selectIsMutating(chat: ChatState): boolean;
|
|
408
|
+
declare function updateRunningTurnFromEvent(running: RunningTurnsBySession, event: LearningTurnEvent): RunningTurnsBySession;
|
|
409
|
+
declare function createSessionEventRouter(options: {
|
|
410
|
+
getActiveSessionId: () => string | undefined;
|
|
411
|
+
getChat: () => ChatState;
|
|
412
|
+
dispatchChat: (action: {
|
|
413
|
+
type: 'event.received';
|
|
414
|
+
event: LearningTurnEvent;
|
|
415
|
+
}) => void;
|
|
416
|
+
onTerminalTurn?: (sessionId: string) => void;
|
|
417
|
+
}): (event: LearningTurnEvent) => void;
|
|
418
|
+
|
|
419
|
+
type SessionHydrationOptions = {
|
|
420
|
+
resolveAttachmentUrl?: (uri: string) => string;
|
|
421
|
+
};
|
|
422
|
+
declare function buildSessionLoadedAction(sessionId: string, detail: ConversationDetail, options?: SessionHydrationOptions): Extract<ChatAction, {
|
|
423
|
+
type: 'session.loaded';
|
|
424
|
+
}>;
|
|
425
|
+
declare function conversationMessagesToChatMessages(messages: ConversationMessage[], options?: SessionHydrationOptions): ChatMessage$1[];
|
|
426
|
+
|
|
427
|
+
export { AgentActivityTimeline, type AgentIdentityStatus, type AgentRunProgress, type AgentRunProgressPhase, type AgentRunThinking, CHAT_HOME_MODES, CathyGOChatApp, type CathyGOChatAppProps, type CathyGOChatScreen, type ChatAction, ChatAgentIdentity, type ChatAttachment, type ChatHomeMode, type ChatHomeModeId, ChatHomeView, ChatListView, type ChatMessage$1 as ChatMessage, type ChatMessagePart, ChatMessage as ChatMessageView, type ChatRole, type ChatRuntimeId, ChatRuntimeSelector, type ChatRuntimeSelectorProps, type ChatState, ChatTopBar, ChatTranscript, ChatView, type ComposerAttachmentPolicy, type ComposerFileLike, type ComposerFileRejection, type ComposerFileRejectionReason, type ComposerFileValidationResult, DEFAULT_CHAT_HOME_MODE_ID, DEFAULT_COMPOSER_ATTACHMENT_POLICY, IconBack, IconBrain, IconCalendar, IconChevronDown, IconChevronRight, IconClose, IconCloud, IconCode, IconDevice, IconDevices, IconDocument, IconExternalLink, IconGatewayConnecting, IconGatewayOffline, IconGatewayOnline, IconGithub, IconHome, IconImageUpload, IconInfo, IconLocalComputer, IconMessage, IconMore, IconNewChat, IconPencil, IconPlug, IconSearch, IconSend, IconSettings, IconShield, IconSmartphone, MathMarkdown, MessageComposer, type MessageComposerHandle, type PendingComposerAttachment, RunStatus, type RunningTurnsBySession, type RuntimeOption, ScrollToBottomButton, type SessionHydrationOptions, type UseCathyGOChatResult, agentDisplayName, agentShortId, buildRuntimeOptions, buildSessionLoadedAction, composerAcceptAttribute, conversationMessagesToChatMessages, createSessionEventRouter, eventSessionId, eventTurnId, findChatHomeMode, fixCommonLatexMistakes, formatBytes, initialChatState, isChatStateMutating, modelStatusText, normalizeComposerAttachmentPolicy, normalizeMathDelimiters, prepareMathMarkdown, reduceChat, selectActiveTurnId, selectCanSend, selectCanStop, selectIsMutating, selectSessionStatus, shouldApplyEventToActiveSession, shouldApplyEventToRuntime, stripMathForPreview, updateRunningTurnFromEvent, useCathyGOChat, validateComposerFiles };
|
package/dist/index.js
CHANGED
|
@@ -1459,7 +1459,7 @@ function ChatMessage({ message }) {
|
|
|
1459
1459
|
"img",
|
|
1460
1460
|
{
|
|
1461
1461
|
alt: part.attachment.original_name ?? "Attached image",
|
|
1462
|
-
src: part.attachment
|
|
1462
|
+
src: attachmentImageSrc(part.attachment)
|
|
1463
1463
|
},
|
|
1464
1464
|
part.attachment_id
|
|
1465
1465
|
) : null
|
|
@@ -1481,6 +1481,18 @@ function ChatMessage({ message }) {
|
|
|
1481
1481
|
}
|
|
1482
1482
|
);
|
|
1483
1483
|
}
|
|
1484
|
+
function attachmentImageSrc(attachment) {
|
|
1485
|
+
if (isRenderableUri(attachment.uri)) return attachment.uri;
|
|
1486
|
+
const thumbnail = attachment.thumbnail;
|
|
1487
|
+
if (thumbnail?.mime_type && thumbnail.data_base64) {
|
|
1488
|
+
return `data:${thumbnail.mime_type};base64,${thumbnail.data_base64}`;
|
|
1489
|
+
}
|
|
1490
|
+
return attachment.uri;
|
|
1491
|
+
}
|
|
1492
|
+
function isRenderableUri(uri) {
|
|
1493
|
+
if (!uri) return false;
|
|
1494
|
+
return uri.startsWith("blob:") || uri.startsWith("data:") || uri.startsWith("http://") || uri.startsWith("https://");
|
|
1495
|
+
}
|
|
1484
1496
|
|
|
1485
1497
|
// src/chat-ui/components/ScrollToBottomButton.tsx
|
|
1486
1498
|
import { useEffect as useEffect3, useState as useState5 } from "react";
|
|
@@ -1877,11 +1889,12 @@ function reduceChat(state, action) {
|
|
|
1877
1889
|
if (action.type === "session.loaded") {
|
|
1878
1890
|
const runtime = action.runtime;
|
|
1879
1891
|
const activeTurnId = optionalString(runtime?.active_turn_id);
|
|
1892
|
+
const messages = reconcileLoadedMessages(state, action.sessionId, action.messages);
|
|
1880
1893
|
return {
|
|
1881
1894
|
...state,
|
|
1882
1895
|
sessionId: action.sessionId,
|
|
1883
1896
|
activeTurnId,
|
|
1884
|
-
messages
|
|
1897
|
+
messages,
|
|
1885
1898
|
activitiesByTurnId: groupActivitiesByTurn(action.activities ?? []),
|
|
1886
1899
|
progressByTurnId: activeTurnId ? setProgress({}, runtimeProgressFromSnapshot(runtime, activeTurnId)) : {},
|
|
1887
1900
|
runtime,
|
|
@@ -1911,6 +1924,8 @@ function reduceChat(state, action) {
|
|
|
1911
1924
|
role: "user",
|
|
1912
1925
|
content: action.content,
|
|
1913
1926
|
parts: action.parts,
|
|
1927
|
+
clientInputId: action.clientInputId,
|
|
1928
|
+
clientTurnId: action.clientTurnId,
|
|
1914
1929
|
status: "done"
|
|
1915
1930
|
}
|
|
1916
1931
|
],
|
|
@@ -1933,10 +1948,15 @@ function reduceChat(state, action) {
|
|
|
1933
1948
|
};
|
|
1934
1949
|
}
|
|
1935
1950
|
if (action.type === "session.input.accepted") {
|
|
1951
|
+
const messages = attachClientIdsToPendingUser(state.messages, {
|
|
1952
|
+
clientInputId: action.clientInputId,
|
|
1953
|
+
clientTurnId: action.clientTurnId
|
|
1954
|
+
});
|
|
1936
1955
|
return {
|
|
1937
1956
|
...state,
|
|
1938
1957
|
sessionId: action.sessionId,
|
|
1939
1958
|
activeTurnId: action.turnId,
|
|
1959
|
+
messages,
|
|
1940
1960
|
runtime: {
|
|
1941
1961
|
...state.runtime ?? emptyRuntimeSnapshot(),
|
|
1942
1962
|
status: "running",
|
|
@@ -1960,6 +1980,9 @@ function reduceChat(state, action) {
|
|
|
1960
1980
|
}
|
|
1961
1981
|
const event = action.event;
|
|
1962
1982
|
const payload = event.payload;
|
|
1983
|
+
if (isDuplicateEvent(state.runtime, event)) {
|
|
1984
|
+
return state;
|
|
1985
|
+
}
|
|
1963
1986
|
const next = {
|
|
1964
1987
|
...state,
|
|
1965
1988
|
eventCount: state.eventCount + 1,
|
|
@@ -2008,6 +2031,14 @@ function reduceChat(state, action) {
|
|
|
2008
2031
|
status: "Stopping"
|
|
2009
2032
|
};
|
|
2010
2033
|
}
|
|
2034
|
+
if (event.event === "user.message.created") {
|
|
2035
|
+
const message = userMessageFromEvent(event);
|
|
2036
|
+
if (!message) return next;
|
|
2037
|
+
return {
|
|
2038
|
+
...next,
|
|
2039
|
+
messages: dedupeMessages([...next.messages, message])
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2011
2042
|
if (event.event === "agent.progress.delta") {
|
|
2012
2043
|
return {
|
|
2013
2044
|
...next,
|
|
@@ -2073,7 +2104,8 @@ function reduceChat(state, action) {
|
|
|
2073
2104
|
next.messages,
|
|
2074
2105
|
payload.message_id,
|
|
2075
2106
|
finalMessage,
|
|
2076
|
-
payload.turn_id
|
|
2107
|
+
payload.turn_id,
|
|
2108
|
+
optionalString(payload.persisted_message_id)
|
|
2077
2109
|
)
|
|
2078
2110
|
};
|
|
2079
2111
|
}
|
|
@@ -2137,20 +2169,34 @@ function appendAssistantDelta(messages, messageId, delta, turnId) {
|
|
|
2137
2169
|
} : message
|
|
2138
2170
|
);
|
|
2139
2171
|
}
|
|
2140
|
-
function finalizeAssistantMessage(messages, messageId, finalMessage, turnId) {
|
|
2172
|
+
function finalizeAssistantMessage(messages, messageId, finalMessage, turnId, persistedMessageId) {
|
|
2141
2173
|
const id = String(messageId || "");
|
|
2142
2174
|
const resolvedTurnId = optionalString(turnId);
|
|
2175
|
+
const existingTurnAssistantIndex = resolvedTurnId ? messages.findIndex(
|
|
2176
|
+
(message) => message.role === "assistant" && message.turnId === resolvedTurnId && message.status === "done" && Boolean(message.content)
|
|
2177
|
+
) : -1;
|
|
2143
2178
|
if (!id && !finalMessage) {
|
|
2144
2179
|
return messages.map(
|
|
2145
2180
|
(message) => message.role === "assistant" && message.status === "streaming" ? { ...message, turnId: message.turnId ?? resolvedTurnId, status: "done" } : message
|
|
2146
2181
|
);
|
|
2147
2182
|
}
|
|
2148
2183
|
const index = messages.findIndex((message) => message.id === id);
|
|
2184
|
+
if (index === -1 && existingTurnAssistantIndex !== -1) {
|
|
2185
|
+
return messages.map(
|
|
2186
|
+
(message, itemIndex) => itemIndex === existingTurnAssistantIndex ? {
|
|
2187
|
+
...message,
|
|
2188
|
+
id: persistedMessageId ?? (id || message.id),
|
|
2189
|
+
content: finalMessage || message.content,
|
|
2190
|
+
turnId: message.turnId ?? resolvedTurnId,
|
|
2191
|
+
status: "done"
|
|
2192
|
+
} : message
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2149
2195
|
if (index === -1 && finalMessage) {
|
|
2150
2196
|
return [
|
|
2151
2197
|
...messages,
|
|
2152
2198
|
{
|
|
2153
|
-
id: id || `msg_done_${messages.length}
|
|
2199
|
+
id: persistedMessageId ?? (id || `msg_done_${messages.length}`),
|
|
2154
2200
|
role: "assistant",
|
|
2155
2201
|
content: finalMessage,
|
|
2156
2202
|
turnId: resolvedTurnId,
|
|
@@ -2161,12 +2207,212 @@ function finalizeAssistantMessage(messages, messageId, finalMessage, turnId) {
|
|
|
2161
2207
|
return messages.map(
|
|
2162
2208
|
(message, itemIndex) => itemIndex === index || message.role === "assistant" && message.status === "streaming" ? {
|
|
2163
2209
|
...message,
|
|
2210
|
+
id: itemIndex === index && persistedMessageId && message.id === id ? persistedMessageId : message.id,
|
|
2164
2211
|
content: message.content || finalMessage,
|
|
2165
2212
|
turnId: message.turnId ?? resolvedTurnId,
|
|
2166
2213
|
status: "done"
|
|
2167
2214
|
} : message
|
|
2168
2215
|
);
|
|
2169
2216
|
}
|
|
2217
|
+
function reconcileLoadedMessages(state, sessionId, loadedMessages) {
|
|
2218
|
+
if (state.sessionId !== sessionId || state.messages.length === 0) {
|
|
2219
|
+
return dedupeMessages(loadedMessages);
|
|
2220
|
+
}
|
|
2221
|
+
const localMessagesByClientId = /* @__PURE__ */ new Map();
|
|
2222
|
+
for (const message of state.messages) {
|
|
2223
|
+
if (message.role !== "user") continue;
|
|
2224
|
+
for (const key of messageClientKeys(message)) {
|
|
2225
|
+
localMessagesByClientId.set(key, message);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
const reconciled = loadedMessages.map((message) => {
|
|
2229
|
+
if (message.role !== "user") return message;
|
|
2230
|
+
const local = messageClientKeys(message).map((key) => localMessagesByClientId.get(key)).find((item) => Boolean(item));
|
|
2231
|
+
if (!local) return message;
|
|
2232
|
+
return mergeLoadedUserMessage(local, message);
|
|
2233
|
+
});
|
|
2234
|
+
const loadedClientKeys = new Set(reconciled.flatMap(messageClientKeys));
|
|
2235
|
+
const pendingLocalMessages = state.messages.filter((message) => {
|
|
2236
|
+
if (message.role !== "user" || message.status === "error") return false;
|
|
2237
|
+
const keys = messageClientKeys(message);
|
|
2238
|
+
return keys.length > 0 && keys.every((key) => !loadedClientKeys.has(key));
|
|
2239
|
+
});
|
|
2240
|
+
return dedupeMessages([...reconciled, ...pendingLocalMessages]);
|
|
2241
|
+
}
|
|
2242
|
+
function mergeLoadedUserMessage(local, loaded) {
|
|
2243
|
+
return {
|
|
2244
|
+
...loaded,
|
|
2245
|
+
content: loaded.content || local.content,
|
|
2246
|
+
parts: mergeMessageParts(local.parts, loaded.parts),
|
|
2247
|
+
clientInputId: loaded.clientInputId ?? local.clientInputId,
|
|
2248
|
+
clientTurnId: loaded.clientTurnId ?? local.clientTurnId,
|
|
2249
|
+
status: loaded.status ?? "done"
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
function mergeMessageParts(localParts, loadedParts) {
|
|
2253
|
+
if (!loadedParts?.length) return localParts;
|
|
2254
|
+
if (!localParts?.length) return loadedParts;
|
|
2255
|
+
return loadedParts.map((part) => {
|
|
2256
|
+
if (part.type !== "image") return part;
|
|
2257
|
+
const local = localParts.find(
|
|
2258
|
+
(item) => item.type === "image" && item.attachment_id === part.attachment_id
|
|
2259
|
+
);
|
|
2260
|
+
if (!local) return part;
|
|
2261
|
+
if (isLocalAttachmentPreview(local.attachment.uri) && !isReachableAttachmentUri(part.attachment.uri)) {
|
|
2262
|
+
return {
|
|
2263
|
+
...part,
|
|
2264
|
+
attachment: {
|
|
2265
|
+
...part.attachment,
|
|
2266
|
+
uri: local.attachment.uri
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
return part;
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
function userMessageFromEvent(event) {
|
|
2274
|
+
const payload = event.payload;
|
|
2275
|
+
const messageId = optionalString(payload.message_id);
|
|
2276
|
+
if (!messageId) return void 0;
|
|
2277
|
+
const metadata = plainRecord(payload.metadata);
|
|
2278
|
+
return {
|
|
2279
|
+
id: messageId,
|
|
2280
|
+
role: "user",
|
|
2281
|
+
content: optionalString(payload.message) ?? "",
|
|
2282
|
+
parts: chatPartsFromPayload(payload.parts),
|
|
2283
|
+
turnId: optionalString(payload.turn_id),
|
|
2284
|
+
clientInputId: optionalString(payload.client_input_id) ?? metadataString(metadata, "client_input_id"),
|
|
2285
|
+
clientTurnId: optionalString(payload.client_turn_id) ?? metadataString(metadata, "client_turn_id"),
|
|
2286
|
+
status: "done"
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
function chatPartsFromPayload(parts) {
|
|
2290
|
+
if (!Array.isArray(parts)) return void 0;
|
|
2291
|
+
const normalized = parts.map((part) => {
|
|
2292
|
+
const raw = plainRecord(part);
|
|
2293
|
+
if (!raw) return void 0;
|
|
2294
|
+
const type = optionalString(raw.type);
|
|
2295
|
+
if (type === "text") {
|
|
2296
|
+
const text = optionalString(raw.text);
|
|
2297
|
+
return text ? { type: "text", text } : void 0;
|
|
2298
|
+
}
|
|
2299
|
+
if (type === "image") {
|
|
2300
|
+
const attachmentId = optionalString(raw.attachment_id);
|
|
2301
|
+
const attachment = chatAttachmentFromPayload(raw.attachment, attachmentId);
|
|
2302
|
+
if (!attachmentId || !attachment) return void 0;
|
|
2303
|
+
return {
|
|
2304
|
+
type: "image",
|
|
2305
|
+
attachment_id: attachmentId,
|
|
2306
|
+
attachment
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
return void 0;
|
|
2310
|
+
}).filter((part) => Boolean(part));
|
|
2311
|
+
return normalized.length ? normalized : void 0;
|
|
2312
|
+
}
|
|
2313
|
+
function chatAttachmentFromPayload(value, attachmentId) {
|
|
2314
|
+
const raw = plainRecord(value);
|
|
2315
|
+
if (!raw && !attachmentId) return void 0;
|
|
2316
|
+
return {
|
|
2317
|
+
id: optionalString(raw?.id) ?? attachmentId ?? "",
|
|
2318
|
+
kind: "image",
|
|
2319
|
+
uri: optionalString(raw?.uri) ?? "",
|
|
2320
|
+
original_name: optionalString(raw?.original_name) ?? null,
|
|
2321
|
+
mime_type: optionalString(raw?.mime_type) ?? null,
|
|
2322
|
+
size_bytes: optionalNumber(raw?.size_bytes),
|
|
2323
|
+
sha256: optionalString(raw?.sha256) ?? null,
|
|
2324
|
+
width: optionalNumber(raw?.width),
|
|
2325
|
+
height: optionalNumber(raw?.height),
|
|
2326
|
+
created_at: optionalString(raw?.created_at) ?? null,
|
|
2327
|
+
thumbnail: thumbnailFromPayload(raw?.thumbnail)
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
function thumbnailFromPayload(value) {
|
|
2331
|
+
const raw = plainRecord(value);
|
|
2332
|
+
if (!raw) return null;
|
|
2333
|
+
const mimeType = optionalString(raw.mime_type);
|
|
2334
|
+
const dataBase64 = optionalString(raw.data_base64);
|
|
2335
|
+
if (!mimeType || !dataBase64) return null;
|
|
2336
|
+
return {
|
|
2337
|
+
mime_type: mimeType,
|
|
2338
|
+
data_base64: dataBase64,
|
|
2339
|
+
width: optionalNumber(raw.width),
|
|
2340
|
+
height: optionalNumber(raw.height)
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
function dedupeMessages(messages) {
|
|
2344
|
+
const next = [];
|
|
2345
|
+
for (const message of messages) {
|
|
2346
|
+
const duplicateIndex = next.findIndex((item) => messagesMatch(item, message));
|
|
2347
|
+
if (duplicateIndex === -1) {
|
|
2348
|
+
next.push(message);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
next[duplicateIndex] = mergeDuplicateMessage(next[duplicateIndex], message);
|
|
2352
|
+
}
|
|
2353
|
+
return next;
|
|
2354
|
+
}
|
|
2355
|
+
function messagesMatch(left, right) {
|
|
2356
|
+
if (left.id === right.id) return true;
|
|
2357
|
+
if (left.role !== right.role) return false;
|
|
2358
|
+
if (left.role === "assistant" && left.turnId && left.turnId === right.turnId) return true;
|
|
2359
|
+
const leftKeys = new Set(messageClientKeys(left));
|
|
2360
|
+
return messageClientKeys(right).some((key) => leftKeys.has(key));
|
|
2361
|
+
}
|
|
2362
|
+
function mergeDuplicateMessage(base, update) {
|
|
2363
|
+
const preferUpdate = base.id.startsWith("user_") && !update.id.startsWith("user_") || base.id.startsWith("msg_") && !update.id.startsWith("msg_") || update.status === "done";
|
|
2364
|
+
const primary = preferUpdate ? update : base;
|
|
2365
|
+
const secondary = preferUpdate ? base : update;
|
|
2366
|
+
return {
|
|
2367
|
+
...primary,
|
|
2368
|
+
content: primary.content || secondary.content,
|
|
2369
|
+
parts: mergeMessageParts(secondary.parts, primary.parts),
|
|
2370
|
+
turnId: primary.turnId ?? secondary.turnId,
|
|
2371
|
+
clientInputId: primary.clientInputId ?? secondary.clientInputId,
|
|
2372
|
+
clientTurnId: primary.clientTurnId ?? secondary.clientTurnId,
|
|
2373
|
+
status: primary.status ?? secondary.status
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
function attachClientIdsToPendingUser(messages, ids) {
|
|
2377
|
+
if (!ids.clientInputId && !ids.clientTurnId) return messages;
|
|
2378
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
2379
|
+
const message = messages[index];
|
|
2380
|
+
if (message.role !== "user") continue;
|
|
2381
|
+
if (message.clientInputId || message.clientTurnId) return messages;
|
|
2382
|
+
return messages.map(
|
|
2383
|
+
(item, itemIndex) => itemIndex === index ? {
|
|
2384
|
+
...item,
|
|
2385
|
+
clientInputId: ids.clientInputId,
|
|
2386
|
+
clientTurnId: ids.clientTurnId
|
|
2387
|
+
} : item
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
return messages;
|
|
2391
|
+
}
|
|
2392
|
+
function messageClientKeys(message) {
|
|
2393
|
+
return [message.clientInputId, message.clientTurnId].map((value) => optionalString(value)).filter((value) => Boolean(value));
|
|
2394
|
+
}
|
|
2395
|
+
function metadataString(metadata, key) {
|
|
2396
|
+
return optionalString(metadata?.[key]);
|
|
2397
|
+
}
|
|
2398
|
+
function isDuplicateEvent(runtime, event) {
|
|
2399
|
+
if (!event.seq || event.seq <= 0) return false;
|
|
2400
|
+
return event.seq <= (runtime?.last_event_seq ?? 0);
|
|
2401
|
+
}
|
|
2402
|
+
function plainRecord(value) {
|
|
2403
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
2404
|
+
return value;
|
|
2405
|
+
}
|
|
2406
|
+
function optionalNumber(value) {
|
|
2407
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2408
|
+
}
|
|
2409
|
+
function isLocalAttachmentPreview(uri) {
|
|
2410
|
+
return Boolean(uri?.startsWith("blob:") || uri?.startsWith("data:"));
|
|
2411
|
+
}
|
|
2412
|
+
function isReachableAttachmentUri(uri) {
|
|
2413
|
+
if (!uri) return false;
|
|
2414
|
+
return uri.startsWith("blob:") || uri.startsWith("data:") || uri.startsWith("http://") || uri.startsWith("https://");
|
|
2415
|
+
}
|
|
2170
2416
|
function isAgentActivityEvent(event) {
|
|
2171
2417
|
return event.event.startsWith("agent.activity.");
|
|
2172
2418
|
}
|
|
@@ -2554,6 +2800,131 @@ function RuntimeIcon({ kind }) {
|
|
|
2554
2800
|
const className = "chat-runtime-icon";
|
|
2555
2801
|
return kind === "cloud" ? /* @__PURE__ */ jsx15(IconCloud, { className }) : /* @__PURE__ */ jsx15(IconLocalComputer, { className });
|
|
2556
2802
|
}
|
|
2803
|
+
|
|
2804
|
+
// src/session-host/session-runtime.ts
|
|
2805
|
+
var TERMINAL_TURN_EVENTS = /* @__PURE__ */ new Set(["turn.completed", "turn.failed", "turn.cancelled"]);
|
|
2806
|
+
function eventSessionId(event) {
|
|
2807
|
+
return optionalString2(event.payload.session_id);
|
|
2808
|
+
}
|
|
2809
|
+
function eventTurnId(event) {
|
|
2810
|
+
return optionalString2(event.payload.turn_id);
|
|
2811
|
+
}
|
|
2812
|
+
function shouldApplyEventToRuntime(chat, event) {
|
|
2813
|
+
const turnId = eventTurnId(event);
|
|
2814
|
+
if (!turnId || event.event === "session.input.accepted") return true;
|
|
2815
|
+
if (!isTurnScopedRuntimeEvent(event.event)) return true;
|
|
2816
|
+
const activeTurnId = chat?.activeTurnId;
|
|
2817
|
+
return !activeTurnId || activeTurnId === turnId;
|
|
2818
|
+
}
|
|
2819
|
+
function shouldApplyEventToActiveSession(event, activeSessionId) {
|
|
2820
|
+
const sessionId = eventSessionId(event);
|
|
2821
|
+
if (!sessionId) return false;
|
|
2822
|
+
if (!activeSessionId) return false;
|
|
2823
|
+
return sessionId === activeSessionId;
|
|
2824
|
+
}
|
|
2825
|
+
function isChatStateMutating(chat) {
|
|
2826
|
+
if (!chat) return false;
|
|
2827
|
+
return Boolean(chat.activeTurnId) || chat.status === "Sending" || chat.status === "Thinking" || chat.status === "Streaming" || chat.status === "Stopping";
|
|
2828
|
+
}
|
|
2829
|
+
function selectCanSend(chat) {
|
|
2830
|
+
return chat.status !== "Loading" && chat.status !== "Disconnected" && chat.status !== "Error";
|
|
2831
|
+
}
|
|
2832
|
+
function selectCanStop(chat) {
|
|
2833
|
+
return Boolean(chat.activeTurnId) || chat.status === "Thinking" || chat.status === "Streaming" || chat.status === "Stopping";
|
|
2834
|
+
}
|
|
2835
|
+
function selectActiveTurnId(chat) {
|
|
2836
|
+
return chat.activeTurnId;
|
|
2837
|
+
}
|
|
2838
|
+
function selectSessionStatus(chat) {
|
|
2839
|
+
return chat.status;
|
|
2840
|
+
}
|
|
2841
|
+
function selectIsMutating(chat) {
|
|
2842
|
+
return isChatStateMutating(chat);
|
|
2843
|
+
}
|
|
2844
|
+
function updateRunningTurnFromEvent(running, event) {
|
|
2845
|
+
const sessionId = eventSessionId(event);
|
|
2846
|
+
const turnId = eventTurnId(event);
|
|
2847
|
+
if (!sessionId || !turnId) return running;
|
|
2848
|
+
if (TERMINAL_TURN_EVENTS.has(event.event)) {
|
|
2849
|
+
if (!running[sessionId] || running[sessionId] === turnId) {
|
|
2850
|
+
const next = { ...running };
|
|
2851
|
+
delete next[sessionId];
|
|
2852
|
+
return next;
|
|
2853
|
+
}
|
|
2854
|
+
return running;
|
|
2855
|
+
}
|
|
2856
|
+
if (event.event === "turn.started" || event.event === "assistant.delta") {
|
|
2857
|
+
if (running[sessionId] === turnId) return running;
|
|
2858
|
+
return { ...running, [sessionId]: turnId };
|
|
2859
|
+
}
|
|
2860
|
+
return running;
|
|
2861
|
+
}
|
|
2862
|
+
function createSessionEventRouter(options) {
|
|
2863
|
+
return (event) => {
|
|
2864
|
+
const sessionId = eventSessionId(event);
|
|
2865
|
+
if (shouldApplyEventToActiveSession(event, options.getActiveSessionId()) && shouldApplyEventToRuntime(options.getChat(), event)) {
|
|
2866
|
+
options.dispatchChat({ type: "event.received", event });
|
|
2867
|
+
}
|
|
2868
|
+
if (sessionId && TERMINAL_TURN_EVENTS.has(event.event)) {
|
|
2869
|
+
options.onTerminalTurn?.(sessionId);
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
function isTurnScopedRuntimeEvent(event) {
|
|
2874
|
+
return event === "turn.started" || event === "assistant.delta" || event === "assistant.completed" || event === "turn.completed" || event === "turn.cancelled" || event === "turn.failed" || event === "agent.activity.started" || event === "agent.activity.updated" || event === "agent.activity.completed" || event === "agent.progress.delta";
|
|
2875
|
+
}
|
|
2876
|
+
function optionalString2(value) {
|
|
2877
|
+
if (value === void 0 || value === null) return void 0;
|
|
2878
|
+
const text = String(value).trim();
|
|
2879
|
+
return text || void 0;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// src/session-host/hydration.ts
|
|
2883
|
+
function buildSessionLoadedAction(sessionId, detail, options = {}) {
|
|
2884
|
+
return {
|
|
2885
|
+
type: "session.loaded",
|
|
2886
|
+
sessionId,
|
|
2887
|
+
messages: conversationMessagesToChatMessages(detail.messages, options),
|
|
2888
|
+
activities: detail.activities ?? [],
|
|
2889
|
+
runtime: detail.runtime ?? detail.session.runtime
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
function conversationMessagesToChatMessages(messages, options = {}) {
|
|
2893
|
+
return messages.map((message) => ({
|
|
2894
|
+
id: message.id,
|
|
2895
|
+
role: message.role,
|
|
2896
|
+
content: message.content,
|
|
2897
|
+
parts: normalizeMessageParts(message.parts, options.resolveAttachmentUrl),
|
|
2898
|
+
turnId: message.turn_id ?? void 0,
|
|
2899
|
+
clientInputId: messageMetadataString(message.metadata, "client_input_id"),
|
|
2900
|
+
clientTurnId: messageMetadataString(message.metadata, "client_turn_id"),
|
|
2901
|
+
status: "done"
|
|
2902
|
+
}));
|
|
2903
|
+
}
|
|
2904
|
+
function normalizeMessageParts(parts, resolveAttachmentUrl) {
|
|
2905
|
+
const normalized = parts?.map((part) => {
|
|
2906
|
+
if (part.type === "text" && part.text) {
|
|
2907
|
+
return { type: "text", text: part.text };
|
|
2908
|
+
}
|
|
2909
|
+
if (part.type === "image" && part.attachment_id && part.attachment) {
|
|
2910
|
+
const uri = part.attachment.uri;
|
|
2911
|
+
return {
|
|
2912
|
+
type: "image",
|
|
2913
|
+
attachment_id: part.attachment_id,
|
|
2914
|
+
attachment: {
|
|
2915
|
+
...part.attachment,
|
|
2916
|
+
uri: uri && resolveAttachmentUrl ? resolveAttachmentUrl(uri) : uri
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
return void 0;
|
|
2921
|
+
}).filter((part) => Boolean(part));
|
|
2922
|
+
return normalized?.length ? normalized : void 0;
|
|
2923
|
+
}
|
|
2924
|
+
function messageMetadataString(metadata, key) {
|
|
2925
|
+
const value = metadata?.[key];
|
|
2926
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2927
|
+
}
|
|
2557
2928
|
export {
|
|
2558
2929
|
AgentActivityTimeline,
|
|
2559
2930
|
CHAT_HOME_MODES,
|
|
@@ -2605,17 +2976,31 @@ export {
|
|
|
2605
2976
|
agentDisplayName,
|
|
2606
2977
|
agentShortId,
|
|
2607
2978
|
buildRuntimeOptions,
|
|
2979
|
+
buildSessionLoadedAction,
|
|
2608
2980
|
composerAcceptAttribute,
|
|
2981
|
+
conversationMessagesToChatMessages,
|
|
2982
|
+
createSessionEventRouter,
|
|
2983
|
+
eventSessionId,
|
|
2984
|
+
eventTurnId,
|
|
2609
2985
|
findChatHomeMode,
|
|
2610
2986
|
fixCommonLatexMistakes,
|
|
2611
2987
|
formatBytes,
|
|
2612
2988
|
initialChatState,
|
|
2989
|
+
isChatStateMutating,
|
|
2613
2990
|
modelStatusText,
|
|
2614
2991
|
normalizeComposerAttachmentPolicy,
|
|
2615
2992
|
normalizeMathDelimiters,
|
|
2616
2993
|
prepareMathMarkdown,
|
|
2617
2994
|
reduceChat,
|
|
2995
|
+
selectActiveTurnId,
|
|
2996
|
+
selectCanSend,
|
|
2997
|
+
selectCanStop,
|
|
2998
|
+
selectIsMutating,
|
|
2999
|
+
selectSessionStatus,
|
|
3000
|
+
shouldApplyEventToActiveSession,
|
|
3001
|
+
shouldApplyEventToRuntime,
|
|
2618
3002
|
stripMathForPreview,
|
|
3003
|
+
updateRunningTurnFromEvent,
|
|
2619
3004
|
useCathyGOChat,
|
|
2620
3005
|
validateComposerFiles
|
|
2621
3006
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beanx/cathygo-web-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./styles.css": "./dist/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@beanx/cathygo-protocol": "0.1.
|
|
19
|
+
"@beanx/cathygo-protocol": "0.1.5",
|
|
20
20
|
"@streamdown/math": "^1.0.2",
|
|
21
21
|
"katex": "^0.16.47",
|
|
22
22
|
"streamdown": "^2.5.0"
|