@nextclaw/ui 0.12.23 → 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 (215) hide show
  1. package/CHANGELOG.md +136 -0
  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-dxsKz7jJ.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-DsYTOyq7.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-V75WQJ2s.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-D1RNsTn_.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-BMyiifTA.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-DTdzR8j8.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-CvKngoNI.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-BywQeHJj.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 +56 -25
  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 +119 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +57 -75
  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-hydrated-ncp-agent.test.tsx +6 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +172 -69
  85. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  87. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
  88. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  89. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
  90. package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
  91. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  92. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
  93. package/src/features/chat/managers/chat-session-list.manager.test.ts +21 -20
  94. package/src/features/chat/managers/chat-session-list.manager.ts +15 -24
  95. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +22 -13
  96. package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
  97. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  98. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  99. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  100. package/src/features/chat/pages/ncp-chat-page.tsx +28 -17
  101. package/src/features/chat/stores/chat-session-list.store.ts +0 -3
  102. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  103. package/src/features/chat/types/chat-stream.types.ts +1 -1
  104. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  105. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  106. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  107. package/src/features/chat/utils/ncp-session-adapter.utils.ts +33 -1
  108. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  109. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  110. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  112. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  113. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  114. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  116. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  117. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  118. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  119. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  120. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  121. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  122. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  123. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  124. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  125. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  126. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  127. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  128. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  129. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  130. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  131. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  132. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  133. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  134. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  135. package/src/index.css +8 -0
  136. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  137. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  138. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  139. package/src/platforms/desktop/index.ts +6 -0
  140. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  141. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  142. package/src/shared/components/common/brand-header.tsx +36 -16
  143. package/src/shared/components/config/provider-form-support.ts +2 -22
  144. package/src/shared/components/cron-config.tsx +12 -58
  145. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  146. package/src/shared/components/ui/select.tsx +19 -7
  147. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  148. package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
  149. package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
  150. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  151. package/src/shared/lib/api/types.ts +12 -1
  152. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  153. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  154. package/src/shared/lib/cron/index.ts +1 -0
  155. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  156. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  157. package/src/shared/lib/i18n/index.ts +20 -59
  158. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  159. package/src/shared/lib/provider-models/index.test.ts +39 -0
  160. package/src/shared/lib/provider-models/index.ts +1 -3
  161. package/src/shared/lib/ui-document-title/index.ts +0 -1
  162. package/tsconfig.json +1 -0
  163. package/vite.config.ts +1 -1
  164. package/vitest.config.ts +1 -1
  165. package/dist/assets/api-BGd3rgv_.js +0 -15
  166. package/dist/assets/app-manager-provider-BuJ_U9eC.js +0 -1
  167. package/dist/assets/app-navigation.config-BTdUuqXS.js +0 -1
  168. package/dist/assets/channels-list-page-BrwymXPe.js +0 -8
  169. package/dist/assets/chat-DGM6K3Qs.js +0 -61
  170. package/dist/assets/chat-page-DpmXMWNS.js +0 -1
  171. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  172. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  173. package/dist/assets/desktop-update-config-BGKiqc6q.js +0 -1
  174. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  175. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  176. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  177. package/dist/assets/folder-CeJKPx5P.js +0 -1
  178. package/dist/assets/hash-BqxRTZW5.js +0 -1
  179. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  180. package/dist/assets/index-BrEdR78s.js +0 -2
  181. package/dist/assets/index-D8MKmXtO.css +0 -1
  182. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  183. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  184. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  185. package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
  186. package/dist/assets/marketplace-page-CPHxlYL8.js +0 -49
  187. package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-CswPXSjf.js +0 -40
  189. package/dist/assets/message-square-z_osm9c0.js +0 -1
  190. package/dist/assets/model-config-Cmruiqdx.js +0 -1
  191. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  192. package/dist/assets/plus-D8eKFY7h.js +0 -1
  193. package/dist/assets/provider-scoped-model-input-D7ACiMAO.js +0 -1
  194. package/dist/assets/providers-list-gg7LrfuB.js +0 -1
  195. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  196. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  197. package/dist/assets/remote-Db2M39Cv.js +0 -1
  198. package/dist/assets/runtime-config-page-BT_VV41p.js +0 -1
  199. package/dist/assets/search-config-0VTPpz-w.js +0 -1
  200. package/dist/assets/secrets-config-DwQbLLEy.js +0 -3
  201. package/dist/assets/sessions-config-page-CAG7Zevv.js +0 -2
  202. package/dist/assets/settings-drbWqzA4.js +0 -1
  203. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  204. package/dist/assets/theme-provider-COAwWFv8.js +0 -2
  205. package/dist/assets/tooltip-BOYp8Ue7.js +0 -1
  206. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  207. package/dist/assets/use-config-DTwhNDQE.js +0 -1
  208. package/dist/assets/use-confirm-dialog-oeSqhmrx.js +0 -1
  209. package/dist/assets/use-infinite-scroll-loader-X3KGuME8.js +0 -1
  210. package/dist/assets/use-viewport-layout-C0NJAVXs.js +0 -1
  211. package/dist/assets/x-CM-XDMpk.js +0 -1
  212. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  213. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  214. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  215. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -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
  };
