@nextclaw/ui 0.12.23 → 0.12.24

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 (61) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/assets/{api-BGd3rgv_.js → api-D2xRKmZd.js} +2 -2
  3. package/dist/assets/{app-manager-provider-BuJ_U9eC.js → app-manager-provider-CNaZboG4.js} +1 -1
  4. package/dist/assets/{app-navigation.config-BTdUuqXS.js → app-navigation.config-Ihhrrt--.js} +1 -1
  5. package/dist/assets/{channels-list-page-BrwymXPe.js → channels-list-page-p26lgxLk.js} +1 -1
  6. package/dist/assets/{chat-DGM6K3Qs.js → chat-Dkh2qtuz.js} +8 -8
  7. package/dist/assets/{chat-page-DpmXMWNS.js → chat-page-DoTmE2wx.js} +1 -1
  8. package/dist/assets/{desktop-update-config-BGKiqc6q.js → desktop-update-config-DlpzDfKM.js} +1 -1
  9. package/dist/assets/{dialog-dxsKz7jJ.js → dialog-C3D7Be0p.js} +1 -1
  10. package/dist/assets/{dist-DsYTOyq7.js → dist-CPlbUgwU.js} +1 -1
  11. package/dist/assets/{es2015-V75WQJ2s.js → es2015-xqN1slyW.js} +1 -1
  12. package/dist/assets/{index-BrEdR78s.js → index-pBvbJ5Mt.js} +2 -2
  13. package/dist/assets/marketplace-page-Cql0kDi-.js +1 -0
  14. package/dist/assets/{marketplace-page-CPHxlYL8.js → marketplace-page-m4P5g_Ht.js} +1 -1
  15. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +1 -0
  16. package/dist/assets/{mcp-marketplace-page-CswPXSjf.js → mcp-marketplace-page-ByzBQZcx.js} +1 -1
  17. package/dist/assets/{model-config-Cmruiqdx.js → model-config-Dbr_0APb.js} +1 -1
  18. package/dist/assets/{notice-card-D1RNsTn_.js → notice-card-BFDbKQDA.js} +1 -1
  19. package/dist/assets/{popover-BMyiifTA.js → popover-B86Dbfhf.js} +1 -1
  20. package/dist/assets/{provider-scoped-model-input-D7ACiMAO.js → provider-scoped-model-input-DFm6N2f7.js} +1 -1
  21. package/dist/assets/{providers-list-gg7LrfuB.js → providers-list-BJcLOjun.js} +1 -1
  22. package/dist/assets/remote-BOxo9iwd.js +1 -0
  23. package/dist/assets/{runtime-config-page-BT_VV41p.js → runtime-config-page-CjLhnbSl.js} +1 -1
  24. package/dist/assets/{search-config-0VTPpz-w.js → search-config-J4Htco-P.js} +1 -1
  25. package/dist/assets/{secrets-config-DwQbLLEy.js → secrets-config-CUdERjco.js} +1 -1
  26. package/dist/assets/{select-DTdzR8j8.js → select-CJ0wbo3D.js} +1 -1
  27. package/dist/assets/{sessions-config-page-CAG7Zevv.js → sessions-config-page-DpK991fs.js} +2 -2
  28. package/dist/assets/{setting-row-CvKngoNI.js → setting-row-D1Yygqp7.js} +1 -1
  29. package/dist/assets/{tag-chip-BywQeHJj.js → tag-chip-FrkmkT8r.js} +1 -1
  30. package/dist/assets/{theme-provider-COAwWFv8.js → theme-provider-0hxjiPc_.js} +1 -1
  31. package/dist/assets/{tooltip-BOYp8Ue7.js → tooltip-Cj4yA0gH.js} +1 -1
  32. package/dist/assets/{use-config-DTwhNDQE.js → use-config-38Ur-89i.js} +1 -1
  33. package/dist/assets/{use-confirm-dialog-oeSqhmrx.js → use-confirm-dialog-DPQThaeU.js} +1 -1
  34. package/dist/assets/{use-infinite-scroll-loader-X3KGuME8.js → use-infinite-scroll-loader-5Gf1xQi7.js} +1 -1
  35. package/dist/assets/{use-viewport-layout-C0NJAVXs.js → use-viewport-layout-D1XzKeip.js} +1 -1
  36. package/dist/index.html +15 -15
  37. package/package.json +9 -9
  38. package/src/features/chat/components/chat-sidebar-session-item.tsx +1 -1
  39. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +2 -2
  40. package/src/features/chat/components/layout/chat-sidebar.test.tsx +74 -0
  41. package/src/features/chat/components/layout/chat-sidebar.tsx +28 -29
  42. package/src/features/chat/hooks/use-hydrated-ncp-agent.test.tsx +6 -0
  43. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +158 -69
  44. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
  45. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
  46. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
  47. package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
  48. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
  49. package/src/features/chat/managers/chat-session-list.manager.test.ts +19 -16
  50. package/src/features/chat/managers/chat-session-list.manager.ts +20 -24
  51. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +23 -12
  52. package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
  53. package/src/features/chat/pages/ncp-chat-page.tsx +23 -13
  54. package/src/features/chat/stores/chat-session-list.store.ts +2 -3
  55. package/src/features/chat/types/chat-stream.types.ts +1 -1
  56. package/src/features/chat/utils/ncp-session-adapter.utils.ts +1 -1
  57. package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
  58. package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
  59. package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
  60. package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
  61. package/dist/assets/remote-Db2M39Cv.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import type * as SharedApi from '@/shared/lib/api';
