@nextclaw/ui 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/ChannelsList-DDfZIiJa.js +1 -0
  3. package/dist/assets/ChatPage-FpRraTxm.js +38 -0
  4. package/dist/assets/{DocBrowser-CVwUDJMO.js → DocBrowser-Kndx8OJj.js} +1 -1
  5. package/dist/assets/LogoBadge-hKHoLH9n.js +1 -0
  6. package/dist/assets/MarketplacePage-CZIJyfjK.js +49 -0
  7. package/dist/assets/McpMarketplacePage-BGrAMA37.js +40 -0
  8. package/dist/assets/{ModelConfig-CsX-_fyy.js → ModelConfig-BpKQeGfb.js} +1 -1
  9. package/dist/assets/ProvidersList-qfUL6mrW.js +1 -0
  10. package/dist/assets/RemoteAccessPage-BQuMsngI.js +1 -0
  11. package/dist/assets/{RuntimeConfig-CX2TGEG1.js → RuntimeConfig-CVlqNWKO.js} +1 -1
  12. package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-DXFV6Mvx.js} +1 -1
  13. package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-BGW9aUqv.js} +2 -2
  14. package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-BByfa1ke.js} +2 -2
  15. package/dist/assets/{chat-message-AWIcksDK.js → chat-message-ZwnDwDuQ.js} +1 -1
  16. package/dist/assets/index-BWvap_iq.js +8 -0
  17. package/dist/assets/index-COrhpAdh.css +1 -0
  18. package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
  19. package/dist/assets/{label-DD61y-4v.js → label-Bklr3fXc.js} +1 -1
  20. package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
  21. package/dist/assets/{page-layout-CfnoVycc.js → page-layout-sNhcbwtm.js} +1 -1
  22. package/dist/assets/{popover-DsugZ6rp.js → popover-C3rJrJJG.js} +1 -1
  23. package/dist/assets/{security-config-DIrf2Z0O.js → security-config-BueosYw1.js} +1 -1
  24. package/dist/assets/skeleton-CiG6msbm.js +1 -0
  25. package/dist/assets/status-dot-CsIV5YrS.js +1 -0
  26. package/dist/assets/{switch-NX5OmUXQ.js → switch-DSdHSIsC.js} +1 -1
  27. package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-BB-VjdL2.js} +1 -1
  28. package/dist/assets/{useConfirmDialog-BuQnVTeR.js → useConfirmDialog-BL5s8KDC.js} +2 -2
  29. package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
  30. package/dist/index.html +3 -3
  31. package/package.json +3 -3
  32. package/src/App.tsx +4 -0
  33. package/src/api/auth.types.ts +24 -0
  34. package/src/api/chat-session-type.types.ts +21 -0
  35. package/src/api/marketplace.ts +8 -2
  36. package/src/api/mcp-marketplace.ts +138 -0
  37. package/src/api/remote.ts +77 -0
  38. package/src/api/remote.types.ts +104 -0
  39. package/src/api/types.ts +28 -34
  40. package/src/components/chat/ChatSidebar.test.tsx +31 -2
  41. package/src/components/chat/ChatSidebar.tsx +26 -2
  42. package/src/components/chat/chat-page-data.ts +36 -38
  43. package/src/components/chat/chat-page-runtime.test.ts +96 -2
  44. package/src/components/chat/chat-page-runtime.ts +1 -135
  45. package/src/components/chat/chat-session-preference-governance.ts +303 -0
  46. package/src/components/chat/legacy/LegacyChatPage.tsx +4 -19
  47. package/src/components/chat/ncp/NcpChatPage.tsx +4 -19
  48. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
  49. package/src/components/chat/ncp/ncp-chat-page-data.ts +62 -21
  50. package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
  51. package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
  52. package/src/components/chat/stores/chat-input.store.ts +14 -1
  53. package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
  54. package/src/components/chat/useChatSessionTypeState.ts +55 -12
  55. package/src/components/layout/Sidebar.tsx +11 -1
  56. package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
  57. package/src/components/marketplace/MarketplacePage.tsx +52 -199
  58. package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
  59. package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
  60. package/src/components/marketplace/marketplace-localization.ts +77 -0
  61. package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
  62. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
  63. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
  64. package/src/components/remote/RemoteAccessPage.tsx +396 -0
  65. package/src/components/ui/input.tsx +1 -1
  66. package/src/components/ui/label.tsx +1 -1
  67. package/src/hooks/useMarketplace.ts +36 -7
  68. package/src/hooks/useMcpMarketplace.ts +99 -0
  69. package/src/hooks/useRemoteAccess.ts +120 -0
  70. package/src/hooks/useWebSocket.ts +25 -16
  71. package/src/lib/i18n.marketplace.ts +91 -0
  72. package/src/lib/i18n.remote.ts +142 -0
  73. package/src/lib/i18n.ts +10 -68
  74. package/dist/assets/ChannelsList-DKD6Llid.js +0 -1
  75. package/dist/assets/ChatPage-BK9X4Tin.js +0 -38
  76. package/dist/assets/LogoBadge-CYQ_b7jk.js +0 -1
  77. package/dist/assets/MarketplacePage-B_2z3ii_.js +0 -49
  78. package/dist/assets/ProvidersList-CZstsyv7.js +0 -1
  79. package/dist/assets/index-BEgClaDH.js +0 -8
  80. package/dist/assets/index-C8GsgIUn.css +0 -1
  81. package/dist/assets/skeleton-DJ-Wen2o.js +0 -1
