@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.
Files changed (85) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/api-BcqDx0tm.js +15 -0
  3. package/dist/assets/app-manager-provider-DVYBjif-.js +1 -0
  4. package/dist/assets/{app-navigation.config-BORqHkbN.js → app-navigation.config-CMoWvFEI.js} +1 -1
  5. package/dist/assets/{channels-list-page-sISO_4Yj.js → channels-list-page-CsoI4OJm.js} +2 -2
  6. package/dist/assets/{chat-ChCu7LQD.js → chat-CA3aRmhx.js} +6 -6
  7. package/dist/assets/chat-page-gdSN6Pr6.js +1 -0
  8. package/dist/assets/chunk-JZWAC4HX-u4uYphxM.js +3 -0
  9. package/dist/assets/{desktop-update-config-BfJ5iSeY.js → desktop-update-config-CD6-2PfI.js} +1 -1
  10. package/dist/assets/{dialog-B-CXiFPZ.js → dialog-csshWetU.js} +1 -1
  11. package/dist/assets/{dist-DYVfg3q5.js → dist-Bl94Ahwx.js} +1 -1
  12. package/dist/assets/{es2015-BXroVnPi.js → es2015-JCM5-KtW.js} +1 -1
  13. package/dist/assets/index-BTDFuKka.js +2 -0
  14. package/dist/assets/marketplace-page-DJGDpTAo.js +1 -0
  15. package/dist/assets/{marketplace-page-C9oZ01rM.js → marketplace-page-DxlxHCFm.js} +2 -2
  16. package/dist/assets/{mcp-marketplace-page-DuEixgSs.js → mcp-marketplace-page-5UjYRWOR.js} +2 -2
  17. package/dist/assets/mcp-marketplace-page-C1XaHZZO.js +1 -0
  18. package/dist/assets/{model-config-mfhqEZBG.js → model-config-PccJ9XyH.js} +1 -1
  19. package/dist/assets/{notice-card-CozHB03G.js → notice-card-CCgk6FvF.js} +1 -1
  20. package/dist/assets/{popover-CPUPma-w.js → popover-YAsxDBhY.js} +1 -1
  21. package/dist/assets/{provider-scoped-model-input-CL9sti2I.js → provider-scoped-model-input-CzpF7cug.js} +1 -1
  22. package/dist/assets/{providers-list-HPmL2akJ.js → providers-list-8qDMER8o.js} +1 -1
  23. package/dist/assets/remote-D4TtLPAp.js +1 -0
  24. package/dist/assets/runtime-config-page-D-4c5H5z.js +1 -0
  25. package/dist/assets/{search-config-Bcnk9VlL.js → search-config-D3a65l3r.js} +1 -1
  26. package/dist/assets/{secrets-config-Dde-5Y1w.js → secrets-config-CoMlR_7i.js} +2 -2
  27. package/dist/assets/{select-BELPuXLW.js → select-DIZrwsKU.js} +1 -1
  28. package/dist/assets/{sessions-config-page-CG49_0Z6.js → sessions-config-page-Cc0TJStn.js} +2 -2
  29. package/dist/assets/{setting-row-D5DtT6Ny.js → setting-row-DiQyrE81.js} +1 -1
  30. package/dist/assets/{tag-chip-D9BWWgYg.js → tag-chip-C3wDBe_-.js} +1 -1
  31. package/dist/assets/{theme-provider-DeBrTglS.js → theme-provider-aOmrJ9J6.js} +1 -1
  32. package/dist/assets/{tooltip-CI0rpNee.js → tooltip-Dq5Xehpk.js} +1 -1
  33. package/dist/assets/use-config-BQJjq1mP.js +1 -0
  34. package/dist/assets/{use-confirm-dialog-hbynwWf2.js → use-confirm-dialog-DBoV5n5P.js} +1 -1
  35. package/dist/assets/{use-infinite-scroll-loader-Cw5qQr3-.js → use-infinite-scroll-loader-JAicqVC5.js} +1 -1
  36. package/dist/assets/{use-viewport-layout-CWHVDC6z.js → use-viewport-layout-BX3XqzJ4.js} +1 -1
  37. package/dist/index.html +16 -16
  38. package/package.json +8 -6
  39. package/src/app/hooks/use-realtime-query-bridge.ts +5 -5
  40. package/src/features/channels/components/config/channel-form.tsx +3 -3
  41. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -6
  42. package/src/features/chat/pages/ncp-chat-page.test.ts +22 -8
  43. package/src/features/chat/utils/chat-session-preference-governance.utils.test.tsx +114 -0
  44. package/src/features/chat/utils/chat-session-preference-governance.utils.ts +30 -36
  45. package/src/shared/lib/api/index.ts +12 -12
  46. package/src/shared/lib/api/ncp-session.test.ts +17 -18
  47. package/src/shared/lib/api/raw-client.utils.ts +3 -126
  48. package/src/shared/lib/api/services/agents.service.ts +18 -0
  49. package/src/shared/lib/api/services/channel-auth.service.ts +21 -0
  50. package/src/shared/lib/api/{client.ts → services/client.service.ts} +45 -1
  51. package/src/shared/lib/api/services/config.service.ts +171 -0
  52. package/src/shared/lib/api/services/marketplace.service.ts +66 -0
  53. package/src/shared/lib/api/services/mcp-marketplace.service.ts +70 -0
  54. package/src/shared/lib/api/services/ncp-attachments.service.ts +14 -0
  55. package/src/shared/lib/api/services/ncp-session.service.ts +39 -0
  56. package/src/shared/lib/api/services/remote.service.ts +50 -0
  57. package/src/shared/lib/api/services/runtime-control.service.ts +18 -0
  58. package/src/shared/lib/api/services/runtime-update.service.ts +26 -0
  59. package/src/shared/lib/api/services/server-path.service.ts +16 -0
  60. package/src/shared/lib/transport/index.ts +1 -0
  61. package/src/shared/lib/transport/local-transport.service.ts +24 -4
  62. package/src/shared/lib/transport/remote-transport.service.ts +1 -1
  63. package/src/shared/lib/transport/request-raw-api-response.utils.ts +133 -0
  64. package/src/shared/lib/transport/transport.types.ts +8 -2
  65. package/dist/assets/api-C412zuay.js +0 -15
  66. package/dist/assets/app-manager-provider-Cm-KiZZG.js +0 -1
  67. package/dist/assets/chat-page-BCaNZJGT.js +0 -1
  68. package/dist/assets/chunk-JZWAC4HX-DvbcIVPf.js +0 -3
  69. package/dist/assets/index-CqPDhosM.js +0 -2
  70. package/dist/assets/marketplace-page-C8uaWkfd.js +0 -1
  71. package/dist/assets/mcp-marketplace-page-rNqr6ZpD.js +0 -1
  72. package/dist/assets/remote-oDlAdgVA.js +0 -1
  73. package/dist/assets/runtime-config-page-BCshTAAE.js +0 -1
  74. package/dist/assets/use-config-CrWZ_TSF.js +0 -1
  75. package/src/shared/lib/api/agents.ts +0 -34
  76. package/src/shared/lib/api/channel-auth.ts +0 -35
  77. package/src/shared/lib/api/config.ts +0 -362
  78. package/src/shared/lib/api/marketplace.ts +0 -156
  79. package/src/shared/lib/api/mcp-marketplace.ts +0 -138
  80. package/src/shared/lib/api/ncp-attachments.ts +0 -41
  81. package/src/shared/lib/api/ncp-session.ts +0 -78
  82. package/src/shared/lib/api/remote.ts +0 -86
  83. package/src/shared/lib/api/runtime-control.ts +0 -34
  84. package/src/shared/lib/api/runtime-update.service.ts +0 -50
  85. 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-CqPDhosM.js"></script>
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/api-C412zuay.js">
84
- <link rel="modulepreload" crossorigin href="/assets/es2015-BXroVnPi.js">
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-BELPuXLW.js">
87
- <link rel="modulepreload" crossorigin href="/assets/dist-DYVfg3q5.js">
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-B-CXiFPZ.js">
90
- <link rel="modulepreload" crossorigin href="/assets/popover-CPUPma-w.js">
91
- <link rel="modulepreload" crossorigin href="/assets/tooltip-CI0rpNee.js">
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/chunk-JZWAC4HX-DvbcIVPf.js">
94
- <link rel="modulepreload" crossorigin href="/assets/use-config-CrWZ_TSF.js">
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-CWHVDC6z.js">
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-ChCu7LQD.js">
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-BORqHkbN.js">
117
- <link rel="modulepreload" crossorigin href="/assets/notice-card-CozHB03G.js">
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-Cm-KiZZG.js">
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.0",
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/kernel": "0.1.2-beta.1",
32
- "@nextclaw/agent-chat-ui": "0.3.13-beta.0",
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/ncp-http-agent-client": "0.3.18-beta.0",
35
- "@nextclaw/ncp-react": "0.4.26-beta.0",
36
- "@nextclaw/agent-chat": "0.1.10"
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: Parameters<Parameters<typeof appClient.subscribe>[0]>[0];
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 appClient.subscribe((event) =>
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 { appClient } from '@/shared/lib/transport';
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 = appClient.subscribe((event) => {
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
- resolveRecentSessionPreferredThinking,
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
- resolveRecentSessionPreferredModel({
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
- resolveRecentSessionPreferredThinking({
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
- resolveRecentSessionPreferredModel,
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
- resolveRecentSessionPreferredModel({
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
- resolveRecentSessionPreferredModel({
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
- resolveRecentSessionPreferredThinking({
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
- export function resolveRecentSessionPreferredModel(params: {
164
- sessions: readonly SessionEntryView[];
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
- useEffect(() => {
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: prev,
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 { api } from './client';
1
+ import { fetchNcpSessionSkills } from './services/ncp-session.service';
2
+ import { nextclawClient } from './services/client.service';
3
3
 
4
- vi.mock('./client', () => ({
5
- api: {
6
- get: vi.fn()
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(api.get).mockReset();
13
- vi.mocked(api.get).mockResolvedValue({
14
- ok: true,
15
- data: {
16
- sessionId: 'session-1',
17
- total: 0,
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(api.get).toHaveBeenCalledWith('/api/ncp/sessions/session-1/skills');
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(api.get).toHaveBeenCalledWith(
34
- '/api/ncp/sessions/session-1/skills?projectRoot=%2Ftmp%2Fproject-alpha'
35
- );
32
+ expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', {
33
+ projectRoot: ' /tmp/project-alpha '
34
+ });
36
35
  });
37
36
  });