@nextclaw/ui 0.5.14 → 0.5.16

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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/ChannelsList-Cz3uXqRf.js +1 -0
  3. package/dist/assets/CronConfig-Cp52VzLN.js +1 -0
  4. package/dist/assets/DocBrowser-f4DlcaoQ.js +1 -0
  5. package/dist/assets/MarketplacePage-lqsZ93qA.js +1 -0
  6. package/dist/assets/ModelConfig-Drz5yiFE.js +1 -0
  7. package/dist/assets/ProvidersList-BYuBSHLz.js +1 -0
  8. package/dist/assets/RuntimeConfig-24TfpNpu.js +1 -0
  9. package/dist/assets/SessionsConfig-Da9p8YBD.js +2 -0
  10. package/dist/assets/action-link-7uv6J28y.js +1 -0
  11. package/dist/assets/card-D-XgwaXI.js +1 -0
  12. package/dist/assets/config-hints-CApS3K_7.js +1 -0
  13. package/dist/assets/dialog-yW7kJ2ni.js +5 -0
  14. package/dist/assets/index-C8ImW-Sf.js +2 -0
  15. package/dist/assets/index-DdpR1fdj.css +1 -0
  16. package/dist/assets/label-DR4VTdER.js +1 -0
  17. package/dist/assets/page-layout-fYkwpvEI.js +1 -0
  18. package/dist/assets/switch-Z8BKkE23.js +1 -0
  19. package/dist/assets/tabs-custom-C-Ex-MdR.js +1 -0
  20. package/dist/assets/useConfig-BC17j2tK.js +1 -0
  21. package/dist/assets/useConfirmDialog-Bydoiiwm.js +1 -0
  22. package/dist/assets/vendor-Bhv7yx8z.js +347 -0
  23. package/dist/index.html +3 -2
  24. package/package.json +1 -1
  25. package/src/App.tsx +25 -14
  26. package/src/api/marketplace.ts +20 -20
  27. package/src/api/types.ts +2 -2
  28. package/src/components/layout/AppLayout.tsx +15 -3
  29. package/src/components/layout/Sidebar.tsx +9 -4
  30. package/src/components/marketplace/MarketplacePage.tsx +82 -49
  31. package/src/hooks/useMarketplace.ts +19 -11
  32. package/src/lib/i18n.ts +29 -13
  33. package/vite.config.ts +2 -2
  34. package/dist/assets/index-CZnQJKDU.js +0 -347
  35. package/dist/assets/index-CfkfeqC5.css +0 -1
package/dist/index.html CHANGED
@@ -6,8 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-CZnQJKDU.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-CfkfeqC5.css">
9
+ <script type="module" crossorigin src="/assets/index-C8ImW-Sf.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-Bhv7yx8z.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-DdpR1fdj.css">
11
12
  </head>
12
13
 
13
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/App.tsx CHANGED
@@ -1,12 +1,6 @@
1
+ import { lazy, Suspense } from 'react';
1
2
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
3
  import { AppLayout } from '@/components/layout/AppLayout';
3
- import { ModelConfig } from '@/components/config/ModelConfig';
4
- import { ProvidersList } from '@/components/config/ProvidersList';
5
- import { ChannelsList } from '@/components/config/ChannelsList';
6
- import { RuntimeConfig } from '@/components/config/RuntimeConfig';
7
- import { SessionsConfig } from '@/components/config/SessionsConfig';
8
- import { CronConfig } from '@/components/config/CronConfig';
9
- import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
10
4
  import { useWebSocket } from '@/hooks/useWebSocket';
11
5
  import { Toaster } from 'sonner';
12
6
  import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
@@ -20,6 +14,22 @@ const queryClient = new QueryClient({
20
14
  }
21
15
  });
22
16
 