@@ -0,0 +1,149 @@
1
+ import type {
2
+ MarketplaceInstalledRecord,
3
+ MarketplaceInstalledView,
4
+ MarketplaceInstallRequest,
5
+ MarketplaceInstallResult,
6
+ MarketplaceManageRequest,
7
+ MarketplaceManageResult,
8
+ MarketplaceItemType
9
+ } from '@/api/types';
10
+
11
+ function dedupeSpecs(records: MarketplaceInstalledRecord[]): string[] {
12
+ return Array.from(new Set(records.map((record) => record.spec).filter(Boolean)));
13
+ }
14
+
15
+ function buildInstalledRecordFromInstall(params: {
16
+ request: MarketplaceInstallRequest;
17
+ result: MarketplaceInstallResult;
18
+ }): MarketplaceInstalledRecord {
19
+ const installedAt = new Date().toISOString();
20
+
21
+ if (params.result.type === 'skill') {
22
+ return {
23
+ type: 'skill',
24
+ spec: params.result.spec,
25
+ id: params.request.skill ?? params.result.spec,
26
+ label: params.request.skill ?? params.result.name ?? params.result.spec,
27
+ source: 'workspace',
28
+ installPath: params.request.installPath,
29
+ installedAt
30
+ };
31
+ }
32
+
33
+ return {
34
+ type: params.result.type,
35
+ spec: params.result.spec,
36
+ id: params.result.name ?? params.result.spec,
37
+ label: params.result.name ?? params.result.spec,
38
+ source: 'marketplace',
39
+ origin: 'marketplace',
40
+ enabled: params.request.enabled ?? true,
41
+ runtimeStatus: params.request.enabled === false ? 'disabled' : 'ready',
42
+ installedAt
43
+ };
44
+ }
45
+
46
+ function matchesInstalledRecord(record: MarketplaceInstalledRecord, params: {
47
+ id?: string;
48
+ spec?: string;
49
+ }): boolean {
50
+ if (params.spec && record.spec === params.spec) {
51
+ return true;
52
+ }
53
+ if (params.id && record.id === params.id) {
54
+ return true;
55
+ }
56
+ return false;
57
+ }
58
+
59
+ function ensureInstalledView(type: MarketplaceItemType, view?: MarketplaceInstalledView): MarketplaceInstalledView {
60
+ return view ?? {
61
+ type,
62
+ total: 0,
63
+ specs: [],
64
+ records: []
65
+ };
66
+ }
67
+
68
+ export function applyInstallResultToInstalledView(params: {
69
+ view?: MarketplaceInstalledView;
70
+ request: MarketplaceInstallRequest;
71
+ result: MarketplaceInstallResult;
72
+ }): MarketplaceInstalledView {
73
+ const current = ensureInstalledView(params.result.type, params.view);
74
+ const optimisticRecord = buildInstalledRecordFromInstall(params);
75
+ const existingIndex = current.records.findIndex((record) => matchesInstalledRecord(record, {
76
+ id: optimisticRecord.id,
77
+ spec: optimisticRecord.spec
78
+ }));
79
+
80
+ const nextRecords = [...current.records];
81
+ if (existingIndex >= 0) {
82
+ nextRecords[existingIndex] = {
83
+ ...nextRecords[existingIndex],
84
+ ...optimisticRecord
85
+ };
86
+ } else {
87
+ nextRecords.unshift(optimisticRecord);
88
+ }
89
+
90
+ return {
91
+ ...current,
92
+ type: params.result.type,
93
+ records: nextRecords,
94
+ specs: dedupeSpecs(nextRecords),
95
+ total: nextRecords.length
96
+ };
97
+ }
98
+
99
+ export function applyManageResultToInstalledView(params: {
100
+ view?: MarketplaceInstalledView;
101
+ request: MarketplaceManageRequest;
102
+ result: MarketplaceManageResult;
103
+ }): MarketplaceInstalledView {
104
+ const current = ensureInstalledView(params.result.type, params.view);
105
+
106
+ if (params.result.action === 'uninstall' || params.result.action === 'remove') {
107
+ const nextRecords = current.records.filter((record) => !matchesInstalledRecord(record, {
108
+ id: params.result.id,
109
+ spec: params.request.spec
110
+ }));
111
+
112
+ return {
113
+ ...current,
114
+ records: nextRecords,
115
+ specs: dedupeSpecs(nextRecords),
116
+ total: nextRecords.length
117
+ };
118
+ }
119
+
120
+ const nextRecords = current.records.map((record) => {
121
+ if (!matchesInstalledRecord(record, {
122
+ id: params.result.id,
123
+ spec: params.request.spec
124
+ })) {
125
+ return record;
126
+ }
127
+
128
+ if (params.result.action === 'disable') {
129
+ return {
130
+ ...record,
131
+ enabled: false,
132
+ runtimeStatus: 'disabled'
133
+ };
134
+ }
135
+
136
+ return {
137
+ ...record,
138
+ enabled: true,
139
+ runtimeStatus: 'ready'
140
+ };
141
+ });
142
+
143
+ return {
144
+ ...current,
145
+ records: nextRecords,
146
+ specs: dedupeSpecs(nextRecords),
147
+ total: nextRecords.length
148
+ };
149
+ }
@@ -0,0 +1,77 @@
1
+ import type { MarketplaceInstalledRecord, MarketplaceLocalizedTextMap } from '@/api/types';
2
+
3
+ export function buildLocaleFallbacks(language: string): string[] {
4
+ const normalized = language.trim().toLowerCase().replace(/_/g, '-');
5
+ const base = normalized.split('-')[0];
6
+ const fallbacks = [normalized, base, 'en'];
7
+ return Array.from(new Set(fallbacks.filter(Boolean)));
8
+ }
9
+
10
+ export function normalizeLocaleTag(locale: string): string {
11
+ return locale.trim().toLowerCase().replace(/_/g, '-');
12
+ }
13
+
14
+ export function pickLocalizedText(
15
+ localized: MarketplaceLocalizedTextMap | undefined,
16
+ fallback: string | undefined,
17
+ localeFallbacks: string[]
18
+ ): string {
19
+ if (localized) {
20
+ const entries = Object.entries(localized)
21
+ .map(([locale, text]) => ({ locale: normalizeLocaleTag(locale), text: typeof text === 'string' ? text.trim() : '' }))
22
+ .filter((entry) => entry.text.length > 0);
23
+
24
+ if (entries.length > 0) {
25
+ const exactMap = new Map(entries.map((entry) => [entry.locale, entry.text] as const));
26
+
27
+ for (const locale of localeFallbacks) {
28
+ const normalizedLocale = normalizeLocaleTag(locale);
29
+ const exact = exactMap.get(normalizedLocale);
30
+ if (exact) {
31
+ return exact;
32
+ }
33
+ }
34
+
35
+ for (const locale of localeFallbacks) {
36
+ const base = normalizeLocaleTag(locale).split('-')[0];
37
+ if (!base) {
38
+ continue;
39
+ }
40
+ const matched = entries.find((entry) => entry.locale === base || entry.locale.startsWith(`${base}-`));
41
+ if (matched) {
42
+ return matched.text;
43
+ }
44
+ }
45
+
46
+ return entries[0]?.text ?? '';
47
+ }
48
+ }
49
+
50
+ return fallback?.trim() ?? '';
51
+ }
52
+
53
+ export function pickInstalledRecordDescription(
54
+ record: MarketplaceInstalledRecord | undefined,
55
+ localeFallbacks: string[]
56
+ ): string {
57
+ if (!record) {
58
+ return '';
59
+ }
60
+
61
+ for (const locale of localeFallbacks) {
62
+ const base = normalizeLocaleTag(locale).split('-')[0];
63
+ if (base === 'zh' && record.descriptionZh?.trim()) {
64
+ return record.descriptionZh.trim();
65
+ }
66
+ }
67
+
68
+ if (record.description?.trim()) {
69
+ return record.description.trim();
70
+ }
71
+
72
+ if (record.descriptionZh?.trim()) {
73
+ return record.descriptionZh.trim();
74
+ }
75
+
76
+ return '';
77
+ }
@@ -0,0 +1,102 @@
1
+ import type { MarketplaceSort } from '@/api/types';
2
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
3
+ import { Skeleton } from '@/components/ui/skeleton';
4
+ import { t } from '@/lib/i18n';
5
+ import { PackageSearch } from 'lucide-react';
6
+
7
+ export function FilterPanel(props: {
8
+ scope: 'all' | 'installed';
9
+ searchText: string;
10
+ searchPlaceholder: string;
11
+ sort: MarketplaceSort;
12
+ onSearchTextChange: (value: string) => void;
13
+ onSortChange: (value: MarketplaceSort) => void;
14
+ }) {
15
+ return (
16
+ <div className="mb-4">
17
+ <div className="flex gap-3 items-center">
18
+ <div className="flex-1 min-w-0 relative">
19
+ <PackageSearch className="h-4 w-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
20
+ <input
21
+ value={props.searchText}
22
+ onChange={(event) => props.onSearchTextChange(event.target.value)}
23
+ placeholder={props.searchPlaceholder}
24
+ 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"
25
+ />
26
+ </div>
27
+
28
+ {props.scope === 'all' && (
29
+ <Select value={props.sort} onValueChange={(value) => props.onSortChange(value as MarketplaceSort)}>
30
+ <SelectTrigger className="h-9 w-[150px] shrink-0 rounded-lg">
31
+ <SelectValue />
32
+ </SelectTrigger>
33
+ <SelectContent>
34
+ <SelectItem value="relevance">{t('marketplaceSortRelevance')}</SelectItem>
35
+ <SelectItem value="updated">{t('marketplaceSortUpdated')}</SelectItem>
36
+ </SelectContent>
37
+ </Select>
38
+ )}
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ export function MarketplaceListSkeleton(props: {
45
+ count: number;
46
+ }) {
47
+ return (
48
+ <>
49
+ {Array.from({ length: props.count }, (_, index) => (
50
+ <article
51
+ key={`marketplace-skeleton-${index}`}
52
+ className="rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm"
53
+ >
54
+ <div className="flex items-start gap-3.5 justify-between">
55
+ <div className="flex min-w-0 flex-1 gap-3">
56
+ <Skeleton className="h-10 w-10 shrink-0 rounded-xl" />
57
+ <div className="min-w-0 flex-1 space-y-2 pt-0.5">
58
+ <Skeleton className="h-4 w-32 max-w-[70%]" />
59
+ <div className="flex items-center gap-2">
60
+ <Skeleton className="h-3 w-12" />
61
+ <Skeleton className="h-3 w-24" />
62
+ </div>
63
+ <Skeleton className="h-3 w-full" />
64
+ </div>
65
+ </div>
66
+ <Skeleton className="h-8 w-20 shrink-0 rounded-xl" />
67
+ </div>
68
+ </article>
69
+ ))}
70
+ </>
71
+ );
72
+ }
73
+
74
+ export function PaginationBar(props: {
75
+ page: number;
76
+ totalPages: number;
77
+ busy: boolean;
78
+ onPrev: () => void;
79
+ onNext: () => void;
80
+ }) {
81
+ return (
82
+ <div className="mt-4 flex items-center justify-end gap-2">
83
+ <button
84
+ className="h-8 px-3 rounded-xl border border-gray-200/80 text-sm text-gray-600 disabled:opacity-40"
85
+ onClick={props.onPrev}
86
+ disabled={props.page <= 1 || props.busy}
87
+ >
88
+ {t('prev')}
89
+ </button>
90
+ <div className="text-sm text-gray-600 min-w-20 text-center">
91
+ {props.totalPages === 0 ? '0 / 0' : `${props.page} / ${props.totalPages}`}
92
+ </div>
93
+ <button
94
+ className="h-8 px-3 rounded-xl border border-gray-200/80 text-sm text-gray-600 disabled:opacity-40"
95
+ onClick={props.onNext}
96
+ disabled={props.totalPages === 0 || props.page >= props.totalPages || props.busy}
97
+ >
98
+ {t('next')}
99
+ </button>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,208 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, beforeEach, expect, it, vi } from 'vitest';
3
+ import { McpMarketplacePage } from '@/components/marketplace/mcp/McpMarketplacePage';
4
+ import type { MarketplaceInstalledView, MarketplaceListView } from '@/api/types';
5
+
6
+ type ItemsQueryState = {
7
+ data?: MarketplaceListView;
8
+ isLoading: boolean;
9
+ isFetching: boolean;
10
+ isError: boolean;
11
+ error: Error | null;
12
+ };
13
+
14
+ type InstalledQueryState = {
15
+ data?: MarketplaceInstalledView;
16
+ isLoading: boolean;
17
+ isFetching: boolean;
18
+ isError: boolean;
19
+ error: Error | null;
20
+ };
21
+
22
+ const mocks = vi.hoisted(() => ({
23
+ itemsQuery: null as unknown as ItemsQueryState,
24
+ installedQuery: null as unknown as InstalledQueryState,
25
+ installMutation: {
26
+ mutateAsync: vi.fn(),
27
+ isPending: false
28
+ },
29
+ manageMutation: {
30
+ mutateAsync: vi.fn(),
31
+ isPending: false
32
+ },
33
+ doctorMutation: {
34
+ mutateAsync: vi.fn(),
35
+ isPending: false
36
+ },
37
+ confirm: vi.fn(),
38
+ docOpen: vi.fn()
39
+ }));
40
+
41
+ vi.mock('@tanstack/react-query', () => ({
42
+ useMutation: () => mocks.doctorMutation
43
+ }));
44
+
45
+ vi.mock('@/components/providers/I18nProvider', () => ({
46
+ useI18n: () => ({
47
+ language: 'zh',
48
+ setLanguage: vi.fn(),
49
+ toggleLanguage: vi.fn(),
50
+ t: (key: string) => key
51
+ })
52
+ }));
53
+
54
+ vi.mock('@/components/doc-browser', () => ({
55
+ useDocBrowser: () => ({
56
+ open: mocks.docOpen
57
+ })
58
+ }));
59
+
60
+ vi.mock('@/hooks/useConfirmDialog', () => ({
61
+ useConfirmDialog: () => ({
62
+ confirm: mocks.confirm,
63
+ ConfirmDialog: () => null
64
+ })
65
+ }));
66
+
67
+ vi.mock('@/hooks/useMcpMarketplace', () => ({
68
+ useMcpMarketplaceItems: () => mocks.itemsQuery,
69
+ useMcpMarketplaceInstalled: () => mocks.installedQuery,
70
+ useInstallMcpMarketplaceItem: () => mocks.installMutation,
71
+ useManageMcpMarketplaceItem: () => mocks.manageMutation
72
+ }));
73
+
74
+ function createItemsQuery(overrides: Partial<ItemsQueryState> = {}): ItemsQueryState {
75
+ return {
76
+ data: undefined,
77
+ isLoading: false,
78
+ isFetching: false,
79
+ isError: false,
80
+ error: null,
81
+ ...overrides
82
+ };
83
+ }
84
+
85
+ function createInstalledQuery(overrides: Partial<InstalledQueryState> = {}): InstalledQueryState {
86
+ return {
87
+ data: {
88
+ type: 'mcp',
89
+ total: 0,
90
+ specs: [],
91
+ records: []
92
+ },
93
+ isLoading: false,
94
+ isFetching: false,
95
+ isError: false,
96
+ error: null,
97
+ ...overrides
98
+ };
99
+ }
100
+
101
+ describe('McpMarketplacePage', () => {
102
+ beforeEach(() => {
103
+ mocks.installMutation.mutateAsync.mockReset();
104
+ mocks.manageMutation.mutateAsync.mockReset();
105
+ mocks.doctorMutation.mutateAsync.mockReset();
106
+ mocks.confirm.mockReset();
107
+ mocks.docOpen.mockReset();
108
+ mocks.itemsQuery = createItemsQuery();
109
+ mocks.installedQuery = createInstalledQuery();
110
+ });
111
+
112
+ it('prefers localized summary copy for the active language', () => {
113
+ mocks.itemsQuery = createItemsQuery({
114
+ data: {
115
+ total: 1,
116
+ page: 1,
117
+ pageSize: 12,
118
+ totalPages: 1,
119
+ sort: 'relevance',
120
+ items: [
121
+ {
122
+ id: 'mcp-chrome-devtools',
123
+ slug: 'chrome-devtools',
124
+ type: 'mcp',
125
+ name: 'Chrome DevTools MCP',
126
+ summary: 'Connect MCP clients to Chrome DevTools for browser inspection and automation.',
127
+ summaryI18n: {
128
+ en: 'Connect MCP clients to Chrome DevTools for browser inspection and automation.',
129
+ zh: '把 MCP 客户端接入 Chrome DevTools,用于浏览器检查与自动化。'
130
+ },
131
+ tags: ['mcp', 'browser'],
132
+ author: 'Chrome DevTools',
133
+ install: {
134
+ kind: 'template',
135
+ spec: 'chrome-devtools',
136
+ command: 'nextclaw mcp add chrome-devtools -- npx -y chrome-devtools-mcp@latest'
137
+ },
138
+ updatedAt: '2026-03-19T00:00:00.000Z'
139
+ }
140
+ ]
141
+ }
142
+ });
143
+
144
+ render(<McpMarketplacePage />);
145
+
146
+ expect(screen.getByText('把 MCP 客户端接入 Chrome DevTools,用于浏览器检查与自动化。')).toBeTruthy();
147
+ });
148
+
149
+ it('hides install button when an installed record matches by spec without catalog slug', () => {
150
+ mocks.itemsQuery = createItemsQuery({
151
+ data: {
152
+ total: 1,
153
+ page: 1,
154
+ pageSize: 12,
155
+ totalPages: 1,
156
+ sort: 'relevance',
157
+ items: [
158
+ {
159
+ id: 'mcp-chrome-devtools',
160
+ slug: 'chrome-devtools',
161
+ type: 'mcp',
162
+ name: 'Chrome DevTools MCP',
163
+ summary: 'Connect MCP clients to Chrome DevTools for browser inspection and automation.',
164
+ summaryI18n: {
165
+ en: 'Connect MCP clients to Chrome DevTools for browser inspection and automation.',
166
+ zh: '把 MCP 客户端接入 Chrome DevTools,用于浏览器检查与自动化。'
167
+ },
168
+ tags: ['mcp', 'browser'],
169
+ author: 'Chrome DevTools',
170
+ install: {
171
+ kind: 'template',
172
+ spec: 'chrome-devtools',
173
+ command: 'nextclaw mcp add chrome-devtools -- npx -y chrome-devtools-mcp@latest'
174
+ },
175
+ updatedAt: '2026-03-19T00:00:00.000Z'
176
+ }
177
+ ]
178
+ }
179
+ });
180
+ mocks.installedQuery = createInstalledQuery({
181
+ data: {
182
+ type: 'mcp',
183
+ total: 1,
184
+ specs: ['chrome-devtools'],
185
+ records: [
186
+ {
187
+ type: 'mcp',
188
+ id: 'chrome-devtools',
189
+ spec: 'chrome-devtools',
190
+ label: 'Chrome DevTools MCP',
191
+ enabled: true,
192
+ runtimeStatus: 'enabled',
193
+ transport: 'stdio',
194
+ scope: {
195
+ allAgents: true,
196
+ agents: []
197
+ }
198
+ }
199
+ ]
200
+ }
201
+ });
202
+
203
+ render(<McpMarketplacePage />);
204
+
205
+ expect(screen.queryByText('Install')).toBeNull();
206
+ expect(screen.getByText('Disable')).toBeTruthy();
207
+ });
208
+ });