@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 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
- - Add `SessionViewState`, session event reducer, and current-session selectors.
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
- `@beanx/cathygo-web-core@0.2.0`.
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
- 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 RuntimeOption, ScrollToBottomButton, type UseCathyGOChatResult, agentDisplayName, agentShortId, buildRuntimeOptions, composerAcceptAttribute, findChatHomeMode, fixCommonLatexMistakes, formatBytes, initialChatState, modelStatusText, normalizeComposerAttachmentPolicy, normalizeMathDelimiters, prepareMathMarkdown, reduceChat, stripMathForPreview, useCathyGOChat, validateComposerFiles };
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.uri
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: action.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.2",
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.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"