@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
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import {
|
|
24
24
|
FilterPanel,
|
|
25
25
|
MarketplaceListSkeleton,
|
|
26
|
-
|
|
26
|
+
MarketplaceInfiniteScrollStatus
|
|
27
27
|
} from '@/components/marketplace/marketplace-page-parts';
|
|
28
28
|
import { buildLocaleFallbacks, pickLocalizedText } from '@/components/marketplace/marketplace-localization';
|
|
29
29
|
import { t } from '@/lib/i18n';
|
|
@@ -31,6 +31,7 @@ import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
|
31
31
|
import { cn } from '@/lib/utils';
|
|
32
32
|
import { useEffect, useMemo, useState } from 'react';
|
|
33
33
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
34
|
+
import { useInfiniteScrollLoader } from '@/hooks/use-infinite-scroll-loader';
|
|
34
35
|
|
|
35
36
|
const PAGE_SIZE = 12;
|
|
36
37
|
const SKELETON_CARD_COUNT = PAGE_SIZE;
|
|
@@ -452,32 +453,38 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
452
453
|
const [query, setQuery] = useState('');
|
|
453
454
|
const [scope, setScope] = useState<ScopeType>('all');
|
|
454
455
|
const [sort, setSort] = useState<MarketplaceSort>('relevance');
|
|
455
|
-
const [page, setPage] = useState(1);
|
|
456
456
|
const [installingSpecs, setInstallingSpecs] = useState<ReadonlySet<string>>(new Set());
|
|
457
457
|
const [managingTargets, setManagingTargets] = useState<ReadonlyMap<string, MarketplaceManageAction>>(new Map());
|
|
458
458
|
|
|
459
459
|
useEffect(() => {
|
|
460
460
|
const timer = setTimeout(() => {
|
|
461
|
-
setPage(1);
|
|
462
461
|
setQuery(searchText.trim());
|
|
463
462
|
}, 250);
|
|
464
463
|
return () => clearTimeout(timer);
|
|
465
464
|
}, [searchText]);
|
|
466
465
|
|
|
467
|
-
useEffect(() => {
|
|
468
|
-
setPage(1);
|
|
469
|
-
}, [typeFilter]);
|
|
470
|
-
|
|
471
466
|
const installedQuery = useMarketplaceInstalled(typeFilter);
|
|
472
467
|
|
|
473
468
|
const itemsQuery = useMarketplaceItems({
|
|
474
469
|
q: query || undefined,
|
|
475
470
|
type: typeFilter,
|
|
476
471
|
sort,
|
|
477
|
-
page,
|
|
478
472
|
pageSize: PAGE_SIZE
|
|
479
473
|
});
|
|
480
474
|
|
|
475
|
+
const infiniteScroll = useInfiniteScrollLoader({
|
|
476
|
+
disabled: scope !== 'all' || itemsQuery.isError || !itemsQuery.hasNextPage || itemsQuery.isFetchingNextPage,
|
|
477
|
+
onLoadMore: () => itemsQuery.fetchNextPage(),
|
|
478
|
+
watchValue: `${typeFilter}:${scope}:${query}:${sort}:${itemsQuery.data?.loadedItems ?? 0}:${itemsQuery.data?.loadedPages ?? 0}`
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
const container = infiniteScroll.containerRef.current;
|
|
483
|
+
if (container && typeof container.scrollTo === 'function') {
|
|
484
|
+
container.scrollTo({ top: 0 });
|
|
485
|
+
}
|
|
486
|
+
}, [infiniteScroll.containerRef, query, scope, sort, typeFilter]);
|
|
487
|
+
|
|
481
488
|
const installMutation = useInstallMarketplaceItem();
|
|
482
489
|
const manageMutation = useManageMarketplaceItem();
|
|
483
490
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
@@ -529,7 +536,6 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
529
536
|
}, [installedRecords, typeFilter, catalogLookup, query, localeFallbacks]);
|
|
530
537
|
|
|
531
538
|
const total = scope === 'installed' ? installedEntries.length : (itemsQuery.data?.total ?? 0);
|
|
532
|
-
const totalPages = scope === 'installed' ? 1 : (itemsQuery.data?.totalPages ?? 0);
|
|
533
539
|
const showCatalogSkeleton = scope === 'all' && itemsQuery.isLoading && !itemsQuery.data;
|
|
534
540
|
const showInstalledSkeleton = scope === 'installed' && installedQuery.isLoading && !installedQuery.data;
|
|
535
541
|
const showListSkeleton = showCatalogSkeleton || showInstalledSkeleton;
|
|
@@ -727,10 +733,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
727
733
|
<Tabs
|
|
728
734
|
tabs={scopeTabs}
|
|
729
735
|
activeTab={scope}
|
|
730
|
-
onChange={(value) =>
|
|
731
|
-
setScope(value as ScopeType);
|
|
732
|
-
setPage(1);
|
|
733
|
-
}}
|
|
736
|
+
onChange={(value) => setScope(value as ScopeType)}
|
|
734
737
|
className="mb-4"
|
|
735
738
|
/>
|
|
736
739
|
|
|
@@ -740,10 +743,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
740
743
|
searchPlaceholder={t(copyKeys.searchPlaceholder)}
|
|
741
744
|
sort={sort}
|
|
742
745
|
onSearchTextChange={setSearchText}
|
|
743
|
-
onSortChange={
|
|
744
|
-
setPage(1);
|
|
745
|
-
setSort(value);
|
|
746
|
-
}}
|
|
746
|
+
onSortChange={setSort}
|
|
747
747
|
/>
|
|
748
748
|
|
|
749
749
|
<section className="flex min-h-0 flex-1 flex-col">
|
|
@@ -765,7 +765,11 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
765
765
|
</div>
|
|
766
766
|
)}
|
|
767
767
|
|
|
768
|
-
<div
|
|
768
|
+
<div
|
|
769
|
+
ref={infiniteScroll.containerRef}
|
|
770
|
+
className="min-h-0 flex-1 overflow-y-auto custom-scrollbar pr-1"
|
|
771
|
+
aria-busy={showListSkeleton || itemsQuery.isFetchingNextPage}
|
|
772
|
+
>
|
|
769
773
|
<div
|
|
770
774
|
data-testid={showListSkeleton ? 'marketplace-list-skeleton' : undefined}
|
|
771
775
|
className="grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3"
|
|
@@ -807,20 +811,16 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
807
811
|
{scope === 'installed' && !showListSkeleton && !installedQuery.isError && installedEntries.length === 0 && (
|
|
808
812
|
<div className="text-[13px] text-gray-500 py-8 text-center">{t(copyKeys.emptyInstalled)}</div>
|
|
809
813
|
)}
|
|
810
|
-
</div>
|
|
811
|
-
</section>
|
|
812
814
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
onNext={() => setPage((current) => (totalPages > 0 ? Math.min(totalPages, current + 1) : current + 1))}
|
|
821
|
-
/>
|
|
815
|
+
{scope === 'all' && !showCatalogSkeleton && !itemsQuery.isError && (
|
|
816
|
+
<MarketplaceInfiniteScrollStatus
|
|
817
|
+
hasMore={Boolean(itemsQuery.hasNextPage)}
|
|
818
|
+
loading={itemsQuery.isFetchingNextPage}
|
|
819
|
+
sentinelRef={infiniteScroll.sentinelRef}
|
|
820
|
+
/>
|
|
821
|
+
)}
|
|
822
822
|
</div>
|
|
823
|
-
|
|
823
|
+
</section>
|
|
824
824
|
<ConfirmDialog />
|
|
825
825
|
</PageLayout>
|
|
826
826
|
);
|
|
@@ -3,6 +3,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|
|
3
3
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
4
4
|
import { t } from '@/lib/i18n';
|
|
5
5
|
import { PackageSearch } from 'lucide-react';
|
|
6
|
+
import type { Ref } from 'react';
|
|
6
7
|
|
|
7
8
|
export function FilterPanel(props: {
|
|
8
9
|
scope: 'all' | 'installed';
|
|
@@ -71,32 +72,23 @@ export function MarketplaceListSkeleton(props: {
|
|
|
71
72
|
);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
export function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
onPrev: () => void;
|
|
79
|
-
onNext: () => void;
|
|
75
|
+
export function MarketplaceInfiniteScrollStatus(props: {
|
|
76
|
+
hasMore: boolean;
|
|
77
|
+
loading: boolean;
|
|
78
|
+
sentinelRef: Ref<HTMLDivElement>;
|
|
80
79
|
}) {
|
|
80
|
+
if (!props.hasMore && !props.loading) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
81
84
|
return (
|
|
82
|
-
<div className="
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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>
|
|
85
|
+
<div className="py-4">
|
|
86
|
+
{props.hasMore && <div ref={props.sentinelRef} className="h-1 w-full" aria-hidden="true" />}
|
|
87
|
+
{props.loading && (
|
|
88
|
+
<div data-testid="marketplace-loading-more" className="pt-3 text-center text-xs text-gray-500">
|
|
89
|
+
{t('loading')}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
100
92
|
</div>
|
|
101
93
|
);
|
|
102
94
|
}
|
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
} from '@/components/marketplace/marketplace-localization';
|
|
37
37
|
import { t } from '@/lib/i18n';
|
|
38
38
|
import { cn } from '@/lib/utils';
|
|
39
|
+
import { MarketplaceInfiniteScrollStatus } from '@/components/marketplace/marketplace-page-parts';
|
|
40
|
+
import { useInfiniteScrollLoader } from '@/hooks/use-infinite-scroll-loader';
|
|
39
41
|
|
|
40
42
|
type ScopeType = 'catalog' | 'installed';
|
|
41
43
|
|
|
@@ -256,7 +258,6 @@ export function McpMarketplacePage() {
|
|
|
256
258
|
const [searchText, setSearchText] = useState('');
|
|
257
259
|
const [query, setQuery] = useState('');
|
|
258
260
|
const [sort, setSort] = useState<MarketplaceSort>('relevance');
|
|
259
|
-
const [page, setPage] = useState(1);
|
|
260
261
|
const [installingItem, setInstallingItem] = useState<MarketplaceItemSummary | null>(null);
|
|
261
262
|
const [doctorTarget, setDoctorTarget] = useState<string | null>(null);
|
|
262
263
|
const [doctorResult, setDoctorResult] = useState<MarketplaceMcpDoctorResult | null>(null);
|
|
@@ -267,7 +268,6 @@ export function McpMarketplacePage() {
|
|
|
267
268
|
|
|
268
269
|
useEffect(() => {
|
|
269
270
|
const timer = window.setTimeout(() => {
|
|
270
|
-
setPage(1);
|
|
271
271
|
setQuery(searchText.trim());
|
|
272
272
|
}, 250);
|
|
273
273
|
return () => window.clearTimeout(timer);
|
|
@@ -276,11 +276,23 @@ export function McpMarketplacePage() {
|
|
|
276
276
|
const itemsQuery = useMcpMarketplaceItems({
|
|
277
277
|
q: query || undefined,
|
|
278
278
|
sort,
|
|
279
|
-
page,
|
|
280
279
|
pageSize: PAGE_SIZE
|
|
281
280
|
});
|
|
282
281
|
const installedQuery = useMcpMarketplaceInstalled();
|
|
283
282
|
|
|
283
|
+
const infiniteScroll = useInfiniteScrollLoader({
|
|
284
|
+
disabled: scope !== 'catalog' || itemsQuery.isError || !itemsQuery.hasNextPage || itemsQuery.isFetchingNextPage,
|
|
285
|
+
onLoadMore: () => itemsQuery.fetchNextPage(),
|
|
286
|
+
watchValue: `${scope}:${query}:${sort}:${itemsQuery.data?.loadedItems ?? 0}:${itemsQuery.data?.loadedPages ?? 0}`
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const container = infiniteScroll.containerRef.current;
|
|
291
|
+
if (container && typeof container.scrollTo === 'function') {
|
|
292
|
+
container.scrollTo({ top: 0 });
|
|
293
|
+
}
|
|
294
|
+
}, [infiniteScroll.containerRef, query, scope, sort]);
|
|
295
|
+
|
|
284
296
|
const installMutation = useInstallMcpMarketplaceItem();
|
|
285
297
|
const manageMutation = useManageMcpMarketplaceItem();
|
|
286
298
|
const doctorMutation = useMutation({
|
|
@@ -502,7 +514,11 @@ export function McpMarketplacePage() {
|
|
|
502
514
|
</span>
|
|
503
515
|
</div>
|
|
504
516
|
|
|
505
|
-
<div
|
|
517
|
+
<div
|
|
518
|
+
ref={infiniteScroll.containerRef}
|
|
519
|
+
className="min-h-0 flex-1 overflow-y-auto pr-1"
|
|
520
|
+
aria-busy={itemsQuery.isLoading || itemsQuery.isFetchingNextPage}
|
|
521
|
+
>
|
|
506
522
|
<div className={cn('grid grid-cols-1 gap-3 lg:grid-cols-2 2xl:grid-cols-3')}>
|
|
507
523
|
{scope === 'catalog' && itemsQuery.isLoading && Array.from({ length: 6 }, (_, index) => (
|
|
508
524
|
<div key={index} className="rounded-2xl border border-gray-200/70 bg-white p-4 shadow-sm">
|
|
@@ -533,30 +549,16 @@ export function McpMarketplacePage() {
|
|
|
533
549
|
{scope === 'installed' && installedRecords.length === 0 && (
|
|
534
550
|
<div className="py-8 text-center text-sm text-gray-500">{t('marketplaceNoInstalledMcp')}</div>
|
|
535
551
|
)}
|
|
536
|
-
</div>
|
|
537
|
-
</section>
|
|
538
552
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
{t('prev')}
|
|
547
|
-
</button>
|
|
548
|
-
<div className="min-w-20 text-center text-sm text-gray-600">
|
|
549
|
-
{itemsQuery.data?.totalPages ? `${page} / ${itemsQuery.data.totalPages}` : '0 / 0'}
|
|
550
|
-
</div>
|
|
551
|
-
<button
|
|
552
|
-
className="h-8 rounded-xl border border-gray-200/80 px-3 text-sm text-gray-600 disabled:opacity-40"
|
|
553
|
-
onClick={() => setPage((current) => current + 1)}
|
|
554
|
-
disabled={!itemsQuery.data?.totalPages || page >= itemsQuery.data.totalPages || itemsQuery.isFetching}
|
|
555
|
-
>
|
|
556
|
-
{t('next')}
|
|
557
|
-
</button>
|
|
553
|
+
{scope === 'catalog' && !itemsQuery.isError && (
|
|
554
|
+
<MarketplaceInfiniteScrollStatus
|
|
555
|
+
hasMore={Boolean(itemsQuery.hasNextPage)}
|
|
556
|
+
loading={itemsQuery.isFetchingNextPage}
|
|
557
|
+
sentinelRef={infiniteScroll.sentinelRef}
|
|
558
|
+
/>
|
|
559
|
+
)}
|
|
558
560
|
</div>
|
|
559
|
-
|
|
561
|
+
</section>
|
|
560
562
|
|
|
561
563
|
<InstallDialog
|
|
562
564
|
item={installingItem}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type DesktopUpdateStatus =
|
|
2
|
+
| 'idle'
|
|
3
|
+
| 'checking'
|
|
4
|
+
| 'update-available'
|
|
5
|
+
| 'downloading'
|
|
6
|
+
| 'downloaded'
|
|
7
|
+
| 'up-to-date'
|
|
8
|
+
| 'failed';
|
|
9
|
+
|
|
10
|
+
export type DesktopUpdatePreferences = {
|
|
11
|
+
automaticChecks: boolean;
|
|
12
|
+
autoDownload: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type DesktopUpdateSnapshot = {
|
|
16
|
+
status: DesktopUpdateStatus;
|
|
17
|
+
launcherVersion: string;
|
|
18
|
+
currentVersion: string | null;
|
|
19
|
+
availableVersion: string | null;
|
|
20
|
+
downloadedVersion: string | null;
|
|
21
|
+
releaseNotesUrl: string | null;
|
|
22
|
+
lastCheckedAt: string | null;
|
|
23
|
+
errorMessage: string | null;
|
|
24
|
+
preferences: DesktopUpdatePreferences;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type NextClawDesktopBridge = {
|
|
28
|
+
platform: string;
|
|
29
|
+
version: string;
|
|
30
|
+
getUpdateState: () => Promise<DesktopUpdateSnapshot>;
|
|
31
|
+
checkForUpdates: () => Promise<DesktopUpdateSnapshot>;
|
|
32
|
+
downloadUpdate: () => Promise<DesktopUpdateSnapshot>;
|
|
33
|
+
applyDownloadedUpdate: () => Promise<DesktopUpdateSnapshot>;
|
|
34
|
+
updatePreferences: (preferences: Partial<DesktopUpdatePreferences>) => Promise<DesktopUpdateSnapshot>;
|
|
35
|
+
onUpdateStateChanged: (listener: (snapshot: DesktopUpdateSnapshot) => void) => () => void;
|
|
36
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DesktopUpdatePreferences,
|
|
3
|
+
DesktopUpdateSnapshot,
|
|
4
|
+
NextClawDesktopBridge
|
|
5
|
+
} from '@/desktop/desktop-update.types';
|
|
6
|
+
import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
|
|
7
|
+
import { t } from '@/lib/i18n';
|
|
8
|
+
import { toast } from 'sonner';
|
|
9
|
+
|
|
10
|
+
type DesktopUpdateBusyAction = 'checking' | 'downloading' | 'applying' | 'saving-preferences';
|
|
11
|
+
|
|
12
|
+
export class DesktopUpdateManager {
|
|
13
|
+
private unsubscribe: (() => void) | null = null;
|
|
14
|
+
|
|
15
|
+
start = async () => {
|
|
16
|
+
const desktopApi = this.getDesktopApi();
|
|
17
|
+
if (!desktopApi) {
|
|
18
|
+
useDesktopUpdateStore.setState({
|
|
19
|
+
supported: false,
|
|
20
|
+
initialized: true,
|
|
21
|
+
snapshot: null
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!this.unsubscribe) {
|
|
27
|
+
this.unsubscribe = desktopApi.onUpdateStateChanged((snapshot) => {
|
|
28
|
+
useDesktopUpdateStore.setState({
|
|
29
|
+
supported: true,
|
|
30
|
+
initialized: true,
|
|
31
|
+
snapshot
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
useDesktopUpdateStore.setState({
|
|
37
|
+
supported: true,
|
|
38
|
+
initialized: false
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const snapshot = await desktopApi.getUpdateState();
|
|
43
|
+
useDesktopUpdateStore.setState({
|
|
44
|
+
supported: true,
|
|
45
|
+
initialized: true,
|
|
46
|
+
snapshot
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
useDesktopUpdateStore.setState({
|
|
50
|
+
supported: true,
|
|
51
|
+
initialized: true
|
|
52
|
+
});
|
|
53
|
+
toast.error(`${t('desktopUpdatesLoadFailed')}: ${this.getErrorMessage(error)}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
stop = () => {
|
|
58
|
+
this.unsubscribe?.();
|
|
59
|
+
this.unsubscribe = null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
checkForUpdates = async () => {
|
|
63
|
+
let snapshot: DesktopUpdateSnapshot;
|
|
64
|
+
try {
|
|
65
|
+
snapshot = await this.runSnapshotCommand('checking', t('desktopUpdatesCheckFailed'), async (desktopApi) => {
|
|
66
|
+
return await desktopApi.checkForUpdates();
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (snapshot.status === 'up-to-date') {
|
|
73
|
+
toast.success(t('desktopUpdatesAlreadyLatest'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (snapshot.status === 'update-available') {
|
|
77
|
+
toast.success(
|
|
78
|
+
t('desktopUpdatesAvailable').replace('{version}', snapshot.availableVersion ?? t('desktopUpdatesUnknownVersion'))
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (snapshot.status === 'downloaded') {
|
|
83
|
+
toast.success(t('desktopUpdatesReadyToApply'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (snapshot.status === 'failed' && snapshot.errorMessage) {
|
|
87
|
+
toast.error(snapshot.errorMessage);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
downloadUpdate = async () => {
|
|
92
|
+
let snapshot: DesktopUpdateSnapshot;
|
|
93
|
+
try {
|
|
94
|
+
snapshot = await this.runSnapshotCommand('downloading', t('desktopUpdatesDownloadFailed'), async (desktopApi) => {
|
|
95
|
+
return await desktopApi.downloadUpdate();
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (snapshot.status === 'downloaded') {
|
|
102
|
+
toast.success(t('desktopUpdatesReadyToApply'));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
applyDownloadedUpdate = async () => {
|
|
107
|
+
try {
|
|
108
|
+
await this.runSnapshotCommand('applying', t('desktopUpdatesApplyFailed'), async (desktopApi) => {
|
|
109
|
+
return await desktopApi.applyDownloadedUpdate();
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
updatePreferences = async (preferences: Partial<DesktopUpdatePreferences>) => {
|
|
117
|
+
try {
|
|
118
|
+
await this.runSnapshotCommand(
|
|
119
|
+
'saving-preferences',
|
|
120
|
+
t('desktopUpdatesPreferencesFailed'),
|
|
121
|
+
async (desktopApi) => await desktopApi.updatePreferences(preferences)
|
|
122
|
+
);
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
private runSnapshotCommand = async (
|
|
129
|
+
busyAction: DesktopUpdateBusyAction,
|
|
130
|
+
fallbackMessage: string,
|
|
131
|
+
job: (desktopApi: NextClawDesktopBridge) => Promise<DesktopUpdateSnapshot>
|
|
132
|
+
): Promise<DesktopUpdateSnapshot> => {
|
|
133
|
+
const desktopApi = this.getDesktopApi();
|
|
134
|
+
if (!desktopApi) {
|
|
135
|
+
throw new Error(t('desktopUpdatesDesktopOnlyDescription'));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
useDesktopUpdateStore.setState({ busyAction });
|
|
139
|
+
try {
|
|
140
|
+
const snapshot = await job(desktopApi);
|
|
141
|
+
useDesktopUpdateStore.setState({ snapshot });
|
|
142
|
+
return snapshot;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
toast.error(`${fallbackMessage}: ${this.getErrorMessage(error)}`);
|
|
145
|
+
throw error;
|
|
146
|
+
} finally {
|
|
147
|
+
useDesktopUpdateStore.setState({ busyAction: null });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
private getDesktopApi = (): NextClawDesktopBridge | null => {
|
|
152
|
+
if (typeof window === 'undefined') {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return window.nextclawDesktop ?? null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
private getErrorMessage = (error: unknown): string => {
|
|
159
|
+
return error instanceof Error ? error.message : t('error');
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const desktopUpdateManager = new DesktopUpdateManager();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DesktopUpdateSnapshot } from '@/desktop/desktop-update.types';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
|
|
4
|
+
type DesktopUpdateBusyAction = 'checking' | 'downloading' | 'applying' | 'saving-preferences' | null;
|
|
5
|
+
|
|
6
|
+
type DesktopUpdateStoreState = {
|
|
7
|
+
supported: boolean;
|
|
8
|
+
initialized: boolean;
|
|
9
|
+
busyAction: DesktopUpdateBusyAction;
|
|
10
|
+
snapshot: DesktopUpdateSnapshot | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const useDesktopUpdateStore = create<DesktopUpdateStoreState>(() => ({
|
|
14
|
+
supported: false,
|
|
15
|
+
initialized: false,
|
|
16
|
+
busyAction: null,
|
|
17
|
+
snapshot: null
|
|
18
|
+
}));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { InfiniteData } from '@tanstack/react-query';
|
|
2
|
+
import type { MarketplaceListView } from '@/api/types';
|
|
3
|
+
|
|
4
|
+
export type InfiniteMarketplaceListView = MarketplaceListView & {
|
|
5
|
+
pages: MarketplaceListView[];
|
|
6
|
+
loadedItems: number;
|
|
7
|
+
loadedPages: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function collapseMarketplaceListPages(
|
|
11
|
+
data: InfiniteData<MarketplaceListView> | undefined
|
|
12
|
+
): InfiniteMarketplaceListView | undefined {
|
|
13
|
+
if (!data || data.pages.length === 0) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const items = data.pages.flatMap((page) => page.items);
|
|
18
|
+
const lastPage = data.pages[data.pages.length - 1];
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
...lastPage,
|
|
22
|
+
items,
|
|
23
|
+
pages: data.pages,
|
|
24
|
+
loadedItems: items.length,
|
|
25
|
+
loadedPages: data.pages.length
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_THRESHOLD_PX = 160;
|
|
4
|
+
|
|
5
|
+
type UseInfiniteScrollLoaderParams = {
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
onLoadMore: () => Promise<unknown> | unknown;
|
|
8
|
+
thresholdPx?: number;
|
|
9
|
+
watchValue?: string | number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function useInfiniteScrollLoader(params: UseInfiniteScrollLoaderParams) {
|
|
13
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
14
|
+
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
|
15
|
+
const onLoadMoreRef = useRef(params.onLoadMore);
|
|
16
|
+
const loadingRef = useRef(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
onLoadMoreRef.current = params.onLoadMore;
|
|
20
|
+
}, [params.onLoadMore]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (params.disabled) {
|
|
24
|
+
loadingRef.current = false;
|
|
25
|
+
}
|
|
26
|
+
}, [params.disabled]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const container = containerRef.current;
|
|
30
|
+
const sentinel = sentinelRef.current;
|
|
31
|
+
const thresholdPx = params.thresholdPx ?? DEFAULT_THRESHOLD_PX;
|
|
32
|
+
|
|
33
|
+
if (params.disabled || !container || !sentinel) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const triggerLoadMore = () => {
|
|
38
|
+
if (loadingRef.current || params.disabled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
loadingRef.current = true;
|
|
43
|
+
Promise.resolve(onLoadMoreRef.current()).finally(() => {
|
|
44
|
+
loadingRef.current = false;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const maybeLoadMore = () => {
|
|
49
|
+
const remainingDistance = sentinel.getBoundingClientRect().top - container.getBoundingClientRect().bottom;
|
|
50
|
+
if (remainingDistance <= thresholdPx) {
|
|
51
|
+
triggerLoadMore();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (typeof IntersectionObserver === 'function') {
|
|
56
|
+
const observer = new IntersectionObserver(
|
|
57
|
+
(entries) => {
|
|
58
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
59
|
+
triggerLoadMore();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
root: container,
|
|
64
|
+
rootMargin: `0px 0px ${thresholdPx}px 0px`
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
observer.observe(sentinel);
|
|
69
|
+
maybeLoadMore();
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
observer.disconnect();
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
container.addEventListener('scroll', maybeLoadMore, { passive: true });
|
|
77
|
+
maybeLoadMore();
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
container.removeEventListener('scroll', maybeLoadMore);
|
|
81
|
+
};
|
|
82
|
+
}, [params.disabled, params.thresholdPx, params.watchValue]);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
containerRef,
|
|
86
|
+
sentinelRef
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
1
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
3
|
import { t } from '@/lib/i18n';
|
|
4
4
|
import {
|
|
@@ -20,11 +20,15 @@ import type {
|
|
|
20
20
|
MarketplaceItemType,
|
|
21
21
|
MarketplaceManageRequest
|
|
22
22
|
} from '@/api/types';
|
|
23
|
+
import { collapseMarketplaceListPages } from '@/hooks/marketplace-list-pages';
|
|
24
|
+
import { useMemo } from 'react';
|
|
23
25
|
|
|
24
26
|
export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
25
|
-
|
|
27
|
+
const query = useInfiniteQuery({
|
|
26
28
|
queryKey: ['marketplace-items', params],
|
|
27
|
-
|
|
29
|
+
initialPageParam: 1,
|
|
30
|
+
queryFn: ({ pageParam }) => fetchMarketplaceItems({ ...params, page: pageParam }),
|
|
31
|
+
getNextPageParam: (lastPage) => (lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined),
|
|
28
32
|
placeholderData: (previousData, previousQuery) => {
|
|
29
33
|
const previousParams = previousQuery?.queryKey?.[1];
|
|
30
34
|
if (!previousParams || typeof previousParams !== 'object' || previousParams === null) {
|
|
@@ -36,6 +40,13 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
|
36
40
|
},
|
|
37
41
|
staleTime: 15_000
|
|
38
42
|
});
|
|
43
|
+
|
|
44
|
+
const data = useMemo(() => collapseMarketplaceListPages(query.data), [query.data]);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...query,
|
|
48
|
+
data
|
|
49
|
+
};
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
export function useMarketplaceRecommendations(type: MarketplaceItemType, params: { scene?: string; limit?: number }) {
|