@nextclaw/ui 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/assets/ChannelsList-ZBPiF0y2.js +1 -0
- package/dist/assets/ChatPage-BOgoolWK.js +38 -0
- package/dist/assets/{DocBrowser-LpzGe8An.js → DocBrowser-BUYNHg0Y.js} +1 -1
- package/dist/assets/LogoBadge-DXPq99LJ.js +1 -0
- package/dist/assets/MarketplacePage-Dx7nexYN.js +49 -0
- package/dist/assets/McpMarketplacePage-064wdotP.js +40 -0
- package/dist/assets/{ModelConfig-DuImUHIX.js → ModelConfig-BDIfLesG.js} +1 -1
- package/dist/assets/ProvidersList-DrlIr46m.js +1 -0
- package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +1 -0
- package/dist/assets/{RuntimeConfig-C6iqpJR_.js → RuntimeConfig-BPxXEGzM.js} +1 -1
- package/dist/assets/{SearchConfig-Dvp1TAXu.js → SearchConfig-BIqnlpne.js} +1 -1
- package/dist/assets/{SecretsConfig-D5Ymlvt9.js → SecretsConfig-jKZEVF2q.js} +2 -2
- package/dist/assets/{SessionsConfig-CIA_jA1P.js → SessionsConfig-C_FXgVe1.js} +2 -2
- package/dist/assets/{chat-message-B60Fh9kI.js → chat-message-DmzpZJc_.js} +1 -1
- package/dist/assets/index-Byfw276e.js +8 -0
- package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
- package/dist/assets/index-bhNuQis7.css +1 -0
- package/dist/assets/{label-D4fGx6Wb.js → label-B1MloEtn.js} +1 -1
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
- package/dist/assets/{page-layout-twy8gmBE.js → page-layout-BGg1EhM5.js} +1 -1
- package/dist/assets/{popover-DYbYpt1j.js → popover-jJMv74Fp.js} +1 -1
- package/dist/assets/{security-config-BcIZ4rpb.js → security-config-Boh9NIMz.js} +1 -1
- package/dist/assets/skeleton-CmATs_b3.js +1 -0
- package/dist/assets/status-dot-DNyCdxPZ.js +1 -0
- package/dist/assets/{switch-DqA6r5XR.js → switch-DE_MYk7x.js} +1 -1
- package/dist/assets/{tabs-custom-C6enKKs1.js → tabs-custom-B-zErYPr.js} +1 -1
- package/dist/assets/{useConfirmDialog-CHBf5Of7.js → useConfirmDialog-BqQ6QfhB.js} +2 -2
- package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
- package/dist/index.html +3 -3
- package/package.json +4 -4
- package/src/App.tsx +4 -0
- package/src/api/auth.types.ts +24 -0
- package/src/api/chat-session-type.types.ts +21 -0
- package/src/api/marketplace.ts +8 -2
- package/src/api/mcp-marketplace.ts +138 -0
- package/src/api/remote.ts +57 -0
- package/src/api/remote.types.ts +80 -0
- package/src/api/types.ts +91 -37
- package/src/components/chat/ChatSidebar.test.tsx +31 -2
- package/src/components/chat/ChatSidebar.tsx +26 -2
- package/src/components/chat/chat-page-data.ts +37 -53
- package/src/components/chat/chat-page-runtime.test.ts +122 -2
- package/src/components/chat/chat-page-runtime.ts +1 -118
- package/src/components/chat/chat-session-preference-governance.ts +303 -0
- package/src/components/chat/legacy/LegacyChatPage.tsx +4 -34
- package/src/components/chat/ncp/NcpChatPage.tsx +4 -34
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +63 -36
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
- package/src/components/chat/stores/chat-input.store.ts +14 -1
- package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
- package/src/components/chat/useChatSessionTypeState.ts +55 -12
- package/src/components/layout/Sidebar.tsx +11 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
- package/src/components/marketplace/MarketplacePage.tsx +52 -199
- package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
- package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
- package/src/components/marketplace/marketplace-localization.ts +77 -0
- package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
- package/src/components/remote/RemoteAccessPage.tsx +320 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/hooks/useMarketplace.ts +36 -7
- package/src/hooks/useMcpMarketplace.ts +99 -0
- package/src/hooks/useRemoteAccess.ts +92 -0
- package/src/hooks/useWebSocket.ts +25 -16
- package/src/lib/i18n.marketplace.ts +91 -0
- package/src/lib/i18n.remote.ts +115 -0
- package/src/lib/i18n.ts +10 -68
- package/dist/assets/ChannelsList-DhvjpZcs.js +0 -1
- package/dist/assets/ChatPage-B8VBaMQm.js +0 -38
- package/dist/assets/LogoBadge-Be4lktJN.js +0 -1
- package/dist/assets/MarketplacePage-Cx9AI3_h.js +0 -49
- package/dist/assets/ProvidersList-Ccleg25k.js +0 -1
- package/dist/assets/index-BiPDnzv0.js +0 -8
- package/dist/assets/index-C8GsgIUn.css +0 -1
- package/dist/assets/skeleton-DypBy7jp.js +0 -1
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import type {
|
|
3
3
|
MarketplaceInstalledRecord,
|
|
4
4
|
MarketplaceItemSummary,
|
|
5
|
-
MarketplaceLocalizedTextMap,
|
|
6
5
|
MarketplaceManageAction,
|
|
7
6
|
MarketplacePluginContentView,
|
|
8
7
|
MarketplaceSkillContentView,
|
|
@@ -10,8 +9,6 @@ import type {
|
|
|
10
9
|
MarketplaceItemType
|
|
11
10
|
} from '@/api/types';
|
|
12
11
|
import { fetchMarketplacePluginContent, fetchMarketplaceSkillContent } from '@/api/marketplace';
|
|
13
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
14
|
-
import { Skeleton } from '@/components/ui/skeleton';
|
|
15
12
|
import { Tabs } from '@/components/ui/tabs-custom';
|
|
16
13
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
17
14
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
@@ -23,10 +20,15 @@ import {
|
|
|
23
20
|
useMarketplaceInstalled,
|
|
24
21
|
useMarketplaceItems
|
|
25
22
|
} from '@/hooks/useMarketplace';
|
|
23
|
+
import {
|
|
24
|
+
FilterPanel,
|
|
25
|
+
MarketplaceListSkeleton,
|
|
26
|
+
PaginationBar
|
|
27
|
+
} from '@/components/marketplace/marketplace-page-parts';
|
|
28
|
+
import { buildLocaleFallbacks, pickLocalizedText } from '@/components/marketplace/marketplace-localization';
|
|
26
29
|
import { t } from '@/lib/i18n';
|
|
27
30
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
28
31
|
import { cn } from '@/lib/utils';
|
|
29
|
-
import { PackageSearch } from 'lucide-react';
|
|
30
32
|
import { useEffect, useMemo, useState } from 'react';
|
|
31
33
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
32
34
|
|
|
@@ -40,9 +42,7 @@ type InstallState = {
|
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
type ManageState = {
|
|
43
|
-
|
|
44
|
-
targetId?: string;
|
|
45
|
-
action?: MarketplaceManageAction;
|
|
45
|
+
actionsByTarget: ReadonlyMap<string, MarketplaceManageAction>;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
type InstalledRenderEntry = {
|
|
@@ -134,56 +134,6 @@ function findCatalogItemForRecord(
|
|
|
134
134
|
return catalogLookup.get(toLookupKey(record.type, record.label));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
function buildLocaleFallbacks(language: string): string[] {
|
|
138
|
-
const normalized = language.trim().toLowerCase().replace(/_/g, '-');
|
|
139
|
-
const base = normalized.split('-')[0];
|
|
140
|
-
const fallbacks = [normalized, base, 'en'];
|
|
141
|
-
return Array.from(new Set(fallbacks.filter(Boolean)));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function normalizeLocaleTag(locale: string): string {
|
|
145
|
-
return locale.trim().toLowerCase().replace(/_/g, '-');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function pickLocalizedText(
|
|
149
|
-
localized: MarketplaceLocalizedTextMap | undefined,
|
|
150
|
-
fallback: string | undefined,
|
|
151
|
-
localeFallbacks: string[]
|
|
152
|
-
): string {
|
|
153
|
-
if (localized) {
|
|
154
|
-
const entries = Object.entries(localized)
|
|
155
|
-
.map(([locale, text]) => ({ locale: normalizeLocaleTag(locale), text: typeof text === 'string' ? text.trim() : '' }))
|
|
156
|
-
.filter((entry) => entry.text.length > 0);
|
|
157
|
-
|
|
158
|
-
if (entries.length > 0) {
|
|
159
|
-
const exactMap = new Map(entries.map((entry) => [entry.locale, entry.text] as const));
|
|
160
|
-
|
|
161
|
-
for (const locale of localeFallbacks) {
|
|
162
|
-
const normalizedLocale = normalizeLocaleTag(locale);
|
|
163
|
-
const exact = exactMap.get(normalizedLocale);
|
|
164
|
-
if (exact) {
|
|
165
|
-
return exact;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
for (const locale of localeFallbacks) {
|
|
170
|
-
const base = normalizeLocaleTag(locale).split('-')[0];
|
|
171
|
-
if (!base) {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
const matched = entries.find((entry) => entry.locale === base || entry.locale.startsWith(`${base}-`));
|
|
175
|
-
if (matched) {
|
|
176
|
-
return matched.text;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return entries[0]?.text ?? '';
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return fallback?.trim() ?? '';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
137
|
function matchInstalledSearch(
|
|
188
138
|
record: MarketplaceInstalledRecord,
|
|
189
139
|
item: MarketplaceItemSummary | undefined,
|
|
@@ -317,43 +267,6 @@ function buildGenericDetailDataUrl(params: {
|
|
|
317
267
|
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
318
268
|
}
|
|
319
269
|
|
|
320
|
-
function FilterPanel(props: {
|
|
321
|
-
scope: ScopeType;
|
|
322
|
-
searchText: string;
|
|
323
|
-
searchPlaceholder: string;
|
|
324
|
-
sort: MarketplaceSort;
|
|
325
|
-
onSearchTextChange: (value: string) => void;
|
|
326
|
-
onSortChange: (value: MarketplaceSort) => void;
|
|
327
|
-
}) {
|
|
328
|
-
return (
|
|
329
|
-
<div className="mb-4">
|
|
330
|
-
<div className="flex gap-3 items-center">
|
|
331
|
-
<div className="flex-1 min-w-0 relative">
|
|
332
|
-
<PackageSearch className="h-4 w-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
|
333
|
-
<input
|
|
334
|
-
value={props.searchText}
|
|
335
|
-
onChange={(event) => props.onSearchTextChange(event.target.value)}
|
|
336
|
-
placeholder={props.searchPlaceholder}
|
|
337
|
-
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"
|
|
338
|
-
/>
|
|
339
|
-
</div>
|
|
340
|
-
|
|
341
|
-
{props.scope === 'all' && (
|
|
342
|
-
<Select value={props.sort} onValueChange={(v) => props.onSortChange(v as MarketplaceSort)}>
|
|
343
|
-
<SelectTrigger className="h-9 w-[150px] shrink-0 rounded-lg">
|
|
344
|
-
<SelectValue />
|
|
345
|
-
</SelectTrigger>
|
|
346
|
-
<SelectContent>
|
|
347
|
-
<SelectItem value="relevance">{t('marketplaceSortRelevance')}</SelectItem>
|
|
348
|
-
<SelectItem value="updated">{t('marketplaceSortUpdated')}</SelectItem>
|
|
349
|
-
</SelectContent>
|
|
350
|
-
</Select>
|
|
351
|
-
)}
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
270
|
function MarketplaceListCard(props: {
|
|
358
271
|
item?: MarketplaceItemSummary;
|
|
359
272
|
record?: MarketplaceInstalledRecord;
|
|
@@ -366,14 +279,14 @@ function MarketplaceListCard(props: {
|
|
|
366
279
|
}) {
|
|
367
280
|
const { item, record, localeFallbacks, installState, manageState, onOpen, onInstall, onManage } = props;
|
|
368
281
|
const pluginRecord = record?.type === 'plugin' ? record : undefined;
|
|
369
|
-
const type = item?.type ?? record?.type;
|
|
370
282
|
const title = item?.name ?? record?.label ?? record?.id ?? record?.spec ?? t('marketplaceUnknownItem');
|
|
371
283
|
const summary = pickLocalizedText(item?.summaryI18n, item?.summary, localeFallbacks)
|
|
372
284
|
|| (record ? t('marketplaceInstalledLocalSummary') : '');
|
|
373
285
|
const spec = item?.install.spec ?? record?.spec ?? '';
|
|
374
286
|
|
|
375
287
|
const targetId = record?.id || record?.spec;
|
|
376
|
-
const
|
|
288
|
+
const busyAction = targetId ? manageState.actionsByTarget.get(targetId) : undefined;
|
|
289
|
+
const busyForRecord = Boolean(busyAction);
|
|
377
290
|
|
|
378
291
|
const canToggle = Boolean(pluginRecord);
|
|
379
292
|
const canUninstallPlugin = record?.type === 'plugin' && record.origin !== 'bundled';
|
|
@@ -384,8 +297,6 @@ function MarketplaceListCard(props: {
|
|
|
384
297
|
const installSpec = item?.install.spec;
|
|
385
298
|
const isInstalling = typeof installSpec === 'string' && installState.installingSpecs.has(installSpec);
|
|
386
299
|
|
|
387
|
-
const displayType = type === 'plugin' ? t('marketplaceTypePlugin') : type === 'skill' ? t('marketplaceTypeSkill') : t('marketplaceTypeExtension');
|
|
388
|
-
|
|
389
300
|
return (
|
|
390
301
|
<article
|
|
391
302
|
onClick={onOpen}
|
|
@@ -405,19 +316,15 @@ function MarketplaceListCard(props: {
|
|
|
405
316
|
</Tooltip>
|
|
406
317
|
|
|
407
318
|
<div className="flex items-center gap-1.5 mt-0.5 mb-1.5">
|
|
408
|
-
<span className="text-[11px] text-gray-500 font-medium">{displayType}</span>
|
|
409
319
|
{spec && (
|
|
410
|
-
|
|
411
|
-
<
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
</TooltipContent>
|
|
419
|
-
</Tooltip>
|
|
420
|
-
</>
|
|
320
|
+
<Tooltip>
|
|
321
|
+
<TooltipTrigger asChild>
|
|
322
|
+
<span className="text-[11px] text-gray-400 truncate max-w-full font-mono">{spec}</span>
|
|
323
|
+
</TooltipTrigger>
|
|
324
|
+
<TooltipContent className="max-w-[300px] text-xs font-mono break-all">
|
|
325
|
+
{spec}
|
|
326
|
+
</TooltipContent>
|
|
327
|
+
</Tooltip>
|
|
421
328
|
)}
|
|
422
329
|
</div>
|
|
423
330
|
|
|
@@ -451,29 +358,29 @@ function MarketplaceListCard(props: {
|
|
|
451
358
|
|
|
452
359
|
{pluginRecord && canToggle && (
|
|
453
360
|
<button
|
|
454
|
-
disabled={
|
|
361
|
+
disabled={busyForRecord}
|
|
455
362
|
onClick={(event) => {
|
|
456
363
|
event.stopPropagation();
|
|
457
364
|
onManage(isDisabled ? 'enable' : 'disable', pluginRecord);
|
|
458
365
|
}}
|
|
459
366
|
className="inline-flex items-center h-8 px-4 rounded-xl text-xs font-medium border border-gray-200/80 text-gray-600 bg-white hover:bg-gray-50 hover:border-gray-300 disabled:opacity-50 transition-colors"
|
|
460
367
|
>
|
|
461
|
-
{
|
|
462
|
-
? (
|
|
368
|
+
{busyAction && busyAction !== 'uninstall'
|
|
369
|
+
? (busyAction === 'enable' ? t('marketplaceEnabling') : t('marketplaceDisabling'))
|
|
463
370
|
: (isDisabled ? t('marketplaceEnable') : t('marketplaceDisable'))}
|
|
464
371
|
</button>
|
|
465
372
|
)}
|
|
466
373
|
|
|
467
374
|
{record && canUninstall && (
|
|
468
375
|
<button
|
|
469
|
-
disabled={
|
|
376
|
+
disabled={busyForRecord}
|
|
470
377
|
onClick={(event) => {
|
|
471
378
|
event.stopPropagation();
|
|
472
379
|
onManage('uninstall', record);
|
|
473
380
|
}}
|
|
474
381
|
className="inline-flex items-center h-8 px-4 rounded-xl text-xs font-medium border border-rose-100 text-rose-500 bg-white hover:bg-rose-50 hover:border-rose-200 disabled:opacity-50 transition-colors"
|
|
475
382
|
>
|
|
476
|
-
{
|
|
383
|
+
{busyAction === 'uninstall' ? t('marketplaceRemoving') : t('marketplaceUninstall')}
|
|
477
384
|
</button>
|
|
478
385
|
)}
|
|
479
386
|
</div>
|
|
@@ -481,68 +388,6 @@ function MarketplaceListCard(props: {
|
|
|
481
388
|
);
|
|
482
389
|
}
|
|
483
390
|
|
|
484
|
-
function MarketplaceListSkeleton(props: {
|
|
485
|
-
count?: number;
|
|
486
|
-
}) {
|
|
487
|
-
const count = props.count ?? SKELETON_CARD_COUNT;
|
|
488
|
-
|
|
489
|
-
return (
|
|
490
|
-
<>
|
|
491
|
-
{Array.from({ length: count }, (_, index) => (
|
|
492
|
-
<article
|
|
493
|
-
key={`marketplace-skeleton-${index}`}
|
|
494
|
-
className="rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm"
|
|
495
|
-
>
|
|
496
|
-
<div className="flex items-start gap-3.5 justify-between">
|
|
497
|
-
<div className="flex min-w-0 flex-1 gap-3">
|
|
498
|
-
<Skeleton className="h-10 w-10 shrink-0 rounded-xl" />
|
|
499
|
-
<div className="min-w-0 flex-1 space-y-2 pt-0.5">
|
|
500
|
-
<Skeleton className="h-4 w-32 max-w-[70%]" />
|
|
501
|
-
<div className="flex items-center gap-2">
|
|
502
|
-
<Skeleton className="h-3 w-12" />
|
|
503
|
-
<Skeleton className="h-3 w-24" />
|
|
504
|
-
</div>
|
|
505
|
-
<Skeleton className="h-3 w-full" />
|
|
506
|
-
</div>
|
|
507
|
-
</div>
|
|
508
|
-
<Skeleton className="h-8 w-20 shrink-0 rounded-xl" />
|
|
509
|
-
</div>
|
|
510
|
-
</article>
|
|
511
|
-
))}
|
|
512
|
-
</>
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function PaginationBar(props: {
|
|
517
|
-
page: number;
|
|
518
|
-
totalPages: number;
|
|
519
|
-
busy: boolean;
|
|
520
|
-
onPrev: () => void;
|
|
521
|
-
onNext: () => void;
|
|
522
|
-
}) {
|
|
523
|
-
return (
|
|
524
|
-
<div className="mt-4 flex items-center justify-end gap-2">
|
|
525
|
-
<button
|
|
526
|
-
className="h-8 px-3 rounded-xl border border-gray-200/80 text-sm text-gray-600 disabled:opacity-40"
|
|
527
|
-
onClick={props.onPrev}
|
|
528
|
-
disabled={props.page <= 1 || props.busy}
|
|
529
|
-
>
|
|
530
|
-
{t('prev')}
|
|
531
|
-
</button>
|
|
532
|
-
<div className="text-sm text-gray-600 min-w-20 text-center">
|
|
533
|
-
{props.totalPages === 0 ? '0 / 0' : `${props.page} / ${props.totalPages}`}
|
|
534
|
-
</div>
|
|
535
|
-
<button
|
|
536
|
-
className="h-8 px-3 rounded-xl border border-gray-200/80 text-sm text-gray-600 disabled:opacity-40"
|
|
537
|
-
onClick={props.onNext}
|
|
538
|
-
disabled={props.totalPages === 0 || props.page >= props.totalPages || props.busy}
|
|
539
|
-
>
|
|
540
|
-
{t('next')}
|
|
541
|
-
</button>
|
|
542
|
-
</div>
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
391
|
export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
547
392
|
const { forcedType } = props;
|
|
548
393
|
const navigate = useNavigate();
|
|
@@ -609,6 +454,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
609
454
|
const [sort, setSort] = useState<MarketplaceSort>('relevance');
|
|
610
455
|
const [page, setPage] = useState(1);
|
|
611
456
|
const [installingSpecs, setInstallingSpecs] = useState<ReadonlySet<string>>(new Set());
|
|
457
|
+
const [managingTargets, setManagingTargets] = useState<ReadonlyMap<string, MarketplaceManageAction>>(new Map());
|
|
612
458
|
|
|
613
459
|
useEffect(() => {
|
|
614
460
|
const timer = setTimeout(() => {
|
|
@@ -687,10 +533,6 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
687
533
|
const showCatalogSkeleton = scope === 'all' && itemsQuery.isLoading && !itemsQuery.data;
|
|
688
534
|
const showInstalledSkeleton = scope === 'installed' && installedQuery.isLoading && !installedQuery.data;
|
|
689
535
|
const showListSkeleton = showCatalogSkeleton || showInstalledSkeleton;
|
|
690
|
-
const isListRefreshing = !showListSkeleton && (
|
|
691
|
-
(scope === 'all' && itemsQuery.isFetching)
|
|
692
|
-
|| (scope === 'installed' && installedQuery.isFetching)
|
|
693
|
-
);
|
|
694
536
|
|
|
695
537
|
const listSummary = useMemo(() => {
|
|
696
538
|
if (scope === 'installed') {
|
|
@@ -710,9 +552,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
710
552
|
const installState: InstallState = { installingSpecs };
|
|
711
553
|
|
|
712
554
|
const manageState: ManageState = {
|
|
713
|
-
|
|
714
|
-
targetId: manageMutation.variables?.id || manageMutation.variables?.spec,
|
|
715
|
-
action: manageMutation.variables?.action
|
|
555
|
+
actionsByTarget: managingTargets
|
|
716
556
|
};
|
|
717
557
|
|
|
718
558
|
const scopeTabs = [
|
|
@@ -759,14 +599,13 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
759
599
|
};
|
|
760
600
|
|
|
761
601
|
const handleManage = async (action: MarketplaceManageAction, record: MarketplaceInstalledRecord) => {
|
|
762
|
-
if (manageMutation.isPending) {
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
602
|
const targetId = record.id || record.spec;
|
|
767
603
|
if (!targetId) {
|
|
768
604
|
return;
|
|
769
605
|
}
|
|
606
|
+
if (managingTargets.has(targetId)) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
770
609
|
|
|
771
610
|
if (action === 'uninstall') {
|
|
772
611
|
const confirmed = await confirm({
|
|
@@ -780,12 +619,29 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
780
619
|
}
|
|
781
620
|
}
|
|
782
621
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
action
|
|
786
|
-
|
|
787
|
-
spec: record.spec
|
|
622
|
+
setManagingTargets((previous) => {
|
|
623
|
+
const next = new Map(previous);
|
|
624
|
+
next.set(targetId, action);
|
|
625
|
+
return next;
|
|
788
626
|
});
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
await manageMutation.mutateAsync({
|
|
630
|
+
type: record.type,
|
|
631
|
+
action,
|
|
632
|
+
id: targetId,
|
|
633
|
+
spec: record.spec
|
|
634
|
+
});
|
|
635
|
+
} finally {
|
|
636
|
+
setManagingTargets((previous) => {
|
|
637
|
+
if (!previous.has(targetId)) {
|
|
638
|
+
return previous;
|
|
639
|
+
}
|
|
640
|
+
const next = new Map(previous);
|
|
641
|
+
next.delete(targetId);
|
|
642
|
+
return next;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
789
645
|
};
|
|
790
646
|
|
|
791
647
|
const openItemDetail = async (item?: MarketplaceItemSummary, record?: MarketplaceInstalledRecord) => {
|
|
@@ -909,15 +765,12 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
909
765
|
</div>
|
|
910
766
|
)}
|
|
911
767
|
|
|
912
|
-
<div className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1" aria-busy={showListSkeleton
|
|
768
|
+
<div className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1" aria-busy={showListSkeleton}>
|
|
913
769
|
<div
|
|
914
770
|
data-testid={showListSkeleton ? 'marketplace-list-skeleton' : undefined}
|
|
915
|
-
className=
|
|
916
|
-
'grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3 transition-opacity',
|
|
917
|
-
isListRefreshing ? 'opacity-70' : 'opacity-100'
|
|
918
|
-
)}
|
|
771
|
+
className="grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3"
|
|
919
772
|
>
|
|
920
|
-
{showListSkeleton && <MarketplaceListSkeleton />}
|
|
773
|
+
{showListSkeleton && <MarketplaceListSkeleton count={SKELETON_CARD_COUNT} />}
|
|
921
774
|
|
|
922
775
|
{!showListSkeleton && scope === 'all' && allItems.map((item) => (
|
|
923
776
|
<MarketplaceListCard
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MarketplaceInstalledView,
|
|
3
|
+
MarketplaceInstallRequest,
|
|
4
|
+
MarketplaceInstallResult,
|
|
5
|
+
MarketplaceManageRequest,
|
|
6
|
+
MarketplaceManageResult
|
|
7
|
+
} from '@/api/types';
|
|
8
|
+
import {
|
|
9
|
+
applyInstallResultToInstalledView,
|
|
10
|
+
applyManageResultToInstalledView
|
|
11
|
+
} from '@/components/marketplace/marketplace-installed-cache';
|
|
12
|
+
|
|
13
|
+
describe('marketplace-installed-cache', () => {
|
|
14
|
+
it('adds a plugin record immediately after install success', () => {
|
|
15
|
+
const request: MarketplaceInstallRequest = {
|
|
16
|
+
type: 'plugin',
|
|
17
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
18
|
+
kind: 'npm'
|
|
19
|
+
};
|
|
20
|
+
const result: MarketplaceInstallResult = {
|
|
21
|
+
type: 'plugin',
|
|
22
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
23
|
+
message: 'installed'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const next = applyInstallResultToInstalledView({ request, result });
|
|
27
|
+
|
|
28
|
+
expect(next.total).toBe(1);
|
|
29
|
+
expect(next.specs).toEqual(['@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk']);
|
|
30
|
+
expect(next.records[0]).toMatchObject({
|
|
31
|
+
type: 'plugin',
|
|
32
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
33
|
+
enabled: true,
|
|
34
|
+
origin: 'marketplace',
|
|
35
|
+
runtimeStatus: 'ready'
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('marks a plugin record as disabled immediately after disable success', () => {
|
|
40
|
+
const view: MarketplaceInstalledView = {
|
|
41
|
+
type: 'plugin',
|
|
42
|
+
total: 1,
|
|
43
|
+
specs: ['@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk'],
|
|
44
|
+
records: [
|
|
45
|
+
{
|
|
46
|
+
type: 'plugin',
|
|
47
|
+
id: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
48
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
49
|
+
label: 'Codex Runtime',
|
|
50
|
+
enabled: true,
|
|
51
|
+
origin: 'marketplace'
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
const request: MarketplaceManageRequest = {
|
|
56
|
+
type: 'plugin',
|
|
57
|
+
action: 'disable',
|
|
58
|
+
id: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
59
|
+
spec: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk'
|
|
60
|
+
};
|
|
61
|
+
const result: MarketplaceManageResult = {
|
|
62
|
+
type: 'plugin',
|
|
63
|
+
action: 'disable',
|
|
64
|
+
id: '@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk',
|
|
65
|
+
message: 'disabled'
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const next = applyManageResultToInstalledView({ view, request, result });
|
|
69
|
+
|
|
70
|
+
expect(next.records[0]).toMatchObject({
|
|
71
|
+
enabled: false,
|
|
72
|
+
runtimeStatus: 'disabled'
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('removes a skill record immediately after uninstall success', () => {
|
|
77
|
+
const view: MarketplaceInstalledView = {
|
|
78
|
+
type: 'skill',
|
|
79
|
+
total: 1,
|
|
80
|
+
specs: ['@nextclaw/web-search'],
|
|
81
|
+
records: [
|
|
82
|
+
{
|
|
83
|
+
type: 'skill',
|
|
84
|
+
id: 'web-search',
|
|
85
|
+
spec: '@nextclaw/web-search',
|
|
86
|
+
label: 'Web Search',
|
|
87
|
+
source: 'workspace'
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
};
|
|
91
|
+
const request: MarketplaceManageRequest = {
|
|
92
|
+
type: 'skill',
|
|
93
|
+
action: 'uninstall',
|
|
94
|
+
id: 'web-search',
|
|
95
|
+
spec: '@nextclaw/web-search'
|
|
96
|
+
};
|
|
97
|
+
const result: MarketplaceManageResult = {
|
|
98
|
+
type: 'skill',
|
|
99
|
+
action: 'uninstall',
|
|
100
|
+
id: 'web-search',
|
|
101
|
+
message: 'removed'
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const next = applyManageResultToInstalledView({ view, request, result });
|
|
105
|
+
|
|
106
|
+
expect(next.total).toBe(0);
|
|
107
|
+
expect(next.records).toEqual([]);
|
|
108
|
+
expect(next.specs).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -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
|
+
}
|