@nextclaw/ui 0.11.8 → 0.11.10
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 +21 -0
- package/dist/assets/{ChannelsList-dsxeZk5v.js → ChannelsList-C63gOoYI.js} +3 -3
- package/dist/assets/ChatPage-Ci3Gz0qh.js +37 -0
- package/dist/assets/{DocBrowser-B9RfxWIh.js → DocBrowser-CI4jOzJY.js} +1 -1
- package/dist/assets/{LogoBadge-iVDzhkNu.js → LogoBadge-DImV63-L.js} +1 -1
- package/dist/assets/{MarketplacePage-4MZIcD0K.js → MarketplacePage-B360oSAV.js} +1 -1
- package/dist/assets/{McpMarketplacePage-cwLMty-D.js → McpMarketplacePage-KIQgx_7h.js} +2 -2
- package/dist/assets/{ModelConfig-CNICBWzw.js → ModelConfig-Ben3tQoX.js} +1 -1
- package/dist/assets/{ProvidersList-CEHGsRSL.js → ProvidersList-DE-S9mq0.js} +1 -1
- package/dist/assets/RemoteAccessPage-DxUia6R-.js +1 -0
- package/dist/assets/RuntimeConfig-CQcGfNZT.js +1 -0
- package/dist/assets/{SearchConfig-CaFAgBMN.js → SearchConfig-DeOa-M6j.js} +1 -1
- package/dist/assets/{SecretsConfig-DzWq8hGZ.js → SecretsConfig-Ci8pJmzd.js} +2 -2
- package/dist/assets/{SessionsConfig-CJJTcxyQ.js → SessionsConfig-B6zq55yu.js} +2 -2
- package/dist/assets/chat-session-display--oo5yuIw.js +1 -0
- package/dist/assets/{index-BlrweCCh.js → index-LhlkB00c.js} +4 -4
- package/dist/assets/{label-DtssWSI4.js → label-3TKt0PoZ.js} +1 -1
- package/dist/assets/{page-layout-DnRqSldv.js → page-layout-CopkIM3Q.js} +1 -1
- package/dist/assets/{popover-Un2VFGcS.js → popover-CUx8uRJw.js} +1 -1
- package/dist/assets/security-config-BL29kTzz.js +1 -0
- package/dist/assets/{skeleton-DTFzTqqO.js → skeleton-Bs4zvcql.js} +1 -1
- package/dist/assets/{status-dot-DOJX6vii.js → status-dot-D6vJMwD7.js} +1 -1
- package/dist/assets/{switch-utBdpBRv.js → switch-A3-ClT1P.js} +1 -1
- package/dist/assets/{tabs-custom-ExyfvfgG.js → tabs-custom-BVSd5urq.js} +1 -1
- package/dist/assets/{useConfirmDialog-DjNtKs4n.js → useConfirmDialog-ChPriea6.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +5 -5
- package/src/api/ncp-session-query-cache.test.ts +89 -0
- package/src/api/ncp-session-query-cache.ts +85 -0
- package/src/api/types.ts +2 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +1 -1
- package/src/components/chat/ChatConversationPanel.tsx +62 -32
- package/src/components/chat/ChatSidebar.test.tsx +87 -92
- package/src/components/chat/ChatSidebar.tsx +21 -36
- package/src/components/chat/adapters/chat-message.adapter.ts +17 -5
- package/src/components/chat/chat-session-label.service.ts +3 -3
- package/src/components/chat/containers/chat-message-list.container.test.tsx +101 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +93 -54
- package/src/components/chat/managers/chat-session-list.manager.ts +0 -18
- package/src/components/chat/ncp/NcpChatPage.tsx +4 -52
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +4 -18
- package/src/components/chat/ncp/ncp-chat.presenter.ts +0 -2
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +0 -23
- package/src/components/chat/ncp/ncp-session-adapter.ts +0 -19
- package/src/components/chat/ncp/use-ncp-session-list-view.ts +42 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +0 -3
- package/src/components/chat/stores/chat-session-list.store.ts +1 -7
- package/src/components/chat/stores/chat-thread.store.ts +3 -3
- package/src/hooks/use-realtime-query-bridge.ts +14 -19
- package/src/hooks/useConfig.ts +10 -11
- package/dist/assets/ChatPage-CWK4Bckz.js +0 -37
- package/dist/assets/RemoteAccessPage-uYxoaQ8V.js +0 -1
- package/dist/assets/RuntimeConfig-CYQq4S_m.js +0 -1
- package/dist/assets/ncp-session-adapter-C-jqQqcV.js +0 -1
- package/dist/assets/security-config-B7Bkebpm.js +0 -1
- package/src/components/chat/managers/chat-run-status.manager.ts +0 -32
- package/src/components/chat/stores/chat-run-status.store.ts +0 -30
|
@@ -2,8 +2,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
|
2
2
|
import { MemoryRouter } from 'react-router-dom';
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { ChatSidebar } from '@/components/chat/ChatSidebar';
|
|
5
|
+
import type { NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
5
6
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
6
|
-
import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
|
|
7
7
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
8
8
|
|
|
9
9
|
const mocks = vi.hoisted(() => ({
|
|
@@ -11,9 +11,15 @@ const mocks = vi.hoisted(() => ({
|
|
|
11
11
|
setQuery: vi.fn(),
|
|
12
12
|
selectSession: vi.fn(),
|
|
13
13
|
docOpen: vi.fn(),
|
|
14
|
-
updateNcpSession: vi.fn()
|
|
14
|
+
updateNcpSession: vi.fn(),
|
|
15
|
+
sessionItems: [] as NcpSessionListItemView[],
|
|
16
|
+
isLoading: false
|
|
15
17
|
}));
|
|
16
18
|
|
|
19
|
+
function createSessionItem(session: NcpSessionListItemView['session']): NcpSessionListItemView {
|
|
20
|
+
return { session };
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
|
|
18
24
|
usePresenter: () => ({
|
|
19
25
|
chatSessionListManager: {
|
|
@@ -34,7 +40,27 @@ vi.mock('@/components/chat/chat-session-label.service', () => ({
|
|
|
34
40
|
useChatSessionLabelService: () => async (params: {
|
|
35
41
|
sessionKey: string;
|
|
36
42
|
label: string | null;
|
|
37
|
-
}) =>
|
|
43
|
+
}) => {
|
|
44
|
+
mocks.sessionItems = mocks.sessionItems.map((item) =>
|
|
45
|
+
item.session.key === params.sessionKey
|
|
46
|
+
? {
|
|
47
|
+
...item,
|
|
48
|
+
session: {
|
|
49
|
+
...item.session,
|
|
50
|
+
...(params.label ? { label: params.label } : { label: undefined })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
: item
|
|
54
|
+
);
|
|
55
|
+
return mocks.updateNcpSession(params.sessionKey, { label: params.label });
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
vi.mock('@/components/chat/ncp/use-ncp-session-list-view', () => ({
|
|
60
|
+
useNcpSessionListView: () => ({
|
|
61
|
+
isLoading: mocks.isLoading,
|
|
62
|
+
items: mocks.sessionItems
|
|
63
|
+
})
|
|
38
64
|
}));
|
|
39
65
|
|
|
40
66
|
vi.mock('@/components/common/BrandHeader', () => ({
|
|
@@ -72,6 +98,8 @@ describe('ChatSidebar', () => {
|
|
|
72
98
|
mocks.docOpen.mockReset();
|
|
73
99
|
mocks.updateNcpSession.mockReset();
|
|
74
100
|
mocks.updateNcpSession.mockResolvedValue({});
|
|
101
|
+
mocks.sessionItems = [];
|
|
102
|
+
mocks.isLoading = false;
|
|
75
103
|
|
|
76
104
|
useChatInputStore.setState({
|
|
77
105
|
snapshot: {
|
|
@@ -86,15 +114,7 @@ describe('ChatSidebar', () => {
|
|
|
86
114
|
useChatSessionListStore.setState({
|
|
87
115
|
snapshot: {
|
|
88
116
|
...useChatSessionListStore.getState().snapshot,
|
|
89
|
-
|
|
90
|
-
query: '',
|
|
91
|
-
isLoading: false
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
useChatRunStatusStore.setState({
|
|
95
|
-
snapshot: {
|
|
96
|
-
...useChatRunStatusStore.getState().snapshot,
|
|
97
|
-
sessionRunStatusByKey: new Map()
|
|
117
|
+
query: ''
|
|
98
118
|
}
|
|
99
119
|
});
|
|
100
120
|
});
|
|
@@ -145,22 +165,17 @@ describe('ChatSidebar', () => {
|
|
|
145
165
|
});
|
|
146
166
|
|
|
147
167
|
it('shows a session type badge for non-native sessions in the list', () => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
messageCount: 2
|
|
160
|
-
}
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
});
|
|
168
|
+
mocks.sessionItems = [
|
|
169
|
+
createSessionItem({
|
|
170
|
+
key: 'session:codex-1',
|
|
171
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
172
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
173
|
+
label: 'Codex Task',
|
|
174
|
+
sessionType: 'codex',
|
|
175
|
+
sessionTypeMutable: false,
|
|
176
|
+
messageCount: 2
|
|
177
|
+
})
|
|
178
|
+
];
|
|
164
179
|
|
|
165
180
|
render(
|
|
166
181
|
<MemoryRouter>
|
|
@@ -180,22 +195,17 @@ describe('ChatSidebar', () => {
|
|
|
180
195
|
sessionTypeOptions: [{ value: 'native', label: 'Native' }]
|
|
181
196
|
}
|
|
182
197
|
});
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
messageCount: 2
|
|
195
|
-
}
|
|
196
|
-
]
|
|
197
|
-
}
|
|
198
|
-
});
|
|
198
|
+
mocks.sessionItems = [
|
|
199
|
+
createSessionItem({
|
|
200
|
+
key: 'session:workspace-agent-1',
|
|
201
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
202
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
203
|
+
label: 'Workspace Task',
|
|
204
|
+
sessionType: 'workspace-agent',
|
|
205
|
+
sessionTypeMutable: false,
|
|
206
|
+
messageCount: 2
|
|
207
|
+
})
|
|
208
|
+
];
|
|
199
209
|
|
|
200
210
|
render(
|
|
201
211
|
<MemoryRouter>
|
|
@@ -214,22 +224,17 @@ describe('ChatSidebar', () => {
|
|
|
214
224
|
sessionTypeOptions: [{ value: 'native', label: 'Native' }]
|
|
215
225
|
}
|
|
216
226
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
messageCount: 1
|
|
229
|
-
}
|
|
230
|
-
]
|
|
231
|
-
}
|
|
232
|
-
});
|
|
227
|
+
mocks.sessionItems = [
|
|
228
|
+
createSessionItem({
|
|
229
|
+
key: 'session:native-1',
|
|
230
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
231
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
232
|
+
label: 'Native Task',
|
|
233
|
+
sessionType: 'native',
|
|
234
|
+
sessionTypeMutable: false,
|
|
235
|
+
messageCount: 1
|
|
236
|
+
})
|
|
237
|
+
];
|
|
233
238
|
|
|
234
239
|
render(
|
|
235
240
|
<MemoryRouter>
|
|
@@ -242,22 +247,17 @@ describe('ChatSidebar', () => {
|
|
|
242
247
|
});
|
|
243
248
|
|
|
244
249
|
it('edits the session label inline and saves through the ncp session api by default', async () => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
messageCount: 1
|
|
257
|
-
}
|
|
258
|
-
]
|
|
259
|
-
}
|
|
260
|
-
});
|
|
250
|
+
mocks.sessionItems = [
|
|
251
|
+
createSessionItem({
|
|
252
|
+
key: 'session:ncp-1',
|
|
253
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
254
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
255
|
+
label: 'Initial Label',
|
|
256
|
+
sessionType: 'native',
|
|
257
|
+
sessionTypeMutable: false,
|
|
258
|
+
messageCount: 1
|
|
259
|
+
})
|
|
260
|
+
];
|
|
261
261
|
|
|
262
262
|
render(
|
|
263
263
|
<MemoryRouter>
|
|
@@ -280,22 +280,17 @@ describe('ChatSidebar', () => {
|
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
it('cancels inline session label editing without saving', () => {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
messageCount: 1
|
|
295
|
-
}
|
|
296
|
-
]
|
|
297
|
-
}
|
|
298
|
-
});
|
|
283
|
+
mocks.sessionItems = [
|
|
284
|
+
createSessionItem({
|
|
285
|
+
key: 'session:ncp-2',
|
|
286
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
287
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
288
|
+
label: 'Cancelable Label',
|
|
289
|
+
sessionType: 'native',
|
|
290
|
+
sessionTypeMutable: false,
|
|
291
|
+
messageCount: 1
|
|
292
|
+
})
|
|
293
|
+
];
|
|
299
294
|
|
|
300
295
|
render(
|
|
301
296
|
<MemoryRouter>
|
|
@@ -8,9 +8,9 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
|
|
8
8
|
import { SelectItem } from '@/components/ui/select';
|
|
9
9
|
import { ChatSidebarSessionItem } from '@/components/chat/chat-sidebar-session-item';
|
|
10
10
|
import { useChatSessionLabelService } from '@/components/chat/chat-session-label.service';
|
|
11
|
+
import { useNcpSessionListView, type NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
11
12
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
12
13
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
13
|
-
import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
|
|
14
14
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
15
15
|
import { cn } from '@/lib/utils';
|
|
16
16
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
@@ -35,38 +35,39 @@ import {
|
|
|
35
35
|
|
|
36
36
|
type DateGroup = {
|
|
37
37
|
label: string;
|
|
38
|
-
|
|
38
|
+
items: NcpSessionListItemView[];
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
function groupSessionsByDate(
|
|
41
|
+
function groupSessionsByDate(items: NcpSessionListItemView[]): DateGroup[] {
|
|
42
42
|
const now = new Date();
|
|
43
43
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
44
44
|
const yesterdayStart = todayStart - 86_400_000;
|
|
45
45
|
const sevenDaysStart = todayStart - 7 * 86_400_000;
|
|
46
46
|
|
|
47
|
-
const today:
|
|
48
|
-
const yesterday:
|
|
49
|
-
const previous7:
|
|
50
|
-
const older:
|
|
47
|
+
const today: NcpSessionListItemView[] = [];
|
|
48
|
+
const yesterday: NcpSessionListItemView[] = [];
|
|
49
|
+
const previous7: NcpSessionListItemView[] = [];
|
|
50
|
+
const older: NcpSessionListItemView[] = [];
|
|
51
51
|
|
|
52
|
-
for (const
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
const { session } = item;
|
|
53
54
|
const ts = new Date(session.updatedAt).getTime();
|
|
54
55
|
if (ts >= todayStart) {
|
|
55
|
-
today.push(
|
|
56
|
+
today.push(item);
|
|
56
57
|
} else if (ts >= yesterdayStart) {
|
|
57
|
-
yesterday.push(
|
|
58
|
+
yesterday.push(item);
|
|
58
59
|
} else if (ts >= sevenDaysStart) {
|
|
59
|
-
previous7.push(
|
|
60
|
+
previous7.push(item);
|
|
60
61
|
} else {
|
|
61
|
-
older.push(
|
|
62
|
+
older.push(item);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
const groups: DateGroup[] = [];
|
|
66
|
-
if (today.length > 0) groups.push({ label: t('chatSidebarToday'),
|
|
67
|
-
if (yesterday.length > 0) groups.push({ label: t('chatSidebarYesterday'),
|
|
68
|
-
if (previous7.length > 0) groups.push({ label: t('chatSidebarPrevious7Days'),
|
|
69
|
-
if (older.length > 0) groups.push({ label: t('chatSidebarOlder'),
|
|
67
|
+
if (today.length > 0) groups.push({ label: t('chatSidebarToday'), items: today });
|
|
68
|
+
if (yesterday.length > 0) groups.push({ label: t('chatSidebarYesterday'), items: yesterday });
|
|
69
|
+
if (previous7.length > 0) groups.push({ label: t('chatSidebarPrevious7Days'), items: previous7 });
|
|
70
|
+
if (older.length > 0) groups.push({ label: t('chatSidebarOlder'), items: older });
|
|
70
71
|
return groups;
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -121,15 +122,15 @@ export function ChatSidebar() {
|
|
|
121
122
|
const [savingSessionKey, setSavingSessionKey] = useState<string | null>(null);
|
|
122
123
|
const inputSnapshot = useChatInputStore((state) => state.snapshot);
|
|
123
124
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
124
|
-
const runSnapshot = useChatRunStatusStore((state) => state.snapshot);
|
|
125
125
|
const connectionStatus = useUiStore((state) => state.connectionStatus);
|
|
126
|
+
const { isLoading, items } = useNcpSessionListView();
|
|
126
127
|
const { language, setLanguage } = useI18n();
|
|
127
128
|
const { theme, setTheme } = useTheme();
|
|
128
129
|
const updateSessionLabel = useChatSessionLabelService();
|
|
129
130
|
const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
|
|
130
131
|
const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
|
|
131
132
|
|
|
132
|
-
const groups = useMemo(() => groupSessionsByDate(
|
|
133
|
+
const groups = useMemo(() => groupSessionsByDate(items), [items]);
|
|
133
134
|
const defaultSessionType = inputSnapshot.defaultSessionType || 'native';
|
|
134
135
|
const nonDefaultSessionTypeOptions = useMemo(
|
|
135
136
|
() => inputSnapshot.sessionTypeOptions.filter((option) => option.value !== defaultSessionType),
|
|
@@ -142,20 +143,6 @@ export function ChatSidebar() {
|
|
|
142
143
|
window.location.reload();
|
|
143
144
|
};
|
|
144
145
|
|
|
145
|
-
const patchSessionLabelInStore = (sessionKey: string, label: string | undefined) => {
|
|
146
|
-
const { sessions } = useChatSessionListStore.getState().snapshot;
|
|
147
|
-
useChatSessionListStore.getState().setSnapshot({
|
|
148
|
-
sessions: sessions.map((session) =>
|
|
149
|
-
session.key === sessionKey
|
|
150
|
-
? {
|
|
151
|
-
...session,
|
|
152
|
-
...(label ? { label } : { label: undefined })
|
|
153
|
-
}
|
|
154
|
-
: session
|
|
155
|
-
)
|
|
156
|
-
});
|
|
157
|
-
};
|
|
158
|
-
|
|
159
146
|
const startEditingSessionLabel = (session: SessionEntryView) => {
|
|
160
147
|
setEditingSessionKey(session.key);
|
|
161
148
|
setDraftLabel(session.label?.trim() ?? '');
|
|
@@ -181,7 +168,6 @@ export function ChatSidebar() {
|
|
|
181
168
|
sessionKey: session.key,
|
|
182
169
|
label: normalizedLabel || null
|
|
183
170
|
});
|
|
184
|
-
patchSessionLabelInStore(session.key, normalizedLabel || undefined);
|
|
185
171
|
cancelEditingSessionLabel();
|
|
186
172
|
} catch {
|
|
187
173
|
setSavingSessionKey(null);
|
|
@@ -297,7 +283,7 @@ export function ChatSidebar() {
|
|
|
297
283
|
<div className="mx-4 border-t border-gray-200/60" />
|
|
298
284
|
|
|
299
285
|
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-3 py-2">
|
|
300
|
-
{
|
|
286
|
+
{isLoading ? (
|
|
301
287
|
<div className="text-xs text-gray-500 p-3">{t('sessionsLoading')}</div>
|
|
302
288
|
) : groups.length === 0 ? (
|
|
303
289
|
<div className="p-4 text-center">
|
|
@@ -312,9 +298,8 @@ export function ChatSidebar() {
|
|
|
312
298
|
{group.label}
|
|
313
299
|
</div>
|
|
314
300
|
<div className="space-y-0.5">
|
|
315
|
-
{group.
|
|
301
|
+
{group.items.map(({ session, runStatus }) => {
|
|
316
302
|
const active = listSnapshot.selectedSessionKey === session.key;
|
|
317
|
-
const runStatus = runSnapshot.sessionRunStatusByKey.get(session.key);
|
|
318
303
|
const sessionTypeLabel = resolveSessionTypeLabel(session.sessionType, inputSnapshot.sessionTypeOptions);
|
|
319
304
|
const isEditing = editingSessionKey === session.key;
|
|
320
305
|
const isSaving = savingSessionKey === session.key;
|
|
@@ -317,12 +317,16 @@ function toRenderableText(value: string): string | null {
|
|
|
317
317
|
return visible ? trimmed : null;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
|
|
321
|
-
uiMessages: ChatMessageSource[];
|
|
320
|
+
type ChatMessageAdapterParams = {
|
|
322
321
|
texts: ChatMessageAdapterTexts;
|
|
323
322
|
formatTimestamp: (value: string) => string;
|
|
324
|
-
}
|
|
325
|
-
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export function adaptChatMessage(
|
|
326
|
+
message: ChatMessageSource,
|
|
327
|
+
params: ChatMessageAdapterParams,
|
|
328
|
+
): ChatMessageViewModel {
|
|
329
|
+
return {
|
|
326
330
|
id: message.id,
|
|
327
331
|
role: resolveUiRole(message.role),
|
|
328
332
|
roleLabel: resolveRoleLabel(message.role, params.texts.roleLabels),
|
|
@@ -421,5 +425,13 @@ export function adaptChatMessages(params: {
|
|
|
421
425
|
};
|
|
422
426
|
})
|
|
423
427
|
.filter((part) => part !== null),
|
|
424
|
-
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function adaptChatMessages(params: {
|
|
432
|
+
uiMessages: ChatMessageSource[];
|
|
433
|
+
texts: ChatMessageAdapterTexts;
|
|
434
|
+
formatTimestamp: (value: string) => string;
|
|
435
|
+
}): ChatMessageViewModel[] {
|
|
436
|
+
return params.uiMessages.map((message) => adaptChatMessage(message, params));
|
|
425
437
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
|
+
import { upsertNcpSessionSummaryInQueryClient } from '@/api/ncp-session-query-cache';
|
|
3
4
|
import { updateNcpSession } from '@/api/ncp-session';
|
|
4
5
|
import { t } from '@/lib/i18n';
|
|
5
6
|
|
|
@@ -13,9 +14,8 @@ export function useChatSessionLabelService() {
|
|
|
13
14
|
|
|
14
15
|
return async (params: UpdateChatSessionLabelParams): Promise<void> => {
|
|
15
16
|
try {
|
|
16
|
-
await updateNcpSession(params.sessionKey, { label: params.label });
|
|
17
|
-
queryClient
|
|
18
|
-
queryClient.invalidateQueries({ queryKey: ['ncp-session-messages', params.sessionKey] });
|
|
17
|
+
const updated = await updateNcpSession(params.sessionKey, { label: params.label });
|
|
18
|
+
upsertNcpSessionSummaryInQueryClient(queryClient, updated);
|
|
19
19
|
toast.success(t('configSavedApplied'));
|
|
20
20
|
} catch (error) {
|
|
21
21
|
toast.error(t('configSaveFailed') + ': ' + (error instanceof Error ? error.message : String(error)));
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import type { NcpMessage } from "@nextclaw/ncp";
|
|
3
|
+
import { beforeEach, expect, it, vi } from "vitest";
|
|
4
|
+
import { ChatMessageListContainer } from "./chat-message-list.container";
|
|
5
|
+
|
|
6
|
+
const captures = vi.hoisted(() => ({
|
|
7
|
+
renders: [] as Array<{ messages: unknown[] }>,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@nextclaw/agent-chat-ui", () => ({
|
|
11
|
+
ChatMessageList: (props: { messages: unknown[] }) => {
|
|
12
|
+
captures.renders.push(props);
|
|
13
|
+
return <div data-testid="chat-message-list" />;
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("@/components/providers/I18nProvider", () => ({
|
|
18
|
+
useI18n: () => ({ language: "en" }),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/lib/i18n", () => ({
|
|
22
|
+
formatDateTime: (value: string) => `formatted:${value}`,
|
|
23
|
+
t: (key: string) => key,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
captures.renders = [];
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("reuses adapted message references when the source message object is unchanged", () => {
|
|
31
|
+
const message = {
|
|
32
|
+
id: "assistant-1",
|
|
33
|
+
sessionId: "session-1",
|
|
34
|
+
role: "assistant",
|
|
35
|
+
status: "streaming",
|
|
36
|
+
timestamp: "2026-03-17T10:00:00.000Z",
|
|
37
|
+
parts: [{ type: "text", text: "hello" }],
|
|
38
|
+
} satisfies NcpMessage;
|
|
39
|
+
|
|
40
|
+
const { rerender } = render(
|
|
41
|
+
<ChatMessageListContainer messages={[message]} isSending={false} />,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const firstMessages =
|
|
45
|
+
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
46
|
+
|
|
47
|
+
rerender(
|
|
48
|
+
<ChatMessageListContainer messages={[message]} isSending={false} />,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const secondMessages =
|
|
52
|
+
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
53
|
+
|
|
54
|
+
expect(secondMessages[0]).toBe(firstMessages[0]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("keeps historical adapted message references stable when only the streaming message changes", () => {
|
|
58
|
+
const historicalMessage = {
|
|
59
|
+
id: "assistant-1",
|
|
60
|
+
sessionId: "session-1",
|
|
61
|
+
role: "assistant",
|
|
62
|
+
status: "final",
|
|
63
|
+
timestamp: "2026-03-17T10:00:00.000Z",
|
|
64
|
+
parts: [{ type: "text", text: "history" }],
|
|
65
|
+
} satisfies NcpMessage;
|
|
66
|
+
const firstStreamingMessage = {
|
|
67
|
+
id: "assistant-2",
|
|
68
|
+
sessionId: "session-1",
|
|
69
|
+
role: "assistant",
|
|
70
|
+
status: "streaming",
|
|
71
|
+
timestamp: "2026-03-17T10:00:01.000Z",
|
|
72
|
+
parts: [{ type: "text", text: "hello" }],
|
|
73
|
+
} satisfies NcpMessage;
|
|
74
|
+
|
|
75
|
+
const { rerender } = render(
|
|
76
|
+
<ChatMessageListContainer
|
|
77
|
+
messages={[historicalMessage, firstStreamingMessage]}
|
|
78
|
+
isSending={false}
|
|
79
|
+
/>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const firstMessages =
|
|
83
|
+
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
84
|
+
const nextStreamingMessage = {
|
|
85
|
+
...firstStreamingMessage,
|
|
86
|
+
parts: [{ type: "text", text: "hello world" }],
|
|
87
|
+
} satisfies NcpMessage;
|
|
88
|
+
|
|
89
|
+
rerender(
|
|
90
|
+
<ChatMessageListContainer
|
|
91
|
+
messages={[historicalMessage, nextStreamingMessage]}
|
|
92
|
+
isSending={false}
|
|
93
|
+
/>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const secondMessages =
|
|
97
|
+
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
98
|
+
|
|
99
|
+
expect(secondMessages[0]).toBe(firstMessages[0]);
|
|
100
|
+
expect(secondMessages[1]).not.toBe(firstMessages[1]);
|
|
101
|
+
});
|