@beanx/cathygo-web-core 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @beanx/cathygo-web-core
2
+
3
+ Shared CathyGO Web UI and state package.
4
+
5
+ Current active upgrade plan:
6
+
7
+ - [`../../docs/session-runtime-vnext-upgrade-plan.md`](../../docs/session-runtime-vnext-upgrade-plan.md)
8
+
9
+ ## vNext Role
10
+
11
+ This package owns the selected-session view model for:
12
+
13
+ - single-session transcript rendering
14
+ - session event reduction
15
+ - optimistic user input reconciliation by `client_input_id`
16
+ - session-scoped stop state
17
+ - replay-safe hydration after refresh or reconnect
18
+ - selectors used by local Web and `beanx-home`
19
+
20
+ Host applications still own account routing, agent selection, transport setup,
21
+ selected session id, lightweight session lists, and product-specific shell UI.
22
+ They should not own a second session execution runtime.
23
+
24
+ ## First vNext Deliverables
25
+
26
+ - Keep the current single-session `ChatState` reducer exported.
27
+ - Add `SessionViewState`, session event reducer, and current-session selectors.
28
+ - Add reducer tests for interrupt, replay, stop, and optimistic reconciliation.
29
+ - Prove the implementation in `cathygo-agent/web` before publishing
30
+ `@beanx/cathygo-web-core@0.2.0`.
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, LearningTurnEvent, ConversationSummary, GatewayModelStatus } from '@beanx/cathygo-protocol';
3
+ import { AgentActivity, SessionRuntimeSnapshot, LearningTurnEvent, ConversationSummary, GatewayModelStatus } 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 = {
@@ -96,6 +104,7 @@ type ChatState = {
96
104
  messages: ChatMessage$1[];
97
105
  activitiesByTurnId: Record<string, AgentActivity[]>;
98
106
  progressByTurnId: Record<string, AgentRunProgress>;
107
+ runtime?: SessionRuntimeSnapshot;
99
108
  status: string;
100
109
  contextMessageCount?: number;
101
110
  error?: string;
@@ -131,6 +140,7 @@ type ChatAction = {
131
140
  sessionId: string;
132
141
  messages: ChatMessage$1[];
133
142
  activities?: AgentActivity[];
143
+ runtime?: SessionRuntimeSnapshot;
134
144
  } | {
135
145
  type: 'session.load.failed';
136
146
  sessionId: string;
@@ -141,10 +151,15 @@ type ChatAction = {
141
151
  id: string;
142
152
  content: string;
143
153
  parts?: ChatMessagePart[];
154
+ clientInputId?: string;
155
+ clientTurnId?: string;
144
156
  } | {
145
- type: 'turn.accepted';
157
+ type: 'session.input.accepted';
146
158
  sessionId: string;
147
159
  turnId: string;
160
+ clientInputId?: string;
161
+ clientTurnId?: string;
162
+ idempotentReplay?: boolean;
148
163
  } | {
149
164
  type: 'send.failed';
150
165
  id: string;
@@ -204,7 +219,7 @@ type UseCathyGOChatResult = {
204
219
  dispatchChat: Dispatch<ChatAction>;
205
220
  resetChat: () => void;
206
221
  createSession: (sessionId: string) => void;
207
- loadSession: (sessionId: string, messages: ChatMessage$1[], activities?: ChatState['activitiesByTurnId'][string]) => void;
222
+ loadSession: (sessionId: string, messages: ChatMessage$1[], activities?: ChatState['activitiesByTurnId'][string], runtime?: SessionRuntimeSnapshot) => void;
208
223
  applyTurnEvent: (event: LearningTurnEvent) => void;
209
224
  };
210
225
  declare function useCathyGOChat(initialState?: ChatState): UseCathyGOChatResult;
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";
@@ -1668,7 +1680,7 @@ function ChatView({
1668
1680
  onSettingsOpen
1669
1681
  }) {
1670
1682
  const loadingSession = chat.status === "Loading";
1671
- const busy = Boolean(chat.activeTurnId) || Boolean(externalBusy) || loadingSession;
1683
+ const busy = Boolean(externalBusy) || loadingSession;
1672
1684
  const ready = Boolean(chat.sessionId) && !loadingSession;
1673
1685
  const usingMock = model?.provider === "mock";
1674
1686
  const modelLabel = model ? model.model || model.display_name || model.provider : "loading model status";
@@ -1715,7 +1727,7 @@ function ChatView({
1715
1727
  activitiesByTurnId: chat.activitiesByTurnId,
1716
1728
  progressByTurnId: chat.progressByTurnId,
1717
1729
  messages: chat.messages,
1718
- onSuggest: ready && !busy ? onSuggest : void 0
1730
+ onSuggest: ready && !busy && !chat.activeTurnId ? onSuggest : void 0
1719
1731
  }
1720
1732
  ),
1721
1733
  !loadingSession && chat.status === "Error" && chat.error ? /* @__PURE__ */ jsx13("p", { className: "chat-session-error", children: chat.error }) : null,
@@ -1875,14 +1887,18 @@ function reduceChat(state, action) {
1875
1887
  };
1876
1888
  }
1877
1889
  if (action.type === "session.loaded") {
1890
+ const runtime = action.runtime;
1891
+ const activeTurnId = optionalString(runtime?.active_turn_id);
1892
+ const messages = reconcileLoadedMessages(state, action.sessionId, action.messages);
1878
1893
  return {
1879
1894
  ...state,
1880
1895
  sessionId: action.sessionId,
1881
- activeTurnId: void 0,
1882
- messages: action.messages,
1896
+ activeTurnId,
1897
+ messages,
1883
1898
  activitiesByTurnId: groupActivitiesByTurn(action.activities ?? []),
1884
- progressByTurnId: {},
1885
- status: "Ready",
1899
+ progressByTurnId: activeTurnId ? setProgress({}, runtimeProgressFromSnapshot(runtime, activeTurnId)) : {},
1900
+ runtime,
1901
+ status: statusFromRuntime(runtime),
1886
1902
  error: void 0,
1887
1903
  errorCode: void 0
1888
1904
  };
@@ -1908,6 +1924,8 @@ function reduceChat(state, action) {
1908
1924
  role: "user",
1909
1925
  content: action.content,
1910
1926
  parts: action.parts,
1927
+ clientInputId: action.clientInputId,
1928
+ clientTurnId: action.clientTurnId,
1911
1929
  status: "done"
1912
1930
  }
1913
1931
  ],
@@ -1929,11 +1947,24 @@ function reduceChat(state, action) {
1929
1947
  errorCode: action.code
1930
1948
  };
1931
1949
  }
1932
- if (action.type === "turn.accepted") {
1950
+ if (action.type === "session.input.accepted") {
1951
+ const messages = attachClientIdsToPendingUser(state.messages, {
1952
+ clientInputId: action.clientInputId,
1953
+ clientTurnId: action.clientTurnId
1954
+ });
1933
1955
  return {
1934
1956
  ...state,
1935
1957
  sessionId: action.sessionId,
1936
1958
  activeTurnId: action.turnId,
1959
+ messages,
1960
+ runtime: {
1961
+ ...state.runtime ?? emptyRuntimeSnapshot(),
1962
+ status: "running",
1963
+ active_turn_id: action.turnId,
1964
+ active_turn_ids: [action.turnId],
1965
+ active_client_input_id: action.clientInputId ?? null,
1966
+ active_client_turn_id: action.clientTurnId ?? null
1967
+ },
1937
1968
  progressByTurnId: setProgress(state.progressByTurnId, {
1938
1969
  turnId: action.turnId,
1939
1970
  phase: "queued",
@@ -1949,12 +1980,65 @@ function reduceChat(state, action) {
1949
1980
  }
1950
1981
  const event = action.event;
1951
1982
  const payload = event.payload;
1983
+ if (isDuplicateEvent(state.runtime, event)) {
1984
+ return state;
1985
+ }
1952
1986
  const next = {
1953
1987
  ...state,
1954
1988
  eventCount: state.eventCount + 1,
1955
1989
  sessionId: payload.session_id ?? state.sessionId,
1956
- activeTurnId: payload.turn_id ?? state.activeTurnId
1990
+ activeTurnId: payload.turn_id ?? state.activeTurnId,
1991
+ runtime: runtimeFromEvent(state.runtime, event)
1957
1992
  };
1993
+ if (event.event === "session.created") {
1994
+ return {
1995
+ ...next,
1996
+ status: "Ready",
1997
+ error: void 0,
1998
+ errorCode: void 0
1999
+ };
2000
+ }
2001
+ if (event.event === "session.deleted") {
2002
+ return {
2003
+ ...next,
2004
+ activeTurnId: void 0,
2005
+ progressByTurnId: {},
2006
+ status: "Archived"
2007
+ };
2008
+ }
2009
+ if (event.event === "session.input.accepted") {
2010
+ const turnId = optionalString(payload.turn_id);
2011
+ if (!turnId) return next;
2012
+ return {
2013
+ ...next,
2014
+ activeTurnId: turnId,
2015
+ progressByTurnId: setProgress(next.progressByTurnId, {
2016
+ turnId,
2017
+ phase: "queued",
2018
+ status: "running",
2019
+ summary: "\u6B63\u5728\u63D0\u4EA4\u95EE\u9898",
2020
+ detail: "\u7B49\u5F85 CathyGO \u5F00\u59CB\u5904\u7406",
2021
+ startedAt: nowIso()
2022
+ }),
2023
+ status: "Thinking",
2024
+ error: void 0,
2025
+ errorCode: void 0
2026
+ };
2027
+ }
2028
+ if (event.event === "session.stop.requested") {
2029
+ return {
2030
+ ...next,
2031
+ status: "Stopping"
2032
+ };
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
+ }
1958
2042
  if (event.event === "agent.progress.delta") {
1959
2043
  return {
1960
2044
  ...next,
@@ -2020,7 +2104,8 @@ function reduceChat(state, action) {
2020
2104
  next.messages,
2021
2105
  payload.message_id,
2022
2106
  finalMessage,
2023
- payload.turn_id
2107
+ payload.turn_id,
2108
+ optionalString(payload.persisted_message_id)
2024
2109
  )
2025
2110
  };
2026
2111
  }
@@ -2029,6 +2114,7 @@ function reduceChat(state, action) {
2029
2114
  ...next,
2030
2115
  activeTurnId: void 0,
2031
2116
  progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
2117
+ runtime: markRuntimeIdle(next.runtime),
2032
2118
  status: "Ready"
2033
2119
  };
2034
2120
  }
@@ -2037,6 +2123,7 @@ function reduceChat(state, action) {
2037
2123
  ...next,
2038
2124
  activeTurnId: void 0,
2039
2125
  progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
2126
+ runtime: markRuntimeIdle(next.runtime),
2040
2127
  status: "Ready"
2041
2128
  };
2042
2129
  }
@@ -2046,6 +2133,7 @@ function reduceChat(state, action) {
2046
2133
  ...next,
2047
2134
  activeTurnId: void 0,
2048
2135
  progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
2136
+ runtime: markRuntimeIdle(next.runtime),
2049
2137
  status: "Error",
2050
2138
  error: error?.message ?? "CathyGO turn failed",
2051
2139
  errorCode: error?.code
@@ -2068,6 +2156,10 @@ function appendAssistantDelta(messages, messageId, delta, turnId) {
2068
2156
  }
2069
2157
  ];
2070
2158
  }
2159
+ const existing = messages[index];
2160
+ if (existing?.role === "assistant" && existing.status === "done" && existing.content) {
2161
+ return messages;
2162
+ }
2071
2163
  return messages.map(
2072
2164
  (message, itemIndex) => itemIndex === index ? {
2073
2165
  ...message,
@@ -2077,20 +2169,34 @@ function appendAssistantDelta(messages, messageId, delta, turnId) {
2077
2169
  } : message
2078
2170
  );
2079
2171
  }
2080
- function finalizeAssistantMessage(messages, messageId, finalMessage, turnId) {
2172
+ function finalizeAssistantMessage(messages, messageId, finalMessage, turnId, persistedMessageId) {
2081
2173
  const id = String(messageId || "");
2082
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;
2083
2178
  if (!id && !finalMessage) {
2084
2179
  return messages.map(
2085
2180
  (message) => message.role === "assistant" && message.status === "streaming" ? { ...message, turnId: message.turnId ?? resolvedTurnId, status: "done" } : message
2086
2181
  );
2087
2182
  }
2088
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
+ }
2089
2195
  if (index === -1 && finalMessage) {
2090
2196
  return [
2091
2197
  ...messages,
2092
2198
  {
2093
- id: id || `msg_done_${messages.length}`,
2199
+ id: persistedMessageId ?? (id || `msg_done_${messages.length}`),
2094
2200
  role: "assistant",
2095
2201
  content: finalMessage,
2096
2202
  turnId: resolvedTurnId,
@@ -2101,12 +2207,212 @@ function finalizeAssistantMessage(messages, messageId, finalMessage, turnId) {
2101
2207
  return messages.map(
2102
2208
  (message, itemIndex) => itemIndex === index || message.role === "assistant" && message.status === "streaming" ? {
2103
2209
  ...message,
2210
+ id: itemIndex === index && persistedMessageId && message.id === id ? persistedMessageId : message.id,
2104
2211
  content: message.content || finalMessage,
2105
2212
  turnId: message.turnId ?? resolvedTurnId,
2106
2213
  status: "done"
2107
2214
  } : message
2108
2215
  );
2109
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
+ }
2110
2416
  function isAgentActivityEvent(event) {
2111
2417
  return event.event.startsWith("agent.activity.");
2112
2418
  }
@@ -2217,6 +2523,78 @@ function progressFromActivity(progressByTurnId, activity, turnActivities, messag
2217
2523
  }
2218
2524
  return progressByTurnId;
2219
2525
  }
2526
+ function runtimeProgressFromSnapshot(runtime, turnId) {
2527
+ return {
2528
+ turnId,
2529
+ phase: runtime?.status === "stopping" ? "model" : "queued",
2530
+ status: "running",
2531
+ summary: runtime?.status === "stopping" ? "\u6B63\u5728\u505C\u6B62" : "\u6B63\u5728\u6062\u590D\u4F1A\u8BDD\u72B6\u6001",
2532
+ detail: runtime?.active_client_input_id ? `client_input_id=${runtime.active_client_input_id}` : void 0,
2533
+ startedAt: nowIso()
2534
+ };
2535
+ }
2536
+ function runtimeFromEvent(current, event) {
2537
+ const payload = event.payload;
2538
+ const turnId = optionalString(payload.turn_id);
2539
+ const turnIds = eventTurnIds(payload, turnId);
2540
+ const base = {
2541
+ ...current ?? emptyRuntimeSnapshot(),
2542
+ last_event_seq: Math.max(current?.last_event_seq ?? 0, event.seq)
2543
+ };
2544
+ if (event.event === "session.input.accepted" || event.event === "turn.started") {
2545
+ return {
2546
+ ...base,
2547
+ status: "running",
2548
+ active_turn_id: turnId ?? base.active_turn_id ?? null,
2549
+ active_turn_ids: turnIds.length > 0 ? turnIds : base.active_turn_ids ?? [],
2550
+ active_client_input_id: optionalString(payload.client_input_id) ?? base.active_client_input_id ?? null,
2551
+ active_client_turn_id: optionalString(payload.client_turn_id) ?? base.active_client_turn_id ?? null
2552
+ };
2553
+ }
2554
+ if (event.event === "session.stop.requested") {
2555
+ return {
2556
+ ...base,
2557
+ status: "stopping",
2558
+ active_turn_id: turnId ?? base.active_turn_id ?? null,
2559
+ active_turn_ids: turnIds.length > 0 ? turnIds : base.active_turn_ids ?? []
2560
+ };
2561
+ }
2562
+ if (event.event === "turn.completed" || event.event === "turn.cancelled" || event.event === "turn.failed" || event.event === "session.deleted") {
2563
+ return markRuntimeIdle(base);
2564
+ }
2565
+ return base;
2566
+ }
2567
+ function eventTurnIds(payload, turnId) {
2568
+ if (Array.isArray(payload.turn_ids)) {
2569
+ return payload.turn_ids.map((value) => optionalString(value)).filter((value) => Boolean(value));
2570
+ }
2571
+ return turnId ? [turnId] : [];
2572
+ }
2573
+ function emptyRuntimeSnapshot() {
2574
+ return {
2575
+ status: "idle",
2576
+ active_turn_id: null,
2577
+ active_turn_ids: [],
2578
+ active_client_input_id: null,
2579
+ active_client_turn_id: null,
2580
+ last_event_seq: 0
2581
+ };
2582
+ }
2583
+ function markRuntimeIdle(runtime) {
2584
+ return {
2585
+ ...runtime ?? emptyRuntimeSnapshot(),
2586
+ status: "idle",
2587
+ active_turn_id: null,
2588
+ active_turn_ids: [],
2589
+ active_client_input_id: null,
2590
+ active_client_turn_id: null
2591
+ };
2592
+ }
2593
+ function statusFromRuntime(runtime) {
2594
+ if (runtime?.status === "running") return "Thinking";
2595
+ if (runtime?.status === "stopping") return "Stopping";
2596
+ return "Ready";
2597
+ }
2220
2598
  function setProgress(progressByTurnId, progress) {
2221
2599
  return {
2222
2600
  ...progressByTurnId,
@@ -2314,8 +2692,8 @@ function useCathyGOChat(initialState = initialChatState) {
2314
2692
  dispatchChat({ type: "session.created", sessionId });
2315
2693
  }, []);
2316
2694
  const loadSession = useCallback(
2317
- (sessionId, messages, activities) => {
2318
- dispatchChat({ type: "session.loaded", sessionId, messages, activities });
2695
+ (sessionId, messages, activities, runtime) => {
2696
+ dispatchChat({ type: "session.loaded", sessionId, messages, activities, runtime });
2319
2697
  },
2320
2698
  []
2321
2699
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beanx/cathygo-web-core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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.0",
19
+ "@beanx/cathygo-protocol": "0.1.2",
20
20
  "@streamdown/math": "^1.0.2",
21
21
  "katex": "^0.16.47",
22
22
  "streamdown": "^2.5.0"