@nextclaw/ui 0.9.2 → 0.9.3

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 (81) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/assets/ChannelsList-ZBPiF0y2.js +1 -0
  3. package/dist/assets/ChatPage-BOgoolWK.js +38 -0
  4. package/dist/assets/{DocBrowser-CVwUDJMO.js → DocBrowser-BUYNHg0Y.js} +1 -1
  5. package/dist/assets/LogoBadge-DXPq99LJ.js +1 -0
  6. package/dist/assets/MarketplacePage-Dx7nexYN.js +49 -0
  7. package/dist/assets/McpMarketplacePage-064wdotP.js +40 -0
  8. package/dist/assets/{ModelConfig-CsX-_fyy.js → ModelConfig-BDIfLesG.js} +1 -1
  9. package/dist/assets/ProvidersList-DrlIr46m.js +1 -0
  10. package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +1 -0
  11. package/dist/assets/{RuntimeConfig-CX2TGEG1.js → RuntimeConfig-BPxXEGzM.js} +1 -1
  12. package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-BIqnlpne.js} +1 -1
  13. package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-jKZEVF2q.js} +2 -2
  14. package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-C_FXgVe1.js} +2 -2
  15. package/dist/assets/{chat-message-AWIcksDK.js → chat-message-DmzpZJc_.js} +1 -1
  16. package/dist/assets/index-Byfw276e.js +8 -0
  17. package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
  18. package/dist/assets/index-bhNuQis7.css +1 -0
  19. package/dist/assets/{label-DD61y-4v.js → label-B1MloEtn.js} +1 -1
  20. package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
  21. package/dist/assets/{page-layout-CfnoVycc.js → page-layout-BGg1EhM5.js} +1 -1
  22. package/dist/assets/{popover-DsugZ6rp.js → popover-jJMv74Fp.js} +1 -1
  23. package/dist/assets/{security-config-DIrf2Z0O.js → security-config-Boh9NIMz.js} +1 -1
  24. package/dist/assets/skeleton-CmATs_b3.js +1 -0
  25. package/dist/assets/status-dot-DNyCdxPZ.js +1 -0
  26. package/dist/assets/{switch-NX5OmUXQ.js → switch-DE_MYk7x.js} +1 -1
  27. package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-B-zErYPr.js} +1 -1
  28. package/dist/assets/{useConfirmDialog-BuQnVTeR.js → useConfirmDialog-BqQ6QfhB.js} +2 -2
  29. package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
  30. package/dist/index.html +3 -3
  31. package/package.json +4 -4
  32. package/src/App.tsx +4 -0
  33. package/src/api/auth.types.ts +24 -0
  34. package/src/api/chat-session-type.types.ts +21 -0
  35. package/src/api/marketplace.ts +8 -2
  36. package/src/api/mcp-marketplace.ts +138 -0
  37. package/src/api/remote.ts +57 -0
  38. package/src/api/remote.types.ts +80 -0
  39. package/src/api/types.ts +28 -34
  40. package/src/components/chat/ChatSidebar.test.tsx +31 -2
  41. package/src/components/chat/ChatSidebar.tsx +26 -2
  42. package/src/components/chat/chat-page-data.ts +36 -38
  43. package/src/components/chat/chat-page-runtime.test.ts +96 -2
  44. package/src/components/chat/chat-page-runtime.ts +1 -135
  45. package/src/components/chat/chat-session-preference-governance.ts +303 -0
  46. package/src/components/chat/legacy/LegacyChatPage.tsx +4 -19
  47. package/src/components/chat/ncp/NcpChatPage.tsx +4 -19
  48. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
  49. package/src/components/chat/ncp/ncp-chat-page-data.ts +62 -21
  50. package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
  51. package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
  52. package/src/components/chat/stores/chat-input.store.ts +14 -1
  53. package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
  54. package/src/components/chat/useChatSessionTypeState.ts +55 -12
  55. package/src/components/layout/Sidebar.tsx +11 -1
  56. package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
  57. package/src/components/marketplace/MarketplacePage.tsx +52 -199
  58. package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
  59. package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
  60. package/src/components/marketplace/marketplace-localization.ts +77 -0
  61. package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
  62. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
  63. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
  64. package/src/components/remote/RemoteAccessPage.tsx +320 -0
  65. package/src/components/ui/input.tsx +1 -1
  66. package/src/components/ui/label.tsx +1 -1
  67. package/src/hooks/useMarketplace.ts +36 -7
  68. package/src/hooks/useMcpMarketplace.ts +99 -0
  69. package/src/hooks/useRemoteAccess.ts +92 -0
  70. package/src/hooks/useWebSocket.ts +25 -16
  71. package/src/lib/i18n.marketplace.ts +91 -0
  72. package/src/lib/i18n.remote.ts +115 -0
  73. package/src/lib/i18n.ts +10 -68
  74. package/dist/assets/ChannelsList-DKD6Llid.js +0 -1
  75. package/dist/assets/ChatPage-BK9X4Tin.js +0 -38
  76. package/dist/assets/LogoBadge-CYQ_b7jk.js +0 -1
  77. package/dist/assets/MarketplacePage-B_2z3ii_.js +0 -49
  78. package/dist/assets/ProvidersList-CZstsyv7.js +0 -1
  79. package/dist/assets/index-BEgClaDH.js +0 -8
  80. package/dist/assets/index-C8GsgIUn.css +0 -1
  81. package/dist/assets/skeleton-DJ-Wen2o.js +0 -1