17
+ const ModelConfigPage = lazy(async () => ({ default: (await import('@/components/config/ModelConfig')).ModelConfig }));
18
+ const ProvidersListPage = lazy(async () => ({ default: (await import('@/components/config/ProvidersList')).ProvidersList }));
19
+ const ChannelsListPage = lazy(async () => ({ default: (await import('@/components/config/ChannelsList')).ChannelsList }));
20
+ const RuntimeConfigPage = lazy(async () => ({ default: (await import('@/components/config/RuntimeConfig')).RuntimeConfig }));
21
+ const SessionsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SessionsConfig')).SessionsConfig }));
22
+ const CronConfigPage = lazy(async () => ({ default: (await import('@/components/config/CronConfig')).CronConfig }));
23
+ const MarketplacePage = lazy(async () => ({ default: (await import('@/components/marketplace/MarketplacePage')).MarketplacePage }));
24
+
25
+ function RouteFallback() {
26
+ return <div className="h-full w-full animate-pulse rounded-2xl border border-border/40 bg-card/40" />;
27
+ }
28
+
29
+ function LazyRoute({ children }: { children: JSX.Element }) {
30
+ return <Suspense fallback={<RouteFallback />}>{children}</Suspense>;
31
+ }
32
+
23
33
  function AppContent() {
24
34
  useWebSocket(queryClient); // Initialize WebSocket connection
25
35
  const location = useLocation();
@@ -29,13 +39,14 @@ function AppContent() {
29
39
  <AppLayout>
30
40
  <div key={location.pathname} className="animate-fade-in w-full h-full">
31
41
  <Routes>
32
- <Route path="/model" element={<ModelConfig />} />
33
- <Route path="/providers" element={<ProvidersList />} />
34
- <Route path="/channels" element={<ChannelsList />} />
35
- <Route path="/runtime" element={<RuntimeConfig />} />
36
- <Route path="/sessions" element={<SessionsConfig />} />
37
- <Route path="/cron" element={<CronConfig />} />
38
- <Route path="/marketplace" element={<MarketplacePage />} />
42
+ <Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
43
+ <Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
44
+ <Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
45
+ <Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
46
+ <Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
47
+ <Route path="/cron" element={<LazyRoute><CronConfigPage /></LazyRoute>} />
48
+ <Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
49
+ <Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
39
50
  <Route path="/" element={<Navigate to="/model" replace />} />
40
51
  <Route path="*" element={<Navigate to="/model" replace />} />
41
52
  </Routes>
@@ -13,23 +13,25 @@ import type {
13
13
  } from './types';
14
14
 
15
15
  export type MarketplaceListParams = {
16
+ type: MarketplaceItemType;
16
17
  q?: string;
17
- type?: MarketplaceItemType;
18
18
  tag?: string;
19
19
  sort?: MarketplaceSort;
20
20
  page?: number;
21
21
  pageSize?: number;
22
22
  };
23
23
 
24
- export async function fetchMarketplaceItems(params: MarketplaceListParams = {}): Promise<MarketplaceListView> {
24
+ function toMarketplaceTypeSegment(type: MarketplaceItemType): 'plugins' | 'skills' {
25
+ return type === 'plugin' ? 'plugins' : 'skills';
26
+ }
27
+
28
+ export async function fetchMarketplaceItems(params: MarketplaceListParams): Promise<MarketplaceListView> {
25
29
  const query = new URLSearchParams();
30
+ const segment = toMarketplaceTypeSegment(params.type);
26
31
 
27
32
  if (params.q?.trim()) {
28
33
  query.set('q', params.q.trim());
29
34
  }
30
- if (params.type) {
31
- query.set('type', params.type);
32
- }
33
35
  if (params.tag?.trim()) {
34
36
  query.set('tag', params.tag.trim());
35
37
  }
@@ -44,7 +46,9 @@ export async function fetchMarketplaceItems(params: MarketplaceListParams = {}):
44
46
  }
45
47
 
46
48
  const suffix = query.toString();
47
- const response = await api.get<MarketplaceListView>(suffix ? `/api/marketplace/items?${suffix}` : '/api/marketplace/items');
49
+ const response = await api.get<MarketplaceListView>(
50
+ suffix ? `/api/marketplace/${segment}/items?${suffix}` : `/api/marketplace/${segment}/items`
51
+ );
48
52
  if (!response.ok) {
49
53
  throw new Error(response.error.message);
50
54
  }
@@ -52,17 +56,10 @@ export async function fetchMarketplaceItems(params: MarketplaceListParams = {}):
52
56
  return response.data;
53
57
  }
54
58
 
55
- export async function fetchMarketplaceItem(slug: string, type?: MarketplaceItemType): Promise<MarketplaceItemView> {
56
- const query = new URLSearchParams();
57
- if (type) {
58
- query.set('type', type);
59
- }
60
-
61
- const suffix = query.toString();
59
+ export async function fetchMarketplaceItem(slug: string, type: MarketplaceItemType): Promise<MarketplaceItemView> {
60
+ const segment = toMarketplaceTypeSegment(type);
62
61
  const response = await api.get<MarketplaceItemView>(
63
- suffix
64
- ? `/api/marketplace/items/${encodeURIComponent(slug)}?${suffix}`
65
- : `/api/marketplace/items/${encodeURIComponent(slug)}`
62
+ `/api/marketplace/${segment}/items/${encodeURIComponent(slug)}`
66
63
  );
67
64
  if (!response.ok) {
68
65
  throw new Error(response.error.message);
@@ -95,7 +92,8 @@ export async function fetchMarketplaceRecommendations(params: {
95
92
  }
96
93
 
97
94
  export async function installMarketplaceItem(request: MarketplaceInstallRequest): Promise<MarketplaceInstallResult> {
98
- const response = await api.post<MarketplaceInstallResult>('/api/marketplace/install', request);
95
+ const segment = toMarketplaceTypeSegment(request.type);
96
+ const response = await api.post<MarketplaceInstallResult>(`/api/marketplace/${segment}/install`, request);
99
97
  if (!response.ok) {
100
98
  throw new Error(response.error.message);
101
99
  }
@@ -103,8 +101,9 @@ export async function installMarketplaceItem(request: MarketplaceInstallRequest)
103
101
  return response.data;
104
102
  }
105
103
 
106
- export async function fetchMarketplaceInstalled(): Promise<MarketplaceInstalledView> {
107
- const response = await api.get<MarketplaceInstalledView>('/api/marketplace/installed');
104
+ export async function fetchMarketplaceInstalled(type: MarketplaceItemType): Promise<MarketplaceInstalledView> {
105
+ const segment = toMarketplaceTypeSegment(type);
106
+ const response = await api.get<MarketplaceInstalledView>(`/api/marketplace/${segment}/installed`);
108
107
  if (!response.ok) {
109
108
  throw new Error(response.error.message);
110
109
  }
@@ -112,7 +111,8 @@ export async function fetchMarketplaceInstalled(): Promise<MarketplaceInstalledV
112
111
  }
113
112
 
114
113
  export async function manageMarketplaceItem(request: MarketplaceManageRequest): Promise<MarketplaceManageResult> {
115
- const response = await api.post<MarketplaceManageResult>('/api/marketplace/manage', request);
114
+ const segment = toMarketplaceTypeSegment(request.type);
115
+ const response = await api.post<MarketplaceManageResult>(`/api/marketplace/${segment}/manage`, request);
116
116
  if (!response.ok) {
117
117
  throw new Error(response.error.message);
118
118
  }
package/src/api/types.ts CHANGED
@@ -355,9 +355,9 @@ export type MarketplaceInstalledRecord = {
355
355
  };
356
356
 
357
357
  export type MarketplaceInstalledView = {
358
+ type: MarketplaceItemType;
358
359
  total: number;
359
- pluginSpecs: string[];
360
- skillSpecs: string[];
360
+ specs: string[];
361
361
  records: MarketplaceInstalledRecord[];
362
362
  };
363
363
 
@@ -1,5 +1,9 @@
1
+ import { lazy, Suspense } from 'react';
1
2
  import { Sidebar } from './Sidebar';
2
- import { DocBrowserProvider, DocBrowser, useDocBrowser, useDocLinkInterceptor } from '@/components/doc-browser';
3
+ import { DocBrowserProvider, useDocBrowser } from '@/components/doc-browser/DocBrowserContext';
4
+ import { useDocLinkInterceptor } from '@/components/doc-browser/useDocLinkInterceptor';
5
+
6
+ const DocBrowser = lazy(async () => ({ default: (await import('@/components/doc-browser/DocBrowser')).DocBrowser }));
3
7
 
4
8
  interface AppLayoutProps {
5
9
  children: React.ReactNode;
@@ -21,9 +25,17 @@ function AppLayoutInner({ children }: AppLayoutProps) {
21
25
  </main>
22
26
  </div>
23
27
  {/* Doc Browser: docked mode renders inline, floating mode renders as overlay */}
24
- {isOpen && mode === 'docked' && <DocBrowser />}
28
+ {isOpen && mode === 'docked' && (
29
+ <Suspense fallback={null}>
30
+ <DocBrowser />
31
+ </Suspense>
32
+ )}
25
33
  </div>
26
- {isOpen && mode === 'floating' && <DocBrowser />}
34
+ {isOpen && mode === 'floating' && (
35
+ <Suspense fallback={null}>
36
+ <DocBrowser />
37
+ </Suspense>
38
+ )}
27
39
  </div>
28
40
  );
29
41
  }
@@ -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, MessageSquare, Sparkles, BookOpen, Store, AlarmClock, Languages, Palette } from 'lucide-react';
4
+ import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette } from 'lucide-react';
5
5
  import { NavLink } from 'react-router-dom';
6
6
  import { useDocBrowser } from '@/components/doc-browser';
7
7
  import { useI18n } from '@/components/providers/I18nProvider';
@@ -62,9 +62,14 @@ export function Sidebar() {
62
62
  icon: AlarmClock,
63
63
  },
64
64
  {
65
- target: '/marketplace',
66
- label: t('marketplace'),
67
- icon: Store,
65
+ target: '/marketplace/plugins',
66
+ label: t('marketplaceFilterPlugins'),
67
+ icon: Plug,
68
+ },
69
+ {
70
+ target: '/marketplace/skills',
71
+ label: t('marketplaceFilterSkills'),
72
+ icon: BrainCircuit,
68
73
  }
69
74
  ];
70
75
 
@@ -1,5 +1,11 @@
1
1
  /* eslint-disable max-lines-per-function */
2
- import type { MarketplaceInstalledRecord, MarketplaceItemSummary, MarketplaceManageAction, MarketplaceSort } from '@/api/types';
2
+ import type {
3
+ MarketplaceInstalledRecord,
4
+ MarketplaceItemSummary,
5
+ MarketplaceManageAction,
6
+ MarketplaceSort,
7
+ MarketplaceItemType
8
+ } from '@/api/types';
3
9
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
4
10
  import { Tabs } from '@/components/ui/tabs-custom';
5
11
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
@@ -15,10 +21,10 @@ import { PageLayout, PageHeader } from '@/components/layout/page-layout';
15
21
  import { cn } from '@/lib/utils';
16
22
  import { PackageSearch } from 'lucide-react';
17
23
  import { useEffect, useMemo, useState } from 'react';
24
+ import { useNavigate, useParams } from 'react-router-dom';
18
25
 
19
26
  const PAGE_SIZE = 12;
20
27
 
21
- type FilterType = 'all' | 'plugin' | 'skill';
22
28
  type ScopeType = 'all' | 'installed';
23
29
 
24
30
  type InstallState = {
@@ -38,6 +44,8 @@ type InstalledRenderEntry = {
38
44
  item?: MarketplaceItemSummary;
39
45
  };
40
46
 
47
+ type MarketplaceRouteType = 'plugins' | 'skills';
48
+
41
49
  function normalizeMarketplaceKey(value: string | undefined): string {
42
50
  return (value ?? '').trim().toLowerCase();
43
51
  }
@@ -168,10 +176,9 @@ function ItemIcon({ name, fallback }: { name?: string; fallback: string }) {
168
176
  function FilterPanel(props: {
169
177
  scope: ScopeType;
170
178
  searchText: string;
171
- typeFilter: FilterType;
179
+ searchPlaceholder: string;
172
180
  sort: MarketplaceSort;
173
181
  onSearchTextChange: (value: string) => void;
174
- onTypeFilterChange: (value: FilterType) => void;
175
182
  onSortChange: (value: MarketplaceSort) => void;
176
183
  }) {
177
184
  return (
@@ -182,33 +189,11 @@ function FilterPanel(props: {
182
189
  <input
183
190
  value={props.searchText}
184
191
  onChange={(event) => props.onSearchTextChange(event.target.value)}
185
- placeholder={t('marketplaceSearchPlaceholder')}
192
+ placeholder={props.searchPlaceholder}
186
193
  className="w-full h-9 border border-gray-200/80 rounded-xl pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40"
187
194
  />
188
195
  </div>
189
196
 
190
- <div className="inline-flex h-9 rounded-xl bg-gray-100/80 p-1 shrink-0">
191
- {([
192
- { value: 'all', label: t('marketplaceFilterAll') },
193
- { value: 'plugin', label: t('marketplaceFilterPlugins') },
194
- { value: 'skill', label: t('marketplaceFilterSkills') },
195
- ] as const).map((opt) => (
196
- <button
197
- key={opt.value}
198
- type="button"
199
- onClick={() => props.onTypeFilterChange(opt.value)}
200
- className={cn(
201
- 'px-3 rounded-lg text-sm font-medium transition-all whitespace-nowrap',
202
- props.typeFilter === opt.value
203
- ? 'bg-white text-gray-900 shadow-sm'
204
- : 'text-gray-500 hover:text-gray-700'
205
- )}
206
- >
207
- {opt.label}
208
- </button>
209
- ))}
210
- </div>
211
-
212
197
  {props.scope === 'all' && (
213
198
  <Select value={props.sort} onValueChange={(v) => props.onSortChange(v as MarketplaceSort)}>
214
199
  <SelectTrigger className="h-9 w-[150px] shrink-0 rounded-lg">
@@ -367,10 +352,57 @@ function PaginationBar(props: {
367
352
  }
368
353
 
369
354
  export function MarketplacePage() {
355
+ const navigate = useNavigate();
356
+ const params = useParams<{ type?: string }>();
357
+
358
+ const routeType: MarketplaceRouteType | null = useMemo(() => {
359
+ if (params.type === 'plugins' || params.type === 'skills') {
360
+ return params.type;
361
+ }
362
+ return null;
363
+ }, [params.type]);
364
+
365
+ useEffect(() => {
366
+ if (!routeType) {
367
+ navigate('/marketplace/plugins', { replace: true });
368
+ }
369
+ }, [routeType, navigate]);
370
+
371
+ const typeFilter: MarketplaceItemType = routeType === 'skills' ? 'skill' : 'plugin';
372
+ const isPluginModule = typeFilter === 'plugin';
373
+ const copyKeys = isPluginModule
374
+ ? {
375
+ pageTitle: 'marketplacePluginsPageTitle',
376
+ pageDescription: 'marketplacePluginsPageDescription',
377
+ tabMarketplace: 'marketplaceTabMarketplacePlugins',
378
+ tabInstalled: 'marketplaceTabInstalledPlugins',
379
+ searchPlaceholder: 'marketplaceSearchPlaceholderPlugins',
380
+ sectionCatalog: 'marketplaceSectionPlugins',
381
+ sectionInstalled: 'marketplaceSectionInstalledPlugins',
382
+ errorLoadData: 'marketplaceErrorLoadingPluginsData',
383
+ errorLoadInstalled: 'marketplaceErrorLoadingInstalledPlugins',
384
+ emptyData: 'marketplaceNoPlugins',
385
+ emptyInstalled: 'marketplaceNoInstalledPlugins',
386
+ installedCountSuffix: 'marketplaceInstalledPluginsCountSuffix'
387
+ }
388
+ : {
389
+ pageTitle: 'marketplaceSkillsPageTitle',
390
+ pageDescription: 'marketplaceSkillsPageDescription',
391
+ tabMarketplace: 'marketplaceTabMarketplaceSkills',
392
+ tabInstalled: 'marketplaceTabInstalledSkills',
393
+ searchPlaceholder: 'marketplaceSearchPlaceholderSkills',
394
+ sectionCatalog: 'marketplaceSectionSkills',
395
+ sectionInstalled: 'marketplaceSectionInstalledSkills',
396
+ errorLoadData: 'marketplaceErrorLoadingSkillsData',
397
+ errorLoadInstalled: 'marketplaceErrorLoadingInstalledSkills',
398
+ emptyData: 'marketplaceNoSkills',
399
+ emptyInstalled: 'marketplaceNoInstalledSkills',
400
+ installedCountSuffix: 'marketplaceInstalledSkillsCountSuffix'
401
+ };
402
+
370
403
  const [searchText, setSearchText] = useState('');
371
404
  const [query, setQuery] = useState('');
372
405
  const [scope, setScope] = useState<ScopeType>('all');
373
- const [typeFilter, setTypeFilter] = useState<FilterType>('all');
374
406
  const [sort, setSort] = useState<MarketplaceSort>('relevance');
375
407
  const [page, setPage] = useState(1);
376
408
 
@@ -382,11 +414,15 @@ export function MarketplacePage() {
382
414
  return () => clearTimeout(timer);
383
415
  }, [searchText]);
384
416
 
385
- const installedQuery = useMarketplaceInstalled();
417
+ useEffect(() => {
418
+ setPage(1);
419
+ }, [typeFilter]);
420
+
421
+ const installedQuery = useMarketplaceInstalled(typeFilter);
386
422
 
387
423
  const itemsQuery = useMarketplaceItems({
388
424
  q: query || undefined,
389
- type: typeFilter === 'all' ? undefined : typeFilter,
425
+ type: typeFilter,
390
426
  sort,
391
427
  page,
392
428
  pageSize: PAGE_SIZE
@@ -418,7 +454,7 @@ export function MarketplacePage() {
418
454
 
419
455
  const installedEntries = useMemo<InstalledRenderEntry[]>(() => {
420
456
  const entries = installedRecords
421
- .filter((record) => (typeFilter === 'all' ? true : record.type === typeFilter))
457
+ .filter((record) => record.type === typeFilter)
422
458
  .map((record) => ({
423
459
  key: `${record.type}:${record.spec}:${record.id ?? ''}`,
424
460
  record,
@@ -450,7 +486,7 @@ export function MarketplacePage() {
450
486
  if (installedQuery.isLoading) {
451
487
  return t('loading');
452
488
  }
453
- return `${installedEntries.length} ${t('marketplaceInstalledCountSuffix')}`;
489
+ return `${installedEntries.length} ${t(copyKeys.installedCountSuffix)}`;
454
490
  }
455
491
 
456
492
  if (!itemsQuery.data) {
@@ -458,7 +494,7 @@ export function MarketplacePage() {
458
494
  }
459
495
 
460
496
  return `${allItems.length} / ${total}`;
461
- }, [scope, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total]);
497
+ }, [scope, installedQuery.isLoading, installedEntries.length, itemsQuery.data, allItems.length, total, copyKeys.installedCountSuffix]);
462
498
 
463
499
  const installState: InstallState = {
464
500
  isPending: installMutation.isPending,
@@ -471,11 +507,10 @@ export function MarketplacePage() {
471
507
  action: manageMutation.variables?.action
472
508
  };
473
509
 
474
- const tabs = [
475
- { id: 'all', label: t('marketplaceTabMarketplace') },
476
- { id: 'installed', label: t('marketplaceTabInstalled'), count: installedQuery.data?.total ?? 0 }
510
+ const scopeTabs = [
511
+ { id: 'all', label: t(copyKeys.tabMarketplace) },
512
+ { id: 'installed', label: t(copyKeys.tabInstalled), count: installedQuery.data?.total ?? 0 }
477
513
  ];
478
-
479
514
  const handleInstall = (item: MarketplaceItemSummary) => {
480
515
  if (installMutation.isPending) {
481
516
  return;
@@ -515,10 +550,10 @@ export function MarketplacePage() {
515
550
 
516
551
  return (
517
552
  <PageLayout>
518
- <PageHeader title={t('marketplacePageTitle')} description={t('marketplacePageDescription')} />
553
+ <PageHeader title={t(copyKeys.pageTitle)} description={t(copyKeys.pageDescription)} />
519
554
 
520
555
  <Tabs
521
- tabs={tabs}
556
+ tabs={scopeTabs}
522
557
  activeTab={scope}
523
558
  onChange={(value) => {
524
559
  setScope(value as ScopeType);
@@ -530,13 +565,9 @@ export function MarketplacePage() {
530
565
  <FilterPanel
531
566
  scope={scope}
532
567
  searchText={searchText}
533
- typeFilter={typeFilter}
568
+ searchPlaceholder={t(copyKeys.searchPlaceholder)}
534
569
  sort={sort}
535
570
  onSearchTextChange={setSearchText}
536
- onTypeFilterChange={(value) => {
537
- setPage(1);
538
- setTypeFilter(value);
539
- }}
540
571
  onSortChange={(value) => {
541
572
  setPage(1);
542
573
  setSort(value);
@@ -545,18 +576,20 @@ export function MarketplacePage() {
545
576
 
546
577
  <section>
547
578
  <div className="flex items-center justify-between mb-3">
548
- <h3 className="text-[14px] font-semibold text-gray-900">{scope === 'installed' ? t('marketplaceSectionInstalled') : t('marketplaceSectionExtensions')}</h3>
579
+ <h3 className="text-[14px] font-semibold text-gray-900">
580
+ {scope === 'installed' ? t(copyKeys.sectionInstalled) : t(copyKeys.sectionCatalog)}
581
+ </h3>
549
582
  <span className="text-[12px] text-gray-500">{listSummary}</span>
550
583
  </div>
551
584
 
552
585
  {scope === 'all' && itemsQuery.isError && (
553
586
  <div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
554
- {t('marketplaceErrorLoadingData')}: {itemsQuery.error.message}
587
+ {t(copyKeys.errorLoadData)}: {itemsQuery.error.message}
555
588
  </div>
556
589
  )}
557
590
  {scope === 'installed' && installedQuery.isError && (
558
591
  <div className="p-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-700 text-sm">
559
- {t('marketplaceErrorLoadingInstalled')}: {installedQuery.error.message}
592
+ {t(copyKeys.errorLoadInstalled)}: {installedQuery.error.message}
560
593
  </div>
561
594
  )}
562
595
 
@@ -587,10 +620,10 @@ export function MarketplacePage() {
587
620
  </div>
588
621
 
589
622
  {scope === 'all' && !itemsQuery.isLoading && !itemsQuery.isError && allItems.length === 0 && (
590
- <div className="text-[13px] text-gray-500 py-8 text-center">{t('marketplaceNoItems')}</div>
623
+ <div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyData)}</div>
591
624
  )}
592
625
  {scope === 'installed' && !installedQuery.isLoading && !installedQuery.isError && installedEntries.length === 0 && (
593
- <div className="text-[13px] text-gray-500 py-8 text-center">{t('marketplaceNoInstalledItems')}</div>
626
+ <div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyInstalled)}</div>
594
627
  )}
595
628
  </section>
596
629
 
@@ -31,16 +31,16 @@ export function useMarketplaceRecommendations(params: { scene?: string; limit?:
31
31
  export function useMarketplaceItem(slug: string | null, type?: MarketplaceItemType) {
32
32
  return useQuery({
33
33
  queryKey: ['marketplace-item', slug, type],
34
- queryFn: () => fetchMarketplaceItem(slug as string, type),
35
- enabled: Boolean(slug),
34
+ queryFn: () => fetchMarketplaceItem(slug as string, type as MarketplaceItemType),
35
+ enabled: Boolean(slug && type),
36
36
  staleTime: 30_000
37
37
  });
38
38
  }
39
39
 
40
- export function useMarketplaceInstalled() {
40
+ export function useMarketplaceInstalled(type: MarketplaceItemType) {
41
41
  return useQuery({
42
- queryKey: ['marketplace-installed'],
43
- queryFn: fetchMarketplaceInstalled,
42
+ queryKey: ['marketplace-installed', type],
43
+ queryFn: () => fetchMarketplaceInstalled(type),
44
44
  staleTime: 10_000
45
45
  });
46
46
  }
@@ -51,10 +51,13 @@ export function useInstallMarketplaceItem() {
51
51
  return useMutation({
52
52
  mutationFn: (request: MarketplaceInstallRequest) => installMarketplaceItem(request),
53
53
  onSuccess: (result) => {
54
- queryClient.invalidateQueries({ queryKey: ['marketplace-installed'] });
55
- queryClient.refetchQueries({ queryKey: ['marketplace-installed'], type: 'active' });
54
+ queryClient.invalidateQueries({ queryKey: ['marketplace-installed', result.type] });
55
+ queryClient.refetchQueries({ queryKey: ['marketplace-installed', result.type], type: 'active' });
56
56
  queryClient.refetchQueries({ queryKey: ['marketplace-items'], type: 'active' });
57
- toast.success(result.message || `${result.type} ${t('marketplaceInstalledCountSuffix')}`);
57
+ const fallback = result.type === 'plugin'
58
+ ? t('marketplaceInstallSuccessPlugin')
59
+ : t('marketplaceInstallSuccessSkill');
60
+ toast.success(result.message || fallback);
58
61
  },
59
62
  onError: (error: Error) => {
60
63
  toast.error(error.message || t('marketplaceInstallFailed'));
@@ -68,11 +71,16 @@ export function useManageMarketplaceItem() {
68
71
  return useMutation({
69
72
  mutationFn: (request: MarketplaceManageRequest) => manageMarketplaceItem(request),
70
73
  onSuccess: (result) => {
71
- queryClient.invalidateQueries({ queryKey: ['marketplace-installed'] });
74
+ queryClient.invalidateQueries({ queryKey: ['marketplace-installed', result.type] });
72
75
  queryClient.invalidateQueries({ queryKey: ['marketplace-items'] });
73
- queryClient.refetchQueries({ queryKey: ['marketplace-installed'], type: 'active' });
76
+ queryClient.refetchQueries({ queryKey: ['marketplace-installed', result.type], type: 'active' });
74
77
  queryClient.refetchQueries({ queryKey: ['marketplace-items'], type: 'active' });
75
- toast.success(result.message || `${result.action} success`);
78
+ const fallback = result.action === 'enable'
79
+ ? t('marketplaceEnableSuccess')
80
+ : result.action === 'disable'
81
+ ? t('marketplaceDisableSuccess')
82
+ : t('marketplaceUninstallSuccess');
83
+ toast.success(result.message || fallback);
76
84
  },
77
85
  onError: (error: Error) => {
78
86
  toast.error(error.message || t('marketplaceOperationFailed'));
package/src/lib/i18n.ts CHANGED
@@ -385,12 +385,16 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
385
385
  cronRunForceConfirm: { zh: '任务已禁用,仍要立即执行', en: 'Cron job disabled. Force run now' },
386
386
 
387
387
  // Marketplace
388
- marketplacePageTitle: { zh: '市场', en: 'Marketplace' },
389
- marketplacePageDescription: { zh: '更清爽的扩展列表,聚焦安装/启用/禁用。', en: 'A cleaner extension list focused on install / enable / disable.' },
390
- marketplaceTabMarketplace: { zh: '市场', en: 'Marketplace' },
391
- marketplaceTabInstalled: { zh: '已安装', en: 'Installed' },
392
- marketplaceSearchPlaceholder: { zh: '搜索扩展...', en: 'Search extensions...' },
393
- marketplaceFilterAll: { zh: '全部', en: 'All' },
388
+ marketplacePluginsPageTitle: { zh: '插件市场', en: 'Plugin Marketplace' },
389
+ marketplacePluginsPageDescription: { zh: '安装、启用与管理插件。', en: 'Install, enable, and manage plugins.' },
390
+ marketplaceSkillsPageTitle: { zh: '技能市场', en: 'Skill Marketplace' },
391
+ marketplaceSkillsPageDescription: { zh: '安装与管理技能。', en: 'Install and manage skills.' },
392
+ marketplaceTabMarketplacePlugins: { zh: '插件市场', en: 'Plugin Market' },
393
+ marketplaceTabMarketplaceSkills: { zh: '技能市场', en: 'Skill Market' },
394
+ marketplaceTabInstalledPlugins: { zh: '已安装插件', en: 'Installed Plugins' },
395
+ marketplaceTabInstalledSkills: { zh: '已安装技能', en: 'Installed Skills' },
396
+ marketplaceSearchPlaceholderPlugins: { zh: '搜索插件...', en: 'Search plugins...' },
397
+ marketplaceSearchPlaceholderSkills: { zh: '搜索技能...', en: 'Search skills...' },
394
398
  marketplaceFilterPlugins: { zh: '插件', en: 'Plugins' },
395
399
  marketplaceFilterSkills: { zh: '技能', en: 'Skills' },
396
400
  marketplaceSortRelevance: { zh: '相关性', en: 'Relevance' },
@@ -408,20 +412,32 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
408
412
  marketplaceDisabling: { zh: '禁用中...', en: 'Disabling...' },
409
413
  marketplaceUninstall: { zh: '卸载', en: 'Uninstall' },
410
414
  marketplaceRemoving: { zh: '卸载中...', en: 'Removing...' },
411
- marketplaceSectionInstalled: { zh: '已安装', en: 'Installed' },
412
- marketplaceSectionExtensions: { zh: '扩展', en: 'Extensions' },
413
- marketplaceErrorLoadingData: { zh: '加载市场数据失败', en: 'Failed to load marketplace data' },
414
- marketplaceErrorLoadingInstalled: { zh: '加载已安装项目失败', en: 'Failed to load installed items' },
415
- marketplaceNoItems: { zh: '未找到项目。', en: 'No items found.' },
416
- marketplaceNoInstalledItems: { zh: '未找到已安装项目。', en: 'No installed items found.' },
415
+ marketplaceSectionPlugins: { zh: '插件列表', en: 'Plugin Catalog' },
416
+ marketplaceSectionSkills: { zh: '技能列表', en: 'Skill Catalog' },
417
+ marketplaceSectionInstalledPlugins: { zh: '已安装插件', en: 'Installed Plugins' },
418
+ marketplaceSectionInstalledSkills: { zh: '已安装技能', en: 'Installed Skills' },
419
+ marketplaceErrorLoadingPluginsData: { zh: '加载插件市场数据失败', en: 'Failed to load plugin marketplace data' },
420
+ marketplaceErrorLoadingSkillsData: { zh: '加载技能市场数据失败', en: 'Failed to load skill marketplace data' },
421
+ marketplaceErrorLoadingInstalledPlugins: { zh: '加载已安装插件失败', en: 'Failed to load installed plugins' },
422
+ marketplaceErrorLoadingInstalledSkills: { zh: '加载已安装技能失败', en: 'Failed to load installed skills' },
423
+ marketplaceNoPlugins: { zh: '未找到插件。', en: 'No plugins found.' },
424
+ marketplaceNoSkills: { zh: '未找到技能。', en: 'No skills found.' },
425
+ marketplaceNoInstalledPlugins: { zh: '未找到已安装插件。', en: 'No installed plugins found.' },
426
+ marketplaceNoInstalledSkills: { zh: '未找到已安装技能。', en: 'No installed skills found.' },
417
427
  marketplaceUninstallTitle: { zh: '确认卸载', en: 'Uninstall' },
418
428
  marketplaceUninstallDescription: {
419
429
  zh: '该操作会移除扩展,后续可在市场中重新安装。',
420
430
  en: 'This will remove the extension. You can install it again from the marketplace.'
421
431
  },
432
+ marketplaceInstallSuccessPlugin: { zh: '插件安装成功', en: 'Plugin installed successfully' },
433
+ marketplaceInstallSuccessSkill: { zh: '技能安装成功', en: 'Skill installed successfully' },
434
+ marketplaceEnableSuccess: { zh: '启用成功', en: 'Enabled successfully' },
435
+ marketplaceDisableSuccess: { zh: '禁用成功', en: 'Disabled successfully' },
436
+ marketplaceUninstallSuccess: { zh: '卸载成功', en: 'Uninstalled successfully' },
422
437
  marketplaceInstallFailed: { zh: '安装失败', en: 'Install failed' },
423
438
  marketplaceOperationFailed: { zh: '操作失败', en: 'Operation failed' },
424
- marketplaceInstalledCountSuffix: { zh: '已安装', en: 'installed' },
439
+ marketplaceInstalledPluginsCountSuffix: { zh: '个已安装插件', en: 'installed plugins' },
440
+ marketplaceInstalledSkillsCountSuffix: { zh: '个已安装技能', en: 'installed skills' },
425
441
 
426
442
  // Status
427
443
  connected: { zh: '已连接', en: 'Connected' },
package/vite.config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defineConfig } from 'vite';
1
+ import { defineConfig, splitVendorChunkPlugin } from 'vite';
2
2
  import react from '@vitejs/plugin-react';
3
3
  import path from 'path';
4
4
 
@@ -6,7 +6,7 @@ const apiBase = process.env.VITE_API_BASE ?? 'http://127.0.0.1:18792';
6
6
  const wsBase = apiBase.replace(/^http/i, 'ws');
7
7
 
8
8
  export default defineConfig({
9
- plugins: [react()],
9
+ plugins: [react(), splitVendorChunkPlugin()],
10
10
  resolve: {
11
11
  alias: {
12
12
  '@': path.resolve(__dirname, './src')