@nextclaw/ui 0.12.20-beta.0 → 0.12.20-beta.1
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 +12 -0
- package/dist/assets/api-BcqDx0tm.js +15 -0
- package/dist/assets/app-manager-provider-DVYBjif-.js +1 -0
- package/dist/assets/{app-navigation.config-BORqHkbN.js → app-navigation.config-CMoWvFEI.js} +1 -1
- package/dist/assets/{channels-list-page-sISO_4Yj.js → channels-list-page-CsoI4OJm.js} +2 -2
- package/dist/assets/{chat-ChCu7LQD.js → chat-CA3aRmhx.js} +6 -6
- package/dist/assets/chat-page-gdSN6Pr6.js +1 -0
- package/dist/assets/chunk-JZWAC4HX-u4uYphxM.js +3 -0
- package/dist/assets/{desktop-update-config-BfJ5iSeY.js → desktop-update-config-CD6-2PfI.js} +1 -1
- package/dist/assets/{dialog-B-CXiFPZ.js → dialog-csshWetU.js} +1 -1
- package/dist/assets/{dist-DYVfg3q5.js → dist-Bl94Ahwx.js} +1 -1
- package/dist/assets/{es2015-BXroVnPi.js → es2015-JCM5-KtW.js} +1 -1
- package/dist/assets/index-BTDFuKka.js +2 -0
- package/dist/assets/marketplace-page-DJGDpTAo.js +1 -0
- package/dist/assets/{marketplace-page-C9oZ01rM.js → marketplace-page-DxlxHCFm.js} +2 -2
- package/dist/assets/{mcp-marketplace-page-DuEixgSs.js → mcp-marketplace-page-5UjYRWOR.js} +2 -2
- package/dist/assets/mcp-marketplace-page-C1XaHZZO.js +1 -0
- package/dist/assets/{model-config-mfhqEZBG.js → model-config-PccJ9XyH.js} +1 -1
- package/dist/assets/{notice-card-CozHB03G.js → notice-card-CCgk6FvF.js} +1 -1
- package/dist/assets/{popover-CPUPma-w.js → popover-YAsxDBhY.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-CL9sti2I.js → provider-scoped-model-input-CzpF7cug.js} +1 -1
- package/dist/assets/{providers-list-HPmL2akJ.js → providers-list-8qDMER8o.js} +1 -1
- package/dist/assets/remote-D4TtLPAp.js +1 -0
- package/dist/assets/runtime-config-page-D-4c5H5z.js +1 -0
- package/dist/assets/{search-config-Bcnk9VlL.js → search-config-D3a65l3r.js} +1 -1
- package/dist/assets/{secrets-config-Dde-5Y1w.js → secrets-config-CoMlR_7i.js} +2 -2
- package/dist/assets/{select-BELPuXLW.js → select-DIZrwsKU.js} +1 -1
- package/dist/assets/{sessions-config-page-CG49_0Z6.js → sessions-config-page-Cc0TJStn.js} +2 -2
- package/dist/assets/{setting-row-D5DtT6Ny.js → setting-row-DiQyrE81.js} +1 -1
- package/dist/assets/{tag-chip-D9BWWgYg.js → tag-chip-C3wDBe_-.js} +1 -1
- package/dist/assets/{theme-provider-DeBrTglS.js → theme-provider-aOmrJ9J6.js} +1 -1
- package/dist/assets/{tooltip-CI0rpNee.js → tooltip-Dq5Xehpk.js} +1 -1
- package/dist/assets/use-config-BQJjq1mP.js +1 -0
- package/dist/assets/{use-confirm-dialog-hbynwWf2.js → use-confirm-dialog-DBoV5n5P.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-Cw5qQr3-.js → use-infinite-scroll-loader-JAicqVC5.js} +1 -1
- package/dist/assets/{use-viewport-layout-CWHVDC6z.js → use-viewport-layout-BX3XqzJ4.js} +1 -1
- package/dist/index.html +16 -16
- package/package.json +8 -6
- package/src/app/hooks/use-realtime-query-bridge.ts +5 -5
- package/src/features/channels/components/config/channel-form.tsx +3 -3
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -6
- package/src/features/chat/pages/ncp-chat-page.test.ts +22 -8
- package/src/features/chat/utils/chat-session-preference-governance.utils.test.tsx +114 -0
- package/src/features/chat/utils/chat-session-preference-governance.utils.ts +30 -36
- package/src/shared/lib/api/index.ts +12 -12
- package/src/shared/lib/api/ncp-session.test.ts +17 -18
- package/src/shared/lib/api/raw-client.utils.ts +3 -126
- package/src/shared/lib/api/services/agents.service.ts +18 -0
- package/src/shared/lib/api/services/channel-auth.service.ts +21 -0
- package/src/shared/lib/api/{client.ts → services/client.service.ts} +45 -1
- package/src/shared/lib/api/services/config.service.ts +171 -0
- package/src/shared/lib/api/services/marketplace.service.ts +66 -0
- package/src/shared/lib/api/services/mcp-marketplace.service.ts +70 -0
- package/src/shared/lib/api/services/ncp-attachments.service.ts +14 -0
- package/src/shared/lib/api/services/ncp-session.service.ts +39 -0
- package/src/shared/lib/api/services/remote.service.ts +50 -0
- package/src/shared/lib/api/services/runtime-control.service.ts +18 -0
- package/src/shared/lib/api/services/runtime-update.service.ts +26 -0
- package/src/shared/lib/api/services/server-path.service.ts +16 -0
- package/src/shared/lib/transport/index.ts +1 -0
- package/src/shared/lib/transport/local-transport.service.ts +24 -4
- package/src/shared/lib/transport/remote-transport.service.ts +1 -1
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +133 -0
- package/src/shared/lib/transport/transport.types.ts +8 -2
- package/dist/assets/api-C412zuay.js +0 -15
- package/dist/assets/app-manager-provider-Cm-KiZZG.js +0 -1
- package/dist/assets/chat-page-BCaNZJGT.js +0 -1
- package/dist/assets/chunk-JZWAC4HX-DvbcIVPf.js +0 -3
- package/dist/assets/index-CqPDhosM.js +0 -2
- package/dist/assets/marketplace-page-C8uaWkfd.js +0 -1
- package/dist/assets/mcp-marketplace-page-rNqr6ZpD.js +0 -1
- package/dist/assets/remote-oDlAdgVA.js +0 -1
- package/dist/assets/runtime-config-page-BCshTAAE.js +0 -1
- package/dist/assets/use-config-CrWZ_TSF.js +0 -1
- package/src/shared/lib/api/agents.ts +0 -34
- package/src/shared/lib/api/channel-auth.ts +0 -35
- package/src/shared/lib/api/config.ts +0 -362
- package/src/shared/lib/api/marketplace.ts +0 -156
- package/src/shared/lib/api/mcp-marketplace.ts +0 -138
- package/src/shared/lib/api/ncp-attachments.ts +0 -41
- package/src/shared/lib/api/ncp-session.ts +0 -78
- package/src/shared/lib/api/remote.ts +0 -86
- package/src/shared/lib/api/runtime-control.ts +0 -34
- package/src/shared/lib/api/runtime-update.service.ts +0 -50
- package/src/shared/lib/api/server-path.ts +0 -46
package/dist/index.html
CHANGED
|
@@ -78,21 +78,21 @@
|
|
|
78
78
|
})();
|
|
79
79
|
</script>
|
|
80
80
|
<title>NextClaw</title>
|
|
81
|
-
<script type="module" crossorigin src="/assets/index-
|
|
81
|
+
<script type="module" crossorigin src="/assets/index-BTDFuKka.js"></script>
|
|
82
82
|
<link rel="modulepreload" crossorigin href="/assets/i18n-C5Mibli1.js">
|
|
83
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
84
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
83
|
+
<link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-u4uYphxM.js">
|
|
84
|
+
<link rel="modulepreload" crossorigin href="/assets/api-BcqDx0tm.js">
|
|
85
|
+
<link rel="modulepreload" crossorigin href="/assets/es2015-JCM5-KtW.js">
|
|
85
86
|
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BZkY6emz.js">
|
|
86
|
-
<link rel="modulepreload" crossorigin href="/assets/select-
|
|
87
|
-
<link rel="modulepreload" crossorigin href="/assets/dist-
|
|
87
|
+
<link rel="modulepreload" crossorigin href="/assets/select-DIZrwsKU.js">
|
|
88
|
+
<link rel="modulepreload" crossorigin href="/assets/dist-Bl94Ahwx.js">
|
|
88
89
|
<link rel="modulepreload" crossorigin href="/assets/x-DpTzXQcX.js">
|
|
89
|
-
<link rel="modulepreload" crossorigin href="/assets/dialog-
|
|
90
|
-
<link rel="modulepreload" crossorigin href="/assets/popover-
|
|
91
|
-
<link rel="modulepreload" crossorigin href="/assets/tooltip-
|
|
90
|
+
<link rel="modulepreload" crossorigin href="/assets/dialog-csshWetU.js">
|
|
91
|
+
<link rel="modulepreload" crossorigin href="/assets/popover-YAsxDBhY.js">
|
|
92
|
+
<link rel="modulepreload" crossorigin href="/assets/tooltip-Dq5Xehpk.js">
|
|
92
93
|
<link rel="modulepreload" crossorigin href="/assets/refresh-cw-BxojR62w.js">
|
|
93
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
94
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
95
|
-
<link rel="modulepreload" crossorigin href="/assets/theme-provider-DeBrTglS.js">
|
|
94
|
+
<link rel="modulepreload" crossorigin href="/assets/use-config-BQJjq1mP.js">
|
|
95
|
+
<link rel="modulepreload" crossorigin href="/assets/theme-provider-aOmrJ9J6.js">
|
|
96
96
|
<link rel="modulepreload" crossorigin href="/assets/search-vChioOoe.js">
|
|
97
97
|
<link rel="modulepreload" crossorigin href="/assets/book-open-DgLqYpNY.js">
|
|
98
98
|
<link rel="modulepreload" crossorigin href="/assets/external-link-Sw3ah_JD.js">
|
|
@@ -107,16 +107,16 @@
|
|
|
107
107
|
<link rel="modulepreload" crossorigin href="/assets/doc-browser-context-DfLHAWbG.js">
|
|
108
108
|
<link rel="modulepreload" crossorigin href="/assets/doc-browser-CzCV73NJ.js">
|
|
109
109
|
<link rel="modulepreload" crossorigin href="/assets/doc-browser-BUlCkZo2.js">
|
|
110
|
-
<link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-
|
|
110
|
+
<link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-BX3XqzJ4.js">
|
|
111
111
|
<link rel="modulepreload" crossorigin href="/assets/logo-badge-BQgKnVtz.js">
|
|
112
112
|
<link rel="modulepreload" crossorigin href="/assets/skeleton-CFQRIUzt.js">
|
|
113
|
-
<link rel="modulepreload" crossorigin href="/assets/chat-
|
|
113
|
+
<link rel="modulepreload" crossorigin href="/assets/chat-CA3aRmhx.js">
|
|
114
114
|
<link rel="modulepreload" crossorigin href="/assets/key-round-CnI1mc9F.js">
|
|
115
115
|
<link rel="modulepreload" crossorigin href="/assets/message-square-D6Z4NwpG.js">
|
|
116
|
-
<link rel="modulepreload" crossorigin href="/assets/app-navigation.config-
|
|
117
|
-
<link rel="modulepreload" crossorigin href="/assets/notice-card-
|
|
116
|
+
<link rel="modulepreload" crossorigin href="/assets/app-navigation.config-CMoWvFEI.js">
|
|
117
|
+
<link rel="modulepreload" crossorigin href="/assets/notice-card-CCgk6FvF.js">
|
|
118
118
|
<link rel="modulepreload" crossorigin href="/assets/status-dot-Dv_hiUVa.js">
|
|
119
|
-
<link rel="modulepreload" crossorigin href="/assets/app-manager-provider-
|
|
119
|
+
<link rel="modulepreload" crossorigin href="/assets/app-manager-provider-DVYBjif-.js">
|
|
120
120
|
<link rel="stylesheet" crossorigin href="/assets/index-CUmk8xFK.css">
|
|
121
121
|
</head>
|
|
122
122
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.12.20-beta.
|
|
3
|
+
"version": "0.12.20-beta.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,12 +28,14 @@
|
|
|
28
28
|
"tailwind-merge": "^2.5.4",
|
|
29
29
|
"zod": "^3.23.8",
|
|
30
30
|
"zustand": "^5.0.2",
|
|
31
|
-
"@nextclaw/
|
|
32
|
-
"@nextclaw/
|
|
31
|
+
"@nextclaw/agent-chat": "0.1.11-beta.0",
|
|
32
|
+
"@nextclaw/kernel": "0.1.2-beta.2",
|
|
33
33
|
"@nextclaw/ncp": "0.5.6-beta.0",
|
|
34
|
-
"@nextclaw/
|
|
35
|
-
"@nextclaw/ncp-
|
|
36
|
-
"@nextclaw/
|
|
34
|
+
"@nextclaw/client-sdk": "0.1.1-beta.1",
|
|
35
|
+
"@nextclaw/ncp-http-agent-client": "0.3.18-beta.1",
|
|
36
|
+
"@nextclaw/server": "0.12.13-beta.1",
|
|
37
|
+
"@nextclaw/agent-chat-ui": "0.3.13-beta.0",
|
|
38
|
+
"@nextclaw/ncp-react": "0.4.26-beta.1"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@testing-library/react": "^16.3.0",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
|
-
import { applyNcpSessionRealtimeEvent } from '@/shared/lib/api';
|
|
2
|
+
import { applyNcpSessionRealtimeEvent, nextclawClient } from '@/shared/lib/api';
|
|
3
3
|
import { systemStatusManager } from '@/features/system-status';
|
|
4
|
-
import { appClient } from '@/shared/lib/transport';
|
|
5
4
|
import type { QueryClient } from '@tanstack/react-query';
|
|
5
|
+
import type { NextClawRealtimeEvent } from '@nextclaw/client-sdk';
|
|
6
6
|
|
|
7
7
|
function shouldInvalidateConfigQuery(configPath: string) {
|
|
8
8
|
const normalized = configPath.trim().toLowerCase();
|
|
@@ -41,7 +41,7 @@ function handleRealtimeEvent(
|
|
|
41
41
|
shouldResyncSessions: boolean;
|
|
42
42
|
clearShouldResyncSessions: () => void;
|
|
43
43
|
markShouldResyncSessions: () => void;
|
|
44
|
-
event:
|
|
44
|
+
event: NextClawRealtimeEvent;
|
|
45
45
|
}
|
|
46
46
|
): void {
|
|
47
47
|
const {
|
|
@@ -88,7 +88,7 @@ export function useRealtimeQueryBridge(queryClient?: QueryClient) {
|
|
|
88
88
|
const shouldResyncSessionsRef = useRef(false);
|
|
89
89
|
|
|
90
90
|
useEffect(() => {
|
|
91
|
-
return
|
|
91
|
+
return nextclawClient.realtime.subscribe((event) =>
|
|
92
92
|
handleRealtimeEvent({
|
|
93
93
|
queryClient,
|
|
94
94
|
shouldResyncSessions: shouldResyncSessionsRef.current,
|
|
@@ -100,6 +100,6 @@ export function useRealtimeQueryBridge(queryClient?: QueryClient) {
|
|
|
100
100
|
},
|
|
101
101
|
event,
|
|
102
102
|
})
|
|
103
|
-
);
|
|
103
|
+
).close;
|
|
104
104
|
}, [queryClient]);
|
|
105
105
|
}
|
|
@@ -5,7 +5,7 @@ import { toast } from 'sonner';
|
|
|
5
5
|
import { LogoBadge } from '@/shared/components/common/logo-badge';
|
|
6
6
|
import { Button } from '@/shared/components/ui/button';
|
|
7
7
|
import { StatusDot } from '@/shared/components/ui/status-dot';
|
|
8
|
-
import {
|
|
8
|
+
import { nextclawClient } from '@/shared/lib/api';
|
|
9
9
|
import { useConfig, useConfigMeta, useConfigSchema, useExecuteConfigAction, useUpdateChannel } from '@/shared/hooks/use-config';
|
|
10
10
|
import type { ConfigActionManifest, ConfigUiHints } from '@/shared/lib/api';
|
|
11
11
|
import { ChannelFormFieldsSection } from '@/features/channels/components/channel-form-fields-section';
|
|
@@ -77,7 +77,7 @@ function ensureChannelApplySubscription() {
|
|
|
77
77
|
if (channelApplyUnsubscribe) {
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
channelApplyUnsubscribe =
|
|
80
|
+
channelApplyUnsubscribe = nextclawClient.realtime.subscribe((event) => {
|
|
81
81
|
if (event.type !== 'channel.config.apply-status') {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
@@ -90,7 +90,7 @@ function ensureChannelApplySubscription() {
|
|
|
90
90
|
: { status: 'failed', message: event.payload.message }
|
|
91
91
|
);
|
|
92
92
|
channelApplyListeners.forEach((listener) => listener());
|
|
93
|
-
});
|
|
93
|
+
}).close;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function subscribeChannelApplyStore(listener: () => void) {
|
|
@@ -6,8 +6,7 @@ import { sessionMatchesQuery } from '@/features/chat/utils/chat-session-display.
|
|
|
6
6
|
import { adaptNcpSessionSummaries } from '@/features/chat/utils/ncp-session-adapter.utils';
|
|
7
7
|
import { useChatSessionTypeState } from '@/features/chat/hooks/use-chat-session-type-state';
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
resolveRecentSessionPreferredModel,
|
|
9
|
+
resolveRecentSessionPreferredValue,
|
|
11
10
|
useSyncSelectedModel,
|
|
12
11
|
useSyncSelectedThinking
|
|
13
12
|
} from '@/features/chat/utils/chat-session-preference-governance.utils';
|
|
@@ -83,19 +82,21 @@ function useRecentSessionPreferences(params: {
|
|
|
83
82
|
const { sessions, sessionKey, sessionType } = params;
|
|
84
83
|
const recentSessionPreferredModel = useMemo(
|
|
85
84
|
() =>
|
|
86
|
-
|
|
85
|
+
resolveRecentSessionPreferredValue<string>({
|
|
87
86
|
sessions,
|
|
88
87
|
selectedSessionKey: sessionKey,
|
|
89
|
-
sessionType
|
|
88
|
+
sessionType,
|
|
89
|
+
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
90
90
|
}),
|
|
91
91
|
[sessionKey, sessionType, sessions]
|
|
92
92
|
);
|
|
93
93
|
const recentSessionPreferredThinking = useMemo(
|
|
94
94
|
() =>
|
|
95
|
-
|
|
95
|
+
resolveRecentSessionPreferredValue<ThinkingLevel>({
|
|
96
96
|
sessions,
|
|
97
97
|
selectedSessionKey: sessionKey,
|
|
98
|
-
sessionType
|
|
98
|
+
sessionType,
|
|
99
|
+
readPreference: (session) => session.preferredThinking ?? undefined
|
|
99
100
|
}),
|
|
100
101
|
[sessionKey, sessionType, sessions]
|
|
101
102
|
);
|
|
@@ -2,8 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import type { SessionEntryView, ThinkingLevel } from '@/shared/lib/api';
|
|
3
3
|
import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
resolveRecentSessionPreferredThinking,
|
|
5
|
+
resolveRecentSessionPreferredValue,
|
|
7
6
|
resolveSelectedModelValue,
|
|
8
7
|
resolveSelectedThinkingLevelValue
|
|
9
8
|
} from '@/features/chat/utils/chat-session-preference-governance.utils';
|
|
@@ -25,6 +24,18 @@ const modelOptions: ChatModelOption[] = [
|
|
|
25
24
|
modelLabel: 'gpt-5',
|
|
26
25
|
providerLabel: 'OpenAI',
|
|
27
26
|
thinkingCapability: null
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
value: 'minimax/MiniMax-M2.7',
|
|
30
|
+
modelLabel: 'MiniMax-M2.7',
|
|
31
|
+
providerLabel: 'MiniMax',
|
|
32
|
+
thinkingCapability: null
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: 'deepseek/deepseek-v4-flash',
|
|
36
|
+
modelLabel: 'deepseek-v4-flash',
|
|
37
|
+
providerLabel: 'DeepSeek',
|
|
38
|
+
thinkingCapability: null
|
|
28
39
|
}
|
|
29
40
|
];
|
|
30
41
|
|
|
@@ -270,10 +281,11 @@ describe('resolveRecentSessionPreferredModel', () => {
|
|
|
270
281
|
];
|
|
271
282
|
|
|
272
283
|
expect(
|
|
273
|
-
|
|
284
|
+
resolveRecentSessionPreferredValue<string>({
|
|
274
285
|
sessions,
|
|
275
286
|
selectedSessionKey: 'draft',
|
|
276
|
-
sessionType: 'codex'
|
|
287
|
+
sessionType: 'codex',
|
|
288
|
+
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
277
289
|
})
|
|
278
290
|
).toBe('openai/gpt-5');
|
|
279
291
|
});
|
|
@@ -300,10 +312,11 @@ describe('resolveRecentSessionPreferredModel', () => {
|
|
|
300
312
|
];
|
|
301
313
|
|
|
302
314
|
expect(
|
|
303
|
-
|
|
315
|
+
resolveRecentSessionPreferredValue<string>({
|
|
304
316
|
sessions,
|
|
305
317
|
selectedSessionKey: 'codex-current',
|
|
306
|
-
sessionType: 'codex'
|
|
318
|
+
sessionType: 'codex',
|
|
319
|
+
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
307
320
|
})
|
|
308
321
|
).toBe('anthropic/claude-sonnet-4');
|
|
309
322
|
});
|
|
@@ -384,10 +397,11 @@ describe('resolveRecentSessionPreferredThinking', () => {
|
|
|
384
397
|
];
|
|
385
398
|
|
|
386
399
|
expect(
|
|
387
|
-
|
|
400
|
+
resolveRecentSessionPreferredValue<ThinkingLevel>({
|
|
388
401
|
sessions,
|
|
389
402
|
selectedSessionKey: 'draft',
|
|
390
|
-
sessionType: 'codex'
|
|
403
|
+
sessionType: 'codex',
|
|
404
|
+
readPreference: (session) => session.preferredThinking ?? undefined
|
|
391
405
|
})
|
|
392
406
|
).toBe('high');
|
|
393
407
|
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
5
|
+
import { useSyncSelectedModel } from '@/features/chat/utils/chat-session-preference-governance.utils';
|
|
6
|
+
|
|
7
|
+
const modelOptions: ChatModelOption[] = [
|
|
8
|
+
{
|
|
9
|
+
value: 'minimax/MiniMax-M2.7',
|
|
10
|
+
modelLabel: 'MiniMax-M2.7',
|
|
11
|
+
providerLabel: 'MiniMax',
|
|
12
|
+
thinkingCapability: null
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
value: 'deepseek/deepseek-v4-flash',
|
|
16
|
+
modelLabel: 'deepseek-v4-flash',
|
|
17
|
+
providerLabel: 'DeepSeek',
|
|
18
|
+
thinkingCapability: null
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: 'openai/gpt-5',
|
|
22
|
+
modelLabel: 'gpt-5',
|
|
23
|
+
providerLabel: 'OpenAI',
|
|
24
|
+
thinkingCapability: null
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
type HookProps = {
|
|
29
|
+
fallbackPreferredModel?: string;
|
|
30
|
+
defaultModel: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('useSyncSelectedModel', () => {
|
|
34
|
+
it('replaces an auto-selected global default with the later-arriving recent same-runtime model for a fresh draft session', async () => {
|
|
35
|
+
const initialProps: HookProps = {
|
|
36
|
+
fallbackPreferredModel: undefined,
|
|
37
|
+
defaultModel: 'minimax/MiniMax-M2.7'
|
|
38
|
+
};
|
|
39
|
+
const { result, rerender } = renderHook(
|
|
40
|
+
(props: HookProps) => {
|
|
41
|
+
const [selectedModel, setSelectedModel] = useState('');
|
|
42
|
+
useSyncSelectedModel({
|
|
43
|
+
modelOptions,
|
|
44
|
+
selectedSessionKey: 'draft-session',
|
|
45
|
+
selectedSessionExists: false,
|
|
46
|
+
fallbackPreferredModel: props.fallbackPreferredModel,
|
|
47
|
+
defaultModel: props.defaultModel,
|
|
48
|
+
setSelectedModel
|
|
49
|
+
});
|
|
50
|
+
return selectedModel;
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
initialProps
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(result.current).toBe('minimax/MiniMax-M2.7');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
rerender({
|
|
62
|
+
fallbackPreferredModel: 'deepseek/deepseek-v4-flash',
|
|
63
|
+
defaultModel: 'minimax/MiniMax-M2.7'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
expect(result.current).toBe('deepseek/deepseek-v4-flash');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not override a manual model selection when recent same-runtime model data arrives later', async () => {
|
|
72
|
+
const initialProps: HookProps = {
|
|
73
|
+
fallbackPreferredModel: undefined,
|
|
74
|
+
defaultModel: 'minimax/MiniMax-M2.7'
|
|
75
|
+
};
|
|
76
|
+
const { result, rerender } = renderHook(
|
|
77
|
+
(props: HookProps) => {
|
|
78
|
+
const [selectedModel, setSelectedModel] = useState('');
|
|
79
|
+
useSyncSelectedModel({
|
|
80
|
+
modelOptions,
|
|
81
|
+
selectedSessionKey: 'draft-session',
|
|
82
|
+
selectedSessionExists: false,
|
|
83
|
+
fallbackPreferredModel: props.fallbackPreferredModel,
|
|
84
|
+
defaultModel: props.defaultModel,
|
|
85
|
+
setSelectedModel
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
selectedModel,
|
|
89
|
+
setSelectedModel
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
initialProps
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(result.current.selectedModel).toBe('minimax/MiniMax-M2.7');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
act(() => {
|
|
102
|
+
result.current.setSelectedModel('openai/gpt-5');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
rerender({
|
|
106
|
+
fallbackPreferredModel: 'deepseek/deepseek-v4-flash',
|
|
107
|
+
defaultModel: 'minimax/MiniMax-M2.7'
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(result.current.selectedModel).toBe('openai/gpt-5');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -160,37 +160,14 @@ export function resolveRecentSessionPreferredValue<T>(params: {
|
|
|
160
160
|
return bestValue;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
selectedSessionKey?: string | null;
|
|
166
|
-
sessionType?: string | null;
|
|
167
|
-
}): string | undefined {
|
|
168
|
-
const { sessions, selectedSessionKey, sessionType } = params;
|
|
169
|
-
return resolveRecentSessionPreferredValue<string>({
|
|
170
|
-
sessions,
|
|
171
|
-
selectedSessionKey,
|
|
172
|
-
sessionType,
|
|
173
|
-
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function resolveRecentSessionPreferredThinking(params: {
|
|
178
|
-
sessions: readonly SessionEntryView[];
|
|
179
|
-
selectedSessionKey?: string | null;
|
|
180
|
-
sessionType?: string | null;
|
|
181
|
-
}): ThinkingLevel | undefined {
|
|
182
|
-
const { sessions, selectedSessionKey, sessionType } = params;
|
|
183
|
-
return resolveRecentSessionPreferredValue<ThinkingLevel>({
|
|
184
|
-
sessions,
|
|
185
|
-
selectedSessionKey,
|
|
186
|
-
sessionType,
|
|
187
|
-
readPreference: (session) => session.preferredThinking ?? undefined
|
|
188
|
-
});
|
|
163
|
+
function buildSyncKey(parts: unknown[]): string {
|
|
164
|
+
return parts.map((part) => (part == null ? '' : String(part))).join('\u0002');
|
|
189
165
|
}
|
|
190
166
|
|
|
191
167
|
type UseSyncSessionPreferenceParams<T> = {
|
|
192
168
|
isPreferenceAvailable: boolean;
|
|
193
169
|
emptyValue: T;
|
|
170
|
+
syncKey: string;
|
|
194
171
|
selectedSessionKey?: string | null;
|
|
195
172
|
selectedSessionExists?: boolean;
|
|
196
173
|
setValue: Dispatch<SetStateAction<T>>;
|
|
@@ -201,6 +178,7 @@ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>)
|
|
|
201
178
|
const {
|
|
202
179
|
isPreferenceAvailable,
|
|
203
180
|
emptyValue,
|
|
181
|
+
syncKey,
|
|
204
182
|
selectedSessionKey,
|
|
205
183
|
selectedSessionExists = false,
|
|
206
184
|
setValue,
|
|
@@ -208,27 +186,31 @@ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>)
|
|
|
208
186
|
} = params;
|
|
209
187
|
const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
|
|
210
188
|
const resolveValueRef = useRef(resolveValue);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
resolveValueRef.current = resolveValue;
|
|
214
|
-
}, [resolveValue]);
|
|
189
|
+
const lastSyncedValueRef = useRef<T>(emptyValue);
|
|
190
|
+
resolveValueRef.current = resolveValue;
|
|
215
191
|
|
|
216
192
|
useEffect(() => {
|
|
217
193
|
const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
|
|
218
194
|
if (!isPreferenceAvailable) {
|
|
219
195
|
setValue(emptyValue);
|
|
196
|
+
lastSyncedValueRef.current = emptyValue;
|
|
220
197
|
previousSessionKeyRef.current = selectedSessionKey;
|
|
221
198
|
return;
|
|
222
199
|
}
|
|
223
|
-
setValue((prev) =>
|
|
224
|
-
resolveValueRef.current({
|
|
225
|
-
currentValue:
|
|
200
|
+
setValue((prev) => {
|
|
201
|
+
const next = resolveValueRef.current({
|
|
202
|
+
currentValue:
|
|
203
|
+
!sessionChanged && Object.is(prev, lastSyncedValueRef.current)
|
|
204
|
+
? emptyValue
|
|
205
|
+
: prev,
|
|
226
206
|
sessionChanged,
|
|
227
207
|
preserveCurrentValueOnSessionChange: sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists
|
|
228
|
-
})
|
|
229
|
-
|
|
208
|
+
});
|
|
209
|
+
lastSyncedValueRef.current = next;
|
|
210
|
+
return next;
|
|
211
|
+
});
|
|
230
212
|
previousSessionKeyRef.current = selectedSessionKey;
|
|
231
|
-
}, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue]);
|
|
213
|
+
}, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue, syncKey]);
|
|
232
214
|
}
|
|
233
215
|
|
|
234
216
|
export function useSyncSelectedModel(params: {
|
|
@@ -244,6 +226,12 @@ export function useSyncSelectedModel(params: {
|
|
|
244
226
|
useSyncSessionPreference<string>({
|
|
245
227
|
isPreferenceAvailable: modelOptions.length > 0,
|
|
246
228
|
emptyValue: '',
|
|
229
|
+
syncKey: buildSyncKey([
|
|
230
|
+
modelOptions.map((option) => option.value).join('\u0001'),
|
|
231
|
+
selectedSessionPreferredModel,
|
|
232
|
+
fallbackPreferredModel,
|
|
233
|
+
defaultModel
|
|
234
|
+
]),
|
|
247
235
|
selectedSessionKey,
|
|
248
236
|
selectedSessionExists,
|
|
249
237
|
setValue: setSelectedModel,
|
|
@@ -273,6 +261,12 @@ export function useSyncSelectedThinking(params: {
|
|
|
273
261
|
useSyncSessionPreference<ThinkingLevel | null>({
|
|
274
262
|
isPreferenceAvailable: supportedThinkingLevels.length > 0,
|
|
275
263
|
emptyValue: null,
|
|
264
|
+
syncKey: buildSyncKey([
|
|
265
|
+
supportedThinkingLevels.join('\u0001'),
|
|
266
|
+
selectedSessionPreferredThinking,
|
|
267
|
+
fallbackPreferredThinking,
|
|
268
|
+
defaultThinkingLevel
|
|
269
|
+
]),
|
|
276
270
|
selectedSessionKey,
|
|
277
271
|
selectedSessionExists,
|
|
278
272
|
setValue: setSelectedThinkingLevel,
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
export * from './agents';
|
|
1
|
+
export * from './services/agents.service';
|
|
2
2
|
export * from './api-base';
|
|
3
3
|
export * from './auth.types';
|
|
4
|
-
export * from './channel-auth';
|
|
4
|
+
export * from './services/channel-auth.service';
|
|
5
5
|
export * from './channel-auth.types';
|
|
6
6
|
export * from './chat-session-type.types';
|
|
7
|
-
export * from './client';
|
|
8
|
-
export * from './config';
|
|
9
|
-
export * from './marketplace';
|
|
10
|
-
export * from './mcp-marketplace';
|
|
11
|
-
export * from './ncp-attachments';
|
|
12
|
-
export * from './ncp-session';
|
|
7
|
+
export * from './services/client.service';
|
|
8
|
+
export * from './services/config.service';
|
|
9
|
+
export * from './services/marketplace.service';
|
|
10
|
+
export * from './services/mcp-marketplace.service';
|
|
11
|
+
export * from './services/ncp-attachments.service';
|
|
12
|
+
export * from './services/ncp-session.service';
|
|
13
13
|
export * from './ncp-session.types';
|
|
14
14
|
export * from './ncp-session-query-cache';
|
|
15
15
|
export * from './raw-client.utils';
|
|
16
|
-
export * from './remote';
|
|
16
|
+
export * from './services/remote.service';
|
|
17
17
|
export * from './remote.types';
|
|
18
|
-
export * from './runtime-control';
|
|
18
|
+
export * from './services/runtime-control.service';
|
|
19
19
|
export * from './runtime-control.types';
|
|
20
|
-
export * from './runtime-update.service';
|
|
21
|
-
export * from './server-path';
|
|
20
|
+
export * from './services/runtime-update.service';
|
|
21
|
+
export * from './services/server-path.service';
|
|
22
22
|
export * from './types';
|
|
@@ -1,37 +1,36 @@
|
|
|
1
|
-
import { fetchNcpSessionSkills } from './ncp-session';
|
|
2
|
-
import {
|
|
1
|
+
import { fetchNcpSessionSkills } from './services/ncp-session.service';
|
|
2
|
+
import { nextclawClient } from './services/client.service';
|
|
3
3
|
|
|
4
|
-
vi.mock('./client', () => ({
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
vi.mock('./services/client.service', () => ({
|
|
5
|
+
nextclawClient: {
|
|
6
|
+
sessions: {
|
|
7
|
+
listSkills: vi.fn()
|
|
8
|
+
}
|
|
7
9
|
}
|
|
8
10
|
}));
|
|
9
11
|
|
|
10
12
|
describe('api/ncp-session', () => {
|
|
11
13
|
beforeEach(() => {
|
|
12
|
-
vi.mocked(
|
|
13
|
-
vi.mocked(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
refs: [],
|
|
19
|
-
records: []
|
|
20
|
-
}
|
|
14
|
+
vi.mocked(nextclawClient.sessions.listSkills).mockReset();
|
|
15
|
+
vi.mocked(nextclawClient.sessions.listSkills).mockResolvedValue({
|
|
16
|
+
sessionId: 'session-1',
|
|
17
|
+
total: 0,
|
|
18
|
+
refs: [],
|
|
19
|
+
records: []
|
|
21
20
|
});
|
|
22
21
|
});
|
|
23
22
|
|
|
24
23
|
it('does not send an empty projectRoot query when no override is provided', async () => {
|
|
25
24
|
await fetchNcpSessionSkills('session-1', { projectRoot: null });
|
|
26
25
|
|
|
27
|
-
expect(
|
|
26
|
+
expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', { projectRoot: null });
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
it('sends projectRoot only when the override is non-empty', async () => {
|
|
31
30
|
await fetchNcpSessionSkills('session-1', { projectRoot: ' /tmp/project-alpha ' });
|
|
32
31
|
|
|
33
|
-
expect(
|
|
34
|
-
'/
|
|
35
|
-
);
|
|
32
|
+
expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', {
|
|
33
|
+
projectRoot: ' /tmp/project-alpha '
|
|
34
|
+
});
|
|
36
35
|
});
|
|
37
36
|
});
|