@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.
- package/CHANGELOG.md +97 -0
- package/dist/assets/{api-BGd3rgv_.js → api-D2xRKmZd.js} +2 -2
- package/dist/assets/{app-manager-provider-BuJ_U9eC.js → app-manager-provider-CNaZboG4.js} +1 -1
- package/dist/assets/{app-navigation.config-BTdUuqXS.js → app-navigation.config-Ihhrrt--.js} +1 -1
- package/dist/assets/{channels-list-page-BrwymXPe.js → channels-list-page-p26lgxLk.js} +1 -1
- package/dist/assets/{chat-DGM6K3Qs.js → chat-Dkh2qtuz.js} +8 -8
- package/dist/assets/{chat-page-DpmXMWNS.js → chat-page-DoTmE2wx.js} +1 -1
- package/dist/assets/{desktop-update-config-BGKiqc6q.js → desktop-update-config-DlpzDfKM.js} +1 -1
- package/dist/assets/{dialog-dxsKz7jJ.js → dialog-C3D7Be0p.js} +1 -1
- package/dist/assets/{dist-DsYTOyq7.js → dist-CPlbUgwU.js} +1 -1
- package/dist/assets/{es2015-V75WQJ2s.js → es2015-xqN1slyW.js} +1 -1
- package/dist/assets/{index-BrEdR78s.js → index-pBvbJ5Mt.js} +2 -2
- package/dist/assets/marketplace-page-Cql0kDi-.js +1 -0
- package/dist/assets/{marketplace-page-CPHxlYL8.js → marketplace-page-m4P5g_Ht.js} +1 -1
- package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +1 -0
- package/dist/assets/{mcp-marketplace-page-CswPXSjf.js → mcp-marketplace-page-ByzBQZcx.js} +1 -1
- package/dist/assets/{model-config-Cmruiqdx.js → model-config-Dbr_0APb.js} +1 -1
- package/dist/assets/{notice-card-D1RNsTn_.js → notice-card-BFDbKQDA.js} +1 -1
- package/dist/assets/{popover-BMyiifTA.js → popover-B86Dbfhf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-D7ACiMAO.js → provider-scoped-model-input-DFm6N2f7.js} +1 -1
- package/dist/assets/{providers-list-gg7LrfuB.js → providers-list-BJcLOjun.js} +1 -1
- package/dist/assets/remote-BOxo9iwd.js +1 -0
- package/dist/assets/{runtime-config-page-BT_VV41p.js → runtime-config-page-CjLhnbSl.js} +1 -1
- package/dist/assets/{search-config-0VTPpz-w.js → search-config-J4Htco-P.js} +1 -1
- package/dist/assets/{secrets-config-DwQbLLEy.js → secrets-config-CUdERjco.js} +1 -1
- package/dist/assets/{select-DTdzR8j8.js → select-CJ0wbo3D.js} +1 -1
- package/dist/assets/{sessions-config-page-CAG7Zevv.js → sessions-config-page-DpK991fs.js} +2 -2
- package/dist/assets/{setting-row-CvKngoNI.js → setting-row-D1Yygqp7.js} +1 -1
- package/dist/assets/{tag-chip-BywQeHJj.js → tag-chip-FrkmkT8r.js} +1 -1
- package/dist/assets/{theme-provider-COAwWFv8.js → theme-provider-0hxjiPc_.js} +1 -1
- package/dist/assets/{tooltip-BOYp8Ue7.js → tooltip-Cj4yA0gH.js} +1 -1
- package/dist/assets/{use-config-DTwhNDQE.js → use-config-38Ur-89i.js} +1 -1
- package/dist/assets/{use-confirm-dialog-oeSqhmrx.js → use-confirm-dialog-DPQThaeU.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-X3KGuME8.js → use-infinite-scroll-loader-5Gf1xQi7.js} +1 -1
- package/dist/assets/{use-viewport-layout-C0NJAVXs.js → use-viewport-layout-D1XzKeip.js} +1 -1
- package/dist/index.html +15 -15
- package/package.json +9 -9
- package/src/features/chat/components/chat-sidebar-session-item.tsx +1 -1
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +2 -2
- package/src/features/chat/components/layout/chat-sidebar.test.tsx +74 -0
- package/src/features/chat/components/layout/chat-sidebar.tsx +28 -29
- package/src/features/chat/hooks/use-hydrated-ncp-agent.test.tsx +6 -0
- package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +158 -69
- package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
- package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
- package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
- package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
- package/src/features/chat/managers/chat-session-list.manager.test.ts +19 -16
- package/src/features/chat/managers/chat-session-list.manager.ts +20 -24
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +23 -12
- package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
- package/src/features/chat/pages/ncp-chat-page.tsx +23 -13
- package/src/features/chat/stores/chat-session-list.store.ts +2 -3
- package/src/features/chat/types/chat-stream.types.ts +1 -1
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +1 -1
- package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
- package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
- package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
- 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
|
|
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
|
-
|
|
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).
|
|
68
|
-
expect(
|
|
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
|
-
|
|
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).
|
|
92
|
-
expect(useChatThreadStore.getState().snapshot.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
|
-
|
|
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).
|
|
113
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
113
114
|
});
|
|
114
115
|
|
|
115
|
-
it('
|
|
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).
|
|
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
|
-
|
|
153
|
+
manager.createSession('native', '/tmp/project-alpha');
|
|
153
154
|
|
|
154
155
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
155
|
-
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).
|
|
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('
|
|
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.
|
|
247
|
+
manager.materializeRootSessionRoute('ncp-materialized-session');
|
|
247
248
|
|
|
248
|
-
expect(
|
|
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 = (
|
|
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):
|
|
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:
|
|
111
|
+
draftSessionKey: null
|
|
114
112
|
});
|
|
115
|
-
this.syncDraftThreadState(
|
|
113
|
+
this.syncDraftThreadState();
|
|
116
114
|
useChatInputStore.getState().setSnapshot({
|
|
117
115
|
pendingSessionType: nextSessionType,
|
|
118
116
|
pendingProjectRoot: normalizedProjectRoot,
|
|
119
|
-
pendingProjectRootSessionKey:
|
|
117
|
+
pendingProjectRootSessionKey: null
|
|
120
118
|
});
|
|
121
119
|
this.uiManager.goToChatRoot();
|
|
122
|
-
return nextSessionKey;
|
|
123
120
|
};
|
|
124
121
|
|
|
125
|
-
startAgentDraftChat = (agentId: string, sessionType: string):
|
|
122
|
+
startAgentDraftChat = (agentId: string, sessionType: string): void => {
|
|
126
123
|
const normalizedAgentId = agentId.trim() || 'main';
|
|
127
|
-
|
|
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(
|
|
137
|
+
this.syncDraftThreadState();
|
|
142
138
|
if (normalizedSessionType) {
|
|
143
139
|
useChatInputStore.getState().setSnapshot({ pendingSessionType: normalizedSessionType });
|
|
144
140
|
}
|
|
145
|
-
return
|
|
141
|
+
return null;
|
|
146
142
|
};
|
|
147
143
|
|
|
148
|
-
|
|
144
|
+
materializeRootSessionRoute = (sessionKey: string) => {
|
|
149
145
|
const normalizedSessionKey = sessionKey.trim();
|
|
150
146
|
if (!normalizedSessionKey) {
|
|
151
147
|
return;
|
|
152
148
|
}
|
|
153
|
-
|
|
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
|
-
|
|
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.
|
|
98
|
+
expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
it('
|
|
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:
|
|
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
|
-
|
|
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).
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 ??
|
|
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:
|
|
52
|
+
draftSessionKey: null,
|
|
54
53
|
selectedAgentId: 'main',
|
|
55
54
|
query: '',
|
|
56
55
|
listMode: 'time-first'
|
|
@@ -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
|
|
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.
|
|
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};
|