@nextclaw/ui 0.12.4 → 0.12.6
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 +66 -0
- package/dist/assets/ChannelsList-D8p4OlM6.js +8 -0
- package/dist/assets/ChatPage-A45t1Rmf.js +58 -0
- package/dist/assets/DocBrowser-B2MpsnU9.js +1 -0
- package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-Cse_F8Nn.js} +1 -1
- package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-Bai1WU2H.js} +1 -1
- package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BdxMPc9v.js} +1 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
- package/dist/assets/MarketplacePage-BbpAkllU.js +49 -0
- package/dist/assets/McpMarketplacePage-CxPFOgxv.js +40 -0
- package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
- package/dist/assets/ProviderScopedModelInput-BYNouw-i.js +1 -0
- package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
- package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
- package/dist/assets/RuntimeConfig-ChdfK4Y_.js +1 -0
- package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
- package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-CCYO6NcV.js} +2 -2
- package/dist/assets/SessionsConfig-Du39vDgt.js +2 -0
- package/dist/assets/app-query-client-Dr5d-K8d.js +1 -0
- package/dist/assets/{book-open-C7TAghTk.js → book-open-Da4OEPqB.js} +1 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
- package/dist/assets/client-CSk58DcF.js +7 -0
- package/dist/assets/config-D8KzikVB.js +1 -0
- package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-83gaZMtv.js} +1 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
- package/dist/assets/dist-aTmhMDVh.js +9 -0
- package/dist/assets/{dist-DP-JKR4G.js → dist-toEYs-MZ.js} +1 -1
- package/dist/assets/{external-link-BkJkiWbH.js → external-link-QQ0TC6X4.js} +1 -1
- package/dist/assets/{hash-CbP6-6R9.js → hash-DaFBEkmi.js} +1 -1
- package/dist/assets/i18n-C3jb83S6.js +1 -0
- package/dist/assets/index-CE4N7ItL.css +1 -0
- package/dist/assets/index-riX7Sg0_.js +6 -0
- package/dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
- package/dist/assets/loader-circle-BjMg63eu.js +1 -0
- package/dist/assets/{logos-N3dbS6-I.js → logos-Dzlz30M3.js} +1 -1
- package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-D2eRufRQ.js} +1 -1
- package/dist/assets/plus-CIXME2pD.js +1 -0
- package/dist/assets/{popover-BKKWGUaG.js → popover-BSXxm5bj.js} +1 -1
- package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-B3zMtN-_.js} +1 -1
- package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
- package/dist/assets/{save-Dh4GQzzX.js → save-Us9fg4Sj.js} +1 -1
- package/dist/assets/search-B_Qr0f6C.js +1 -0
- package/dist/assets/security-config-BGWYwxNr.js +1 -0
- package/dist/assets/{select-BtIi5fnh.js → select-DLYqySQK.js} +1 -1
- package/dist/assets/skeleton-CYQJazv6.js +1 -0
- package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-DGayudyB.js} +1 -1
- package/dist/assets/{switch-DPegGIa_.js → switch-Dz2ScsKx.js} +1 -1
- package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-CdKyjiGk.js} +1 -1
- package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-Db-mZOZs.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
- package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-DL0a-oGC.js} +1 -1
- package/dist/assets/useMutation-BdZm-9PL.js +1 -0
- package/dist/assets/x-B8Tho_xC.js +1 -0
- package/dist/index.html +20 -18
- package/package.json +6 -6
- package/src/App.tsx +2 -0
- package/src/account/components/account-panel.tsx +46 -4
- package/src/account/managers/account.manager.ts +19 -4
- package/src/api/raw-client.test.ts +37 -0
- package/src/api/raw-client.ts +51 -8
- package/src/api/remote.ts +9 -0
- package/src/api/remote.types.ts +5 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +344 -142
- package/src/components/chat/ChatSidebar.test.tsx +109 -4
- package/src/components/chat/ChatSidebar.tsx +62 -9
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
- package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
- package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
- package/src/components/chat/chat-child-session-panel.tsx +155 -59
- package/src/components/chat/chat-page-runtime.test.ts +16 -19
- package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
- package/src/components/chat/chat-session-preference-sync.ts +9 -7
- package/src/components/chat/chat-sidebar-session-item.tsx +189 -121
- package/src/components/chat/containers/chat-message-list.container.test.tsx +21 -3
- package/src/components/chat/containers/chat-message-list.container.tsx +14 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
- package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
- package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
- package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
- package/src/components/chat/managers/chat-session-list.manager.test.ts +79 -5
- package/src/components/chat/managers/chat-session-list.manager.ts +31 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +1 -1
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +45 -5
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
- package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
- package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -11
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +35 -9
- package/src/components/chat/stores/chat-session-list.store.ts +99 -5
- package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
- package/src/components/chat/useChatSessionTypeState.ts +3 -5
- package/src/components/config/ChannelsList.test.tsx +68 -0
- package/src/components/config/ChannelsList.tsx +22 -4
- package/src/components/config/ProviderForm.tsx +9 -15
- package/src/components/config/ProvidersList.tsx +17 -3
- package/src/components/config/desktop-update-config.tsx +230 -0
- package/src/components/config/providers-list.test.tsx +68 -0
- package/src/components/layout/Sidebar.tsx +19 -14
- package/src/components/layout/sidebar.layout.test.tsx +33 -1
- package/src/components/marketplace/MarketplacePage.tsx +30 -30
- package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
- package/src/desktop/desktop-update.types.ts +36 -0
- package/src/desktop/managers/desktop-update.manager.ts +163 -0
- package/src/desktop/stores/desktop-update.store.ts +18 -0
- package/src/hooks/marketplace-list-pages.ts +27 -0
- package/src/hooks/use-infinite-scroll-loader.ts +88 -0
- package/src/hooks/useMarketplace.ts +14 -3
- package/src/hooks/useMcpMarketplace.ts +14 -3
- package/src/lib/desktop-update-labels.utils.ts +72 -0
- package/src/lib/i18n.chat.ts +13 -0
- package/src/lib/i18n.remote.ts +15 -0
- package/src/lib/i18n.ts +3 -9
- package/src/lib/ui-document-title.ts +1 -0
- package/src/transport/local.transport.ts +57 -18
- package/src/vite-env.d.ts +10 -0
- package/dist/assets/ChannelsList-CobWeI2V.js +0 -8
- package/dist/assets/ChatPage-ZIdFFVAv.js +0 -43
- package/dist/assets/DocBrowser-D55C0iyl.js +0 -1
- package/dist/assets/MarketplacePage-BFYsRss_.js +0 -49
- package/dist/assets/MarketplacePage-DII-q-Y1.js +0 -1
- package/dist/assets/McpMarketplacePage-CPqsGJzz.js +0 -40
- package/dist/assets/ModelConfig-Bvuo_IpS.js +0 -1
- package/dist/assets/ProviderScopedModelInput-BfY8rGsf.js +0 -1
- package/dist/assets/ProvidersList-3tlaqwSS.js +0 -1
- package/dist/assets/RuntimeConfig-CAd5Kta3.js +0 -1
- package/dist/assets/SearchConfig-DFwgaAa7.js +0 -1
- package/dist/assets/SessionsConfig-vYrvc2Fk.js +0 -2
- package/dist/assets/chat-session-display-5dVFkJyw.js +0 -1
- package/dist/assets/config-CMiW0yaK.js +0 -1
- package/dist/assets/dist-BFc_H-lY.js +0 -15
- package/dist/assets/i18n-C_2dKw6w.js +0 -1
- package/dist/assets/index-ChUXhq0G.css +0 -1
- package/dist/assets/index-DAE8Srx-.js +0 -6
- package/dist/assets/label-D8yyejJS.js +0 -1
- package/dist/assets/loader-circle-B0sKKO29.js +0 -1
- package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
- package/dist/assets/plus-CYXs3JtZ.js +0 -1
- package/dist/assets/react-8EIEQjMP.js +0 -1
- package/dist/assets/search-DOsLw-P9.js +0 -1
- package/dist/assets/security-config-CM_tQRXQ.js +0 -1
- package/dist/assets/skeleton-GbHLjPC0.js +0 -1
- package/dist/assets/useMutation-DSinpgEq.js +0 -1
- package/dist/assets/x-Bnco_K8b.js +0 -1
- package/src/components/chat/ChatSessionsSidebar.tsx +0 -100
- /package/dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
- /package/dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
|
@@ -100,6 +100,31 @@ describe('ChannelsList', () => {
|
|
|
100
100
|
mocks.updateChannelMutateAsync.mockReset();
|
|
101
101
|
mocks.startChannelAuthMutateAsync.mockReset();
|
|
102
102
|
mocks.pollChannelAuthMutateAsync.mockReset();
|
|
103
|
+
mocks.configQuery.data = {
|
|
104
|
+
channels: {
|
|
105
|
+
weixin: {
|
|
106
|
+
enabled: false,
|
|
107
|
+
defaultAccountId: '1344b2b24720@im.bot',
|
|
108
|
+
baseUrl: 'https://ilinkai.weixin.qq.com',
|
|
109
|
+
pollTimeoutMs: 35000,
|
|
110
|
+
allowFrom: ['o9cq804svxfyCCTIqzddDqRBeMC0@im.wechat'],
|
|
111
|
+
accounts: {
|
|
112
|
+
'1344b2b24720@im.bot': {
|
|
113
|
+
enabled: true
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
mocks.metaQuery.data = {
|
|
120
|
+
channels: [
|
|
121
|
+
{
|
|
122
|
+
name: 'weixin',
|
|
123
|
+
displayName: 'Weixin',
|
|
124
|
+
enabled: false
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
};
|
|
103
128
|
});
|
|
104
129
|
|
|
105
130
|
it('renders weixin qr auth card and starts channel auth', async () => {
|
|
@@ -140,6 +165,49 @@ describe('ChannelsList', () => {
|
|
|
140
165
|
});
|
|
141
166
|
});
|
|
142
167
|
|
|
168
|
+
it('keeps Weixin, Feishu, Discord, and QQ at the front of the channel list', async () => {
|
|
169
|
+
const user = userEvent.setup();
|
|
170
|
+
mocks.configQuery.data = {
|
|
171
|
+
channels: {
|
|
172
|
+
telegram: { enabled: false },
|
|
173
|
+
qq: { enabled: false },
|
|
174
|
+
discord: { enabled: false },
|
|
175
|
+
weixin: { enabled: false },
|
|
176
|
+
feishu: { enabled: false }
|
|
177
|
+
}
|
|
178
|
+
} as unknown as typeof mocks.configQuery.data;
|
|
179
|
+
mocks.metaQuery.data = {
|
|
180
|
+
channels: [
|
|
181
|
+
{ name: 'telegram', displayName: 'Telegram', enabled: false },
|
|
182
|
+
{ name: 'qq', displayName: 'QQ', enabled: false },
|
|
183
|
+
{ name: 'discord', displayName: 'Discord', enabled: false },
|
|
184
|
+
{ name: 'weixin', displayName: 'Weixin', enabled: false },
|
|
185
|
+
{ name: 'feishu', displayName: 'Feishu', enabled: false }
|
|
186
|
+
]
|
|
187
|
+
} as typeof mocks.metaQuery.data;
|
|
188
|
+
|
|
189
|
+
const { container } = render(<ChannelsList />);
|
|
190
|
+
|
|
191
|
+
await user.click(await screen.findByRole('button', { name: /All Channels/i }));
|
|
192
|
+
|
|
193
|
+
const sidebarSection = container.querySelector('section');
|
|
194
|
+
if (!(sidebarSection instanceof HTMLElement)) {
|
|
195
|
+
throw new Error('channel sidebar not found');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const channelButtons = Array.from(sidebarSection.querySelectorAll('button[type="button"]')).filter((button) => (
|
|
199
|
+
['Weixin', 'Feishu', 'Discord', 'QQ', 'Telegram'].some((label) => button.textContent?.includes(label))
|
|
200
|
+
));
|
|
201
|
+
|
|
202
|
+
expect(channelButtons.map((button) => button.textContent)).toEqual([
|
|
203
|
+
expect.stringContaining('Weixin'),
|
|
204
|
+
expect.stringContaining('Feishu'),
|
|
205
|
+
expect.stringContaining('Discord'),
|
|
206
|
+
expect.stringContaining('QQ'),
|
|
207
|
+
expect.stringContaining('Telegram')
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
143
211
|
it('saves weixin advanced settings from the advanced section', async () => {
|
|
144
212
|
const user = userEvent.setup();
|
|
145
213
|
|
|
@@ -24,6 +24,24 @@ const channelDescriptionKeys: Record<string, string> = {
|
|
|
24
24
|
weixin: 'channelDescWeixin'
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
const prioritizedChannelNames = ['weixin', 'feishu', 'discord', 'qq'] as const;
|
|
28
|
+
|
|
29
|
+
function sortChannelsForDisplay<T extends { name: string }>(channels: T[]): T[] {
|
|
30
|
+
const priorityByName = new Map<string, number>(prioritizedChannelNames.map((name, index) => [name, index]));
|
|
31
|
+
|
|
32
|
+
return channels
|
|
33
|
+
.map((channel, index) => ({ channel, index }))
|
|
34
|
+
.sort((left, right) => {
|
|
35
|
+
const leftPriority = priorityByName.get(left.channel.name) ?? Number.POSITIVE_INFINITY;
|
|
36
|
+
const rightPriority = priorityByName.get(right.channel.name) ?? Number.POSITIVE_INFINITY;
|
|
37
|
+
if (leftPriority !== rightPriority) {
|
|
38
|
+
return leftPriority - rightPriority;
|
|
39
|
+
}
|
|
40
|
+
return left.index - right.index;
|
|
41
|
+
})
|
|
42
|
+
.map(({ channel }) => channel);
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
export function ChannelsList() {
|
|
28
46
|
const { data: config } = useConfig();
|
|
29
47
|
const { data: meta } = useConfigMeta();
|
|
@@ -32,17 +50,17 @@ export function ChannelsList() {
|
|
|
32
50
|
const [selectedChannel, setSelectedChannel] = useState<string | undefined>();
|
|
33
51
|
const [query, setQuery] = useState('');
|
|
34
52
|
const uiHints = schema?.uiHints;
|
|
35
|
-
const channels = meta?.channels;
|
|
53
|
+
const channels = useMemo(() => sortChannelsForDisplay(meta?.channels ?? []), [meta?.channels]);
|
|
36
54
|
const channelConfigs = config?.channels;
|
|
37
55
|
|
|
38
56
|
const tabs = [
|
|
39
|
-
{ id: 'enabled', label: t('channelsTabEnabled'), count:
|
|
40
|
-
{ id: 'all', label: t('channelsTabAll'), count:
|
|
57
|
+
{ id: 'enabled', label: t('channelsTabEnabled'), count: channels.filter((c) => channelConfigs?.[c.name]?.enabled).length },
|
|
58
|
+
{ id: 'all', label: t('channelsTabAll'), count: channels.length }
|
|
41
59
|
];
|
|
42
60
|
|
|
43
61
|
const filteredChannels = useMemo(() => {
|
|
44
62
|
const keyword = query.trim().toLowerCase();
|
|
45
|
-
return
|
|
63
|
+
return channels
|
|
46
64
|
.filter((channel) => {
|
|
47
65
|
const enabled = channelConfigs?.[channel.name]?.enabled || false;
|
|
48
66
|
if (activeTab === 'enabled') {
|
|
@@ -143,6 +143,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
143
143
|
() => providerAuth?.methods ?? [],
|
|
144
144
|
[providerAuth?.methods]
|
|
145
145
|
);
|
|
146
|
+
const supportsWireApi = Boolean(providerSpec?.supportsWireApi) || isCustomProvider;
|
|
146
147
|
const providerAuthMethodOptions = useMemo(
|
|
147
148
|
() =>
|
|
148
149
|
providerAuthMethods.map((method) => ({
|
|
@@ -174,25 +175,18 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
174
175
|
() => providerAuthMethods.find((method) => method.id === resolvedAuthMethodId),
|
|
175
176
|
[providerAuthMethods, resolvedAuthMethodId]
|
|
176
177
|
);
|
|
177
|
-
const selectedAuthMethodHint =
|
|
178
|
-
selectedAuthMethod?.hint?.[language] || selectedAuthMethod?.hint?.en || '';
|
|
179
178
|
const shouldUseAuthMethodPills = shouldUsePillSelector({
|
|
180
179
|
required: providerAuth?.kind === 'device_code',
|
|
181
180
|
hasDefault: Boolean(providerAuth?.defaultMethodId?.trim()),
|
|
182
181
|
optionCount: providerAuthMethods.length
|
|
183
182
|
});
|
|
184
|
-
const providerAuthNote =
|
|
185
|
-
providerAuth?.note?.[language] ||
|
|
186
|
-
providerAuth?.note?.en ||
|
|
187
|
-
providerAuth?.displayName ||
|
|
188
|
-
'';
|
|
189
183
|
const wireApiOptions = providerSpec?.wireApiOptions || ['auto', 'chat', 'responses'];
|
|
190
184
|
const wireApiSelectOptions: PillSelectOption[] = wireApiOptions.map((option) => ({
|
|
191
185
|
value: option,
|
|
192
186
|
label: option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')
|
|
193
187
|
}));
|
|
194
188
|
const shouldUseWireApiPills = shouldUsePillSelector({
|
|
195
|
-
required:
|
|
189
|
+
required: supportsWireApi,
|
|
196
190
|
hasDefault: typeof providerSpec?.defaultWireApi === 'string' && providerSpec.defaultWireApi.length > 0,
|
|
197
191
|
optionCount: wireApiSelectOptions.length
|
|
198
192
|
});
|
|
@@ -302,7 +296,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
302
296
|
const apiKeyChanged = apiKey.trim().length > 0;
|
|
303
297
|
const apiBaseChanged = apiBase.trim() !== currentApiBase.trim();
|
|
304
298
|
const headersChanged = !headersEqual(extraHeaders, currentHeaders);
|
|
305
|
-
const wireApiChanged =
|
|
299
|
+
const wireApiChanged = supportsWireApi ? wireApi !== currentWireApi : false;
|
|
306
300
|
const modelsChanged = !modelListsEqual(models, currentEditableModels);
|
|
307
301
|
const modelThinkingChanged = !modelThinkingEqual(modelThinking, currentModelThinking);
|
|
308
302
|
const displayNameChanged = isCustomProvider
|
|
@@ -331,7 +325,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
331
325
|
currentApiBase,
|
|
332
326
|
extraHeaders,
|
|
333
327
|
currentHeaders,
|
|
334
|
-
|
|
328
|
+
supportsWireApi,
|
|
335
329
|
wireApi,
|
|
336
330
|
currentWireApi,
|
|
337
331
|
models,
|
|
@@ -422,7 +416,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
422
416
|
payload.extraHeaders = normalizedHeaders;
|
|
423
417
|
}
|
|
424
418
|
|
|
425
|
-
if (
|
|
419
|
+
if (supportsWireApi && wireApi !== currentWireApi) {
|
|
426
420
|
payload.wireApi = wireApi;
|
|
427
421
|
}
|
|
428
422
|
if (!modelListsEqual(models, currentEditableModels)) {
|
|
@@ -450,7 +444,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
450
444
|
if (apiKey.trim().length > 0) {
|
|
451
445
|
payload.apiKey = apiKey.trim();
|
|
452
446
|
}
|
|
453
|
-
if (
|
|
447
|
+
if (supportsWireApi) {
|
|
454
448
|
payload.wireApi = wireApi;
|
|
455
449
|
}
|
|
456
450
|
|
|
@@ -608,10 +602,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
608
602
|
|
|
609
603
|
<ProviderAuthSection
|
|
610
604
|
providerAuth={providerAuth}
|
|
611
|
-
providerAuthNote={
|
|
605
|
+
providerAuthNote={providerAuth?.note?.[language] || providerAuth?.note?.en || providerAuth?.displayName || ''}
|
|
612
606
|
providerAuthMethodOptions={providerAuthMethodOptions}
|
|
613
607
|
providerAuthMethodsCount={providerAuthMethods.length}
|
|
614
|
-
selectedAuthMethodHint={
|
|
608
|
+
selectedAuthMethodHint={selectedAuthMethod?.hint?.[language] || selectedAuthMethod?.hint?.en || ''}
|
|
615
609
|
shouldUseAuthMethodPills={shouldUseAuthMethodPills}
|
|
616
610
|
resolvedAuthMethodId={resolvedAuthMethodId}
|
|
617
611
|
onAuthMethodChange={setAuthMethodId}
|
|
@@ -663,7 +657,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
663
657
|
<ProviderAdvancedSettingsSection
|
|
664
658
|
showAdvanced={showAdvanced}
|
|
665
659
|
onShowAdvancedChange={setShowAdvanced}
|
|
666
|
-
supportsWireApi={
|
|
660
|
+
supportsWireApi={supportsWireApi}
|
|
667
661
|
wireApiLabel={wireApiHint?.label ?? t('wireApi')}
|
|
668
662
|
wireApi={wireApi}
|
|
669
663
|
onWireApiChange={setWireApi}
|
|
@@ -26,6 +26,20 @@ function formatBasePreview(base?: string | null): string | null {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function sortProvidersForDisplay<T extends { name: string }>(providers: T[]): T[] {
|
|
30
|
+
return providers
|
|
31
|
+
.map((provider, index) => ({ provider, index }))
|
|
32
|
+
.sort((left, right) => {
|
|
33
|
+
const leftPriority = left.provider.name === 'nextclaw' ? 1 : 0;
|
|
34
|
+
const rightPriority = right.provider.name === 'nextclaw' ? 1 : 0;
|
|
35
|
+
if (leftPriority !== rightPriority) {
|
|
36
|
+
return leftPriority - rightPriority;
|
|
37
|
+
}
|
|
38
|
+
return left.index - right.index;
|
|
39
|
+
})
|
|
40
|
+
.map(({ provider }) => provider);
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
export function ProvidersList() {
|
|
30
44
|
const { data: config } = useConfig();
|
|
31
45
|
const { data: meta } = useConfigMeta();
|
|
@@ -37,7 +51,7 @@ export function ProvidersList() {
|
|
|
37
51
|
const [query, setQuery] = useState('');
|
|
38
52
|
|
|
39
53
|
const uiHints = schema?.uiHints;
|
|
40
|
-
const providers = meta?.providers ?? [];
|
|
54
|
+
const providers = useMemo(() => sortProvidersForDisplay(meta?.providers ?? []), [meta?.providers]);
|
|
41
55
|
const providersConfig = config?.providers ?? {};
|
|
42
56
|
const configuredCount = providers.filter((provider) => {
|
|
43
57
|
const current = providersConfig[provider.name];
|
|
@@ -50,7 +64,7 @@ export function ProvidersList() {
|
|
|
50
64
|
];
|
|
51
65
|
|
|
52
66
|
const filteredProviders = useMemo(() => {
|
|
53
|
-
const baseProviders =
|
|
67
|
+
const baseProviders = providers;
|
|
54
68
|
const baseConfig = config?.providers ?? {};
|
|
55
69
|
const keyword = query.trim().toLowerCase();
|
|
56
70
|
return baseProviders
|
|
@@ -69,7 +83,7 @@ export function ProvidersList() {
|
|
|
69
83
|
const display = (configDisplayName || provider.displayName || provider.name).toLowerCase();
|
|
70
84
|
return display.includes(keyword) || provider.name.toLowerCase().includes(keyword);
|
|
71
85
|
});
|
|
72
|
-
}, [
|
|
86
|
+
}, [providers, config, activeTab, query]);
|
|
73
87
|
|
|
74
88
|
useEffect(() => {
|
|
75
89
|
if (filteredProviders.length === 0) {
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
4
|
+
import { Label } from '@/components/ui/label';
|
|
5
|
+
import { Switch } from '@/components/ui/switch';
|
|
6
|
+
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
7
|
+
import { desktopUpdateManager } from '@/desktop/managers/desktop-update.manager';
|
|
8
|
+
import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
|
|
9
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
import { Download, ExternalLink, RefreshCw, RotateCw } from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
function formatVersion(value: string | null): string {
|
|
14
|
+
return value?.trim() || '-';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatLastCheckedAt(value: string | null): string {
|
|
18
|
+
return value ? formatDateTime(value) : '-';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getStatusLabel(status: string): string {
|
|
22
|
+
if (status === 'checking') {
|
|
23
|
+
return t('desktopUpdatesStatusChecking');
|
|
24
|
+
}
|
|
25
|
+
if (status === 'update-available') {
|
|
26
|
+
return t('desktopUpdatesStatusAvailable');
|
|
27
|
+
}
|
|
28
|
+
if (status === 'downloading') {
|
|
29
|
+
return t('desktopUpdatesStatusDownloading');
|
|
30
|
+
}
|
|
31
|
+
if (status === 'downloaded') {
|
|
32
|
+
return t('desktopUpdatesStatusDownloaded');
|
|
33
|
+
}
|
|
34
|
+
if (status === 'up-to-date') {
|
|
35
|
+
return t('desktopUpdatesStatusUpToDate');
|
|
36
|
+
}
|
|
37
|
+
if (status === 'failed') {
|
|
38
|
+
return t('desktopUpdatesStatusFailed');
|
|
39
|
+
}
|
|
40
|
+
return t('desktopUpdatesStatusIdle');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getStatusTone(status: string): string {
|
|
44
|
+
if (status === 'downloaded') {
|
|
45
|
+
return 'bg-emerald-50 text-emerald-700 ring-emerald-100';
|
|
46
|
+
}
|
|
47
|
+
if (status === 'update-available' || status === 'downloading' || status === 'checking') {
|
|
48
|
+
return 'bg-amber-50 text-amber-700 ring-amber-100';
|
|
49
|
+
}
|
|
50
|
+
if (status === 'failed') {
|
|
51
|
+
return 'bg-red-50 text-red-700 ring-red-100';
|
|
52
|
+
}
|
|
53
|
+
return 'bg-gray-100 text-gray-700 ring-gray-200';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DesktopUpdateConfig() {
|
|
57
|
+
const { supported, initialized, busyAction, snapshot } = useDesktopUpdateStore();
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
void desktopUpdateManager.start();
|
|
61
|
+
return () => {
|
|
62
|
+
desktopUpdateManager.stop();
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
if (!initialized) {
|
|
67
|
+
return <div className="p-8 text-gray-400">{t('loading')}</div>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!supported || !snapshot) {
|
|
71
|
+
return (
|
|
72
|
+
<PageLayout className="space-y-6">
|
|
73
|
+
<PageHeader
|
|
74
|
+
title={t('desktopUpdatesPageTitle')}
|
|
75
|
+
description={t('desktopUpdatesPageDescription')}
|
|
76
|
+
/>
|
|
77
|
+
<Card>
|
|
78
|
+
<CardHeader>
|
|
79
|
+
<CardTitle>{t('desktopUpdatesDesktopOnlyTitle')}</CardTitle>
|
|
80
|
+
<CardDescription>{t('desktopUpdatesDesktopOnlyDescription')}</CardDescription>
|
|
81
|
+
</CardHeader>
|
|
82
|
+
<CardContent>
|
|
83
|
+
<p className="text-sm text-gray-500">{t('desktopUpdatesDesktopOnlyFutureHint')}</p>
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
</PageLayout>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isChecking = busyAction === 'checking';
|
|
91
|
+
const isDownloading = busyAction === 'downloading';
|
|
92
|
+
const isApplying = busyAction === 'applying';
|
|
93
|
+
const isSavingPreferences = busyAction === 'saving-preferences';
|
|
94
|
+
const canDownload = snapshot.status === 'update-available' && !isDownloading && !isApplying;
|
|
95
|
+
const canApply = snapshot.status === 'downloaded' && !isApplying;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<PageLayout className="space-y-6">
|
|
99
|
+
<PageHeader
|
|
100
|
+
title={t('desktopUpdatesPageTitle')}
|
|
101
|
+
description={t('desktopUpdatesPageDescription')}
|
|
102
|
+
actions={(
|
|
103
|
+
<Button
|
|
104
|
+
variant="outline"
|
|
105
|
+
onClick={() => void desktopUpdateManager.checkForUpdates()}
|
|
106
|
+
disabled={isChecking || isDownloading || isApplying}
|
|
107
|
+
>
|
|
108
|
+
<RefreshCw className={cn('mr-2 h-4 w-4', isChecking && 'animate-spin')} />
|
|
109
|
+
{t('desktopUpdatesCheckNow')}
|
|
110
|
+
</Button>
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<Card>
|
|
115
|
+
<CardHeader>
|
|
116
|
+
<CardTitle>{t('desktopUpdatesOverviewTitle')}</CardTitle>
|
|
117
|
+
<CardDescription>{t('desktopUpdatesOverviewDescription')}</CardDescription>
|
|
118
|
+
</CardHeader>
|
|
119
|
+
<CardContent className="space-y-5">
|
|
120
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
121
|
+
<span className="text-sm font-medium text-gray-700">{t('desktopUpdatesStatusLabel')}</span>
|
|
122
|
+
<span className={cn('inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1', getStatusTone(snapshot.status))}>
|
|
123
|
+
{getStatusLabel(snapshot.status)}
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
128
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
129
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{t('desktopUpdatesLauncherVersion')}</p>
|
|
130
|
+
<p className="mt-2 text-base font-semibold text-gray-900">{formatVersion(snapshot.launcherVersion)}</p>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
133
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{t('desktopUpdatesCurrentBundleVersion')}</p>
|
|
134
|
+
<p className="mt-2 text-base font-semibold text-gray-900">{formatVersion(snapshot.currentVersion)}</p>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
137
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{t('desktopUpdatesAvailableVersion')}</p>
|
|
138
|
+
<p className="mt-2 text-base font-semibold text-gray-900">{formatVersion(snapshot.availableVersion)}</p>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
141
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{t('desktopUpdatesLastCheckedAt')}</p>
|
|
142
|
+
<p className="mt-2 text-base font-semibold text-gray-900">{formatLastCheckedAt(snapshot.lastCheckedAt)}</p>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{snapshot.downloadedVersion ? (
|
|
147
|
+
<div className="rounded-2xl border border-emerald-200 bg-emerald-50/70 p-4">
|
|
148
|
+
<p className="text-sm font-semibold text-emerald-800">{t('desktopUpdatesDownloadedBannerTitle')}</p>
|
|
149
|
+
<p className="mt-1 text-sm text-emerald-700">
|
|
150
|
+
{t('desktopUpdatesDownloadedBannerDescription').replace('{version}', snapshot.downloadedVersion)}
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
) : null}
|
|
154
|
+
|
|
155
|
+
{snapshot.errorMessage ? (
|
|
156
|
+
<div className="rounded-2xl border border-red-200 bg-red-50/70 p-4 text-sm text-red-700">
|
|
157
|
+
{snapshot.errorMessage}
|
|
158
|
+
</div>
|
|
159
|
+
) : null}
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
|
|
163
|
+
<Card>
|
|
164
|
+
<CardHeader>
|
|
165
|
+
<CardTitle>{t('desktopUpdatesPreferencesTitle')}</CardTitle>
|
|
166
|
+
<CardDescription>{t('desktopUpdatesPreferencesDescription')}</CardDescription>
|
|
167
|
+
</CardHeader>
|
|
168
|
+
<CardContent className="space-y-5">
|
|
169
|
+
<div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
|
|
170
|
+
<div className="space-y-1">
|
|
171
|
+
<Label>{t('desktopUpdatesAutomaticChecks')}</Label>
|
|
172
|
+
<p className="text-sm text-gray-500">{t('desktopUpdatesAutomaticChecksHelp')}</p>
|
|
173
|
+
</div>
|
|
174
|
+
<Switch
|
|
175
|
+
checked={snapshot.preferences.automaticChecks}
|
|
176
|
+
disabled={isSavingPreferences}
|
|
177
|
+
onCheckedChange={(checked) => void desktopUpdateManager.updatePreferences({ automaticChecks: checked })}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
|
|
182
|
+
<div className="space-y-1">
|
|
183
|
+
<Label>{t('desktopUpdatesAutoDownload')}</Label>
|
|
184
|
+
<p className="text-sm text-gray-500">{t('desktopUpdatesAutoDownloadHelp')}</p>
|
|
185
|
+
</div>
|
|
186
|
+
<Switch
|
|
187
|
+
checked={snapshot.preferences.autoDownload}
|
|
188
|
+
disabled={isSavingPreferences}
|
|
189
|
+
onCheckedChange={(checked) => void desktopUpdateManager.updatePreferences({ autoDownload: checked })}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
</CardContent>
|
|
193
|
+
</Card>
|
|
194
|
+
|
|
195
|
+
<Card>
|
|
196
|
+
<CardHeader>
|
|
197
|
+
<CardTitle>{t('desktopUpdatesActionsTitle')}</CardTitle>
|
|
198
|
+
<CardDescription>{t('desktopUpdatesActionsDescription')}</CardDescription>
|
|
199
|
+
</CardHeader>
|
|
200
|
+
<CardContent className="flex flex-wrap items-center gap-3">
|
|
201
|
+
<Button
|
|
202
|
+
variant="outline"
|
|
203
|
+
onClick={() => void desktopUpdateManager.checkForUpdates()}
|
|
204
|
+
disabled={isChecking || isDownloading || isApplying}
|
|
205
|
+
>
|
|
206
|
+
<RefreshCw className={cn('mr-2 h-4 w-4', isChecking && 'animate-spin')} />
|
|
207
|
+
{t('desktopUpdatesCheckNow')}
|
|
208
|
+
</Button>
|
|
209
|
+
|
|
210
|
+
<Button onClick={() => void desktopUpdateManager.downloadUpdate()} disabled={!canDownload}>
|
|
211
|
+
<Download className={cn('mr-2 h-4 w-4', isDownloading && 'animate-bounce')} />
|
|
212
|
+
{t('desktopUpdatesDownloadNow')}
|
|
213
|
+
</Button>
|
|
214
|
+
|
|
215
|
+
<Button variant="secondary" onClick={() => void desktopUpdateManager.applyDownloadedUpdate()} disabled={!canApply}>
|
|
216
|
+
<RotateCw className={cn('mr-2 h-4 w-4', isApplying && 'animate-spin')} />
|
|
217
|
+
{t('desktopUpdatesApplyNow')}
|
|
218
|
+
</Button>
|
|
219
|
+
|
|
220
|
+
{snapshot.releaseNotesUrl ? (
|
|
221
|
+
<Button variant="ghost" onClick={() => window.open(snapshot.releaseNotesUrl ?? '', '_blank', 'noopener,noreferrer')}>
|
|
222
|
+
<ExternalLink className="mr-2 h-4 w-4" />
|
|
223
|
+
{t('desktopUpdatesReleaseNotes')}
|
|
224
|
+
</Button>
|
|
225
|
+
) : null}
|
|
226
|
+
</CardContent>
|
|
227
|
+
</Card>
|
|
228
|
+
</PageLayout>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ProvidersList } from '@/components/config/ProvidersList';
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
createProviderMutateAsync: vi.fn(),
|
|
7
|
+
configQuery: {
|
|
8
|
+
data: {
|
|
9
|
+
providers: {
|
|
10
|
+
openai: { enabled: true, apiKeySet: true },
|
|
11
|
+
anthropic: { enabled: true, apiKeySet: true },
|
|
12
|
+
nextclaw: { enabled: true, apiKeySet: true }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
isLoading: false
|
|
16
|
+
},
|
|
17
|
+
metaQuery: {
|
|
18
|
+
data: {
|
|
19
|
+
providers: [
|
|
20
|
+
{ name: 'nextclaw', displayName: 'NextClaw Builtin', defaultApiBase: 'https://ai-gateway-api.nextclaw.io/v1' },
|
|
21
|
+
{ name: 'openai', displayName: 'OpenAI', defaultApiBase: 'https://api.openai.com/v1' },
|
|
22
|
+
{ name: 'anthropic', displayName: 'Anthropic', defaultApiBase: 'https://api.anthropic.com/v1' }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
schemaQuery: {
|
|
27
|
+
data: {
|
|
28
|
+
uiHints: {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('@/hooks/useConfig', () => ({
|
|
34
|
+
useConfig: () => mocks.configQuery,
|
|
35
|
+
useConfigMeta: () => mocks.metaQuery,
|
|
36
|
+
useConfigSchema: () => mocks.schemaQuery,
|
|
37
|
+
useCreateProvider: () => ({
|
|
38
|
+
mutateAsync: mocks.createProviderMutateAsync,
|
|
39
|
+
isPending: false
|
|
40
|
+
})
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('@/components/config/ProviderForm', () => ({
|
|
44
|
+
ProviderForm: ({ providerName }: { providerName?: string }) => (
|
|
45
|
+
<div data-testid="provider-form">{providerName ?? 'none'}</div>
|
|
46
|
+
)
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
describe('ProvidersList', () => {
|
|
50
|
+
it('keeps the nextclaw builtin provider at the end of the list', () => {
|
|
51
|
+
const { container } = render(<ProvidersList />);
|
|
52
|
+
|
|
53
|
+
const sidebarSection = container.querySelector('section');
|
|
54
|
+
if (!(sidebarSection instanceof HTMLElement)) {
|
|
55
|
+
throw new Error('provider sidebar not found');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const providerButtons = Array.from(sidebarSection.querySelectorAll('button[type="button"]')).filter((button) => (
|
|
59
|
+
['OpenAI', 'Anthropic', 'NextClaw Builtin'].some((label) => button.textContent?.includes(label))
|
|
60
|
+
));
|
|
61
|
+
|
|
62
|
+
expect(providerButtons.map((button) => button.textContent)).toEqual([
|
|
63
|
+
expect.stringContaining('OpenAI'),
|
|
64
|
+
expect.stringContaining('Anthropic'),
|
|
65
|
+
expect.stringContaining('NextClaw Builtin')
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -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, Wrench, Wifi, Bot } from 'lucide-react';
|
|
4
|
+
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search, Shield, Wrench, Wifi, Bot, Download } 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';
|
|
@@ -80,21 +80,36 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
80
80
|
label: t('providers'),
|
|
81
81
|
icon: Sparkles,
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
target: '/channels',
|
|
85
|
+
label: t('channels'),
|
|
86
|
+
icon: MessageSquare,
|
|
87
|
+
},
|
|
83
88
|
{
|
|
84
89
|
target: '/search',
|
|
85
90
|
label: t('searchChannels'),
|
|
86
91
|
icon: Search,
|
|
87
92
|
},
|
|
88
93
|
{
|
|
89
|
-
target: '/
|
|
90
|
-
label: t('
|
|
91
|
-
icon:
|
|
94
|
+
target: '/marketplace/plugins',
|
|
95
|
+
label: t('marketplaceFilterPlugins'),
|
|
96
|
+
icon: Plug,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
target: '/marketplace/mcp',
|
|
100
|
+
label: t('marketplaceFilterMcp'),
|
|
101
|
+
icon: Wrench,
|
|
92
102
|
},
|
|
93
103
|
{
|
|
94
104
|
target: '/runtime',
|
|
95
105
|
label: t('runtime'),
|
|
96
106
|
icon: GitBranch,
|
|
97
107
|
},
|
|
108
|
+
{
|
|
109
|
+
target: '/updates',
|
|
110
|
+
label: t('updates'),
|
|
111
|
+
icon: Download,
|
|
112
|
+
},
|
|
98
113
|
{
|
|
99
114
|
target: '/remote',
|
|
100
115
|
label: t('remote'),
|
|
@@ -114,16 +129,6 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
114
129
|
target: '/secrets',
|
|
115
130
|
label: t('secrets'),
|
|
116
131
|
icon: KeyRound,
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
target: '/marketplace/plugins',
|
|
120
|
-
label: t('marketplaceFilterPlugins'),
|
|
121
|
-
icon: Plug,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
target: '/marketplace/mcp',
|
|
125
|
-
label: t('marketplaceFilterMcp'),
|
|
126
|
-
icon: Wrench,
|
|
127
132
|
}
|
|
128
133
|
];
|
|
129
134
|
const navItems = isSettingsMode ? settingsNavItems : mainNavItems;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { render, screen, within } from '@testing-library/react';
|
|
2
2
|
import { MemoryRouter } from 'react-router-dom';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { Sidebar } from '@/components/layout/Sidebar';
|
|
@@ -85,6 +85,38 @@ describe('Sidebar', () => {
|
|
|
85
85
|
expect(backLink.className).toContain('hover:bg-gray-200/60');
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it('keeps the settings navigation in the expected product order', () => {
|
|
89
|
+
const { container } = render(
|
|
90
|
+
<MemoryRouter initialEntries={['/model']}>
|
|
91
|
+
<Sidebar mode="settings" />
|
|
92
|
+
</MemoryRouter>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const nav = container.querySelector('nav');
|
|
96
|
+
if (!(nav instanceof HTMLElement)) {
|
|
97
|
+
throw new Error('settings nav not found');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const linkTexts = within(nav)
|
|
101
|
+
.getAllByRole('link')
|
|
102
|
+
.map((link) => link.textContent?.trim() || '');
|
|
103
|
+
|
|
104
|
+
expect(linkTexts).toEqual([
|
|
105
|
+
'Model',
|
|
106
|
+
'Providers',
|
|
107
|
+
'Channels',
|
|
108
|
+
'Search Channels',
|
|
109
|
+
'Plugins',
|
|
110
|
+
'MCP',
|
|
111
|
+
'Routing & Runtime',
|
|
112
|
+
'Updates',
|
|
113
|
+
'Remote Access',
|
|
114
|
+
'Security',
|
|
115
|
+
'Sessions',
|
|
116
|
+
'Secrets'
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
88
120
|
it('keeps the footer utilities compact without changing the top header structure', () => {
|
|
89
121
|
render(
|
|
90
122
|
<MemoryRouter initialEntries={['/model']}>
|