@nextclaw/ui 0.9.1 → 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.
- package/CHANGELOG.md +12 -0
- package/dist/assets/ChannelsList-ZBPiF0y2.js +1 -0
- package/dist/assets/ChatPage-BOgoolWK.js +38 -0
- package/dist/assets/{DocBrowser-LpzGe8An.js → DocBrowser-BUYNHg0Y.js} +1 -1
- package/dist/assets/LogoBadge-DXPq99LJ.js +1 -0
- package/dist/assets/MarketplacePage-Dx7nexYN.js +49 -0
- package/dist/assets/McpMarketplacePage-064wdotP.js +40 -0
- package/dist/assets/{ModelConfig-DuImUHIX.js → ModelConfig-BDIfLesG.js} +1 -1
- package/dist/assets/ProvidersList-DrlIr46m.js +1 -0
- package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +1 -0
- package/dist/assets/{RuntimeConfig-C6iqpJR_.js → RuntimeConfig-BPxXEGzM.js} +1 -1
- package/dist/assets/{SearchConfig-Dvp1TAXu.js → SearchConfig-BIqnlpne.js} +1 -1
- package/dist/assets/{SecretsConfig-D5Ymlvt9.js → SecretsConfig-jKZEVF2q.js} +2 -2
- package/dist/assets/{SessionsConfig-CIA_jA1P.js → SessionsConfig-C_FXgVe1.js} +2 -2
- package/dist/assets/{chat-message-B60Fh9kI.js → chat-message-DmzpZJc_.js} +1 -1
- package/dist/assets/index-Byfw276e.js +8 -0
- package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
- package/dist/assets/index-bhNuQis7.css +1 -0
- package/dist/assets/{label-D4fGx6Wb.js → label-B1MloEtn.js} +1 -1
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
- package/dist/assets/{page-layout-twy8gmBE.js → page-layout-BGg1EhM5.js} +1 -1
- package/dist/assets/{popover-DYbYpt1j.js → popover-jJMv74Fp.js} +1 -1
- package/dist/assets/{security-config-BcIZ4rpb.js → security-config-Boh9NIMz.js} +1 -1
- package/dist/assets/skeleton-CmATs_b3.js +1 -0
- package/dist/assets/status-dot-DNyCdxPZ.js +1 -0
- package/dist/assets/{switch-DqA6r5XR.js → switch-DE_MYk7x.js} +1 -1
- package/dist/assets/{tabs-custom-C6enKKs1.js → tabs-custom-B-zErYPr.js} +1 -1
- package/dist/assets/{useConfirmDialog-CHBf5Of7.js → useConfirmDialog-BqQ6QfhB.js} +2 -2
- package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
- package/dist/index.html +3 -3
- package/package.json +4 -4
- package/src/App.tsx +4 -0
- package/src/api/auth.types.ts +24 -0
- package/src/api/chat-session-type.types.ts +21 -0
- package/src/api/marketplace.ts +8 -2
- package/src/api/mcp-marketplace.ts +138 -0
- package/src/api/remote.ts +57 -0
- package/src/api/remote.types.ts +80 -0
- package/src/api/types.ts +91 -37
- package/src/components/chat/ChatSidebar.test.tsx +31 -2
- package/src/components/chat/ChatSidebar.tsx +26 -2
- package/src/components/chat/chat-page-data.ts +37 -53
- package/src/components/chat/chat-page-runtime.test.ts +122 -2
- package/src/components/chat/chat-page-runtime.ts +1 -118
- package/src/components/chat/chat-session-preference-governance.ts +303 -0
- package/src/components/chat/legacy/LegacyChatPage.tsx +4 -34
- package/src/components/chat/ncp/NcpChatPage.tsx +4 -34
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +63 -36
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
- package/src/components/chat/stores/chat-input.store.ts +14 -1
- package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
- package/src/components/chat/useChatSessionTypeState.ts +55 -12
- package/src/components/layout/Sidebar.tsx +11 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
- package/src/components/marketplace/MarketplacePage.tsx +52 -199
- package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
- package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
- package/src/components/marketplace/marketplace-localization.ts +77 -0
- package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
- package/src/components/remote/RemoteAccessPage.tsx +320 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/hooks/useMarketplace.ts +36 -7
- package/src/hooks/useMcpMarketplace.ts +99 -0
- package/src/hooks/useRemoteAccess.ts +92 -0
- package/src/hooks/useWebSocket.ts +25 -16
- package/src/lib/i18n.marketplace.ts +91 -0
- package/src/lib/i18n.remote.ts +115 -0
- package/src/lib/i18n.ts +10 -68
- package/dist/assets/ChannelsList-DhvjpZcs.js +0 -1
- package/dist/assets/ChatPage-B8VBaMQm.js +0 -38
- package/dist/assets/LogoBadge-Be4lktJN.js +0 -1
- package/dist/assets/MarketplacePage-Cx9AI3_h.js +0 -49
- package/dist/assets/ProvidersList-Ccleg25k.js +0 -1
- package/dist/assets/index-BiPDnzv0.js +0 -8
- package/dist/assets/index-C8GsgIUn.css +0 -1
- package/dist/assets/skeleton-DypBy7jp.js +0 -1
|
@@ -1,17 +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 {
|
|
11
|
-
|
|
8
|
+
resolveRecentSessionPreferredThinking,
|
|
12
9
|
resolveRecentSessionPreferredModel,
|
|
13
|
-
useSyncSelectedModel
|
|
14
|
-
|
|
10
|
+
useSyncSelectedModel,
|
|
11
|
+
useSyncSelectedThinking
|
|
12
|
+
} from '@/components/chat/chat-session-preference-governance';
|
|
15
13
|
import {
|
|
16
14
|
useConfig,
|
|
17
15
|
useConfigMeta,
|
|
@@ -24,9 +22,11 @@ import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCa
|
|
|
24
22
|
type UseNcpChatPageDataParams = {
|
|
25
23
|
query: string;
|
|
26
24
|
selectedSessionKey: string | null;
|
|
25
|
+
currentSelectedModel: string;
|
|
27
26
|
pendingSessionType: string;
|
|
28
27
|
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
29
28
|
setSelectedModel: Dispatch<SetStateAction<string>>;
|
|
29
|
+
setSelectedThinkingLevel: Dispatch<SetStateAction<ThinkingLevel | null>>;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
function filterSessionsByQuery(sessions: SessionEntryView[], query: string): SessionEntryView[] {
|
|
@@ -37,6 +37,18 @@ function filterSessionsByQuery(sessions: SessionEntryView[], query: string): Ses
|
|
|
37
37
|
return sessions.filter((session) => session.key.toLowerCase().includes(normalizedQuery));
|
|
38
38
|
}
|
|
39
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
|
+
|
|
40
52
|
export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
41
53
|
const configQuery = useConfig();
|
|
42
54
|
const configMetaQuery = useConfigMeta();
|
|
@@ -95,19 +107,10 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
95
107
|
() => allSessions.find((session) => session.key === params.selectedSessionKey) ?? null,
|
|
96
108
|
[allSessions, params.selectedSessionKey]
|
|
97
109
|
);
|
|
98
|
-
const selectedSessionSummary = useMemo(
|
|
99
|
-
() => sessionSummaries.find((session) => session.sessionId === params.selectedSessionKey) ?? null,
|
|
100
|
-
[params.selectedSessionKey, sessionSummaries]
|
|
101
|
-
);
|
|
102
110
|
const skillRecords = useMemo(
|
|
103
111
|
() => installedSkillsQuery.data?.records ?? [],
|
|
104
112
|
[installedSkillsQuery.data?.records]
|
|
105
113
|
);
|
|
106
|
-
const selectedSessionThinkingLevel = useMemo(
|
|
107
|
-
() => (selectedSessionSummary ? readNcpSessionPreferredThinking(selectedSessionSummary) : null),
|
|
108
|
-
[selectedSessionSummary]
|
|
109
|
-
);
|
|
110
|
-
|
|
111
114
|
const sessionTypeState = useChatSessionTypeState({
|
|
112
115
|
selectedSession,
|
|
113
116
|
selectedSessionKey: params.selectedSessionKey,
|
|
@@ -115,6 +118,14 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
115
118
|
setPendingSessionType: params.setPendingSessionType,
|
|
116
119
|
sessionTypesData: sessionTypesQuery.data
|
|
117
120
|
});
|
|
121
|
+
const filteredModelOptions = useMemo(
|
|
122
|
+
() =>
|
|
123
|
+
filterModelOptionsBySessionType({
|
|
124
|
+
modelOptions,
|
|
125
|
+
supportedModels: sessionTypeState.selectedSessionTypeOption?.supportedModels
|
|
126
|
+
}),
|
|
127
|
+
[modelOptions, sessionTypeState.selectedSessionTypeOption?.supportedModels]
|
|
128
|
+
);
|
|
118
129
|
const recentSessionPreferredModel = useMemo(
|
|
119
130
|
() =>
|
|
120
131
|
resolveRecentSessionPreferredModel({
|
|
@@ -124,28 +135,46 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
124
135
|
}),
|
|
125
136
|
[allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
|
|
126
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
|
+
);
|
|
127
159
|
|
|
128
160
|
useSyncSelectedModel({
|
|
129
|
-
modelOptions,
|
|
161
|
+
modelOptions: filteredModelOptions,
|
|
130
162
|
selectedSessionKey: params.selectedSessionKey,
|
|
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
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
preferSessionPreferredModel: true
|
|
146
|
-
}),
|
|
147
|
-
[configQuery.data?.agents.defaults.model, modelOptions, recentSessionPreferredModel, selectedSession?.preferredModel]
|
|
148
|
-
);
|
|
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
|
+
});
|
|
149
178
|
|
|
150
179
|
return {
|
|
151
180
|
configQuery,
|
|
@@ -154,13 +183,11 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
154
183
|
sessionTypesQuery,
|
|
155
184
|
installedSkillsQuery,
|
|
156
185
|
isProviderStateResolved,
|
|
157
|
-
modelOptions,
|
|
186
|
+
modelOptions: filteredModelOptions,
|
|
158
187
|
sessionSummaries,
|
|
159
188
|
sessions,
|
|
160
189
|
skillRecords,
|
|
161
190
|
selectedSession,
|
|
162
|
-
hydratedSessionModel,
|
|
163
|
-
selectedSessionThinkingLevel,
|
|
164
191
|
...sessionTypeState
|
|
165
192
|
};
|
|
166
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<{
|
|
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?:
|
|
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:
|
|
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 =
|
|
151
|
-
selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
2
3
|
import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
|
|
3
4
|
import type {
|
|
5
|
+
MarketplaceInstalledRecord,
|
|
4
6
|
MarketplaceInstalledView,
|
|
5
7
|
MarketplaceItemSummary,
|
|
6
8
|
MarketplaceListView
|
|
@@ -35,6 +37,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
35
37
|
},
|
|
36
38
|
manageMutation: {
|
|
37
39
|
mutate: vi.fn(),
|
|
40
|
+
mutateAsync: vi.fn(),
|
|
38
41
|
isPending: false,
|
|
39
42
|
variables: undefined
|
|
40
43
|
}
|
|
@@ -95,6 +98,37 @@ function createMarketplaceItem(overrides: Partial<MarketplaceItemSummary> = {}):
|
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
function createPluginMarketplaceItem(overrides: Partial<MarketplaceItemSummary> = {}): MarketplaceItemSummary {
|
|
102
|
+
return createMarketplaceItem({
|
|
103
|
+
id: 'plugin-codex-runtime',
|
|
104
|
+
slug: 'codex-runtime',
|
|
105
|
+
type: 'plugin',
|
|
106
|
+
name: 'Codex SDK NCP Runtime',
|
|
107
|
+
summary: 'Optional Codex runtime for NextClaw',
|
|
108
|
+
summaryI18n: { en: 'Optional Codex runtime for NextClaw' },
|
|
109
|
+
install: {
|
|
110
|
+
kind: 'npm',
|
|
111
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
112
|
+
command: 'npm install @nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk'
|
|
113
|
+
},
|
|
114
|
+
...overrides
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function createInstalledRecord(overrides: Partial<MarketplaceInstalledRecord> = {}): MarketplaceInstalledRecord {
|
|
119
|
+
return {
|
|
120
|
+
type: 'plugin',
|
|
121
|
+
id: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
122
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
123
|
+
label: 'Codex SDK NCP Runtime',
|
|
124
|
+
enabled: true,
|
|
125
|
+
origin: 'marketplace',
|
|
126
|
+
source: 'marketplace',
|
|
127
|
+
installedAt: '2026-03-19T00:00:00.000Z',
|
|
128
|
+
...overrides
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
98
132
|
function createItemsQuery(overrides: Partial<Record<string, unknown>> = {}) {
|
|
99
133
|
return {
|
|
100
134
|
data: undefined as MarketplaceListView | undefined,
|
|
@@ -129,6 +163,7 @@ describe('MarketplacePage', () => {
|
|
|
129
163
|
mocks.confirm.mockReset();
|
|
130
164
|
mocks.installMutation.mutateAsync.mockReset();
|
|
131
165
|
mocks.manageMutation.mutate.mockReset();
|
|
166
|
+
mocks.manageMutation.mutateAsync.mockReset();
|
|
132
167
|
mocks.installMutation.isPending = false;
|
|
133
168
|
mocks.installMutation.variables = undefined;
|
|
134
169
|
mocks.manageMutation.isPending = false;
|
|
@@ -167,4 +202,121 @@ describe('MarketplacePage', () => {
|
|
|
167
202
|
expect(screen.queryByTestId('marketplace-list-skeleton')).toBeNull();
|
|
168
203
|
expect(screen.getByText('Web Search')).toBeTruthy();
|
|
169
204
|
});
|
|
205
|
+
|
|
206
|
+
it('does not render the redundant plugin type label in plugin cards', () => {
|
|
207
|
+
mocks.itemsQuery = createItemsQuery({
|
|
208
|
+
data: {
|
|
209
|
+
total: 1,
|
|
210
|
+
page: 1,
|
|
211
|
+
pageSize: 12,
|
|
212
|
+
totalPages: 1,
|
|
213
|
+
sort: 'relevance',
|
|
214
|
+
items: [
|
|
215
|
+
createMarketplaceItem({
|
|
216
|
+
id: 'plugin-codex-runtime',
|
|
217
|
+
slug: 'codex-runtime',
|
|
218
|
+
type: 'plugin',
|
|
219
|
+
name: 'Codex SDK NCP Runtime',
|
|
220
|
+
summary: 'Optional Codex runtime for NextClaw',
|
|
221
|
+
summaryI18n: { en: 'Optional Codex runtime for NextClaw' },
|
|
222
|
+
install: {
|
|
223
|
+
kind: 'npm',
|
|
224
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
225
|
+
command: 'npm install @nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk'
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
]
|
|
229
|
+
} satisfies MarketplaceListView
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const { container } = render(<MarketplacePage forcedType="plugins" />);
|
|
233
|
+
const card = container.querySelector('article');
|
|
234
|
+
|
|
235
|
+
expect(card?.textContent).toContain('@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk');
|
|
236
|
+
expect(card?.textContent).not.toContain('Plugin');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('does not dim the loaded list during background refresh', () => {
|
|
240
|
+
mocks.itemsQuery = createItemsQuery({
|
|
241
|
+
data: {
|
|
242
|
+
total: 1,
|
|
243
|
+
page: 1,
|
|
244
|
+
pageSize: 12,
|
|
245
|
+
totalPages: 1,
|
|
246
|
+
sort: 'relevance',
|
|
247
|
+
items: [createPluginMarketplaceItem()]
|
|
248
|
+
} satisfies MarketplaceListView,
|
|
249
|
+
isFetching: true
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const { container } = render(<MarketplacePage forcedType="plugins" />);
|
|
253
|
+
|
|
254
|
+
expect(screen.getByText('Codex SDK NCP Runtime')).toBeTruthy();
|
|
255
|
+
expect(container.querySelector('.opacity-70')).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('only disables the targeted plugin action while a manage request is pending', async () => {
|
|
259
|
+
const user = userEvent.setup();
|
|
260
|
+
let resolveMutation: (() => void) | undefined;
|
|
261
|
+
mocks.itemsQuery = createItemsQuery({
|
|
262
|
+
data: {
|
|
263
|
+
total: 2,
|
|
264
|
+
page: 1,
|
|
265
|
+
pageSize: 12,
|
|
266
|
+
totalPages: 1,
|
|
267
|
+
sort: 'relevance',
|
|
268
|
+
items: [
|
|
269
|
+
createPluginMarketplaceItem(),
|
|
270
|
+
createPluginMarketplaceItem({
|
|
271
|
+
id: 'plugin-claude-runtime',
|
|
272
|
+
slug: 'claude-runtime',
|
|
273
|
+
name: 'Claude Agent Runtime',
|
|
274
|
+
install: {
|
|
275
|
+
kind: 'npm',
|
|
276
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk',
|
|
277
|
+
command: 'npm install @nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk'
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
]
|
|
281
|
+
} satisfies MarketplaceListView
|
|
282
|
+
});
|
|
283
|
+
mocks.installedQuery = createInstalledQuery({
|
|
284
|
+
data: {
|
|
285
|
+
type: 'plugin',
|
|
286
|
+
total: 2,
|
|
287
|
+
specs: [
|
|
288
|
+
'@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
289
|
+
'@nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk'
|
|
290
|
+
],
|
|
291
|
+
records: [
|
|
292
|
+
createInstalledRecord(),
|
|
293
|
+
createInstalledRecord({
|
|
294
|
+
id: '@nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk',
|
|
295
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-claude-code-sdk',
|
|
296
|
+
label: 'Claude Agent Runtime'
|
|
297
|
+
})
|
|
298
|
+
]
|
|
299
|
+
} satisfies MarketplaceInstalledView
|
|
300
|
+
});
|
|
301
|
+
mocks.manageMutation.mutateAsync.mockImplementation(
|
|
302
|
+
() => new Promise<void>((resolve) => {
|
|
303
|
+
resolveMutation = resolve;
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
render(<MarketplacePage forcedType="plugins" />);
|
|
308
|
+
|
|
309
|
+
const disableButtons = screen.getAllByRole('button', { name: 'Disable' });
|
|
310
|
+
const firstDisableButton = disableButtons[0];
|
|
311
|
+
const secondDisableButton = disableButtons[1];
|
|
312
|
+
|
|
313
|
+
await user.click(firstDisableButton);
|
|
314
|
+
|
|
315
|
+
expect(mocks.manageMutation.mutateAsync).toHaveBeenCalledTimes(1);
|
|
316
|
+
expect(firstDisableButton.hasAttribute('disabled')).toBe(true);
|
|
317
|
+
expect(firstDisableButton.textContent).toContain('Disabling');
|
|
318
|
+
expect(secondDisableButton.hasAttribute('disabled')).toBe(false);
|
|
319
|
+
|
|
320
|
+
resolveMutation?.();
|
|
321
|
+
});
|
|
170
322
|
});
|