@@ -18,13 +18,13 @@ export function LegacyChatPage({ view }: ChatPageProps) {
18
18
  const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
19
19
  const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
20
20
  const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
21
+ const currentSelectedModel = useChatInputStore((state) => state.snapshot.selectedModel);
21
22
  const { confirm, ConfirmDialog } = useConfirmDialog();
22
23
  const location = useLocation();
23
24
  const navigate = useNavigate();
24
25
  const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
25
26
  const threadRef = useRef<HTMLDivElement | null>(null);
26
27
  const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
27
- const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
28
28
  const routeSessionKey = useMemo(
29
29
  () => parseSessionKeyFromRoute(routeSessionIdParam),
30
30
  [routeSessionIdParam]
@@ -40,7 +40,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
40
40
  skillRecords,
41
41
  selectedSession,
42
42
  historyMessages,
43
- selectedSessionThinkingLevel,
44
43
  sessionTypeOptions,
45
44
  defaultSessionType,
46
45
  selectedSessionType,
@@ -51,9 +50,11 @@ export function LegacyChatPage({ view }: ChatPageProps) {
51
50
  query,
52
51
  selectedSessionKey,
53
52
  selectedAgentId,
53
+ currentSelectedModel,
54
54
  pendingSessionType,
55
55
  setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
56
- setSelectedModel: presenter.chatInputManager.setSelectedModel
56
+ setSelectedModel: presenter.chatInputManager.setSelectedModel,
57
+ setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
57
58
  });
58
59
  const {
59
60
  uiMessages,
@@ -138,14 +139,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
138
139
  }, [presenter, sessionsQuery.refetch]);
139
140
 