@@ -106,9 +106,6 @@ function useNcpChatPageBaseState(presenter: NcpChatPresenter) {
106
106
  const selectedSessionKey = useChatSessionListStore(
107
107
  (state) => state.snapshot.selectedSessionKey,
108
108
  );
109
- const draftSessionKey = useChatSessionListStore(
110
- (state) => state.snapshot.draftSessionKey,
111
- );
112
109
  const selectedAgentId = useChatSessionListStore(
113
110
  (state) => state.snapshot.selectedAgentId,
114
111
  );
@@ -137,16 +134,16 @@ function useNcpChatPageBaseState(presenter: NcpChatPresenter) {
137
134
  () => parseSessionKeyFromRoute(routeSessionIdParam),
138
135
  [routeSessionIdParam],
139
136
  );
140
- const sessionKey = routeSessionKey ?? selectedSessionKey ?? draftSessionKey;
137
+ const sessionKey = routeSessionKey ?? undefined;
141
138
  const hasSessionProjectRootOverride =
142
139
  pendingProjectRoot !== null &&
143
- pendingProjectRootSessionKey === sessionKey;
140
+ (!sessionKey || pendingProjectRootSessionKey === sessionKey);
144
141
  const sessionProjectRootOverride = hasSessionProjectRootOverride
145
142
  ? pendingProjectRoot
146
143
  : undefined;
147
144
  const pageData = useNcpChatPageData({
148
145
  query,
149
- sessionKey,
146
+ sessionKey: sessionKey ?? null,
150
147
  projectRootOverride: sessionProjectRootOverride,
151
148
  currentSelectedModel,
152
149
  pendingSessionType,
@@ -213,7 +210,7 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
213
210
  ? (agentsQuery.data?.agents ?? [])
214
211
  : [{ id: selectedSession?.agentId ?? selectedAgentId }];
215
212
  const derivedState = useNcpChatDerivedState({
216
- sessionKey,
213
+ sessionKey: sessionKey ?? null,
217
214
  selectedSession,
218
215
  selectedAgentId,
219
216
  availableAgents,
@@ -222,15 +219,16 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
222
219
  selectedSessionType,
223
220
  sessionTypeOptions,
224
221
  });
222
+ const currentSessionRunning = agent.isRunning || selectedSession?.status === "running";
225
223
  return {
226
224
  ...baseState,
227
225
  availableAgents,
228
226
  effectiveSessionProjectRoot,
229
227
  effectiveSessionProjectName,
230
- isSending: agent.isSending || agent.isRunning,
231
- isAwaitingAssistantOutput: agent.isRunning,
232
- canStopCurrentRun: agent.isRunning,
233
- stopDisabledReason: agent.isRunning ? null : "__preparing__",
228
+ isSending: agent.isSending || currentSessionRunning,
229
+ isAwaitingAssistantOutput: currentSessionRunning,
230
+ canStopCurrentRun: currentSessionRunning,
231
+ stopDisabledReason: currentSessionRunning ? null : "__preparing__",
234
232
  lastSendError:
235
233
  isRuntimeBlocked
236
234
  ? null
@@ -251,14 +249,13 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
251
249
  pendingProjectRootSessionKey,
252
250
  presenter,
253
251
  selectedSession,
254
- selectedSessionKey,
255
252
  selectedSessionKeyRef,
256
253
  sessionKey,
257
254
  } = params;
258
255
  useEffect(() => {
259
256
  presenter.chatStreamActionsManager.bind({
260
257
  sendMessage: async (payload) => {
261
- if (payload.sessionKey !== sessionKey) {
258
+ if ((payload.sessionKey ?? null) !== (sessionKey ?? null)) {
262
259
  return;
263
260
  }
264
261
  const metadata = buildNcpSendMetadata({
@@ -267,7 +264,7 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
267
264
  thinkingLevel: payload.thinkingLevel,
268
265
  sessionType: payload.sessionType,
269
266
  projectRoot:
270
- payload.sessionKey === pendingProjectRootSessionKey
267
+ !payload.sessionKey || payload.sessionKey === pendingProjectRootSessionKey
271
268
  ? pendingProjectRoot
272
269
  : (selectedSession?.projectRoot ?? null),
273
270
  requestedSkills: payload.requestedSkills,
@@ -322,7 +319,6 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
322
319
  pendingProjectRoot,
323
320
  pendingProjectRootSessionKey,
324
321
  presenter,
325
- selectedSessionKey,
326
322
  selectedSession?.projectRoot,
327
323
  selectedSessionKeyRef,
328
324
  sessionKey,
@@ -336,7 +332,6 @@ function usePendingProjectRootOverrideCleanup(
336
332
  pendingProjectRoot,
337
333
  pendingProjectRootSessionKey,
338
334
  selectedSession,
339
- selectedSessionKey,
340
335
  } = params;
341
336
  useEffect(() => {
342
337
  if (
@@ -358,7 +353,6 @@ function usePendingProjectRootOverrideCleanup(
358
353
  pendingProjectRoot,
359
354
  pendingProjectRootSessionKey,
360
355
  selectedSession,
361
- selectedSessionKey,
362
356
  ]);
363
357
  }
364
358
 
@@ -385,6 +379,22 @@ function useSelectedSessionAgentSync(params: ReturnType<typeof useNcpChatPageSta
385
379
  }, [presenter, selectedAgentId, selectedSession?.agentId]);
386
380
  }
387
381
 
382
+ function useMaterializedRootSessionRouteSync(
383
+ params: ReturnType<typeof useNcpChatPageState>,
384
+ ) {
385
+ const { agent, presenter, routeSessionKey } = params;
386
+ const materializedSessionKey =
387
+ agent.snapshot.activeRun?.sessionId ??
388
+ agent.visibleMessages.find((message) => message.sessionId.trim())?.sessionId ??
389
+ null;
390
+ useEffect(() => {
391
+ if (routeSessionKey || !materializedSessionKey) {
392
+ return;
393
+ }
394
+ presenter.chatSessionListManager.materializeRootSessionRoute(materializedSessionKey);
395
+ }, [materializedSessionKey, presenter, routeSessionKey]);
396
+ }
397
+
388
398
  export function NcpChatPage({ view }: ChatPageProps) {
389
399
  const [presenter] = useState(() => new NcpChatPresenter());
390
400
  const state = useNcpChatPageState(presenter);
@@ -392,6 +402,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
392
402
  usePendingProjectRootOverrideCleanup(state);
393
403
  useNcpChatUiBindings(state);
394
404
  useSelectedSessionAgentSync(state);
405
+ useMaterializedRootSessionRouteSync(state);
395
406
  useChatSessionSync({
396
407
  view,
397
408
  routeSessionKey: state.routeSessionKey,
@@ -1,10 +1,8 @@
1
1
  import { create, type StateCreator } from 'zustand';
2
- import { createNcpSessionId } from '@/features/chat/utils/ncp-session-adapter.utils';
3
2
  import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
4
3
  export type ChatSessionListMode = 'time-first' | 'project-first';
5
4
  export type ChatSessionListSnapshot = {
6
5
  selectedSessionKey: string | null;
7
- draftSessionKey: string;
8
6
  selectedAgentId: string;
9
7
  query: string;
10
8
  listMode: ChatSessionListMode;
@@ -50,7 +48,6 @@ type ChatSessionListStoreSet = Parameters<StateCreator<ChatSessionListStore>>[0]
50
48
 
51
49
  const initialSnapshot: ChatSessionListSnapshot = {
52
50
  selectedSessionKey: null,
53
- draftSessionKey: createNcpSessionId(),
54
51
  selectedAgentId: 'main',
55
52
  query: '',
56
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: [],
@@ -5,7 +5,7 @@ import type { ThinkingLevel } from '@/shared/lib/api';
5
5
 
6
6
  export type SendMessageParams = {
7
7
  message: string;
8
- sessionKey: string;
8
+ sessionKey?: string;
9
9
  agentId: string;
10
10
  sessionType?: string;
11
11
  model?: string;
@@ -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
  }
@@ -70,6 +70,28 @@ describe('adaptNcpSessionSummary', () => {
70
70
  });
71
71
  });
72
72
 
73
+ it('maps session activity preview metadata into the session entry', () => {
74
+ const adapted = adaptNcpSessionSummary(
75
+ createSummary({
76
+ metadata: {
77
+ last_activity_preview: {
78
+ state: 'completed',
79
+ replyText: '已经整理好方案',
80
+ statusText: '工具调用完成',
81
+ timestamp: '2026-05-16T01:00:00.000Z',
82
+ },
83
+ },
84
+ }),
85
+ );
86
+
87
+ expect(adapted.activityPreview).toEqual({
88
+ state: 'completed',
89
+ replyText: '已经整理好方案',
90
+ statusText: '工具调用完成',
91
+ timestamp: '2026-05-16T01:00:00.000Z',
92
+ });
93
+ });
94
+
73
95
  it('does not hydrate context window metadata from persisted session summaries', () => {
74
96
  const adapted = adaptNcpSessionSummary(
75
97
  createSummary({
@@ -3,6 +3,7 @@ import type { NcpMessagePart } from '@nextclaw/ncp';
3
3
  import type {
4
4
  NcpMessageView,
5
5
  NcpSessionSummaryView,
6
+ SessionActivityPreviewView,
6
7
  SessionEntryView,
7
8
  ThinkingLevel
8
9
  } from '@/shared/lib/api';
@@ -13,6 +14,12 @@ import {
13
14
  } from '@/shared/lib/session-project';
14
15
 
15
16
  const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
17
+ const SESSION_ACTIVITY_PREVIEW_STATE_SET = new Set<SessionActivityPreviewView['state']>([
18
+ 'running',
19
+ 'completed',
20
+ 'failed',
21
+ 'idle'
22
+ ]);
16
23
 
17
24
  function stringifyUnknown(value: unknown): string {
18
25
  if (typeof value === 'string') {
@@ -46,6 +53,28 @@ function readMetadata(summary: NcpSessionSummaryView): Record<string, unknown> |
46
53
  return metadata as Record<string, unknown>;
47
54
  }
48
55
 
56
+ function readNcpSessionActivityPreview(summary: NcpSessionSummaryView): SessionActivityPreviewView | null {
57
+ const metadata = readMetadata(summary);
58
+ const preview = metadata?.last_activity_preview;
59
+ if (!preview || typeof preview !== 'object' || Array.isArray(preview)) {
60
+ return null;
61
+ }
62
+ const previewRecord = preview as Record<string, unknown>;
63
+ const { state } = previewRecord;
64
+ const timestamp = readOptionalString(previewRecord.timestamp);
65
+ if (!SESSION_ACTIVITY_PREVIEW_STATE_SET.has(state as SessionActivityPreviewView['state']) || !timestamp) {
66
+ return null;
67
+ }
68
+ const statusText = readOptionalString(previewRecord.statusText);
69
+ const replyText = readOptionalString(previewRecord.replyText);
70
+ return {
71
+ state: state as SessionActivityPreviewView['state'],
72
+ timestamp,
73
+ ...(statusText ? { statusText } : {}),
74
+ ...(replyText ? { replyText } : {})
75
+ };
76
+ }
77
+
49
78
  export function readNcpSessionPreferredModel(summary: NcpSessionSummaryView): string | null {
50
79
  const metadata = readMetadata(summary);
51
80
  if (!metadata) {
@@ -286,9 +315,10 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
286
315
  const parentSessionId = readNcpParentSessionId(summary);
287
316
  const spawnedByRequestId = readNcpSpawnedByRequestId(summary);
288
317
  const isPromotedChildSession = readPromotedChildSession(summary);
318
+ const activityPreview = readNcpSessionActivityPreview(summary);
289
319
  return {
290
320
  key: summary.sessionId,
291
- createdAt: summary.updatedAt,
321
+ createdAt: summary.createdAt ?? summary.updatedAt,
292
322
  updatedAt: summary.updatedAt,
293
323
  ...(lastMessageAt ? { lastMessageAt } : {}),
294
324
  ...(readAt ? { readAt } : {}),
@@ -301,10 +331,12 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
301
331
  ...(projectName ? { projectName } : {}),
302
332
  sessionType: readNcpSessionType(summary),
303
333
  sessionTypeMutable: false,
334
+ status: summary.status ?? 'idle',
304
335
  isChildSession: Boolean(parentSessionId),
305
336
  ...(isPromotedChildSession ? { isPromotedChildSession } : {}),
306
337
  ...(parentSessionId ? { parentSessionId } : {}),
307
338
  ...(spawnedByRequestId ? { spawnedByRequestId } : {}),
339
+ ...(activityPreview ? { activityPreview } : {}),
308
340
  messageCount: summary.messageCount
309
341
  };
310
342
  }