@nextclaw/ui 0.12.24 → 0.12.25

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.
Files changed (206) hide show
  1. package/CHANGELOG.md +68 -29
  2. package/dist/assets/api-DGD9_Bg4.js +15 -0
  3. package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
  4. package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
  5. package/dist/assets/channels-list-page-FJDuPwU6.js +8 -0
  6. package/dist/assets/chat-page-D1fMNBrT.js +1 -0
  7. package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
  8. package/dist/assets/cpu-DPPwMzoC.js +3 -0
  9. package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
  10. package/dist/assets/desktop-kk7qvZ-v.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-CPlbUgwU.js → dist-CFiwgaLs.js} +1 -1
  14. package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
  15. package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
  16. package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
  17. package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
  18. package/dist/assets/download-CMM8po31.js +1 -0
  19. package/dist/assets/{es2015-xqN1slyW.js → es2015-BhznEEyJ.js} +1 -1
  20. package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
  21. package/dist/assets/i18n-D1144VAA.js +1 -0
  22. package/dist/assets/index-D-AAMKCt.js +103 -0
  23. package/dist/assets/index-DnBeV2Xm.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BrCLRIc4.js +105 -0
  26. package/dist/assets/marketplace-page-odDpPYEs.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DIq_SpMe.js +40 -0
  29. package/dist/assets/model-config-Bc6VVnxy.js +1 -0
  30. package/dist/assets/{notice-card-BFDbKQDA.js → notice-card-Dr6xCwva.js} +1 -1
  31. package/dist/assets/play-AqrNslHI.js +1 -0
  32. package/dist/assets/plus-B-YHtTNC.js +1 -0
  33. package/dist/assets/{popover-B86Dbfhf.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DN0tvISH.js +1 -0
  36. package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
  37. package/dist/assets/remote-Dr3jcfWP.js +1 -0
  38. package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
  39. package/dist/assets/runtime-config-page-CRWOwBbl.js +1 -0
  40. package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
  41. package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
  42. package/dist/assets/search-config-C4c1yZSP.js +1 -0
  43. package/dist/assets/secrets-config-zAF30YfO.js +3 -0
  44. package/dist/assets/{select-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-D1Yygqp7.js → setting-row-BavcnXw1.js} +1 -1
  46. package/dist/assets/settings-MWL2SMyk.js +1 -0
  47. package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
  48. package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
  49. package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
  50. package/dist/assets/{tag-chip-FrkmkT8r.js → tag-chip-Dm2Lqnpu.js} +1 -1
  51. package/dist/assets/use-config-Cyv5IuSt.js +1 -0
  52. package/dist/assets/use-infinite-scroll-loader-Cvz8ZteY.js +1 -0
  53. package/dist/assets/x-BeyYA_h6.js +1 -0
  54. package/dist/index.html +29 -40
  55. package/package.json +9 -9
  56. package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
  57. package/src/app/components/theme-provider.tsx +1 -0
  58. package/src/app/configs/app-navigation.config.ts +0 -6
  59. package/src/app/index.tsx +4 -7
  60. package/src/features/agents/components/agents-page.test.tsx +25 -15
  61. package/src/features/agents/components/agents-page.tsx +133 -172
  62. package/src/features/channels/components/config/channel-form.test.tsx +1 -0
  63. package/src/features/channels/components/config/channel-form.tsx +4 -3
  64. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
  65. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
  66. package/src/features/channels/index.ts +1 -1
  67. package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
  68. package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
  69. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
  70. package/src/features/chat/components/chat-session-workspace-panel.tsx +34 -2
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +9 -3
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +71 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +6 -0
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +181 -61
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +54 -23
  76. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +26 -5
  78. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  79. package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
  81. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  82. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  83. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +14 -0
  84. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  85. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
  86. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  87. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  88. package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
  89. package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
  90. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +0 -2
  91. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  92. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  93. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  94. package/src/features/chat/pages/ncp-chat-page.tsx +5 -4
  95. package/src/features/chat/stores/chat-session-list.store.ts +0 -2
  96. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  97. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  98. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  99. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  100. package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
  101. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  102. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  103. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  104. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  105. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  106. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  107. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  108. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  109. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  110. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  111. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  112. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  113. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  114. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  115. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  116. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  117. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  118. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  119. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  120. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  121. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  122. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  123. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  124. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  125. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  126. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  127. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  128. package/src/index.css +8 -0
  129. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  130. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  131. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  132. package/src/platforms/desktop/index.ts +6 -0
  133. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  134. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  135. package/src/shared/components/common/brand-header.tsx +36 -16
  136. package/src/shared/components/config/provider-form-support.ts +2 -22
  137. package/src/shared/components/cron-config.tsx +12 -58
  138. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  139. package/src/shared/components/ui/select.tsx +19 -7
  140. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  141. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  142. package/src/shared/lib/api/types.ts +12 -1
  143. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  144. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  145. package/src/shared/lib/cron/index.ts +1 -0
  146. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  147. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  148. package/src/shared/lib/i18n/index.ts +20 -59
  149. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  150. package/src/shared/lib/provider-models/index.test.ts +39 -0
  151. package/src/shared/lib/provider-models/index.ts +1 -3
  152. package/src/shared/lib/ui-document-title/index.ts +0 -1
  153. package/tsconfig.json +1 -0
  154. package/vite.config.ts +1 -1
  155. package/vitest.config.ts +1 -1
  156. package/dist/assets/api-D2xRKmZd.js +0 -15
  157. package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
  158. package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
  159. package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
  160. package/dist/assets/chat-Dkh2qtuz.js +0 -61
  161. package/dist/assets/chat-page-DoTmE2wx.js +0 -1
  162. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  163. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  164. package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
  165. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  166. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  167. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  168. package/dist/assets/folder-CeJKPx5P.js +0 -1
  169. package/dist/assets/hash-BqxRTZW5.js +0 -1
  170. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  171. package/dist/assets/index-D8MKmXtO.css +0 -1
  172. package/dist/assets/index-pBvbJ5Mt.js +0 -2
  173. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  174. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  175. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  176. package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
  177. package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
  178. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
  180. package/dist/assets/message-square-z_osm9c0.js +0 -1
  181. package/dist/assets/model-config-Dbr_0APb.js +0 -1
  182. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  183. package/dist/assets/plus-D8eKFY7h.js +0 -1
  184. package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
  185. package/dist/assets/providers-list-BJcLOjun.js +0 -1
  186. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  187. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  188. package/dist/assets/remote-BOxo9iwd.js +0 -1
  189. package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
  190. package/dist/assets/search-config-J4Htco-P.js +0 -1
  191. package/dist/assets/secrets-config-CUdERjco.js +0 -3
  192. package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
  193. package/dist/assets/settings-drbWqzA4.js +0 -1
  194. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  195. package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
  196. package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
  197. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  198. package/dist/assets/use-config-38Ur-89i.js +0 -1
  199. package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
  200. package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
  201. package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
  202. package/dist/assets/x-CM-XDMpk.js +0 -1
  203. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  204. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  205. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  206. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -0,0 +1,70 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { useNcpChatPageData } from './use-ncp-chat-page-data';
4
+
5
+ const useNcpSessionSkillsMock = vi.fn();
6
+
7
+ vi.mock('@/shared/hooks/use-config', () => ({
8
+ useConfig: () => ({
9
+ data: {
10
+ agents: { defaults: {} },
11
+ providers: {}
12
+ },
13
+ isFetched: true,
14
+ isSuccess: true
15
+ }),
16
+ useConfigMeta: () => ({
17
+ data: { providers: [] },
18
+ isFetched: true,
19
+ isSuccess: true
20
+ }),
21
+ useNcpSessions: () => ({
22
+ data: { sessions: [] }
23
+ }),
24
+ useNcpSessionSkills: (params: unknown) => useNcpSessionSkillsMock(params)
25
+ }));
26
+
27
+ vi.mock('./use-ncp-chat-session-types', () => ({
28
+ useNcpChatSessionTypes: () => ({
29
+ data: {
30
+ defaultType: 'native',
31
+ options: [{ value: 'native', label: 'Native' }]
32
+ }
33
+ })
34
+ }));
35
+
36
+ function renderPageData(params: { sessionKey: string | null }) {
37
+ return renderHook(() =>
38
+ useNcpChatPageData({
39
+ sessionKey: params.sessionKey,
40
+ query: '',
41
+ currentSelectedModel: '',
42
+ pendingSessionType: '',
43
+ setPendingSessionType: vi.fn(),
44
+ setSelectedModel: vi.fn(),
45
+ setSelectedThinkingLevel: vi.fn()
46
+ })
47
+ );
48
+ }
49
+
50
+ describe('useNcpChatPageData skills query', () => {
51
+ it('loads draft-session skills before a new chat materializes', () => {
52
+ useNcpSessionSkillsMock.mockReturnValue({ data: { records: [] }, isLoading: false });
53
+
54
+ renderPageData({ sessionKey: null });
55
+
56
+ expect(useNcpSessionSkillsMock).toHaveBeenCalledWith({
57
+ sessionId: 'draft-session'
58
+ });
59
+ });
60
+
61
+ it('loads real session skills after materialization', () => {
62
+ useNcpSessionSkillsMock.mockReturnValue({ data: { records: [] }, isLoading: false });
63
+
64
+ renderPageData({ sessionKey: 'session-1' });
65
+
66
+ expect(useNcpSessionSkillsMock).toHaveBeenCalledWith({
67
+ sessionId: 'session-1'
68
+ });
69
+ });
70
+ });
@@ -158,7 +158,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
158
158
  const sessionsQuery = useNcpSessions({ limit: 200 });
159
159
  const sessionTypesQuery = useNcpChatSessionTypes();
160
160
  const sessionSkillsQuery = useNcpSessionSkills({
161
- sessionId: sessionKey ?? null,
161
+ sessionId: sessionKey?.trim() || 'draft-session',
162
162
  ...(Object.prototype.hasOwnProperty.call(params, 'projectRootOverride')
163
163
  ? { projectRoot: projectRootOverride ?? null }
164
164
  : {})
@@ -49,16 +49,10 @@ export function useNcpChildSessionTabsView(
49
49
  return new Map(sessions.map((session) => [session.key, session]));
50
50
  }, [summaries]);
51
51
 
52
- const summaryByKey = useMemo(
53
- () => new Map(summaries.map((summary) => [summary.sessionId, summary])),
54
- [summaries],
55
- );
56
-
57
52
  return useMemo(
58
53
  () =>
59
54
  tabs.map((tab) => {
60
55
  const session = sessionByKey.get(tab.sessionKey) ?? null;
61
- const summary = summaryByKey.get(tab.sessionKey) ?? null;
62
56
  const agentId = tab.agentId?.trim() || session?.agentId || null;
63
57
  return {
64
58
  sessionKey: tab.sessionKey,
@@ -68,7 +62,7 @@ export function useNcpChildSessionTabsView(
68
62
  updatedAt: session?.updatedAt ?? null,
69
63
  lastMessageAt: session?.lastMessageAt ?? null,
70
64
  readAt: session?.readAt ?? null,
71
- runStatus: summary?.status === "running" ? "running" : undefined,
65
+ runStatus: session?.status === "running" ? "running" : undefined,
72
66
  sessionTypeLabel: session?.sessionType
73
67
  ? resolveSessionTypeLabel(session.sessionType)
74
68
  : null,
@@ -77,6 +71,6 @@ export function useNcpChildSessionTabsView(
77
71
  projectRoot: session?.projectRoot?.trim() || null,
78
72
  };
79
73
  }),
80
- [sessionByKey, summaryByKey, tabs],
74
+ [sessionByKey, tabs],
81
75
  );
82
76
  }
@@ -32,11 +32,10 @@ export function useNcpSessionListView(params: { limit?: number } = {}) {
32
32
  shouldShowSessionInSidebar,
33
33
  );
34
34
  const filteredSessions = filterSessionsByQuery(sessions, query);
35
- const summaryBySessionId = new Map(summaries.map((summary) => [summary.sessionId, summary]));
36
35
 
37
36
  return filteredSessions.map((session) => ({
38
37
  session,
39
- runStatus: summaryBySessionId.get(session.key)?.status === 'running' ? 'running' : undefined
38
+ runStatus: session.status === 'running' ? 'running' : undefined
40
39
  }));
41
40
  }, [query, sessionsQuery.data?.sessions]);
42
41
 
@@ -35,7 +35,6 @@ describe('ChatSessionListManager', () => {
35
35
  snapshot: {
36
36
  ...useChatSessionListStore.getState().snapshot,
37
37
  selectedSessionKey: 'session-1',
38
- draftSessionKey: 'draft-root-1',
39
38
  listMode: 'time-first'
40
39
  }
41
40
  });
@@ -65,8 +64,8 @@ describe('ChatSessionListManager', () => {
65
64
  expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
66
65
  expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
67
66
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
68
- expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
69
67
  expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
68
+ expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
70
69
  expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
71
70
  expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
72
71
  expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
@@ -89,8 +88,8 @@ describe('ChatSessionListManager', () => {
89
88
  expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
90
89
  expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe('researcher');
91
90
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
92
- expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
93
91
  expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
92
+ expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
94
93
  expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
95
94
  expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
96
95
  expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
@@ -117,8 +116,7 @@ describe('ChatSessionListManager', () => {
117
116
  useChatSessionListStore.setState({
118
117
  snapshot: {
119
118
  ...useChatSessionListStore.getState().snapshot,
120
- selectedSessionKey: null,
121
- draftSessionKey: 'draft-root-2'
119
+ selectedSessionKey: null
122
120
  }
123
121
  });
124
122
  const uiManager = {
@@ -137,6 +135,7 @@ describe('ChatSessionListManager', () => {
137
135
  expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
138
136
  expect(uiManager.goToSession).not.toHaveBeenCalled();
139
137
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
138
+ expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(true);
140
139
  });
141
140
 
142
141
  it('does not eagerly replace the old selected session before the route finishes switching', () => {
@@ -225,12 +224,11 @@ describe('ChatSessionListManager', () => {
225
224
  expect(mocks.updateNcpSession).not.toHaveBeenCalled();
226
225
  });
227
226
 
228
- it('routes to the backend-materialized root session after the first send starts', () => {
227
+ it('routes to the backend-materialized root session without duplicating route-owned selection state', () => {
229
228
  useChatSessionListStore.setState({
230
229
  snapshot: {
231
230
  ...useChatSessionListStore.getState().snapshot,
232
231
  selectedSessionKey: null,
233
- draftSessionKey: 'draft-root-2',
234
232
  }
235
233
  });
236
234
  const uiManager = {
@@ -246,8 +244,8 @@ describe('ChatSessionListManager', () => {
246
244
  manager.ensureDraftSession('native');
247
245
  manager.materializeRootSessionRoute('ncp-materialized-session');
248
246
 
249
- expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('ncp-materialized-session');
250
- expect(useChatThreadStore.getState().snapshot.sessionKey).toBe('ncp-materialized-session');
247
+ expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
248
+ expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
251
249
  expect(uiManager.goToSession).toHaveBeenCalledWith('ncp-materialized-session', { replace: true });
252
250
  });
253
251
  });
@@ -12,7 +12,7 @@ export class ChatSessionListManager {
12
12
  private streamActionsManager: ChatStreamActionsManager
13
13
  ) {}
14
14
 
15
- private syncDraftThreadState = () => {
15
+ private syncDraftThreadState = (hasSubmittedDraftMessage = false) => {
16
16
  useChatThreadStore.getState().setSnapshot({
17
17
  sessionKey: null,
18
18
  sessionDisplayName: undefined,
@@ -21,9 +21,11 @@ export class ChatSessionListManager {
21
21
  messages: [],
22
22
  isSending: false,
23
23
  isAwaitingAssistantOutput: false,
24
+ hasSubmittedDraftMessage,
24
25
  parentSessionKey: null,
25
26
  parentSessionLabel: null,
26
27
  workspacePanelParentKey: null,
28
+ activeWorkspacePanelKind: null,
27
29
  childSessionTabs: [],
28
30
  activeChildSessionKey: null,
29
31
  activeWorkspaceFileKey: null,
@@ -108,7 +110,6 @@ export class ChatSessionListManager {
108
110
  this.streamActionsManager.resetStreamState();
109
111
  useChatSessionListStore.getState().setSnapshot({
110
112
  selectedSessionKey: null,
111
- draftSessionKey: null
112
113
  });
113
114
  this.syncDraftThreadState();
114
115
  useChatInputStore.getState().setSnapshot({
@@ -134,7 +135,7 @@ export class ChatSessionListManager {
134
135
  typeof sessionType === 'string' && sessionType.trim().length > 0
135
136
  ? sessionType.trim()
136
137
  : null;
137
- this.syncDraftThreadState();
138
+ this.syncDraftThreadState(true);
138
139
  if (normalizedSessionType) {
139
140
  useChatInputStore.getState().setSnapshot({ pendingSessionType: normalizedSessionType });
140
141
  }
@@ -149,19 +150,13 @@ export class ChatSessionListManager {
149
150
  if (!this.uiManager.isAtChatRoot()) {
150
151
  return;
151
152
  }
152
- useChatSessionListStore.getState().setSnapshot({
153
- selectedSessionKey: normalizedSessionKey,
154
- draftSessionKey: null,
155
- });
156
- useChatThreadStore.getState().setSnapshot({
157
- sessionKey: normalizedSessionKey,
158
- });
159
153
  this.uiManager.goToSession(normalizedSessionKey, { replace: true });
160
154
  };
161
155
 
162
156
  selectSession = (sessionKey: string) => {
163
157
  useChatThreadStore.getState().setSnapshot({
164
158
  workspacePanelParentKey: null,
159
+ activeWorkspacePanelKind: null,
165
160
  activeChildSessionKey: null,
166
161
  activeWorkspaceFileKey: null,
167
162
  });
@@ -58,7 +58,6 @@ describe('NcpChatInputManager', () => {
58
58
  snapshot: {
59
59
  ...useChatSessionListStore.getState().snapshot,
60
60
  selectedSessionKey: 'stale-selected-session',
61
- draftSessionKey: 'draft-root-session',
62
61
  selectedAgentId: 'main',
63
62
  },
64
63
  });
@@ -109,7 +108,6 @@ describe('NcpChatInputManager', () => {
109
108
  snapshot: {
110
109
  ...useChatSessionListStore.getState().snapshot,
111
110
  selectedSessionKey: null,
112
- draftSessionKey: null,
113
111
  },
114
112
  });
115
113
  const streamActionsManager = {
@@ -18,4 +18,10 @@ export class NcpChatPresenter {
18
18
  this.chatSessionListManager,
19
19
  this.chatStreamActionsManager
20
20
  );
21
+
22
+ startAgentCreationDraft = (prompt: string) => {
23
+ this.chatSessionListManager.createSession();
24
+ this.chatSessionListManager.setSelectedAgentId('main');
25
+ this.chatInputManager.setDraft(prompt);
26
+ };
21
27
  }
@@ -3,6 +3,7 @@ import { appQueryClient } from '@/app-query-client';
3
3
  import { NcpChatThreadManager } from '@/features/chat/managers/ncp-chat-thread.manager';
4
4
  import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
5
5
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
6
+ import type * as SharedApi from '@/shared/lib/api';
6
7
 
7
8
  const { deleteNcpSessionMock, deleteSummaryMock } = vi.hoisted(() => ({
8
9
  deleteNcpSessionMock: vi.fn(async () => ({ deleted: true, sessionId: 'parent-session-1' })),
@@ -10,7 +11,7 @@ const { deleteNcpSessionMock, deleteSummaryMock } = vi.hoisted(() => ({
10
11
  }));
11
12
 
12
13
  vi.mock('@/shared/lib/api', async (importOriginal) => {
13
- const actual = await importOriginal<typeof import('@/shared/lib/api')>();
14
+ const actual = await importOriginal<typeof SharedApi>();
14
15
  return {
15
16
  ...actual,
16
17
  deleteNcpSession: deleteNcpSessionMock,
@@ -97,6 +98,56 @@ describe('NcpChatThreadManager', () => {
97
98
  expect(uiManager.goToSession).toHaveBeenCalledWith('parent-session-1');
98
99
  });
99
100
 
101
+ it('opens the session cron panel without changing route when already selected', () => {
102
+ const uiManager = {
103
+ goToSession: vi.fn(),
104
+ goToChatRoot: vi.fn(),
105
+ goToProviders: vi.fn(),
106
+ confirm: vi.fn(),
107
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
108
+
109
+ const manager = new NcpChatThreadManager(
110
+ uiManager,
111
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[1],
112
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[2],
113
+ );
114
+
115
+ manager.openSessionCronPanel('parent-session-1');
116
+
117
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
118
+ workspacePanelParentKey: 'parent-session-1',
119
+ activeWorkspacePanelKind: 'cron',
120
+ activeChildSessionKey: null,
121
+ activeWorkspaceFileKey: null,
122
+ });
123
+ expect(uiManager.goToSession).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it('routes to the session before opening its cron panel when needed', () => {
127
+ useChatSessionListStore.setState({
128
+ snapshot: {
129
+ ...useChatSessionListStore.getState().snapshot,
130
+ selectedSessionKey: 'another-session',
131
+ },
132
+ });
133
+ const uiManager = {
134
+ goToSession: vi.fn(),
135
+ goToChatRoot: vi.fn(),
136
+ goToProviders: vi.fn(),
137
+ confirm: vi.fn(),
138
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
139
+
140
+ const manager = new NcpChatThreadManager(
141
+ uiManager,
142
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[1],
143
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[2],
144
+ );
145
+
146
+ manager.openSessionCronPanel('parent-session-1');
147
+
148
+ expect(uiManager.goToSession).toHaveBeenCalledWith('parent-session-1');
149
+ });
150
+
100
151
  it('keeps preview and diff for the same file as separate workspace tabs', () => {
101
152
  const uiManager = {
102
153
  goToSession: vi.fn(),
@@ -62,6 +62,7 @@ export class NcpChatThreadManager {
62
62
  parentSessionKey: null,
63
63
  parentSessionLabel: null,
64
64
  workspacePanelParentKey: null,
65
+ activeWorkspacePanelKind: null,
65
66
  childSessionTabs: [],
66
67
  activeChildSessionKey: null,
67
68
  workspaceFileTabs: [],
@@ -156,6 +157,7 @@ export class NcpChatThreadManager {
156
157
  const activeChildSessionKey = params.activeChildSessionKey?.trim() || null;
157
158
  useChatThreadStore.getState().setSnapshot({
158
159
  workspacePanelParentKey: parentSessionKey,
160
+ activeWorkspacePanelKind: 'child-session',
159
161
  activeChildSessionKey,
160
162
  activeWorkspaceFileKey: null,
161
163
  });
@@ -170,6 +172,7 @@ export class NcpChatThreadManager {
170
172
  }
171
173
  useChatThreadStore.getState().setSnapshot({
172
174
  workspacePanelParentKey: parentSessionKey,
175
+ activeWorkspacePanelKind: 'file',
173
176
  workspaceFileTabs: this.upsertWorkspaceFileTab(nextTab),
174
177
  activeWorkspaceFileKey: nextTab.key,
175
178
  activeChildSessionKey: null,
@@ -196,6 +199,7 @@ export class NcpChatThreadManager {
196
199
  }
197
200
  useChatThreadStore.getState().setSnapshot({
198
201
  workspacePanelParentKey: null,
202
+ activeWorkspacePanelKind: null,
199
203
  activeChildSessionKey: null,
200
204
  activeWorkspaceFileKey: null,
201
205
  });
@@ -214,6 +218,7 @@ export class NcpChatThreadManager {
214
218
  useChatThreadStore.getState().setSnapshot({
215
219
  activeChildSessionKey: normalizedSessionKey,
216
220
  activeWorkspaceFileKey: null,
221
+ activeWorkspacePanelKind: 'child-session',
217
222
  });
218
223
  };
219
224
 
@@ -229,6 +234,7 @@ export class NcpChatThreadManager {
229
234
  useChatThreadStore.getState().setSnapshot({
230
235
  activeWorkspaceFileKey: normalizedFileKey,
231
236
  activeChildSessionKey: null,
237
+ activeWorkspacePanelKind: 'file',
232
238
  });
233
239
  };
234
240
 
@@ -254,11 +260,26 @@ export class NcpChatThreadManager {
254
260
  closeWorkspacePanel = () => {
255
261
  useChatThreadStore.getState().setSnapshot({
256
262
  workspacePanelParentKey: null,
263
+ activeWorkspacePanelKind: null,
257
264
  activeChildSessionKey: null,
258
265
  activeWorkspaceFileKey: null,
259
266
  });
260
267
  };
261
268
 
269
+ openSessionCronPanel = (sessionKey: string) => {
270
+ const parentSessionKey = sessionKey.trim();
271
+ if (!parentSessionKey) {
272
+ return;
273
+ }
274
+ useChatThreadStore.getState().setSnapshot({
275
+ workspacePanelParentKey: parentSessionKey,
276
+ activeWorkspacePanelKind: 'cron',
277
+ activeChildSessionKey: null,
278
+ activeWorkspaceFileKey: null,
279
+ });
280
+ this.ensureWorkspaceParentRoute(parentSessionKey);
281
+ };
282
+
262
283
  closeChildSessionDetail = () => {
263
284
  this.closeWorkspacePanel();
264
285
  };
@@ -219,15 +219,16 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
219
219
  selectedSessionType,
220
220
  sessionTypeOptions,
221
221
  });
222
+ const currentSessionRunning = agent.isRunning || selectedSession?.status === "running";
222
223
  return {
223
224
  ...baseState,
224
225
  availableAgents,
225
226
  effectiveSessionProjectRoot,
226
227
  effectiveSessionProjectName,
227
- isSending: agent.isSending || agent.isRunning,
228
- isAwaitingAssistantOutput: agent.isRunning,
229
- canStopCurrentRun: agent.isRunning,
230
- stopDisabledReason: agent.isRunning ? null : "__preparing__",
228
+ isSending: agent.isSending || currentSessionRunning,
229
+ isAwaitingAssistantOutput: currentSessionRunning,
230
+ canStopCurrentRun: currentSessionRunning,
231
+ stopDisabledReason: currentSessionRunning ? null : "__preparing__",
231
232
  lastSendError:
232
233
  isRuntimeBlocked
233
234
  ? null
@@ -3,7 +3,6 @@ import type { SessionRunStatus } from '@/features/chat/types/session-run-status.
3
3
  export type ChatSessionListMode = 'time-first' | 'project-first';
4
4
  export type ChatSessionListSnapshot = {
5
5
  selectedSessionKey: string | null;
6
- draftSessionKey: string | null;
7
6
  selectedAgentId: string;
8
7
  query: string;
9
8
  listMode: ChatSessionListMode;
@@ -49,7 +48,6 @@ type ChatSessionListStoreSet = Parameters<StateCreator<ChatSessionListStore>>[0]
49
48
 
50
49
  const initialSnapshot: ChatSessionListSnapshot = {
51
50
  selectedSessionKey: null,
52
- draftSessionKey: null,
53
51
  selectedAgentId: 'main',
54
52
  query: '',
55
53
  listMode: 'time-first'
@@ -55,9 +55,11 @@ export type ChatThreadSnapshot = {
55
55
  messages: readonly NcpMessage[];
56
56
  isSending: boolean;
57
57
  isAwaitingAssistantOutput: boolean;
58
+ hasSubmittedDraftMessage: boolean;
58
59
  parentSessionKey?: string | null;
59
60
  parentSessionLabel?: string | null;
60
61
  workspacePanelParentKey?: string | null;
62
+ activeWorkspacePanelKind?: "child-session" | "file" | "cron" | null;
61
63
  childSessionTabs: ChatChildSessionTab[];
62
64
  activeChildSessionKey?: string | null;
63
65
  workspaceFileTabs: ChatWorkspaceFileTab[];
@@ -92,9 +94,11 @@ const initialSnapshot: ChatThreadSnapshot = {
92
94
  messages: [],
93
95
  isSending: false,
94
96
  isAwaitingAssistantOutput: false,
97
+ hasSubmittedDraftMessage: false,
95
98
  parentSessionKey: null,
96
99
  parentSessionLabel: null,
97
100
  workspacePanelParentKey: null,
101
+ activeWorkspacePanelKind: null,
98
102
  childSessionTabs: [],
99
103
  activeChildSessionKey: null,
100
104
  workspaceFileTabs: [],
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type { SessionEntryView } from '@/shared/lib/api';
3
- import { sessionDisplayName, sessionMatchesQuery } from './chat-session-display.utils';
3
+ import {
4
+ formatSessionListTime,
5
+ sessionActivityPreviewText,
6
+ sessionDisplayName,
7
+ sessionMatchesQuery
8
+ } from './chat-session-display.utils';
4
9
 
5
10
  function createSession(overrides: Partial<SessionEntryView> = {}): SessionEntryView {
6
11
  return {
@@ -52,4 +57,81 @@ describe('chat-session-display', () => {
52
57
  it('treats an empty query as a match', () => {
53
58
  expect(sessionMatchesQuery(createSession({ label: 'Anything' }), ' ')).toBe(true);
54
59
  });
60
+
61
+ it('shows running activity before the previous reply preview', () => {
62
+ expect(
63
+ sessionActivityPreviewText(
64
+ createSession({
65
+ activityPreview: {
66
+ state: 'running',
67
+ statusText: '正在调用工具:shell',
68
+ replyText: '之前的回复',
69
+ timestamp: '2026-05-16T01:00:00.000Z'
70
+ }
71
+ })
72
+ )
73
+ ).toBe('正在调用工具:shell');
74
+ });
75
+
76
+ it('shows the final assistant reply after completion', () => {
77
+ expect(
78
+ sessionActivityPreviewText(
79
+ createSession({
80
+ activityPreview: {
81
+ state: 'completed',
82
+ statusText: '工具调用完成',
83
+ replyText: '最终回复内容',
84
+ timestamp: '2026-05-16T01:00:00.000Z'
85
+ }
86
+ })
87
+ )
88
+ ).toBe('最终回复内容');
89
+ });
90
+
91
+ it('formats today activity as time like WeChat session lists', () => {
92
+ expect(
93
+ formatSessionListTime(
94
+ new Date(2026, 4, 16, 9, 5),
95
+ 'zh',
96
+ new Date(2026, 4, 16, 12, 0)
97
+ )
98
+ ).toBe('09:05');
99
+ });
100
+
101
+ it('formats yesterday activity as yesterday like WeChat session lists', () => {
102
+ expect(
103
+ formatSessionListTime(
104
+ new Date(2026, 4, 15, 23, 30),
105
+ 'zh',
106
+ new Date(2026, 4, 16, 12, 0)
107
+ )
108
+ ).toBe('昨天');
109
+ });
110
+
111
+ it('formats recent activity within a week as weekday', () => {
112
+ expect(
113
+ formatSessionListTime(
114
+ new Date(2026, 4, 12, 18, 30),
115
+ 'zh',
116
+ new Date(2026, 4, 16, 12, 0)
117
+ )
118
+ ).toBe('星期二');
119
+ });
120
+
121
+ it('formats older activity as date and keeps year only across years', () => {
122
+ expect(
123
+ formatSessionListTime(
124
+ new Date(2026, 4, 1, 18, 30),
125
+ 'zh',
126
+ new Date(2026, 4, 16, 12, 0)
127
+ )
128
+ ).toBe('5月1日');
129
+ expect(
130
+ formatSessionListTime(
131
+ new Date(2025, 11, 31, 18, 30),
132
+ 'zh',
133
+ new Date(2026, 4, 16, 12, 0)
134
+ )
135
+ ).toBe('2025年12月31日');
136
+ });
55
137
  });
@@ -1,4 +1,5 @@
1
1
  import type { SessionEntryView } from '@/shared/lib/api';
2
+ import { getLanguage, getLocale, t, type I18nLanguage } from '@/shared/lib/i18n';
2
3
 
3
4
  export function sessionDisplayName(session: SessionEntryView): string {
4
5
  const label = session.label?.trim();
@@ -9,6 +10,78 @@ export function sessionDisplayName(session: SessionEntryView): string {
9
10
  return chunks[chunks.length - 1] || session.key;
10
11
  }
11
12
 
13
+ export function sessionActivityPreviewText(session: SessionEntryView): string | null {
14
+ const preview = session.activityPreview;
15
+ if (!preview) {
16
+ return null;
17
+ }
18
+ if (preview.state === 'failed' || preview.state === 'running') {
19
+ return preview.statusText ?? preview.replyText ?? null;
20
+ }
21
+ if (preview.state === 'completed') {
22
+ return preview.replyText ?? preview.statusText ?? null;
23
+ }
24
+ return preview.statusText ?? preview.replyText ?? null;
25
+ }
26
+
27
+ function startOfLocalDate(date: Date): number {
28
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
29
+ }
30
+
31
+ function isSameLocalYear(left: Date, right: Date): boolean {
32
+ return left.getFullYear() === right.getFullYear();
33
+ }
34
+
35
+ function formatChineseSessionDate(date: Date, now: Date): string {
36
+ const monthDay = `${date.getMonth() + 1}月${date.getDate()}日`;
37
+ return isSameLocalYear(date, now) ? monthDay : `${date.getFullYear()}年${monthDay}`;
38
+ }
39
+
40
+ export function formatSessionListTime(
41
+ value?: string | Date,
42
+ lang: I18nLanguage = getLanguage(),
43
+ now: Date = new Date()
44
+ ): string {
45
+ if (!value) {
46
+ return '-';
47
+ }
48
+
49
+ const date = value instanceof Date ? value : new Date(value);
50
+ if (Number.isNaN(date.getTime())) {
51
+ return typeof value === 'string' ? value : '-';
52
+ }
53
+
54
+ const locale = getLocale(lang);
55
+ const dateStart = startOfLocalDate(date);
56
+ const todayStart = startOfLocalDate(now);
57
+ const daysAgo = Math.floor((todayStart - dateStart) / 86_400_000);
58
+
59
+ if (daysAgo === 0) {
60
+ return new Intl.DateTimeFormat(locale, {
61
+ hour: '2-digit',
62
+ minute: '2-digit',
63
+ hour12: false
64
+ }).format(date);
65
+ }
66
+
67
+ if (daysAgo === 1) {
68
+ return t('chatSidebarYesterday', lang);
69
+ }
70
+
71
+ if (daysAgo > 1 && daysAgo < 7) {
72
+ return new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(date);
73
+ }
74
+
75
+ if (lang === 'zh') {
76
+ return formatChineseSessionDate(date, now);
77
+ }
78
+
79
+ const options: Intl.DateTimeFormatOptions = isSameLocalYear(date, now)
80
+ ? { month: 'numeric', day: 'numeric' }
81
+ : { year: 'numeric', month: 'numeric', day: 'numeric' };
82
+ return new Intl.DateTimeFormat(locale, options).format(date);
83
+ }
84
+
12
85
  function normalizeSessionSearchValue(value: string): string {
13
86
  return value.trim().toLowerCase();
14
87
  }