140
141
  useEffect(() => {
141
- const shouldHydrateThinkingFromHistory =
142
- !isSending &&
143
- !isAwaitingAssistantOutput &&
144
- !historyQuery.isLoading &&
145
- isProviderStateResolved &&
146
- modelOptions.length > 0 &&
147
- selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
148
-
149
142
  presenter.chatInputManager.syncSnapshot({
150
143
  isProviderStateResolved,
151
144
  defaultSessionType,
@@ -158,18 +151,11 @@ export function LegacyChatPage({ view }: ChatPageProps) {
158
151
  modelOptions,
159
152
  sessionTypeOptions,
160
153
  selectedSessionType,
161
- ...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
162
154
  canEditSessionType,
163
155
  sessionTypeUnavailable,
164
156
  skillRecords,
165
157
  isSkillsLoading: installedSkillsQuery.isLoading
166
158
  });
167
- if (shouldHydrateThinkingFromHistory) {
168
- thinkingHydratedSessionKeyRef.current = selectedSessionKey;
169
- }
170
- if (!selectedSessionKey) {
171
- thinkingHydratedSessionKeyRef.current = null;
172
- }
173
159
  presenter.chatSessionListManager.syncSnapshot({
174
160
  sessions,
175
161
  query,
@@ -216,7 +202,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
216
202
  query,
217
203
  selectedSession,
218
204
  selectedSessionKey,
219
- selectedSessionThinkingLevel,
220
205
  selectedSessionType,
221
206
  sessionRunStatusByKey,
222
207
  sessionTypeOptions,
@@ -65,13 +65,13 @@ export function NcpChatPage({ view }: ChatPageProps) {
65
65
  const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
66
66
  const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
67
67
  const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
68
+ const currentSelectedModel = useChatInputStore((state) => state.snapshot.selectedModel);
68
69
  const { confirm, ConfirmDialog } = useConfirmDialog();
69
70
  const location = useLocation();
70
71
  const navigate = useNavigate();
71
72
  const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
72
73
  const threadRef = useRef<HTMLDivElement | null>(null);
73
74
  const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
74
- const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
75
75
  const routeSessionKey = useMemo(
76
76
  () => parseSessionKeyFromRoute(routeSessionIdParam),
77
77
  [routeSessionIdParam]
@@ -85,7 +85,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
85
85
  sessions,
86
86
  skillRecords,
87
87
  selectedSession,
88
- selectedSessionThinkingLevel,
89
88
  sessionTypeOptions,
90
89
  defaultSessionType,
91
90
  selectedSessionType,
@@ -95,9 +94,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
95
94
  } = useNcpChatPageData({
96
95
  query,
97
96
  selectedSessionKey,
97
+ currentSelectedModel,
98
98
  pendingSessionType,
99
99
  setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
100
- setSelectedModel: presenter.chatInputManager.setSelectedModel
100
+ setSelectedModel: presenter.chatInputManager.setSelectedModel,
101
+ setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
101
102
  });
102
103
  const refetchSessions = sessionsQuery.refetch;
103
104
 
@@ -276,14 +277,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
276
277
  }, [presenter, sessionsQuery.refetch]);
277
278
 
278
279
  useEffect(() => {
279
- const shouldHydrateThinkingFromSession =
280
- !isSending &&
281
- !isAwaitingAssistantOutput &&
282
- !agent.isHydrating &&
283
- isProviderStateResolved &&
284
- modelOptions.length > 0 &&
285
- selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
286
-
287
280
  presenter.chatInputManager.syncSnapshot({
288
281
  isProviderStateResolved,
289
282
  defaultSessionType,
@@ -296,18 +289,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
296
289
  modelOptions,
297
290
  sessionTypeOptions,
298
291
  selectedSessionType,
299
- ...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
300
292
  canEditSessionType,
301
293
  sessionTypeUnavailable,
302
294
  skillRecords,
303
295
  isSkillsLoading: installedSkillsQuery.isLoading
304
296
  });
305
- if (shouldHydrateThinkingFromSession) {
306
- thinkingHydratedSessionKeyRef.current = selectedSessionKey;
307
- }
308
- if (!selectedSessionKey) {
309
- thinkingHydratedSessionKeyRef.current = null;
310
- }
311
297
  presenter.chatSessionListManager.syncSnapshot({
312
298
  sessions,
313
299
  query,
@@ -352,7 +338,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
352
338
  query,
353
339
  selectedSession,
354
340
  selectedSessionKey,
355
- selectedSessionThinkingLevel,
356
341
  selectedSessionType,
357
342
  sessionRunStatusByKey,
358
343
  sessionTypeOptions,
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { filterModelOptionsBySessionType } from '@/components/chat/ncp/ncp-chat-page-data';
3
+ import type { ChatModelOption } from '@/components/chat/chat-input.types';
4
+
5
+ const modelOptions: ChatModelOption[] = [
6
+ {
7
+ value: 'dashscope/qwen3-coder-next',
8
+ modelLabel: 'qwen3-coder-next',
9
+ providerLabel: 'DashScope'
10
+ },
11
+ {
12
+ value: 'anthropic/claude-sonnet-4-5',
13
+ modelLabel: 'claude-sonnet-4-5',
14
+ providerLabel: 'Anthropic'
15
+ }
16
+ ];
17
+
18
+ describe('filterModelOptionsBySessionType', () => {
19
+ it('keeps only session-type-supported models when the runtime publishes a filtered list', () => {
20
+ expect(
21
+ filterModelOptionsBySessionType({
22
+ modelOptions,
23
+ supportedModels: ['dashscope/qwen3-coder-next']
24
+ })
25
+ ).toEqual([modelOptions[0]]);
26
+ });
27
+
28
+ it('falls back to the full model catalog when the advertised models do not match the current catalog', () => {
29
+ expect(
30
+ filterModelOptionsBySessionType({
31
+ modelOptions,
32
+ supportedModels: ['unknown/model']
33
+ })
34
+ ).toEqual(modelOptions);
35
+ });
36
+ });
@@ -1,16 +1,15 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { Dispatch, SetStateAction } from 'react';
3
- import type { SessionEntryView } from '@/api/types';
3
+ import type { SessionEntryView, ThinkingLevel } from '@/api/types';
4
4
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
- import {
6
- adaptNcpSessionSummaries,
7
- readNcpSessionPreferredThinking
8
- } from '@/components/chat/ncp/ncp-session-adapter';
5
+ import { adaptNcpSessionSummaries } from '@/components/chat/ncp/ncp-session-adapter';
9
6
  import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
10
7
  import {
8
+ resolveRecentSessionPreferredThinking,
11
9
  resolveRecentSessionPreferredModel,
12
- useSyncSelectedModel
13
- } from '@/components/chat/chat-page-runtime';
10
+ useSyncSelectedModel,
11
+ useSyncSelectedThinking
12
+ } from '@/components/chat/chat-session-preference-governance';
14
13
  import {
15
14
  useConfig,
16
15
  useConfigMeta,
@@ -23,9 +22,11 @@ import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCa
23
22
  type UseNcpChatPageDataParams = {
24
23
  query: string;
25
24
  selectedSessionKey: string | null;
25
+ currentSelectedModel: string;
26
26
  pendingSessionType: string;
27
27
  setPendingSessionType: Dispatch<SetStateAction<string>>;
28
28
  setSelectedModel: Dispatch<SetStateAction<string>>;
29
+ setSelectedThinkingLevel: Dispatch<SetStateAction<ThinkingLevel | null>>;
29
30
  };
30
31
 
31
32
  function filterSessionsByQuery(sessions: SessionEntryView[], query: string): SessionEntryView[] {
@@ -36,6 +37,18 @@ function filterSessionsByQuery(sessions: SessionEntryView[], query: string): Ses
36
37
  return sessions.filter((session) => session.key.toLowerCase().includes(normalizedQuery));
37
38
  }
38
39
 
40
+ export function filterModelOptionsBySessionType(params: {
41
+ modelOptions: ChatModelOption[];
42
+ supportedModels?: string[];
43
+ }): ChatModelOption[] {
44
+ if (!params.supportedModels || params.supportedModels.length === 0) {
45
+ return params.modelOptions;
46
+ }
47
+ const supportedModelSet = new Set(params.supportedModels);
48
+ const filtered = params.modelOptions.filter((option) => supportedModelSet.has(option.value));
49
+ return filtered.length > 0 ? filtered : params.modelOptions;
50
+ }
51
+
39
52
  export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
40
53
  const configQuery = useConfig();
41
54
  const configMetaQuery = useConfigMeta();
@@ -94,19 +107,10 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
94
107
  () => allSessions.find((session) => session.key === params.selectedSessionKey) ?? null,
95
108
  [allSessions, params.selectedSessionKey]
96
109
  );
97
- const selectedSessionSummary = useMemo(
98
- () => sessionSummaries.find((session) => session.sessionId === params.selectedSessionKey) ?? null,
99
- [params.selectedSessionKey, sessionSummaries]
100
- );
101
110
  const skillRecords = useMemo(
102
111
  () => installedSkillsQuery.data?.records ?? [],
103
112
  [installedSkillsQuery.data?.records]
104
113
  );
105
- const selectedSessionThinkingLevel = useMemo(
106
- () => (selectedSessionSummary ? readNcpSessionPreferredThinking(selectedSessionSummary) : null),
107
- [selectedSessionSummary]
108
- );
109
-
110
114
  const sessionTypeState = useChatSessionTypeState({
111
115
  selectedSession,
112
116
  selectedSessionKey: params.selectedSessionKey,
@@ -114,6 +118,14 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
114
118
  setPendingSessionType: params.setPendingSessionType,
115
119
  sessionTypesData: sessionTypesQuery.data
116
120
  });
121
+ const filteredModelOptions = useMemo(
122
+ () =>
123
+ filterModelOptionsBySessionType({
124
+ modelOptions,
125
+ supportedModels: sessionTypeState.selectedSessionTypeOption?.supportedModels
126
+ }),
127
+ [modelOptions, sessionTypeState.selectedSessionTypeOption?.supportedModels]
128
+ );
117
129
  const recentSessionPreferredModel = useMemo(
118
130
  () =>
119
131
  resolveRecentSessionPreferredModel({
@@ -123,16 +135,46 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
123
135
  }),
124
136
  [allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
125
137
  );
138
+ const currentModelOption = useMemo(
139
+ () => filteredModelOptions.find((option) => option.value === params.currentSelectedModel),
140
+ [filteredModelOptions, params.currentSelectedModel]
141
+ );
142
+ const supportedThinkingLevels = useMemo(
143
+ () => (currentModelOption?.thinkingCapability?.supported as ThinkingLevel[] | undefined) ?? [],
144
+ [currentModelOption?.thinkingCapability?.supported]
145
+ );
146
+ const defaultThinkingLevel = useMemo(
147
+ () => (currentModelOption?.thinkingCapability?.default as ThinkingLevel | null | undefined) ?? null,
148
+ [currentModelOption?.thinkingCapability?.default]
149
+ );
150
+ const recentSessionPreferredThinking = useMemo(
151
+ () =>
152
+ resolveRecentSessionPreferredThinking({
153
+ sessions: allSessions,
154
+ selectedSessionKey: params.selectedSessionKey,
155
+ sessionType: sessionTypeState.selectedSessionType
156
+ }),
157
+ [allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
158
+ );
126
159
 
127
160
  useSyncSelectedModel({
128
- modelOptions,
161
+ modelOptions: filteredModelOptions,
129
162
  selectedSessionKey: params.selectedSessionKey,
130
163
  selectedSessionExists: Boolean(selectedSession),
131
164
  selectedSessionPreferredModel: selectedSession?.preferredModel,
132
- fallbackPreferredModel: recentSessionPreferredModel,
133
- defaultModel: configQuery.data?.agents.defaults.model,
165
+ fallbackPreferredModel: sessionTypeState.selectedSessionTypeOption?.recommendedModel ?? recentSessionPreferredModel,
166
+ defaultModel: sessionTypeState.selectedSessionTypeOption?.recommendedModel ?? configQuery.data?.agents.defaults.model,
134
167
  setSelectedModel: params.setSelectedModel
135
168
  });
169
+ useSyncSelectedThinking({
170
+ supportedThinkingLevels,
171
+ selectedSessionKey: params.selectedSessionKey,
172
+ selectedSessionExists: Boolean(selectedSession),
173
+ selectedSessionPreferredThinking: selectedSession?.preferredThinking ?? null,
174
+ fallbackPreferredThinking: recentSessionPreferredThinking ?? null,
175
+ defaultThinkingLevel,
176
+ setSelectedThinkingLevel: params.setSelectedThinkingLevel
177
+ });
136
178
 
137
179
  return {
138
180
  configQuery,
@@ -141,12 +183,11 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
141
183
  sessionTypesQuery,
142
184
  installedSkillsQuery,
143
185
  isProviderStateResolved,
144
- modelOptions,
186
+ modelOptions: filteredModelOptions,
145
187
  sessionSummaries,
146
188
  sessions,
147
189
  skillRecords,
148
190
  selectedSession,
149
- selectedSessionThinkingLevel,
150
191
  ...sessionTypeState
151
192
  };
152
193
  }
@@ -22,6 +22,7 @@ describe('adaptNcpSessionSummary', () => {
22
22
  metadata: {
23
23
  label: 'NCP Planning Thread',
24
24
  model: 'openai/gpt-5',
25
+ preferred_thinking: 'medium',
25
26
  session_type: 'native'
26
27
  }
27
28
  })
@@ -31,6 +32,7 @@ describe('adaptNcpSessionSummary', () => {
31
32
  key: 'ncp-session-1',
32
33
  label: 'NCP Planning Thread',
33
34
  preferredModel: 'openai/gpt-5',
35
+ preferredThinking: 'medium',
34
36
  sessionType: 'native',
35
37
  sessionTypeMutable: false,
36
38
  messageCount: 3
@@ -175,12 +175,14 @@ export function adaptNcpMessagesToUiMessages(messages: readonly NcpMessageView[]
175
175
  export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionEntryView {
176
176
  const label = readNcpSessionLabel(summary);
177
177
  const preferredModel = readNcpSessionPreferredModel(summary);
178
+ const preferredThinking = readNcpSessionPreferredThinking(summary);
178
179
  return {
179
180
  key: summary.sessionId,
180
181
  createdAt: summary.updatedAt,
181
182
  updatedAt: summary.updatedAt,
182
183
  ...(label ? { label } : {}),
183
184
  ...(preferredModel ? { preferredModel } : {}),
185
+ ...(preferredThinking ? { preferredThinking } : {}),
184
186
  sessionType: readNcpSessionType(summary),
185
187
  sessionTypeMutable: false,
186
188
  messageCount: summary.messageCount
@@ -18,7 +18,20 @@ export type ChatInputSnapshot = {
18
18
  modelOptions: ChatModelOption[];
19
19
  selectedModel: string;
20
20
  selectedThinkingLevel: ThinkingLevel | null;
21
- sessionTypeOptions: Array<{ value: string; label: string }>;
21
+ sessionTypeOptions: Array<{
22
+ value: string;
23
+ label: string;
24
+ ready?: boolean;
25
+ reason?: string | null;
26
+ reasonMessage?: string | null;
27
+ supportedModels?: string[];
28
+ recommendedModel?: string | null;
29
+ cta?: {
30
+ kind: string;
31
+ label?: string;
32
+ href?: string;
33
+ } | null;
34
+ }>;
22
35
  selectedSessionType?: string;
23
36
  stopSupported: boolean;
24
37
  stopReason?: string;
@@ -55,4 +55,33 @@ describe('useChatSessionTypeState', () => {
55
55
 
56
56
  expect(setPendingSessionType).toHaveBeenCalledWith('codex-sdk');
57
57
  });
58
+
59
+ it('marks the selected draft session type as unavailable when runtime setup is incomplete', () => {
60
+ const setPendingSessionType = vi.fn();
61
+
62
+ const { result } = renderHook(() =>
63
+ useChatSessionTypeState({
64
+ selectedSession: null,
65
+ selectedSessionKey: null,
66
+ pendingSessionType: 'claude',
67
+ setPendingSessionType,
68
+ sessionTypesData: {
69
+ defaultType: 'native',
70
+ options: [
71
+ { value: 'native', label: 'Native', ready: true },
72
+ {
73
+ value: 'claude',
74
+ label: 'Claude',
75
+ ready: false,
76
+ reasonMessage: 'Configure a provider API key first.'
77
+ }
78
+ ]
79
+ }
80
+ })
81
+ );
82
+
83
+ expect(result.current.selectedSessionTypeOption?.ready).toBe(false);
84
+ expect(result.current.sessionTypeUnavailable).toBe(true);
85
+ expect(result.current.sessionTypeUnavailableMessage).toBe('Configure a provider API key first.');
86
+ });
58
87
  });
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, useRef } from 'react';
2
2
  import type { Dispatch, SetStateAction } from 'react';
3
- import type { SessionEntryView } from '@/api/types';
3
+ import type { ChatSessionTypeOptionView, SessionEntryView } from '@/api/types';
4
4
  import { t } from '@/lib/i18n';
5
5
 
6
6
  export const DEFAULT_SESSION_TYPE = 'native';
@@ -8,6 +8,16 @@ export const DEFAULT_SESSION_TYPE = 'native';
8
8
  export type ChatSessionTypeOption = {
9
9
  value: string;
10
10
  label: string;
11
+ ready: boolean;
12
+ reason?: string | null;
13
+ reasonMessage?: string | null;
14
+ supportedModels?: string[];
15
+ recommendedModel?: string | null;
16
+ cta?: {
17
+ kind: string;
18
+ label?: string;
19
+ href?: string;
20
+ } | null;
11
21
  };
12
22
 
13
23
  type UseChatSessionTypeStateParams = {
@@ -17,7 +27,7 @@ type UseChatSessionTypeStateParams = {
17
27
  setPendingSessionType: Dispatch<SetStateAction<string>>;
18
28
  sessionTypesData?: {
19
29
  defaultType?: string;
20
- options?: Array<{ value: string; label: string }>;
30
+ options?: ChatSessionTypeOptionView[];
21
31
  } | null;
22
32
  };
23
33
 
@@ -46,20 +56,32 @@ export function resolveSessionTypeLabel(sessionType: string, fallbackLabel?: str
46
56
  }
47
57
 
48
58
  function buildSessionTypeOptions(
49
- options: Array<{ value: string; label: string }>
59
+ options: ChatSessionTypeOptionView[]
50
60
  ): ChatSessionTypeOption[] {
51
61
  const deduped = new Map<string, ChatSessionTypeOption>();
52
62
  for (const option of options) {
53
63
  const value = normalizeSessionType(option.value);
54
64
  deduped.set(value, {
55
65
  value,
56
- label: option.label?.trim() || resolveSessionTypeLabel(value)
66
+ label: option.label?.trim() || resolveSessionTypeLabel(value),
67
+ ready: option.ready ?? true,
68
+ reason: option.reason ?? null,
69
+ reasonMessage: option.reasonMessage ?? null,
70
+ supportedModels: option.supportedModels,
71
+ recommendedModel: option.recommendedModel ?? null,
72
+ cta: option.cta ?? null
57
73
  });
58
74
  }
59
75
  if (!deduped.has(DEFAULT_SESSION_TYPE)) {
60
76
  deduped.set(DEFAULT_SESSION_TYPE, {
61
77
  value: DEFAULT_SESSION_TYPE,
62
- label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE)
78
+ label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE),
79
+ ready: true,
80
+ reason: null,
81
+ reasonMessage: null,
82
+ supportedModels: undefined,
83
+ recommendedModel: null,
84
+ cta: null
63
85
  });
64
86
  }
65
87
  return Array.from(deduped.values()).sort((left, right) => {
@@ -75,6 +97,7 @@ function buildSessionTypeOptions(
75
97
 
76
98
  export function useChatSessionTypeState(params: UseChatSessionTypeStateParams): {
77
99
  sessionTypeOptions: ChatSessionTypeOption[];
100
+ selectedSessionTypeOption: ChatSessionTypeOption | null;
78
101
  defaultSessionType: string;
79
102
  selectedSessionType: string;
80
103
  canEditSessionType: boolean;
@@ -99,7 +122,13 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
99
122
  if (!options.some((option) => option.value === currentSessionType)) {
100
123
  options.push({
101
124
  value: currentSessionType,
102
- label: resolveSessionTypeLabel(currentSessionType)
125
+ label: resolveSessionTypeLabel(currentSessionType),
126
+ ready: true,
127
+ reason: null,
128
+ reasonMessage: null,
129
+ supportedModels: undefined,
130
+ recommendedModel: null,
131
+ cta: null
103
132
  });
104
133
  }
105
134
  return options.sort((left, right) => {
@@ -121,6 +150,10 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
121
150
  () => normalizeSessionType(selectedSession?.sessionType ?? pendingSessionType ?? defaultSessionType),
122
151
  [defaultSessionType, pendingSessionType, selectedSession?.sessionType]
123
152
  );
153
+ const selectedSessionTypeOption = useMemo(
154
+ () => sessionTypeOptions.find((option) => option.value === selectedSessionType) ?? null,
155
+ [selectedSessionType, sessionTypeOptions]
156
+ );
124
157
 
125
158
  useEffect(() => {
126
159
  if (selectedSessionKey) {
@@ -147,15 +180,25 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
147
180
  () => new Set(runtimeSessionTypeOptions.map((option) => option.value)),
148
181
  [runtimeSessionTypeOptions]
149
182
  );
150
- const sessionTypeUnavailable = Boolean(
151
- selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))
152
- );
153
- const sessionTypeUnavailableMessage = sessionTypeUnavailable
154
- ? `${resolveSessionTypeLabel(selectedSessionType)} ${t('chatSessionTypeUnavailableSuffix')}`
155
- : null;
183
+ const sessionTypeUnavailable = useMemo(() => {
184
+ if (selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))) {
185
+ return true;
186
+ }
187
+ return selectedSessionTypeOption?.ready === false;
188
+ }, [availableSessionTypeSet, selectedSession, selectedSessionTypeOption?.ready]);
189
+ const sessionTypeUnavailableMessage = useMemo(() => {
190
+ if (selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))) {
191
+ return `${resolveSessionTypeLabel(selectedSessionType)} ${t('chatSessionTypeUnavailableSuffix')}`;
192
+ }
193
+ if (selectedSessionTypeOption?.ready === false) {
194
+ return selectedSessionTypeOption.reasonMessage?.trim() || `${selectedSessionTypeOption.label} setup required`;
195
+ }
196
+ return null;
197
+ }, [availableSessionTypeSet, selectedSession, selectedSessionType, selectedSessionTypeOption]);
156
198
 
157
199
  return {
158
200
  sessionTypeOptions,
201
+ selectedSessionTypeOption,
159
202
  defaultSessionType,
160
203
  selectedSessionType,
161
204
  canEditSessionType,
@@ -1,7 +1,7 @@
1
1
  import { cn } from '@/lib/utils';
2
2
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
3
3
  import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
4
- import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search, Shield } from 'lucide-react';
4
+ import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search, Shield, Wrench, Wifi } from 'lucide-react';
5
5
  import { NavLink } from 'react-router-dom';
6
6
  import { useDocBrowser } from '@/components/doc-browser';
7
7
  import { BrandHeader } from '@/components/common/BrandHeader';
@@ -82,6 +82,11 @@ export function Sidebar({ mode }: SidebarProps) {
82
82
  label: t('runtime'),
83
83
  icon: GitBranch,
84
84
  },
85
+ {
86
+ target: '/remote',
87
+ label: t('remote'),
88
+ icon: Wifi,
89
+ },
85
90
  {
86
91
  target: '/security',
87
92
  label: t('security'),
@@ -101,6 +106,11 @@ export function Sidebar({ mode }: SidebarProps) {
101
106
  target: '/marketplace/plugins',
102
107
  label: t('marketplaceFilterPlugins'),
103
108
  icon: Plug,
109
+ },
110
+ {
111
+ target: '/marketplace/mcp',
112
+ label: t('marketplaceFilterMcp'),
113
+ icon: Wrench,
104
114
  }
105
115
  ];
106
116
  const navItems = mode === 'main' ? mainNavItems : settingsNavItems;