@beanx/cathygo-web-core 0.1.0 → 0.1.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 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;
@@ -96,6 +96,7 @@ type ChatState = {
96
96
  messages: ChatMessage$1[];
97
97
  activitiesByTurnId: Record<string, AgentActivity[]>;
98
98
  progressByTurnId: Record<string, AgentRunProgress>;
99
+ runtime?: SessionRuntimeSnapshot;
99
100
  status: string;
100
101
  contextMessageCount?: number;
101
102
  error?: string;
@@ -123,20 +124,32 @@ type AgentRunThinking = {
123
124
  type ChatAction = {
124
125
  type: 'session.created';
125
126
  sessionId: string;
127
+ } | {
128
+ type: 'session.loading';
129
+ sessionId: string;
126
130
  } | {
127
131
  type: 'session.loaded';
128
132
  sessionId: string;
129
133
  messages: ChatMessage$1[];
130
134
  activities?: AgentActivity[];
135
+ runtime?: SessionRuntimeSnapshot;
136
+ } | {
137
+ type: 'session.load.failed';
138
+ sessionId: string;
139
+ error: string;
140
+ code?: string;
131
141
  } | {
132
142
  type: 'user.sent';
133
143
  id: string;
134
144
  content: string;
135
145
  parts?: ChatMessagePart[];
136
146
  } | {
137
- type: 'turn.accepted';
147
+ type: 'session.input.accepted';
138
148
  sessionId: string;
139
149
  turnId: string;
150
+ clientInputId?: string;
151
+ clientTurnId?: string;
152
+ idempotentReplay?: boolean;
140
153
  } | {
141
154
  type: 'send.failed';
142
155
  id: string;
@@ -174,6 +187,9 @@ type CathyGOChatAppProps = {
174
187
  gatewayLinked?: boolean;
175
188
  model?: GatewayModelStatus;
176
189
  agentStatus?: AgentIdentityStatus;
190
+ activeSessionId?: string;
191
+ loadingSessionId?: string;
192
+ debugPanels?: boolean;
177
193
  homeAvatarSrc?: string;
178
194
  onDraftChange: (value: string) => void;
179
195
  onFilesSelected?: (files: File[]) => void;
@@ -184,15 +200,16 @@ type CathyGOChatAppProps = {
184
200
  onSuggest?: (text: string) => void;
185
201
  onOpenChat: (sessionId: string) => void;
186
202
  onNewChat: () => void;
203
+ onSettingsOpen?: () => void;
187
204
  };
188
- declare function CathyGOChatApp({ screen, chat, chats, draft, attachments, attachmentPolicy, composerError, busy, connectionStatus, gatewayLinked, model, agentStatus, homeAvatarSrc, onDraftChange, onFilesSelected, onRemoveAttachment, onSendMessage, onSendHomeMessage, onAbort, onSuggest, onOpenChat, onNewChat, }: CathyGOChatAppProps): react.JSX.Element;
205
+ declare function CathyGOChatApp({ screen, chat, chats, draft, attachments, attachmentPolicy, composerError, busy, connectionStatus, gatewayLinked, model, agentStatus, activeSessionId, loadingSessionId, debugPanels, homeAvatarSrc, onDraftChange, onFilesSelected, onRemoveAttachment, onSendMessage, onSendHomeMessage, onAbort, onSuggest, onOpenChat, onNewChat, onSettingsOpen, }: CathyGOChatAppProps): react.JSX.Element;
189
206
 
190
207
  type UseCathyGOChatResult = {
191
208
  chat: ChatState;
192
209
  dispatchChat: Dispatch<ChatAction>;
193
210
  resetChat: () => void;
194
211
  createSession: (sessionId: string) => void;
195
- loadSession: (sessionId: string, messages: ChatMessage$1[], activities?: ChatState['activitiesByTurnId'][string]) => void;
212
+ loadSession: (sessionId: string, messages: ChatMessage$1[], activities?: ChatState['activitiesByTurnId'][string], runtime?: SessionRuntimeSnapshot) => void;
196
213
  applyTurnEvent: (event: LearningTurnEvent) => void;
197
214
  };
198
215
  declare function useCathyGOChat(initialState?: ChatState): UseCathyGOChatResult;
@@ -246,8 +263,9 @@ type ChatTopBarProps = {
246
263
  debugUI?: boolean;
247
264
  canAbort?: boolean;
248
265
  onAbort?: () => void;
266
+ onSettingsOpen?: () => void;
249
267
  };
250
- declare function ChatTopBar({ agentStatus, connectionStatus, gatewayLinked, model, debugUI, canAbort, onAbort, }: ChatTopBarProps): react.JSX.Element;
268
+ declare function ChatTopBar({ agentStatus, connectionStatus, gatewayLinked, model, debugUI, canAbort, onAbort, onSettingsOpen, }: ChatTopBarProps): react.JSX.Element;
251
269
 
252
270
  type ChatTranscriptProps = {
253
271
  messages: ChatMessage$1[];
@@ -325,19 +343,22 @@ interface ChatHomeViewProps {
325
343
  model?: GatewayModelStatus;
326
344
  agentStatus?: AgentIdentityStatus;
327
345
  avatarSrc?: string;
346
+ onSettingsOpen?: () => void;
328
347
  onDraftChange: (value: string) => void;
329
348
  onFilesSelected?: (files: File[]) => void;
330
349
  onRemoveAttachment?: (id: string) => void;
331
350
  onSend: (modeId: ChatHomeModeId) => void;
332
351
  }
333
- declare function ChatHomeView({ draft, attachments, attachmentPolicy, error, busy, connectionStatus, gatewayLinked, model, agentStatus, avatarSrc, onDraftChange, onFilesSelected, onRemoveAttachment, onSend, }: ChatHomeViewProps): react.JSX.Element;
352
+ declare function ChatHomeView({ draft, attachments, attachmentPolicy, error, busy, connectionStatus, gatewayLinked, model, agentStatus, avatarSrc, onSettingsOpen, onDraftChange, onFilesSelected, onRemoveAttachment, onSend, }: ChatHomeViewProps): react.JSX.Element;
334
353
 
335
354
  interface ChatListViewProps {
336
355
  chats: ConversationSummary[];
356
+ activeSessionId?: string;
357
+ loadingSessionId?: string;
337
358
  onOpen: (sessionId: string) => void;
338
359
  onNewChat: () => void;
339
360
  }
340
- declare function ChatListView({ chats, onOpen, onNewChat }: ChatListViewProps): react.JSX.Element;
361
+ declare function ChatListView({ chats, activeSessionId, loadingSessionId, onOpen, onNewChat, }: ChatListViewProps): react.JSX.Element;
341
362
 
342
363
  interface ChatViewProps {
343
364
  chat: ChatState;
@@ -345,18 +366,21 @@ interface ChatViewProps {
345
366
  attachments?: PendingComposerAttachment[];
346
367
  attachmentPolicy?: ComposerAttachmentPolicy;
347
368
  composerError?: string;
369
+ busy?: boolean;
348
370
  connectionStatus: string;
349
371
  gatewayLinked?: boolean;
350
372
  model?: GatewayModelStatus;
351
373
  agentStatus?: AgentIdentityStatus;
374
+ debugPanels?: boolean;
352
375
  onDraftChange: (value: string) => void;
353
376
  onFilesSelected?: (files: File[]) => void;
354
377
  onRemoveAttachment?: (id: string) => void;
355
378
  onSend: () => void;
356
379
  onAbort: () => void;
357
380
  onSuggest?: (text: string) => void;
381
+ onSettingsOpen?: () => void;
358
382
  }
359
- declare function ChatView({ chat, draft, attachments, attachmentPolicy, composerError, connectionStatus, gatewayLinked, model, agentStatus, onDraftChange, onFilesSelected, onRemoveAttachment, onSend, onAbort, onSuggest, }: ChatViewProps): react.JSX.Element;
383
+ declare function ChatView({ chat, draft, attachments, attachmentPolicy, composerError, busy: externalBusy, connectionStatus, gatewayLinked, model, agentStatus, debugPanels, onDraftChange, onFilesSelected, onRemoveAttachment, onSend, onAbort, onSuggest, onSettingsOpen, }: ChatViewProps): react.JSX.Element;
360
384
 
361
385
  declare function modelStatusText(model: GatewayModelStatus): string;
362
386
 
package/dist/index.js CHANGED
@@ -1,15 +1,6 @@
1
1
  // src/features/chat/ChatHomeView.tsx
2
2
  import { useRef as useRef2, useState as useState2 } from "react";
3
3
 
4
- // src/chat-ui/agentIdentity.ts
5
- function agentDisplayName(status) {
6
- return status?.display_name?.trim() || status?.agent_id?.trim() || "CathyGO Agent";
7
- }
8
- function agentShortId(status) {
9
- const value = status?.agent_short_id?.trim();
10
- return value || void 0;
11
- }
12
-
13
4
  // src/chat-ui/icons.tsx
14
5
  import { jsx, jsxs } from "react/jsx-runtime";
15
6
  function IconNewChat({ className }) {
@@ -773,6 +764,15 @@ function IconSend({ className }) {
773
764
  );
774
765
  }
775
766
 
767
+ // src/chat-ui/agentIdentity.ts
768
+ function agentDisplayName(status) {
769
+ return status?.display_name?.trim() || status?.agent_id?.trim() || "CathyGO Agent";
770
+ }
771
+ function agentShortId(status) {
772
+ const value = status?.agent_short_id?.trim();
773
+ return value || void 0;
774
+ }
775
+
776
776
  // src/chat-ui/components/ChatAgentIdentity.tsx
777
777
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
778
778
  function ChatAgentIdentity({ agentStatus }) {
@@ -816,7 +816,8 @@ function ChatTopBar({
816
816
  model,
817
817
  debugUI,
818
818
  canAbort,
819
- onAbort
819
+ onAbort,
820
+ onSettingsOpen
820
821
  }) {
821
822
  const tone = connectionTone(connectionStatus, gatewayLinked);
822
823
  const statusLabel = gatewayLinked ? "Connected" : connectionStatus;
@@ -833,6 +834,17 @@ function ChatTopBar({
833
834
  }
834
835
  ),
835
836
  debugUI ? /* @__PURE__ */ jsx3("span", { className: "chat-topbar-debug", children: "Debug" }) : null,
837
+ onSettingsOpen ? /* @__PURE__ */ jsx3(
838
+ "button",
839
+ {
840
+ "aria-label": "\u6253\u5F00\u8BBE\u7F6E",
841
+ className: "chat-topbar-icon-btn",
842
+ onClick: onSettingsOpen,
843
+ title: "\u8BBE\u7F6E",
844
+ type: "button",
845
+ children: /* @__PURE__ */ jsx3(IconSettings, {})
846
+ }
847
+ ) : null,
836
848
  canAbort ? /* @__PURE__ */ jsx3("button", { className: "chat-topbar-abort", onClick: onAbort, type: "button", children: "\u505C\u6B62" }) : null
837
849
  ] })
838
850
  ] });
@@ -1108,6 +1120,7 @@ function ChatHomeView({
1108
1120
  model,
1109
1121
  agentStatus,
1110
1122
  avatarSrc,
1123
+ onSettingsOpen,
1111
1124
  onDraftChange,
1112
1125
  onFilesSelected,
1113
1126
  onRemoveAttachment,
@@ -1130,7 +1143,8 @@ function ChatHomeView({
1130
1143
  connectionStatus,
1131
1144
  gatewayLinked,
1132
1145
  model,
1133
- agentStatus
1146
+ agentStatus,
1147
+ onSettingsOpen
1134
1148
  }
1135
1149
  ),
1136
1150
  /* @__PURE__ */ jsxs5("div", { className: "chat-home-center", children: [
@@ -1188,29 +1202,37 @@ function stripMathForPreview(text, maxLen = 80) {
1188
1202
 
1189
1203
  // src/features/chat/ChatListView.tsx
1190
1204
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1191
- function ChatListView({ chats, onOpen, onNewChat }) {
1205
+ function ChatListView({
1206
+ chats,
1207
+ activeSessionId,
1208
+ loadingSessionId,
1209
+ onOpen,
1210
+ onNewChat
1211
+ }) {
1192
1212
  return /* @__PURE__ */ jsxs6("section", { className: "cathygo-chat-panel list-workspace", children: [
1193
1213
  /* @__PURE__ */ jsxs6("header", { className: "page-topbar", children: [
1194
1214
  /* @__PURE__ */ jsx6("h1", { className: "page-topbar-title", children: "\u5BF9\u8BDD\u5386\u53F2" }),
1195
1215
  /* @__PURE__ */ jsx6("button", { className: "primary-action", onClick: onNewChat, type: "button", children: "\u65B0\u5BF9\u8BDD" })
1196
1216
  ] }),
1197
- /* @__PURE__ */ jsx6("div", { className: "chat-column list-column", children: chats.length ? /* @__PURE__ */ jsx6("div", { className: "history-list", children: chats.map((chat) => /* @__PURE__ */ jsxs6(
1198
- "button",
1199
- {
1200
- className: "history-row",
1201
- onClick: () => onOpen(chat.session_id),
1202
- type: "button",
1203
- children: [
1204
- /* @__PURE__ */ jsx6("strong", { children: chat.title }),
1205
- /* @__PURE__ */ jsx6("span", { children: chat.preview ? stripMathForPreview(chat.preview, 120) : "\u6682\u65E0\u9884\u89C8" }),
1206
- /* @__PURE__ */ jsxs6("small", { children: [
1207
- chat.message_count,
1208
- " \u6761\u6D88\u606F"
1209
- ] })
1210
- ]
1211
- },
1212
- chat.session_id
1213
- )) }) : /* @__PURE__ */ jsx6("p", { className: "empty", children: "\u6682\u65E0\u5BF9\u8BDD\u3002\u521B\u5EFA\u65B0\u5BF9\u8BDD\u540E\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\u3002" }) })
1217
+ /* @__PURE__ */ jsx6("div", { className: "chat-column list-column", children: chats.length ? /* @__PURE__ */ jsx6("div", { className: "history-list", children: chats.map((chat) => {
1218
+ const active = chat.session_id === activeSessionId;
1219
+ const loading = chat.session_id === loadingSessionId;
1220
+ return /* @__PURE__ */ jsxs6(
1221
+ "button",
1222
+ {
1223
+ "aria-current": active ? "true" : void 0,
1224
+ className: `history-row${active ? " active" : ""}${loading ? " loading" : ""}`,
1225
+ onClick: () => onOpen(chat.session_id),
1226
+ type: "button",
1227
+ children: [
1228
+ /* @__PURE__ */ jsx6("strong", { children: chat.title }),
1229
+ /* @__PURE__ */ jsx6("span", { children: chat.preview ? stripMathForPreview(chat.preview, 120) : "\u6682\u65E0\u9884\u89C8" }),
1230
+ /* @__PURE__ */ jsx6("small", { children: loading ? "\u6B63\u5728\u6253\u5F00" : `${chat.message_count} \u6761\u6D88\u606F` })
1231
+ ]
1232
+ },
1233
+ chat.session_id
1234
+ );
1235
+ }) }) : /* @__PURE__ */ jsx6("p", { className: "empty", children: "\u6682\u65E0\u5BF9\u8BDD\u3002\u521B\u5EFA\u65B0\u5BF9\u8BDD\u540E\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\u3002" }) })
1214
1236
  ] });
1215
1237
  }
1216
1238
 
@@ -1631,22 +1653,26 @@ function ChatView({
1631
1653
  attachments,
1632
1654
  attachmentPolicy,
1633
1655
  composerError,
1656
+ busy: externalBusy,
1634
1657
  connectionStatus,
1635
1658
  gatewayLinked,
1636
1659
  model,
1637
1660
  agentStatus,
1661
+ debugPanels = true,
1638
1662
  onDraftChange,
1639
1663
  onFilesSelected,
1640
1664
  onRemoveAttachment,
1641
1665
  onSend,
1642
1666
  onAbort,
1643
- onSuggest
1667
+ onSuggest,
1668
+ onSettingsOpen
1644
1669
  }) {
1645
- const busy = Boolean(chat.activeTurnId);
1646
- const ready = Boolean(chat.sessionId);
1670
+ const loadingSession = chat.status === "Loading";
1671
+ const busy = Boolean(externalBusy) || loadingSession;
1672
+ const ready = Boolean(chat.sessionId) && !loadingSession;
1647
1673
  const usingMock = model?.provider === "mock";
1648
1674
  const modelLabel = model ? model.model || model.display_name || model.provider : "loading model status";
1649
- const debugUI = isDebugUIEnabled();
1675
+ const debugUI = debugPanels && isDebugUIEnabled();
1650
1676
  return /* @__PURE__ */ jsxs11("section", { className: "cathygo-chat-panel chat-workspace", children: [
1651
1677
  /* @__PURE__ */ jsx13(
1652
1678
  ChatTopBar,
@@ -1657,7 +1683,8 @@ function ChatView({
1657
1683
  agentStatus,
1658
1684
  debugUI,
1659
1685
  canAbort: Boolean(chat.activeTurnId),
1660
- onAbort
1686
+ onAbort,
1687
+ onSettingsOpen
1661
1688
  }
1662
1689
  ),
1663
1690
  debugUI ? /* @__PURE__ */ jsxs11("div", { className: "chat-debug-panels", children: [
@@ -1681,16 +1708,17 @@ function ChatView({
1681
1708
  )
1682
1709
  ] }) : null,
1683
1710
  /* @__PURE__ */ jsxs11("div", { className: "chat-main", children: [
1684
- /* @__PURE__ */ jsx13(
1711
+ loadingSession ? /* @__PURE__ */ jsx13(ChatSessionLoading, {}) : /* @__PURE__ */ jsx13(
1685
1712
  ChatTranscript,
1686
1713
  {
1687
1714
  activeTurnId: chat.activeTurnId,
1688
1715
  activitiesByTurnId: chat.activitiesByTurnId,
1689
1716
  progressByTurnId: chat.progressByTurnId,
1690
1717
  messages: chat.messages,
1691
- onSuggest: ready && !busy ? onSuggest : void 0
1718
+ onSuggest: ready && !busy && !chat.activeTurnId ? onSuggest : void 0
1692
1719
  }
1693
1720
  ),
1721
+ !loadingSession && chat.status === "Error" && chat.error ? /* @__PURE__ */ jsx13("p", { className: "chat-session-error", children: chat.error }) : null,
1694
1722
  /* @__PURE__ */ jsx13(
1695
1723
  MessageComposer,
1696
1724
  {
@@ -1708,6 +1736,17 @@ function ChatView({
1708
1736
  ] })
1709
1737
  ] });
1710
1738
  }
1739
+ function ChatSessionLoading() {
1740
+ return /* @__PURE__ */ jsx13("div", { className: "chat-transcript-wrap", children: /* @__PURE__ */ jsx13("section", { className: "chat-transcript", "aria-busy": "true", "aria-label": "\u6B63\u5728\u52A0\u8F7D\u4F1A\u8BDD", children: /* @__PURE__ */ jsxs11("div", { className: "chat-column session-loading", children: [
1741
+ /* @__PURE__ */ jsx13("div", { className: "session-loading-title" }),
1742
+ /* @__PURE__ */ jsx13("div", { className: "session-loading-line wide" }),
1743
+ /* @__PURE__ */ jsx13("div", { className: "session-loading-line" }),
1744
+ /* @__PURE__ */ jsxs11("div", { className: "session-loading-card", children: [
1745
+ /* @__PURE__ */ jsx13("div", {}),
1746
+ /* @__PURE__ */ jsx13("div", {})
1747
+ ] })
1748
+ ] }) }) });
1749
+ }
1711
1750
 
1712
1751
  // src/CathyGOChatApp.tsx
1713
1752
  import { jsx as jsx14 } from "react/jsx-runtime";
@@ -1724,6 +1763,9 @@ function CathyGOChatApp({
1724
1763
  gatewayLinked,
1725
1764
  model,
1726
1765
  agentStatus,
1766
+ activeSessionId,
1767
+ loadingSessionId,
1768
+ debugPanels = true,
1727
1769
  homeAvatarSrc,
1728
1770
  onDraftChange,
1729
1771
  onFilesSelected,
@@ -1733,10 +1775,20 @@ function CathyGOChatApp({
1733
1775
  onAbort,
1734
1776
  onSuggest,
1735
1777
  onOpenChat,
1736
- onNewChat
1778
+ onNewChat,
1779
+ onSettingsOpen
1737
1780
  }) {
1738
1781
  if (screen === "history") {
1739
- return /* @__PURE__ */ jsx14(ChatListView, { chats, onOpen: onOpenChat, onNewChat });
1782
+ return /* @__PURE__ */ jsx14(
1783
+ ChatListView,
1784
+ {
1785
+ activeSessionId,
1786
+ chats,
1787
+ loadingSessionId,
1788
+ onOpen: onOpenChat,
1789
+ onNewChat
1790
+ }
1791
+ );
1740
1792
  }
1741
1793
  if (screen === "home") {
1742
1794
  return /* @__PURE__ */ jsx14(
@@ -1752,6 +1804,7 @@ function CathyGOChatApp({
1752
1804
  gatewayLinked,
1753
1805
  model,
1754
1806
  agentStatus,
1807
+ onSettingsOpen,
1755
1808
  onDraftChange,
1756
1809
  onFilesSelected,
1757
1810
  onRemoveAttachment,
@@ -1766,17 +1819,20 @@ function CathyGOChatApp({
1766
1819
  draft,
1767
1820
  attachments,
1768
1821
  attachmentPolicy,
1822
+ busy,
1769
1823
  composerError,
1770
1824
  connectionStatus,
1771
1825
  gatewayLinked,
1772
1826
  model,
1773
1827
  agentStatus,
1828
+ debugPanels,
1774
1829
  onDraftChange,
1775
1830
  onFilesSelected,
1776
1831
  onRemoveAttachment,
1777
1832
  onSend: onSendMessage,
1778
1833
  onAbort,
1779
- onSuggest
1834
+ onSuggest,
1835
+ onSettingsOpen
1780
1836
  }
1781
1837
  );
1782
1838
  }
@@ -1805,19 +1861,46 @@ function reduceChat(state, action) {
1805
1861
  errorCode: void 0
1806
1862
  };
1807
1863
  }
1808
- if (action.type === "session.loaded") {
1864
+ if (action.type === "session.loading") {
1809
1865
  return {
1810
1866
  ...state,
1811
1867
  sessionId: action.sessionId,
1812
1868
  activeTurnId: void 0,
1869
+ messages: [],
1870
+ activitiesByTurnId: {},
1871
+ progressByTurnId: {},
1872
+ status: "Loading",
1873
+ error: void 0,
1874
+ errorCode: void 0
1875
+ };
1876
+ }
1877
+ if (action.type === "session.loaded") {
1878
+ const runtime = action.runtime;
1879
+ const activeTurnId = optionalString(runtime?.active_turn_id);
1880
+ return {
1881
+ ...state,
1882
+ sessionId: action.sessionId,
1883
+ activeTurnId,
1813
1884
  messages: action.messages,
1814
1885
  activitiesByTurnId: groupActivitiesByTurn(action.activities ?? []),
1815
- progressByTurnId: {},
1816
- status: "Ready",
1886
+ progressByTurnId: activeTurnId ? setProgress({}, runtimeProgressFromSnapshot(runtime, activeTurnId)) : {},
1887
+ runtime,
1888
+ status: statusFromRuntime(runtime),
1817
1889
  error: void 0,
1818
1890
  errorCode: void 0
1819
1891
  };
1820
1892
  }
1893
+ if (action.type === "session.load.failed") {
1894
+ return {
1895
+ ...state,
1896
+ sessionId: action.sessionId,
1897
+ activeTurnId: void 0,
1898
+ progressByTurnId: {},
1899
+ status: "Error",
1900
+ error: action.error,
1901
+ errorCode: action.code
1902
+ };
1903
+ }
1821
1904
  if (action.type === "user.sent") {
1822
1905
  return {
1823
1906
  ...state,
@@ -1849,11 +1932,19 @@ function reduceChat(state, action) {
1849
1932
  errorCode: action.code
1850
1933
  };
1851
1934
  }
1852
- if (action.type === "turn.accepted") {
1935
+ if (action.type === "session.input.accepted") {
1853
1936
  return {
1854
1937
  ...state,
1855
1938
  sessionId: action.sessionId,
1856
1939
  activeTurnId: action.turnId,
1940
+ runtime: {
1941
+ ...state.runtime ?? emptyRuntimeSnapshot(),
1942
+ status: "running",
1943
+ active_turn_id: action.turnId,
1944
+ active_turn_ids: [action.turnId],
1945
+ active_client_input_id: action.clientInputId ?? null,
1946
+ active_client_turn_id: action.clientTurnId ?? null
1947
+ },
1857
1948
  progressByTurnId: setProgress(state.progressByTurnId, {
1858
1949
  turnId: action.turnId,
1859
1950
  phase: "queued",
@@ -1873,8 +1964,50 @@ function reduceChat(state, action) {
1873
1964
  ...state,
1874
1965
  eventCount: state.eventCount + 1,
1875
1966
  sessionId: payload.session_id ?? state.sessionId,
1876
- activeTurnId: payload.turn_id ?? state.activeTurnId
1967
+ activeTurnId: payload.turn_id ?? state.activeTurnId,
1968
+ runtime: runtimeFromEvent(state.runtime, event)
1877
1969
  };
1970
+ if (event.event === "session.created") {
1971
+ return {
1972
+ ...next,
1973
+ status: "Ready",
1974
+ error: void 0,
1975
+ errorCode: void 0
1976
+ };
1977
+ }
1978
+ if (event.event === "session.deleted") {
1979
+ return {
1980
+ ...next,
1981
+ activeTurnId: void 0,
1982
+ progressByTurnId: {},
1983
+ status: "Archived"
1984
+ };
1985
+ }
1986
+ if (event.event === "session.input.accepted") {
1987
+ const turnId = optionalString(payload.turn_id);
1988
+ if (!turnId) return next;
1989
+ return {
1990
+ ...next,
1991
+ activeTurnId: turnId,
1992
+ progressByTurnId: setProgress(next.progressByTurnId, {
1993
+ turnId,
1994
+ phase: "queued",
1995
+ status: "running",
1996
+ summary: "\u6B63\u5728\u63D0\u4EA4\u95EE\u9898",
1997
+ detail: "\u7B49\u5F85 CathyGO \u5F00\u59CB\u5904\u7406",
1998
+ startedAt: nowIso()
1999
+ }),
2000
+ status: "Thinking",
2001
+ error: void 0,
2002
+ errorCode: void 0
2003
+ };
2004
+ }
2005
+ if (event.event === "session.stop.requested") {
2006
+ return {
2007
+ ...next,
2008
+ status: "Stopping"
2009
+ };
2010
+ }
1878
2011
  if (event.event === "agent.progress.delta") {
1879
2012
  return {
1880
2013
  ...next,
@@ -1949,6 +2082,7 @@ function reduceChat(state, action) {
1949
2082
  ...next,
1950
2083
  activeTurnId: void 0,
1951
2084
  progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
2085
+ runtime: markRuntimeIdle(next.runtime),
1952
2086
  status: "Ready"
1953
2087
  };
1954
2088
  }
@@ -1957,6 +2091,7 @@ function reduceChat(state, action) {
1957
2091
  ...next,
1958
2092
  activeTurnId: void 0,
1959
2093
  progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
2094
+ runtime: markRuntimeIdle(next.runtime),
1960
2095
  status: "Ready"
1961
2096
  };
1962
2097
  }
@@ -1966,6 +2101,7 @@ function reduceChat(state, action) {
1966
2101
  ...next,
1967
2102
  activeTurnId: void 0,
1968
2103
  progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
2104
+ runtime: markRuntimeIdle(next.runtime),
1969
2105
  status: "Error",
1970
2106
  error: error?.message ?? "CathyGO turn failed",
1971
2107
  errorCode: error?.code
@@ -1988,6 +2124,10 @@ function appendAssistantDelta(messages, messageId, delta, turnId) {
1988
2124
  }
1989
2125
  ];
1990
2126
  }
2127
+ const existing = messages[index];
2128
+ if (existing?.role === "assistant" && existing.status === "done" && existing.content) {
2129
+ return messages;
2130
+ }
1991
2131
  return messages.map(
1992
2132
  (message, itemIndex) => itemIndex === index ? {
1993
2133
  ...message,
@@ -2137,6 +2277,78 @@ function progressFromActivity(progressByTurnId, activity, turnActivities, messag
2137
2277
  }
2138
2278
  return progressByTurnId;
2139
2279
  }
2280
+ function runtimeProgressFromSnapshot(runtime, turnId) {
2281
+ return {
2282
+ turnId,
2283
+ phase: runtime?.status === "stopping" ? "model" : "queued",
2284
+ status: "running",
2285
+ summary: runtime?.status === "stopping" ? "\u6B63\u5728\u505C\u6B62" : "\u6B63\u5728\u6062\u590D\u4F1A\u8BDD\u72B6\u6001",
2286
+ detail: runtime?.active_client_input_id ? `client_input_id=${runtime.active_client_input_id}` : void 0,
2287
+ startedAt: nowIso()
2288
+ };
2289
+ }
2290
+ function runtimeFromEvent(current, event) {
2291
+ const payload = event.payload;
2292
+ const turnId = optionalString(payload.turn_id);
2293
+ const turnIds = eventTurnIds(payload, turnId);
2294
+ const base = {
2295
+ ...current ?? emptyRuntimeSnapshot(),
2296
+ last_event_seq: Math.max(current?.last_event_seq ?? 0, event.seq)
2297
+ };
2298
+ if (event.event === "session.input.accepted" || event.event === "turn.started") {
2299
+ return {
2300
+ ...base,
2301
+ status: "running",
2302
+ active_turn_id: turnId ?? base.active_turn_id ?? null,
2303
+ active_turn_ids: turnIds.length > 0 ? turnIds : base.active_turn_ids ?? [],
2304
+ active_client_input_id: optionalString(payload.client_input_id) ?? base.active_client_input_id ?? null,
2305
+ active_client_turn_id: optionalString(payload.client_turn_id) ?? base.active_client_turn_id ?? null
2306
+ };
2307
+ }
2308
+ if (event.event === "session.stop.requested") {
2309
+ return {
2310
+ ...base,
2311
+ status: "stopping",
2312
+ active_turn_id: turnId ?? base.active_turn_id ?? null,
2313
+ active_turn_ids: turnIds.length > 0 ? turnIds : base.active_turn_ids ?? []
2314
+ };
2315
+ }
2316
+ if (event.event === "turn.completed" || event.event === "turn.cancelled" || event.event === "turn.failed" || event.event === "session.deleted") {
2317
+ return markRuntimeIdle(base);
2318
+ }
2319
+ return base;
2320
+ }
2321
+ function eventTurnIds(payload, turnId) {
2322
+ if (Array.isArray(payload.turn_ids)) {
2323
+ return payload.turn_ids.map((value) => optionalString(value)).filter((value) => Boolean(value));
2324
+ }
2325
+ return turnId ? [turnId] : [];
2326
+ }
2327
+ function emptyRuntimeSnapshot() {
2328
+ return {
2329
+ status: "idle",
2330
+ active_turn_id: null,
2331
+ active_turn_ids: [],
2332
+ active_client_input_id: null,
2333
+ active_client_turn_id: null,
2334
+ last_event_seq: 0
2335
+ };
2336
+ }
2337
+ function markRuntimeIdle(runtime) {
2338
+ return {
2339
+ ...runtime ?? emptyRuntimeSnapshot(),
2340
+ status: "idle",
2341
+ active_turn_id: null,
2342
+ active_turn_ids: [],
2343
+ active_client_input_id: null,
2344
+ active_client_turn_id: null
2345
+ };
2346
+ }
2347
+ function statusFromRuntime(runtime) {
2348
+ if (runtime?.status === "running") return "Thinking";
2349
+ if (runtime?.status === "stopping") return "Stopping";
2350
+ return "Ready";
2351
+ }
2140
2352
  function setProgress(progressByTurnId, progress) {
2141
2353
  return {
2142
2354
  ...progressByTurnId,
@@ -2234,8 +2446,8 @@ function useCathyGOChat(initialState = initialChatState) {
2234
2446
  dispatchChat({ type: "session.created", sessionId });
2235
2447
  }, []);
2236
2448
  const loadSession = useCallback(
2237
- (sessionId, messages, activities) => {
2238
- dispatchChat({ type: "session.loaded", sessionId, messages, activities });
2449
+ (sessionId, messages, activities, runtime) => {
2450
+ dispatchChat({ type: "session.loaded", sessionId, messages, activities, runtime });
2239
2451
  },
2240
2452
  []
2241
2453
  );
package/dist/styles.css CHANGED
@@ -240,6 +240,25 @@
240
240
  padding: 5px 12px;
241
241
  }
242
242
 
243
+ .chat-topbar-icon-btn {
244
+ align-items: center;
245
+ background: transparent;
246
+ border: 1px solid var(--border-subtle);
247
+ border-radius: 50%;
248
+ color: var(--text-muted);
249
+ cursor: pointer;
250
+ display: inline-flex;
251
+ flex-shrink: 0;
252
+ height: 32px;
253
+ justify-content: center;
254
+ width: 32px;
255
+ }
256
+
257
+ .chat-topbar-icon-btn:hover {
258
+ background: var(--bg-hover);
259
+ color: var(--text-primary);
260
+ }
261
+
243
262
  .chat-topbar-abort:hover {
244
263
  background: var(--bg-hover);
245
264
  }
@@ -469,6 +488,68 @@
469
488
  background: var(--bg-hover);
470
489
  }
471
490
 
491
+ .session-loading {
492
+ gap: 14px;
493
+ margin-top: 72px;
494
+ max-width: 40rem;
495
+ }
496
+
497
+ .session-loading-title,
498
+ .session-loading-line,
499
+ .session-loading-card {
500
+ animation: session-loading-pulse 1.2s ease-in-out infinite;
501
+ background: linear-gradient(90deg, #ececf1 0%, #f7f7f8 48%, #ececf1 100%);
502
+ background-size: 200% 100%;
503
+ border-radius: 8px;
504
+ }
505
+
506
+ .session-loading-title {
507
+ height: 28px;
508
+ width: 180px;
509
+ }
510
+
511
+ .session-loading-line {
512
+ height: 16px;
513
+ width: 68%;
514
+ }
515
+
516
+ .session-loading-line.wide {
517
+ width: 92%;
518
+ }
519
+
520
+ .session-loading-card {
521
+ border-radius: 12px;
522
+ display: grid;
523
+ gap: 12px;
524
+ margin-left: auto;
525
+ min-height: 132px;
526
+ padding: 18px;
527
+ width: min(360px, 80%);
528
+ }
529
+
530
+ .session-loading-card div {
531
+ background: rgba(255, 255, 255, 0.65);
532
+ border-radius: 7px;
533
+ }
534
+
535
+ .chat-session-error {
536
+ color: #b42318;
537
+ font-size: 14px;
538
+ margin: 0 auto 8px;
539
+ max-width: var(--chat-column-width);
540
+ padding: 0 20px;
541
+ width: 100%;
542
+ }
543
+
544
+ @keyframes session-loading-pulse {
545
+ 0% {
546
+ background-position: 100% 0;
547
+ }
548
+ 100% {
549
+ background-position: -100% 0;
550
+ }
551
+ }
552
+
472
553
  .empty-chat h2,
473
554
  .empty-state h1 {
474
555
  color: #1f2937;
@@ -1293,10 +1374,19 @@
1293
1374
  width: 100%;
1294
1375
  }
1295
1376
 
1296
- .history-row:hover {
1377
+ .history-row:hover,
1378
+ .history-row.active {
1297
1379
  background: var(--bg-hover);
1298
1380
  }
1299
1381
 
1382
+ .history-row.active {
1383
+ box-shadow: inset 3px 0 0 #2563eb;
1384
+ }
1385
+
1386
+ .history-row.loading small {
1387
+ color: #2563eb;
1388
+ }
1389
+
1300
1390
  .list-surface {
1301
1391
  align-content: start;
1302
1392
  display: grid;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beanx/cathygo-web-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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.1",
20
20
  "@streamdown/math": "^1.0.2",
21
21
  "katex": "^0.16.47",
22
22
  "streamdown": "^2.5.0"