@nextclaw/ui 0.12.13 → 0.12.15
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 +20 -0
- package/dist/assets/{api-C51456xV.js → api-HsS9C7l0.js} +1 -1
- package/dist/assets/{app-manager-provider-D_cKqqRG.js → app-manager-provider-CNa6cmOk.js} +1 -1
- package/dist/assets/{app-navigation.config-Dve1W20Y.js → app-navigation.config-Dy69P_3O.js} +1 -1
- package/dist/assets/{book-open-B4mOKdz8.js → book-open-DJSe9YLj.js} +1 -1
- package/dist/assets/{channels-list-page-WJ7d4zMI.js → channels-list-page-DtjYJ1FX.js} +1 -1
- package/dist/assets/chat-Rmwi-kQn.js +58 -0
- package/dist/assets/{chat-page-CHKwiqPY.js → chat-page-Cslvp6SG.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-ptDyT_1C.js → chunk-JZWAC4HX-CMPxE3BJ.js} +1 -1
- package/dist/assets/{config-split-page-B3PRA_AV.js → config-split-page-v7p1CQjU.js} +1 -1
- package/dist/assets/{createLucideIcon-C_GFKVuW.js → createLucideIcon-Y1cWg3R9.js} +1 -1
- package/dist/assets/{desktop-update-config-gnna2NaS.js → desktop-update-config-CqJmmpjR.js} +1 -1
- package/dist/assets/{dialog-BHcaU6NE.js → dialog-CYis0wx_.js} +1 -1
- package/dist/assets/{dist-DtBFqZ6_.js → dist-DIWOVzPY.js} +1 -1
- package/dist/assets/{doc-browser-CwgI7ipB.js → doc-browser-BQT0fa3g.js} +1 -1
- package/dist/assets/{doc-browser-CoKIUCJj.js → doc-browser-CP6kjGaF.js} +1 -1
- package/dist/assets/doc-browser-DK2tXLi_.js +1 -0
- package/dist/assets/{doc-browser-context-Dib9sS83.js → doc-browser-context-BacwCbOG.js} +1 -1
- package/dist/assets/{es2015-BlNhrQUG.js → es2015-3VnZDFPs.js} +1 -1
- package/dist/assets/{external-link-DP2IJ7AM.js → external-link-CPhZo92k.js} +1 -1
- package/dist/assets/{folder-BPwc278w.js → folder-orBwjR8h.js} +1 -1
- package/dist/assets/{hash-CvcvtMBq.js → hash-BvWW8Scd.js} +1 -1
- package/dist/assets/i18n-BhoFaF7E.js +1 -0
- package/dist/assets/{index-CxzW1dQ9.js → index-D0_J-bk_.js} +2 -2
- package/dist/assets/index-OrUEVLgT.css +1 -0
- package/dist/assets/{key-round-BQXmPSxD.js → key-round-B5u2XQwa.js} +1 -1
- package/dist/assets/loader-circle-DotuB8NZ.js +1 -0
- package/dist/assets/{logo-badge-uB4SwANR.js → logo-badge-BLQdO_th.js} +1 -1
- package/dist/assets/{logos-BcELLmYh.js → logos-WPtZ1joO.js} +1 -1
- package/dist/assets/marketplace-page-CJHkdgv_.js +1 -0
- package/dist/assets/{marketplace-page-DiqqX25V.js → marketplace-page-It54wla_.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-C_akqPwv.js → mcp-marketplace-page-C64VUmni.js} +1 -1
- package/dist/assets/mcp-marketplace-page-DIItTRy_.js +1 -0
- package/dist/assets/message-square-CO_7rU-N.js +1 -0
- package/dist/assets/{model-config-B0L43HTL.js → model-config-Cu6zLI8a.js} +1 -1
- package/dist/assets/{notice-card-C9PFAR67.js → notice-card-DDVM6IFQ.js} +1 -1
- package/dist/assets/play-kSfmJAme.js +1 -0
- package/dist/assets/plus-BNaF7Wq5.js +1 -0
- package/dist/assets/{popover-B8msg2FQ.js → popover-Dliq2K8O.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DeAo2Y65.js → provider-scoped-model-input-D0oXIrWA.js} +1 -1
- package/dist/assets/{providers-list-5_VShcn7.js → providers-list-CrwTWzge.js} +1 -1
- package/dist/assets/{refresh-ccw-CeG203yU.js → refresh-ccw-pX3BUxPF.js} +1 -1
- package/dist/assets/remote-BpAw88FR.js +1 -0
- package/dist/assets/{rotate-cw-F7aThvYj.js → rotate-cw-Bm8W8iaF.js} +1 -1
- package/dist/assets/{runtime-config-page-BuWmH7fe.js → runtime-config-page-CYZyCr04.js} +1 -1
- package/dist/assets/{save-7ztImRj7.js → save-CZ9zvmBJ.js} +1 -1
- package/dist/assets/{search-DZSNKEGp.js → search-DpxqxcpP.js} +1 -1
- package/dist/assets/{search-config-DJTm9Fno.js → search-config-D_H6cr07.js} +1 -1
- package/dist/assets/{secrets-config-DKFeFii1.js → secrets-config-BG9fic_R.js} +1 -1
- package/dist/assets/{select-DRDejPLk.js → select-D-gjRjhr.js} +1 -1
- package/dist/assets/{sessions-config-page-DZrdd2zT.js → sessions-config-page-DzSOCrQt.js} +1 -1
- package/dist/assets/{setting-row-BcF6eTW0.js → setting-row-Cl4_XpIR.js} +1 -1
- package/dist/assets/{settings-DjvNMJde.js → settings-C9WgQdGW.js} +1 -1
- package/dist/assets/skeleton-BN6fOana.js +1 -0
- package/dist/assets/{sparkles-CyDTgTM4.js → sparkles-gvRilA9f.js} +1 -1
- package/dist/assets/{status-dot-aQU9Mia4.js → status-dot-W2TPs1Zf.js} +1 -1
- package/dist/assets/{tabs-custom-C4P7g4vR.js → tabs-custom-BxZfWeD1.js} +1 -1
- package/dist/assets/{tag-chip-CVIqyMv7.js → tag-chip-DfmhmN50.js} +1 -1
- package/dist/assets/{theme-provider-dHqcWU-j.js → theme-provider-CajCgE_N.js} +1 -1
- package/dist/assets/{tooltip-C6VPreZ7.js → tooltip-C1XVx_h1.js} +1 -1
- package/dist/assets/{trash-2-C1cdqL6V.js → trash-2-WC4Dlakj.js} +1 -1
- package/dist/assets/{use-config-DFja1sda.js → use-config-BIErdQNR.js} +1 -1
- package/dist/assets/{use-confirm-dialog-DvIbSUX3.js → use-confirm-dialog-B3ZsM4V-.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-D8h0k-iL.js → use-infinite-scroll-loader-rO55YHGO.js} +1 -1
- package/dist/assets/{use-viewport-layout-D-pjxsyz.js → use-viewport-layout-zLAsjh3A.js} +1 -1
- package/dist/assets/x-D1867E7F.js +1 -0
- package/dist/index.html +39 -39
- package/package.json +3 -3
- package/src/features/chat/components/chat-session-type-option-item.test.tsx +16 -0
- package/src/features/chat/components/chat-session-type-option-item.tsx +4 -1
- package/src/features/chat/components/chat-sidebar-session-item.tsx +22 -24
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +18 -1
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +2 -1
- package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -1
- package/src/features/chat/components/layout/chat-sidebar.tsx +36 -45
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +29 -0
- package/src/features/chat/utils/chat-input-bar.utils.test.ts +27 -1
- package/src/features/chat/utils/chat-input-toolbar.utils.ts +20 -19
- package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +35 -3
- package/src/features/chat/utils/ncp-chat-input-availability.utils.ts +4 -6
- package/src/shared/lib/i18n/chat.ts +1 -1
- package/dist/assets/chat-DCi1-y8U.js +0 -58
- package/dist/assets/doc-browser-DYKpRqe-.js +0 -1
- package/dist/assets/i18n-BnNAQpVM.js +0 -1
- package/dist/assets/index-mRmSAB-e.css +0 -1
- package/dist/assets/loader-circle-C6gg2m2a.js +0 -1
- package/dist/assets/marketplace-page-0sEdt5sA.js +0 -1
- package/dist/assets/mcp-marketplace-page-B8vmu9xe.js +0 -1
- package/dist/assets/message-square-CLVODA23.js +0 -1
- package/dist/assets/play-DeNVUA5C.js +0 -1
- package/dist/assets/plus-BptLViq1.js +0 -1
- package/dist/assets/remote-pzp4oLcL.js +0 -1
- package/dist/assets/skeleton-5Mg6vZHN.js +0 -1
- package/dist/assets/x-BjMO7v8q.js +0 -1
|
@@ -7,7 +7,8 @@ import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
|
|
|
7
7
|
import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
|
|
8
8
|
|
|
9
9
|
const mocks = vi.hoisted(() => ({
|
|
10
|
-
createSession: vi.fn(),
|
|
10
|
+
createSession: vi.fn(() => 'draft-session-key'),
|
|
11
|
+
goToSession: vi.fn(),
|
|
11
12
|
setQuery: vi.fn(),
|
|
12
13
|
setListMode: vi.fn(),
|
|
13
14
|
selectSession: vi.fn(),
|
|
@@ -28,6 +29,9 @@ function createSessionItem(
|
|
|
28
29
|
|
|
29
30
|
vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
|
|
30
31
|
usePresenter: () => ({
|
|
32
|
+
chatUiManager: {
|
|
33
|
+
goToSession: mocks.goToSession,
|
|
34
|
+
},
|
|
31
35
|
chatSessionListManager: {
|
|
32
36
|
createSession: mocks.createSession,
|
|
33
37
|
setQuery: mocks.setQuery,
|
|
@@ -115,6 +119,8 @@ vi.mock('@/features/system-status', () => ({
|
|
|
115
119
|
|
|
116
120
|
function resetSidebarTestState() {
|
|
117
121
|
mocks.createSession.mockReset();
|
|
122
|
+
mocks.createSession.mockReturnValue('draft-session-key');
|
|
123
|
+
mocks.goToSession.mockReset();
|
|
118
124
|
mocks.setQuery.mockReset();
|
|
119
125
|
mocks.setListMode.mockReset();
|
|
120
126
|
mocks.selectSession.mockReset();
|
|
@@ -228,6 +234,7 @@ describe('ChatSidebar create and list basics', () => {
|
|
|
228
234
|
|
|
229
235
|
expect(mocks.setQuery).toHaveBeenCalledWith('release notes');
|
|
230
236
|
expect(mocks.createSession).toHaveBeenCalledWith('codex');
|
|
237
|
+
expect(mocks.goToSession).toHaveBeenCalledWith('draft-session-key');
|
|
231
238
|
});
|
|
232
239
|
|
|
233
240
|
it('creates the default session directly from the compact mobile add button when no menu is needed', () => {
|
|
@@ -248,6 +255,7 @@ describe('ChatSidebar create and list basics', () => {
|
|
|
248
255
|
fireEvent.click(screen.getByRole('button', { name: 'New Task' }));
|
|
249
256
|
|
|
250
257
|
expect(mocks.createSession).toHaveBeenCalledWith('native');
|
|
258
|
+
expect(mocks.goToSession).toHaveBeenCalledWith('draft-session-key');
|
|
251
259
|
});
|
|
252
260
|
|
|
253
261
|
it('shows a session type badge for non-native sessions in the list', () => {
|
|
@@ -408,6 +416,7 @@ describe('ChatSidebar project-first mode', () => {
|
|
|
408
416
|
fireEvent.click(screen.getByText('Codex'));
|
|
409
417
|
|
|
410
418
|
expect(mocks.createSession).toHaveBeenCalledWith('codex', '/tmp/project-beta');
|
|
419
|
+
expect(mocks.goToSession).not.toHaveBeenCalled();
|
|
411
420
|
});
|
|
412
421
|
|
|
413
422
|
it('creates immediately when there is only one available runtime type', () => {
|
|
@@ -447,6 +456,41 @@ describe('ChatSidebar project-first mode', () => {
|
|
|
447
456
|
fireEvent.click(screen.getByRole('button', { name: 'New Task · project-gamma' }));
|
|
448
457
|
|
|
449
458
|
expect(mocks.createSession).toHaveBeenCalledWith('native', '/tmp/project-gamma');
|
|
459
|
+
expect(mocks.goToSession).not.toHaveBeenCalled();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('opens the draft detail after creating a project-bound session on mobile', () => {
|
|
463
|
+
useChatSessionListStore.setState({
|
|
464
|
+
snapshot: {
|
|
465
|
+
...useChatSessionListStore.getState().snapshot,
|
|
466
|
+
listMode: 'project-first'
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
mocks.sessionItems = [
|
|
470
|
+
createSessionItem({
|
|
471
|
+
key: 'session:project-mobile-1',
|
|
472
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
473
|
+
updatedAt: '2026-03-19T11:05:00.000Z',
|
|
474
|
+
label: 'Grouped Mobile Task',
|
|
475
|
+
projectRoot: '/tmp/project-mobile',
|
|
476
|
+
projectName: 'project-mobile',
|
|
477
|
+
sessionType: 'native',
|
|
478
|
+
sessionTypeMutable: false,
|
|
479
|
+
messageCount: 2
|
|
480
|
+
})
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
render(
|
|
484
|
+
<MemoryRouter>
|
|
485
|
+
<ChatSidebar variant="mobile" />
|
|
486
|
+
</MemoryRouter>
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
fireEvent.click(screen.getByRole('button', { name: 'New Task · project-mobile' }));
|
|
490
|
+
fireEvent.click(screen.getByText('Codex'));
|
|
491
|
+
|
|
492
|
+
expect(mocks.createSession).toHaveBeenCalledWith('codex', '/tmp/project-mobile');
|
|
493
|
+
expect(mocks.goToSession).toHaveBeenCalledWith('draft-session-key');
|
|
450
494
|
});
|
|
451
495
|
});
|
|
452
496
|
|
|
@@ -8,10 +8,7 @@ import { useChatSidebarSessionLabelEditor } from '@/features/chat/hooks/use-chat
|
|
|
8
8
|
import { useNcpSessionListView, type NcpSessionListItemView } from '@/features/chat/hooks/use-ncp-session-list-view';
|
|
9
9
|
import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
|
|
10
10
|
import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
|
|
11
|
-
import {
|
|
12
|
-
shouldShowUnreadSessionIndicator,
|
|
13
|
-
useChatSessionListStore
|
|
14
|
-
} from '@/features/chat/stores/chat-session-list.store';
|
|
11
|
+
import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
|
|
15
12
|
import { useSystemStatus } from '@/features/system-status';
|
|
16
13
|
import { useAgents } from '@/shared/hooks/use-agents';
|
|
17
14
|
import { getSessionProjectName } from '@/shared/lib/session-project';
|
|
@@ -230,32 +227,33 @@ export function ChatSidebar({
|
|
|
230
227
|
setLanguage(nextLang);
|
|
231
228
|
window.location.reload();
|
|
232
229
|
};
|
|
233
|
-
const renderSessionItem = (item: NcpSessionListItemView) =>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
230
|
+
const renderSessionItem = (item: NcpSessionListItemView) => (
|
|
231
|
+
<ChatSidebarSessionEntry
|
|
232
|
+
key={item.session.key}
|
|
233
|
+
item={item}
|
|
234
|
+
selectedSessionKey={listSnapshot.selectedSessionKey}
|
|
235
|
+
optimisticReadAtBySessionKey={optimisticReadAtBySessionKey}
|
|
236
|
+
agentsById={agentsById}
|
|
237
|
+
childSessionsByParentKey={childSessionsByParentKey}
|
|
238
|
+
editingSessionKey={editingSessionKey}
|
|
239
|
+
draftLabel={draftLabel}
|
|
240
|
+
savingSessionKey={savingSessionKey}
|
|
241
|
+
sessionTitle={sessionTitle}
|
|
242
|
+
onSelectSession={presenter.chatSessionListManager.selectSession}
|
|
243
|
+
onOpenChildSessions={(parentSessionKey, activeChildSessionKey) => presenter.chatThreadManager.openChildSessionPanel({ parentSessionKey, activeChildSessionKey })}
|
|
244
|
+
onStartEditingSessionLabel={startEditingSessionLabel}
|
|
245
|
+
onDraftLabelChange={setDraftLabel}
|
|
246
|
+
onSaveSessionLabel={saveSessionLabel}
|
|
247
|
+
onCancelEditingSessionLabel={cancelEditingSessionLabel}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
const createSessionAndOpenIfNeeded = (sessionType: string, projectRoot?: string | null) => {
|
|
251
|
+
const sessionKey = typeof projectRoot === "string"
|
|
252
|
+
? presenter.chatSessionListManager.createSession(sessionType, projectRoot)
|
|
253
|
+
: presenter.chatSessionListManager.createSession(sessionType);
|
|
254
|
+
if (isMobileVariant) presenter.chatUiManager.goToSession(sessionKey);
|
|
255
|
+
};
|
|
256
|
+
|
|
259
257
|
return (
|
|
260
258
|
<aside
|
|
261
259
|
className={cn(
|
|
@@ -282,7 +280,7 @@ export function ChatSidebar({
|
|
|
282
280
|
nonDefaultSessionTypeOptions={nonDefaultSessionTypeOptions}
|
|
283
281
|
isCreateMenuOpen={isCreateMenuOpen}
|
|
284
282
|
onCreateMenuOpenChange={setIsCreateMenuOpen}
|
|
285
|
-
onCreateSession={
|
|
283
|
+
onCreateSession={createSessionAndOpenIfNeeded}
|
|
286
284
|
onQueryChange={presenter.chatSessionListManager.setQuery}
|
|
287
285
|
/>
|
|
288
286
|
) : (
|
|
@@ -293,7 +291,7 @@ export function ChatSidebar({
|
|
|
293
291
|
nonDefaultSessionTypeOptions={nonDefaultSessionTypeOptions}
|
|
294
292
|
isCreateMenuOpen={isCreateMenuOpen}
|
|
295
293
|
onCreateMenuOpenChange={setIsCreateMenuOpen}
|
|
296
|
-
onCreateSession={
|
|
294
|
+
onCreateSession={createSessionAndOpenIfNeeded}
|
|
297
295
|
onQueryChange={presenter.chatSessionListManager.setQuery}
|
|
298
296
|
/>
|
|
299
297
|
)}
|
|
@@ -301,18 +299,11 @@ export function ChatSidebar({
|
|
|
301
299
|
{!isMobileVariant ? (
|
|
302
300
|
<div className="px-3 pb-2">
|
|
303
301
|
<ul className="space-y-0.5">
|
|
304
|
-
{navItems.map((item) =>
|
|
305
|
-
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
label={item.label()}
|
|
310
|
-
icon={item.icon}
|
|
311
|
-
density="compact"
|
|
312
|
-
/>
|
|
313
|
-
</li>
|
|
314
|
-
);
|
|
315
|
-
})}
|
|
302
|
+
{navItems.map((item) => (
|
|
303
|
+
<li key={item.target}>
|
|
304
|
+
<SidebarNavLinkItem to={item.target} label={item.label()} icon={item.icon} density="compact" />
|
|
305
|
+
</li>
|
|
306
|
+
))}
|
|
316
307
|
</ul>
|
|
317
308
|
</div>
|
|
318
309
|
) : null}
|
|
@@ -346,7 +337,7 @@ export function ChatSidebar({
|
|
|
346
337
|
defaultSessionType={defaultSessionType}
|
|
347
338
|
sessionTypeOptions={inputSnapshot.sessionTypeOptions}
|
|
348
339
|
renderSessionItem={renderSessionItem}
|
|
349
|
-
onCreateSession={
|
|
340
|
+
onCreateSession={createSessionAndOpenIfNeeded}
|
|
350
341
|
/>
|
|
351
342
|
)
|
|
352
343
|
) : groups.length === 0 ? (
|
|
@@ -146,4 +146,33 @@ describe('NcpChatInputManager', () => {
|
|
|
146
146
|
expect(streamActionsManager.sendMessage).not.toHaveBeenCalled();
|
|
147
147
|
expect(sessionListManager.promoteRootDraftSessionRoute).not.toHaveBeenCalled();
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
it('still attempts to send when provider metadata is stale or the session type is marked unavailable', async () => {
|
|
151
|
+
useChatInputStore.setState({
|
|
152
|
+
snapshot: {
|
|
153
|
+
...useChatInputStore.getState().snapshot,
|
|
154
|
+
isProviderStateResolved: true,
|
|
155
|
+
modelOptions: [],
|
|
156
|
+
sessionTypeUnavailable: true,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const streamActionsManager = {
|
|
160
|
+
sendMessage: vi.fn().mockResolvedValue(undefined),
|
|
161
|
+
stopCurrentRun: vi.fn().mockResolvedValue(undefined),
|
|
162
|
+
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
|
|
163
|
+
const sessionListManager = {
|
|
164
|
+
ensureDraftSession: vi.fn(() => 'draft-session'),
|
|
165
|
+
promoteRootDraftSessionRoute: vi.fn(),
|
|
166
|
+
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
|
|
167
|
+
const manager = new NcpChatInputManager(
|
|
168
|
+
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
169
|
+
streamActionsManager,
|
|
170
|
+
sessionListManager,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await manager.send();
|
|
174
|
+
|
|
175
|
+
expect(streamActionsManager.sendMessage).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('current-route-session');
|
|
177
|
+
});
|
|
149
178
|
});
|
|
@@ -212,13 +212,39 @@ describe('buildModelToolbarSelect', () => {
|
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
expect(select.value).toBe('minimax/MiniMax-M2.7');
|
|
215
|
-
expect(select.selectedLabel).toBe('MiniMax
|
|
215
|
+
expect(select.selectedLabel).toBe('MiniMax-M2.7');
|
|
216
216
|
expect(select.options[0]).toEqual({
|
|
217
217
|
value: 'minimax/MiniMax-M2.7',
|
|
218
218
|
label: 'MiniMax/MiniMax-M2.7'
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
it('keeps provider labels in the dropdown while using only the model name in the trigger', () => {
|
|
223
|
+
const select = buildModelToolbarSelect({
|
|
224
|
+
modelOptions: [
|
|
225
|
+
{
|
|
226
|
+
value: 'anthropic/claude-sonnet-4-very-long-name',
|
|
227
|
+
modelLabel: 'claude-sonnet-4-very-long-name',
|
|
228
|
+
providerLabel: 'Anthropic'
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
recentModelValues: [],
|
|
232
|
+
selectedModel: 'anthropic/claude-sonnet-4-very-long-name',
|
|
233
|
+
isModelOptionsLoading: false,
|
|
234
|
+
hasModelOptions: true,
|
|
235
|
+
onValueChange: vi.fn(),
|
|
236
|
+
texts: {
|
|
237
|
+
modelSelectPlaceholder: 'Select model',
|
|
238
|
+
modelNoOptionsLabel: 'No models',
|
|
239
|
+
recentModelsLabel: 'Recent',
|
|
240
|
+
allModelsLabel: 'All models'
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(select.selectedLabel).toBe('claude-sonnet-4-very-long-name');
|
|
245
|
+
expect(select.options[0]?.label).toBe('Anthropic/claude-sonnet-4-very-long-name');
|
|
246
|
+
});
|
|
247
|
+
|
|
222
248
|
it('groups recent models ahead of the remaining catalog', () => {
|
|
223
249
|
const select = buildModelToolbarSelect({
|
|
224
250
|
modelOptions: [
|
|
@@ -52,7 +52,9 @@ export function buildModelStateHint(params: {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function buildModelToolbarSelect(
|
|
55
|
+
export function buildModelToolbarSelect({
|
|
56
|
+
modelOptions, recentModelValues, selectedModel, isModelOptionsLoading, hasModelOptions, onValueChange, texts,
|
|
57
|
+
}: {
|
|
56
58
|
modelOptions: ChatModelRecord[];
|
|
57
59
|
recentModelValues?: string[];
|
|
58
60
|
selectedModel: string;
|
|
@@ -67,22 +69,21 @@ export function buildModelToolbarSelect(params: {
|
|
|
67
69
|
| "allModelsLabel"
|
|
68
70
|
>;
|
|
69
71
|
}): ChatToolbarSelect {
|
|
70
|
-
const selectedModelOption =
|
|
71
|
-
(option) => option.value ===
|
|
72
|
+
const selectedModelOption = modelOptions.find(
|
|
73
|
+
(option) => option.value === selectedModel,
|
|
72
74
|
);
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const resolvedValue = params.hasModelOptions
|
|
75
|
+
const resolvedModelOption = selectedModelOption ?? modelOptions[0];
|
|
76
|
+
const resolvedValue = hasModelOptions
|
|
76
77
|
? resolvedModelOption?.value
|
|
77
78
|
: undefined;
|
|
78
|
-
const recentValueSet = new Set(
|
|
79
|
+
const recentValueSet = new Set(recentModelValues ?? []);
|
|
79
80
|
const modelOptionMap = new Map(
|
|
80
|
-
|
|
81
|
+
modelOptions.map((option) => [option.value, option] as const),
|
|
81
82
|
);
|
|
82
|
-
const recentOptions = (
|
|
83
|
+
const recentOptions = (recentModelValues ?? [])
|
|
83
84
|
.map((value) => modelOptionMap.get(value))
|
|
84
85
|
.filter((option): option is ChatModelRecord => Boolean(option));
|
|
85
|
-
const remainingOptions =
|
|
86
|
+
const remainingOptions = modelOptions.filter(
|
|
86
87
|
(option) => !recentValueSet.has(option.value),
|
|
87
88
|
);
|
|
88
89
|
const optionGroups =
|
|
@@ -90,7 +91,7 @@ export function buildModelToolbarSelect(params: {
|
|
|
90
91
|
? [
|
|
91
92
|
{
|
|
92
93
|
key: "recent-models",
|
|
93
|
-
label:
|
|
94
|
+
label: texts.recentModelsLabel,
|
|
94
95
|
options: recentOptions.map((option) => ({
|
|
95
96
|
value: option.value,
|
|
96
97
|
label: formatModelOptionLabel(option),
|
|
@@ -98,7 +99,7 @@ export function buildModelToolbarSelect(params: {
|
|
|
98
99
|
},
|
|
99
100
|
{
|
|
100
101
|
key: "all-models",
|
|
101
|
-
label:
|
|
102
|
+
label: texts.allModelsLabel,
|
|
102
103
|
options: remainingOptions.map((option) => ({
|
|
103
104
|
value: option.value,
|
|
104
105
|
label: formatModelOptionLabel(option),
|
|
@@ -110,20 +111,20 @@ export function buildModelToolbarSelect(params: {
|
|
|
110
111
|
return {
|
|
111
112
|
key: "model",
|
|
112
113
|
value: resolvedValue,
|
|
113
|
-
placeholder:
|
|
114
|
+
placeholder: texts.modelSelectPlaceholder,
|
|
114
115
|
selectedLabel: resolvedModelOption
|
|
115
|
-
? formatModelOptionLabel(resolvedModelOption)
|
|
116
|
+
? resolvedModelOption.modelLabel.trim() || formatModelOptionLabel(resolvedModelOption)
|
|
116
117
|
: undefined,
|
|
117
118
|
icon: "sparkles",
|
|
118
|
-
options:
|
|
119
|
+
options: modelOptions.map((option) => ({
|
|
119
120
|
value: option.value,
|
|
120
121
|
label: formatModelOptionLabel(option),
|
|
121
122
|
})),
|
|
122
123
|
groups: optionGroups,
|
|
123
|
-
disabled: !
|
|
124
|
-
loading:
|
|
125
|
-
emptyLabel:
|
|
126
|
-
onValueChange
|
|
124
|
+
disabled: !hasModelOptions,
|
|
125
|
+
loading: isModelOptionsLoading,
|
|
126
|
+
emptyLabel: texts.modelNoOptionsLabel,
|
|
127
|
+
onValueChange,
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -41,7 +41,7 @@ function createSnapshot(
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
describe('ncp-chat-input-availability.utils', () => {
|
|
44
|
-
it('keeps the composer editable during cold start while
|
|
44
|
+
it('keeps the composer editable during cold start while runtime blocking still prevents send', () => {
|
|
45
45
|
const snapshot = createSnapshot({
|
|
46
46
|
isProviderStateResolved: false,
|
|
47
47
|
modelOptions: [],
|
|
@@ -60,6 +60,22 @@ describe('ncp-chat-input-availability.utils', () => {
|
|
|
60
60
|
).toBe(true);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it('does not block send only because model options have not loaded yet', () => {
|
|
64
|
+
const snapshot = createSnapshot({
|
|
65
|
+
isProviderStateResolved: false,
|
|
66
|
+
modelOptions: [],
|
|
67
|
+
sessionTypeUnavailable: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
isNcpChatSendDisabled({
|
|
72
|
+
snapshot,
|
|
73
|
+
hasSendableDraft: true,
|
|
74
|
+
isRuntimeBlocked: false,
|
|
75
|
+
})
|
|
76
|
+
).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
63
79
|
it('marks model options as empty only after provider state resolves', () => {
|
|
64
80
|
const loadingSnapshot = createSnapshot({
|
|
65
81
|
isProviderStateResolved: false,
|
|
@@ -74,19 +90,35 @@ describe('ncp-chat-input-availability.utils', () => {
|
|
|
74
90
|
expect(isNcpChatModelOptionsEmpty(emptySnapshot)).toBe(true);
|
|
75
91
|
});
|
|
76
92
|
|
|
77
|
-
it('
|
|
93
|
+
it('keeps editing and sending available when the selected session type reports unavailable', () => {
|
|
78
94
|
const snapshot = createSnapshot({
|
|
79
95
|
isProviderStateResolved: true,
|
|
80
96
|
sessionTypeUnavailable: true,
|
|
81
97
|
});
|
|
82
98
|
|
|
83
|
-
expect(isNcpChatComposerDisabled(snapshot)).toBe(
|
|
99
|
+
expect(isNcpChatComposerDisabled(snapshot)).toBe(false);
|
|
84
100
|
expect(
|
|
85
101
|
isNcpChatSendDisabled({
|
|
86
102
|
snapshot,
|
|
87
103
|
hasSendableDraft: true,
|
|
88
104
|
isRuntimeBlocked: false,
|
|
89
105
|
})
|
|
106
|
+
).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('blocks send when there is no sendable draft', () => {
|
|
110
|
+
const snapshot = createSnapshot({
|
|
111
|
+
isProviderStateResolved: true,
|
|
112
|
+
modelOptions: [],
|
|
113
|
+
sessionTypeUnavailable: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(
|
|
117
|
+
isNcpChatSendDisabled({
|
|
118
|
+
snapshot,
|
|
119
|
+
hasSendableDraft: false,
|
|
120
|
+
isRuntimeBlocked: false,
|
|
121
|
+
})
|
|
90
122
|
).toBe(true);
|
|
91
123
|
});
|
|
92
124
|
});
|
|
@@ -24,9 +24,9 @@ export function isNcpChatModelOptionsEmpty(
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function isNcpChatComposerDisabled(
|
|
27
|
-
|
|
27
|
+
_snapshot: NcpChatInputAvailabilitySnapshot
|
|
28
28
|
): boolean {
|
|
29
|
-
return
|
|
29
|
+
return false;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function isNcpChatSendDisabled(params: {
|
|
@@ -34,12 +34,10 @@ export function isNcpChatSendDisabled(params: {
|
|
|
34
34
|
snapshot: NcpChatInputAvailabilitySnapshot;
|
|
35
35
|
isRuntimeBlocked: boolean;
|
|
36
36
|
}): boolean {
|
|
37
|
-
const { hasSendableDraft, isRuntimeBlocked
|
|
37
|
+
const { hasSendableDraft, isRuntimeBlocked } = params;
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
isRuntimeBlocked ||
|
|
41
|
-
!hasSendableDraft
|
|
42
|
-
!hasNcpChatModelOptions(snapshot) ||
|
|
43
|
-
snapshot.sessionTypeUnavailable
|
|
41
|
+
!hasSendableDraft
|
|
44
42
|
);
|
|
45
43
|
}
|
|
@@ -67,7 +67,7 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
67
67
|
zh: '聊天能力启动失败,请稍后重试或检查服务日志。',
|
|
68
68
|
en: 'Chat startup failed. Please retry in a moment or inspect the service logs.'
|
|
69
69
|
},
|
|
70
|
-
chatInputPlaceholder: { zh: '
|
|
70
|
+
chatInputPlaceholder: { zh: '发消息...', en: 'Message NextClaw...' },
|
|
71
71
|
chatInputHint: { zh: '支持多轮上下文,默认走当前会话。', en: 'Multi-turn context is preserved in the current session.' },
|
|
72
72
|
chatSlashSectionCommands: { zh: '命令', en: 'Commands' },
|
|
73
73
|
chatSlashSectionSkills: { zh: '技能', en: 'Skills' },
|