2
3
  import { ChatSessionListManager } from '@/features/chat/managers/chat-session-list.manager';
3
4
  import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
4
5
  import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
@@ -9,7 +10,7 @@ const mocks = vi.hoisted(() => ({
9
10
  }));
10
11
 
11
12
  vi.mock('@/shared/lib/api', async (importOriginal) => {
12
- const actual = await importOriginal<typeof import('@/shared/lib/api')>();
13
+ const actual = await importOriginal<typeof SharedApi>();
13
14
  return {
14
15
  ...actual,
15
16
  updateNcpSession: mocks.updateNcpSession,
@@ -59,13 +60,13 @@ describe('ChatSessionListManager', () => {
59
60
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
60
61
 
61
62
  const manager = new ChatSessionListManager(uiManager, streamActionsManager);
62
- const sessionKey = manager.createSession('codex');
63
+ manager.createSession('codex');
63
64
 
64
65
  expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
65
66
  expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
66
67
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
67
- expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBe(sessionKey);
68
- expect(useChatSessionListStore.getState().snapshot.draftSessionKey).not.toBe('draft-root-1');
68
+ expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
69
+ expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
69
70
  expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
70
71
  expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
71
72
  expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
@@ -82,14 +83,14 @@ describe('ChatSessionListManager', () => {
82
83
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
83
84
 
84
85
  const manager = new ChatSessionListManager(uiManager, streamActionsManager);
85
- const sessionKey = manager.startAgentDraftChat('researcher', 'codex');
86
+ manager.startAgentDraftChat('researcher', 'codex');
86
87
 
87
88
  expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
88
89
  expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
89
90
  expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe('researcher');
90
91
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
91
- expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBe(sessionKey);
92
- expect(useChatThreadStore.getState().snapshot.sessionKey).toBe(sessionKey);
92
+ expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
93
+ expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
93
94
  expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
94
95
  expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
95
96
  expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
@@ -106,13 +107,13 @@ describe('ChatSessionListManager', () => {
106
107
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
107
108
 
108
109
  const manager = new ChatSessionListManager(uiManager, streamActionsManager);
109
- const sessionKey = manager.createSession('native', '/tmp/project-alpha');
110
+ manager.createSession('native', '/tmp/project-alpha');
110
111
 
111
112
  expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBe('/tmp/project-alpha');
112
- expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe(sessionKey);
113
+ expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
113
114
  });
114
115
 
115
- it('reuses the current root draft when send flow needs a concrete session key', () => {
116
+ it('keeps the root draft key empty when send flow has no concrete session yet', () => {
116
117
  useChatSessionListStore.setState({
117
118
  snapshot: {
118
119
  ...useChatSessionListStore.getState().snapshot,
@@ -132,7 +133,7 @@ describe('ChatSessionListManager', () => {
132
133
  const manager = new ChatSessionListManager(uiManager, streamActionsManager);
133
134
  const sessionKey = manager.ensureDraftSession('native');
134
135
 
135
- expect(sessionKey).toBe('draft-root-2');
136
+ expect(sessionKey).toBeNull();
136
137
  expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
137
138
  expect(uiManager.goToSession).not.toHaveBeenCalled();
138
139
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
@@ -149,10 +150,10 @@ describe('ChatSessionListManager', () => {
149
150
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
150
151
 
151
152
  const manager = new ChatSessionListManager(uiManager, streamActionsManager);
152
- const sessionKey = manager.createSession('native', '/tmp/project-alpha');
153
+ manager.createSession('native', '/tmp/project-alpha');
153
154
 
154
155
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
155
- expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe(sessionKey);
156
+ expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
156
157
  });
157
158
 
158
159
  it('delegates existing-session selection to routing without eagerly mutating the selected session state', () => {
@@ -224,7 +225,7 @@ describe('ChatSessionListManager', () => {
224
225
  expect(mocks.updateNcpSession).not.toHaveBeenCalled();
225
226
  });
226
227
 
227
- it('promotes the active root draft to a concrete session route after the draft has been sent successfully', () => {
228
+ it('routes to the backend-materialized root session after the first send starts', () => {
228
229
  useChatSessionListStore.setState({
229
230
  snapshot: {
230
231
  ...useChatSessionListStore.getState().snapshot,
@@ -243,8 +244,10 @@ describe('ChatSessionListManager', () => {
243
244
  const manager = new ChatSessionListManager(uiManager, streamActionsManager);
244
245
 
245
246
  manager.ensureDraftSession('native');
246
- manager.promoteRootDraftSessionRoute('draft-root-2');
247
+ manager.materializeRootSessionRoute('ncp-materialized-session');
247
248
 
248
- expect(uiManager.goToSession).toHaveBeenCalledWith('draft-root-2', { replace: true });
249
+ expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('ncp-materialized-session');
250
+ expect(useChatThreadStore.getState().snapshot.sessionKey).toBe('ncp-materialized-session');
251
+ expect(uiManager.goToSession).toHaveBeenCalledWith('ncp-materialized-session', { replace: true });
249
252
  });
250
253
  });
@@ -5,7 +5,6 @@ import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
5
5
  import type { SetStateAction } from 'react';
6
6
  import type { ChatStreamActionsManager } from '@/features/chat/managers/chat-stream-actions.manager';
7
7
  import { normalizeSessionProjectRootValue } from '@/shared/lib/session-project';
8
- import { createNcpSessionId } from '@/features/chat/utils/ncp-session-adapter.utils';
9
8
  import { updateNcpSession } from '@/shared/lib/api';
10
9
  export class ChatSessionListManager {
11
10
  constructor(
@@ -13,9 +12,9 @@ export class ChatSessionListManager {
13
12
  private streamActionsManager: ChatStreamActionsManager
14
13
  ) {}
15
14
 
16
- private syncDraftThreadState = (sessionKey: string) => {
15
+ private syncDraftThreadState = () => {
17
16
  useChatThreadStore.getState().setSnapshot({
18
- sessionKey,
17
+ sessionKey: null,
19
18
  sessionDisplayName: undefined,
20
19
  canDeleteSession: false,
21
20
  isHistoryLoading: false,
@@ -97,7 +96,7 @@ export class ChatSessionListManager {
97
96
  void updateNcpSession(normalizedSessionKey, { uiReadAt: normalizedReadAt }).catch(() => undefined);
98
97
  };
99
98
 
100
- createSession = (sessionType?: string, projectRoot?: string | null): string => {
99
+ createSession = (sessionType?: string, projectRoot?: string | null): void => {
101
100
  const { snapshot } = useChatInputStore.getState();
102
101
  const { defaultSessionType: configuredDefaultSessionType } = snapshot;
103
102
  const defaultSessionType = configuredDefaultSessionType || 'native';
@@ -106,30 +105,27 @@ export class ChatSessionListManager {
106
105
  ? sessionType.trim()
107
106
  : defaultSessionType;
108
107
  const normalizedProjectRoot = normalizeSessionProjectRootValue(projectRoot);
109
- const nextSessionKey = createNcpSessionId();
110
108
  this.streamActionsManager.resetStreamState();
111
109
  useChatSessionListStore.getState().setSnapshot({
112
110
  selectedSessionKey: null,
113
- draftSessionKey: nextSessionKey
111
+ draftSessionKey: null
114
112
  });
115
- this.syncDraftThreadState(nextSessionKey);
113
+ this.syncDraftThreadState();
116
114
  useChatInputStore.getState().setSnapshot({
117
115
  pendingSessionType: nextSessionType,
118
116
  pendingProjectRoot: normalizedProjectRoot,
119
- pendingProjectRootSessionKey: normalizedProjectRoot ? nextSessionKey : null
117
+ pendingProjectRootSessionKey: null
120
118
  });
121
119
  this.uiManager.goToChatRoot();
122
- return nextSessionKey;
123
120
  };
124
121
 
125
- startAgentDraftChat = (agentId: string, sessionType: string): string => {
122
+ startAgentDraftChat = (agentId: string, sessionType: string): void => {
126
123
  const normalizedAgentId = agentId.trim() || 'main';
127
- const nextSessionKey = this.createSession(sessionType);
124
+ this.createSession(sessionType);
128
125
  this.setSelectedAgentId(normalizedAgentId);
129
- return nextSessionKey;
130
126
  };
131
127
 
132
- ensureDraftSession = (sessionType?: string): string => {
128
+ ensureDraftSession = (sessionType?: string): string | null => {
133
129
  const { snapshot } = useChatSessionListStore.getState();
134
130
  if (snapshot.selectedSessionKey) {
135
131
  return snapshot.selectedSessionKey;
@@ -138,28 +134,28 @@ export class ChatSessionListManager {
138
134
  typeof sessionType === 'string' && sessionType.trim().length > 0
139
135
  ? sessionType.trim()
140
136
  : null;
141
- this.syncDraftThreadState(snapshot.draftSessionKey);
137
+ this.syncDraftThreadState();
142
138
  if (normalizedSessionType) {
143
139
  useChatInputStore.getState().setSnapshot({ pendingSessionType: normalizedSessionType });
144
140
  }
145
- return snapshot.draftSessionKey;
141
+ return null;
146
142
  };
147
143
 
148
- promoteRootDraftSessionRoute = (sessionKey: string) => {
144
+ materializeRootSessionRoute = (sessionKey: string) => {
149
145
  const normalizedSessionKey = sessionKey.trim();
150
146
  if (!normalizedSessionKey) {
151
147
  return;
152
148
  }
153
- const { snapshot } = useChatSessionListStore.getState();
154
- const { sessionKey: currentThreadSessionKey } = useChatThreadStore.getState().snapshot;
155
- if (
156
- snapshot.selectedSessionKey !== null ||
157
- snapshot.draftSessionKey !== normalizedSessionKey ||
158
- currentThreadSessionKey !== normalizedSessionKey ||
159
- !this.uiManager.isAtChatRoot()
160
- ) {
149
+ if (!this.uiManager.isAtChatRoot()) {
161
150
  return;
162
151
  }
152
+ useChatSessionListStore.getState().setSnapshot({
153
+ selectedSessionKey: normalizedSessionKey,
154
+ draftSessionKey: null,
155
+ });
156
+ useChatThreadStore.getState().setSnapshot({
157
+ sessionKey: normalizedSessionKey,
158
+ });
163
159
  this.uiManager.goToSession(normalizedSessionKey, { replace: true });
164
160
  };
165
161
 
@@ -77,7 +77,7 @@ describe('NcpChatInputManager', () => {
77
77
  } as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
78
78
  const sessionListManager = {
79
79
  ensureDraftSession: vi.fn(() => 'draft-session'),
80
- promoteRootDraftSessionRoute: vi.fn(),
80
+ materializeRootSessionRoute: vi.fn(),
81
81
  } as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
82
82
  const manager = new NcpChatInputManager(
83
83
  {} as ConstructorParameters<typeof NcpChatInputManager>[0],
@@ -95,14 +95,21 @@ describe('NcpChatInputManager', () => {
95
95
  }),
96
96
  );
97
97
  expect(sessionListManager.ensureDraftSession).not.toHaveBeenCalled();
98
- expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('current-route-session');
98
+ expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
99
99
  });
100
100
 
101
- it('keeps sending through the current root draft session while /chat is still in blank-draft mode', async () => {
101
+ it('sends without a session key while /chat is still in blank-draft mode', async () => {
102
102
  useChatThreadStore.setState({
103
103
  snapshot: {
104
104
  ...useChatThreadStore.getState().snapshot,
105
- sessionKey: 'draft-root-session',
105
+ sessionKey: null,
106
+ },
107
+ });
108
+ useChatSessionListStore.setState({
109
+ snapshot: {
110
+ ...useChatSessionListStore.getState().snapshot,
111
+ selectedSessionKey: null,
112
+ draftSessionKey: null,
106
113
  },
107
114
  });
108
115
  const streamActionsManager = {
@@ -111,7 +118,7 @@ describe('NcpChatInputManager', () => {
111
118
  } as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
112
119
  const sessionListManager = {
113
120
  ensureDraftSession: vi.fn(() => 'materialized-draft-session'),
114
- promoteRootDraftSessionRoute: vi.fn(),
121
+ materializeRootSessionRoute: vi.fn(),
115
122
  } as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
116
123
  const manager = new NcpChatInputManager(
117
124
  {} as ConstructorParameters<typeof NcpChatInputManager>[0],
@@ -121,14 +128,18 @@ describe('NcpChatInputManager', () => {
121
128
 
122
129
  await manager.send();
123
130
 
124
- expect(sessionListManager.ensureDraftSession).not.toHaveBeenCalled();
131
+ expect(sessionListManager.ensureDraftSession).toHaveBeenCalledWith('native');
132
+ expect(streamActionsManager.sendMessage).toHaveBeenCalledWith(
133
+ expect.not.objectContaining({
134
+ sessionKey: expect.any(String),
135
+ }),
136
+ );
125
137
  expect(streamActionsManager.sendMessage).toHaveBeenCalledWith(
126
138
  expect.objectContaining({
127
- sessionKey: 'draft-root-session',
128
139
  message: 'hello from current thread',
129
140
  }),
130
141
  );
131
- expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('draft-root-session');
142
+ expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
132
143
  });
133
144
 
134
145
  it('does not send while the runtime is still blocked during startup', async () => {
@@ -158,7 +169,7 @@ describe('NcpChatInputManager', () => {
158
169
  } as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
159
170
  const sessionListManager = {
160
171
  ensureDraftSession: vi.fn(() => 'draft-session'),
161
- promoteRootDraftSessionRoute: vi.fn(),
172
+ materializeRootSessionRoute: vi.fn(),
162
173
  } as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
163
174
  const manager = new NcpChatInputManager(
164
175
  {} as ConstructorParameters<typeof NcpChatInputManager>[0],
@@ -169,7 +180,7 @@ describe('NcpChatInputManager', () => {
169
180
  await manager.send();
170
181
 
171
182
  expect(streamActionsManager.sendMessage).not.toHaveBeenCalled();
172
- expect(sessionListManager.promoteRootDraftSessionRoute).not.toHaveBeenCalled();
183
+ expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
173
184
  });
174
185
 
175
186
  it('still attempts to send when provider metadata is stale or the session type is marked unavailable', async () => {
@@ -187,7 +198,7 @@ describe('NcpChatInputManager', () => {
187
198
  } as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
188
199
  const sessionListManager = {
189
200
  ensureDraftSession: vi.fn(() => 'draft-session'),
190
- promoteRootDraftSessionRoute: vi.fn(),
201
+ materializeRootSessionRoute: vi.fn(),
191
202
  } as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
192
203
  const manager = new NcpChatInputManager(
193
204
  {} as ConstructorParameters<typeof NcpChatInputManager>[0],
@@ -198,6 +209,6 @@ describe('NcpChatInputManager', () => {
198
209
  await manager.send();
199
210
 
200
211
  expect(streamActionsManager.sendMessage).toHaveBeenCalledTimes(1);
201
- expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('current-route-session');
212
+ expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
202
213
  });
203
214
  });
@@ -197,11 +197,14 @@ export class NcpChatInputManager {
197
197
  const sessionKey =
198
198
  threadSnapshot.sessionKey ??
199
199
  sessionSnapshot.selectedSessionKey ??
200
+ null;
201
+ if (!sessionKey && inputSnapshot.selectedSessionType?.trim()) {
200
202
  this.sessionListManager.ensureDraftSession(inputSnapshot.selectedSessionType);
203
+ }
201
204
  this.setComposerNodes(createInitialChatComposerNodes());
202
205
  await this.streamActionsManager.sendMessage({
203
206
  message,
204
- sessionKey,
207
+ ...(sessionKey ? { sessionKey } : {}),
205
208
  agentId: sessionSnapshot.selectedAgentId,
206
209
  sessionType: inputSnapshot.selectedSessionType,
207
210
  model: inputSnapshot.selectedModel || undefined,
@@ -213,7 +216,6 @@ export class NcpChatInputManager {
213
216
  restoreDraftOnError: true,
214
217
  composerNodes
215
218
  });
216
- this.sessionListManager.promoteRootDraftSessionRoute(sessionKey);
217
219
  };
218
220
 
219
221
  stop = async () => {
@@ -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,
@@ -251,14 +248,13 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
251
248
  pendingProjectRootSessionKey,
252
249
  presenter,
253
250
  selectedSession,
254
- selectedSessionKey,
255
251
  selectedSessionKeyRef,
256
252
  sessionKey,
257
253
  } = params;
258
254
  useEffect(() => {
259
255
  presenter.chatStreamActionsManager.bind({
260
256
  sendMessage: async (payload) => {
261
- if (payload.sessionKey !== sessionKey) {
257
+ if ((payload.sessionKey ?? null) !== (sessionKey ?? null)) {
262
258
  return;
263
259
  }
264
260
  const metadata = buildNcpSendMetadata({
@@ -267,7 +263,7 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
267
263
  thinkingLevel: payload.thinkingLevel,
268
264
  sessionType: payload.sessionType,
269
265
  projectRoot:
270
- payload.sessionKey === pendingProjectRootSessionKey
266
+ !payload.sessionKey || payload.sessionKey === pendingProjectRootSessionKey
271
267
  ? pendingProjectRoot
272
268
  : (selectedSession?.projectRoot ?? null),
273
269
  requestedSkills: payload.requestedSkills,
@@ -322,7 +318,6 @@ function useNcpChatStreamBindings(params: ReturnType<typeof useNcpChatPageState>
322
318
  pendingProjectRoot,
323
319
  pendingProjectRootSessionKey,
324
320
  presenter,
325
- selectedSessionKey,
326
321
  selectedSession?.projectRoot,
327
322
  selectedSessionKeyRef,
328
323
  sessionKey,
@@ -336,7 +331,6 @@ function usePendingProjectRootOverrideCleanup(
336
331
  pendingProjectRoot,
337
332
  pendingProjectRootSessionKey,
338
333
  selectedSession,
339
- selectedSessionKey,
340
334
  } = params;
341
335
  useEffect(() => {
342
336
  if (
@@ -358,7 +352,6 @@ function usePendingProjectRootOverrideCleanup(
358
352
  pendingProjectRoot,
359
353
  pendingProjectRootSessionKey,
360
354
  selectedSession,
361
- selectedSessionKey,
362
355
  ]);
363
356
  }
364
357
 
@@ -385,6 +378,22 @@ function useSelectedSessionAgentSync(params: ReturnType<typeof useNcpChatPageSta
385
378
  }, [presenter, selectedAgentId, selectedSession?.agentId]);
386
379
  }
387
380
 
381
+ function useMaterializedRootSessionRouteSync(
382
+ params: ReturnType<typeof useNcpChatPageState>,
383
+ ) {
384
+ const { agent, presenter, routeSessionKey } = params;
385
+ const materializedSessionKey =
386
+ agent.snapshot.activeRun?.sessionId ??
387
+ agent.visibleMessages.find((message) => message.sessionId.trim())?.sessionId ??
388
+ null;
389
+ useEffect(() => {
390
+ if (routeSessionKey || !materializedSessionKey) {
391
+ return;
392
+ }
393
+ presenter.chatSessionListManager.materializeRootSessionRoute(materializedSessionKey);
394
+ }, [materializedSessionKey, presenter, routeSessionKey]);
395
+ }
396
+
388
397
  export function NcpChatPage({ view }: ChatPageProps) {
389
398
  const [presenter] = useState(() => new NcpChatPresenter());
390
399
  const state = useNcpChatPageState(presenter);
@@ -392,6 +401,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
392
401
  usePendingProjectRootOverrideCleanup(state);
393
402
  useNcpChatUiBindings(state);
394
403
  useSelectedSessionAgentSync(state);
404
+ useMaterializedRootSessionRouteSync(state);
395
405
  useChatSessionSync({
396
406
  view,
397
407
  routeSessionKey: state.routeSessionKey,
@@ -1,10 +1,9 @@
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;
6
+ draftSessionKey: string | null;
8
7
  selectedAgentId: string;
9
8
  query: string;
10
9
  listMode: ChatSessionListMode;
@@ -50,7 +49,7 @@ type ChatSessionListStoreSet = Parameters<StateCreator<ChatSessionListStore>>[0]
50
49
 
51
50
  const initialSnapshot: ChatSessionListSnapshot = {
52
51
  selectedSessionKey: null,
53
- draftSessionKey: createNcpSessionId(),
52
+ draftSessionKey: null,
54
53
  selectedAgentId: 'main',
55
54
  query: '',
56
55
  listMode: 'time-first'
@@ -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;
@@ -288,7 +288,7 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
288
288
  const isPromotedChildSession = readPromotedChildSession(summary);
289
289
  return {
290
290
  key: summary.sessionId,
291
- createdAt: summary.updatedAt,
291
+ createdAt: summary.createdAt ?? summary.updatedAt,
292
292
  updatedAt: summary.updatedAt,
293
293
  ...(lastMessageAt ? { lastMessageAt } : {}),
294
294
  ...(readAt ? { readAt } : {}),
@@ -14,14 +14,18 @@ function createSessionsList(): NcpSessionsListView {
14
14
  {
15
15
  sessionId: 'session-1',
16
16
  messageCount: 1,
17
+ createdAt: '2026-03-29T08:00:00.000Z',
17
18
  updatedAt: '2026-03-29T10:00:00.000Z',
19
+ lastMessageAt: '2026-03-29T10:00:00.000Z',
18
20
  status: 'idle',
19
21
  metadata: {}
20
22
  },
21
23
  {
22
24
  sessionId: 'session-2',
23
25
  messageCount: 2,
26
+ createdAt: '2026-03-29T07:00:00.000Z',
24
27
  updatedAt: '2026-03-29T09:00:00.000Z',
28
+ lastMessageAt: '2026-03-29T09:00:00.000Z',
25
29
  status: 'idle',
26
30
  metadata: {}
27
31
  }
@@ -31,11 +35,13 @@ function createSessionsList(): NcpSessionsListView {
31
35
  }
32
36
 
33
37
  describe('ncp-session-query-cache', () => {
34
- it('upserts summaries and keeps the list sorted by updatedAt descending', () => {
38
+ it('upserts summaries and keeps the list sorted by last message time descending', () => {
35
39
  const updated = upsertNcpSessionSummaryList(createSessionsList(), {
36
40
  sessionId: 'session-2',
37
41
  messageCount: 3,
42
+ createdAt: '2026-03-29T07:00:00.000Z',
38
43
  updatedAt: '2026-03-29T11:00:00.000Z',
44
+ lastMessageAt: '2026-03-29T11:00:00.000Z',
39
45
  status: 'running',
40
46
  metadata: { label: 'Latest' }
41
47
  });
@@ -48,6 +54,23 @@ describe('ncp-session-query-cache', () => {
48
54
  });
49
55
  });
50
56
 
57
+ it('does not reorder when only session metadata updatedAt changes', () => {
58
+ const updated = upsertNcpSessionSummaryList(createSessionsList(), {
59
+ sessionId: 'session-2',
60
+ messageCount: 2,
61
+ createdAt: '2026-03-29T07:00:00.000Z',
62
+ updatedAt: '2026-03-29T12:00:00.000Z',
63
+ lastMessageAt: '2026-03-29T09:00:00.000Z',
64
+ status: 'idle',
65
+ metadata: { ui_last_read_at: '2026-03-29T09:00:00.000Z' }
66
+ });
67
+
68
+ expect(updated?.sessions.map((session) => session.sessionId)).toEqual(['session-1', 'session-2']);
69
+ expect(updated?.sessions[1]?.metadata).toEqual({
70
+ ui_last_read_at: '2026-03-29T09:00:00.000Z'
71
+ });
72
+ });
73
+
51
74
  it('ignores stale summaries that would move a session back to an older running state', () => {
52
75
  const updated = upsertNcpSessionSummaryList(createSessionsList(), {
53
76
  sessionId: 'session-1',
@@ -128,7 +151,9 @@ describe('ncp-session-query-cache', () => {
128
151
  summary: {
129
152
  sessionId: 'session-3',
130
153
  messageCount: 1,
154
+ createdAt: '2026-03-29T12:00:00.000Z',
131
155
  updatedAt: '2026-03-29T12:00:00.000Z',
156
+ lastMessageAt: '2026-03-29T12:00:00.000Z',
132
157
  status: 'running',
133
158
  metadata: {}
134
159
  }
@@ -1,8 +1,12 @@
1
1
  import type { QueryClient } from '@tanstack/react-query';
2
2
  import type { NcpSessionSummaryView, NcpSessionsListView, WsEvent } from '@/shared/lib/api';
3
3
 
4
+ function readSessionActivityAt(summary: NcpSessionSummaryView): string {
5
+ return summary.lastMessageAt ?? summary.createdAt ?? summary.updatedAt;
6
+ }
7
+
4
8
  function sortSessionSummaries(summaries: readonly NcpSessionSummaryView[]): NcpSessionSummaryView[] {
5
- return [...summaries].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
9
+ return [...summaries].sort((left, right) => readSessionActivityAt(right).localeCompare(readSessionActivityAt(left)));
6
10
  }
7
11
 
8
12
  function shouldReplaceSessionSummary(
@@ -1 +0,0 @@
1
- import{t as e}from"./marketplace-page-CPHxlYL8.js";export{e as MarketplacePage};
@@ -1 +0,0 @@
1
- import{t as e}from"./mcp-marketplace-page-CswPXSjf.js";export{e as McpMarketplacePage};
@@ -1 +0,0 @@
1
- import{m as e}from"./app-manager-provider-BuJ_U9eC.js";export{e as RemoteAccessPage};