@nextclaw/ui 0.13.12 → 0.13.13
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 +19 -0
- package/dist/assets/{api-DmxQnm30.js → api-D0hARyyl.js} +1 -1
- package/dist/assets/app-presenter-provider-C7EsUuEJ.js +3 -0
- package/dist/assets/arrow-left-CzSkR5L6.js +1 -0
- package/dist/assets/book-open-DmeQPqkd.js +1 -0
- package/dist/assets/{channels-list-page-D-WYFmcG.js → channels-list-page-B0yiiH01.js} +2 -2
- package/dist/assets/chat-page-ChPjx_X9.js +105 -0
- package/dist/assets/{config-split-page-CdJEkWxv.js → config-split-page-BzfZSyAr.js} +1 -1
- package/dist/assets/{confirm-dialog-BWRNsRgb.js → confirm-dialog-CTcaBp-8.js} +1 -1
- package/dist/assets/desktop-update-config-CExpdkqD.js +1 -0
- package/dist/assets/{dist-CyWdaJvr.js → dist-C59BKqbX.js} +1 -1
- package/dist/assets/{dist-DzM2ZEVN.js → dist-Di6EHp2x.js} +1 -1
- package/dist/assets/{doc-browser-DewXa6oX.js → doc-browser-EZbnEcdk.js} +1 -1
- package/dist/assets/doc-browser-XIwe1Tg-.js +1 -0
- package/dist/assets/{doc-browser-context-CpL2m4Sp.js → doc-browser-context-BzEA1lm1.js} +1 -1
- package/dist/assets/{ellipsis-BDgTYNDX.js → ellipsis-hXUNwi63.js} +1 -1
- package/dist/assets/{es2015-WqekV0OK.js → es2015-CJlVSTh-.js} +1 -1
- package/dist/assets/{external-link-BLjo8z2a.js → external-link-J0fCK-vu.js} +1 -1
- package/dist/assets/host-capabilities-DjiRQ0Q9.js +1 -0
- package/dist/assets/index-DfZTO6Pi.js +101 -0
- package/dist/assets/index-mIb_YMrc.css +1 -0
- package/dist/assets/{key-round-Du8x3sWa.js → key-round-BzYZ0Nno.js} +1 -1
- package/dist/assets/loader-circle-BbdIlBbQ.js +1 -0
- package/dist/assets/mcp-marketplace-page-L22aCzHX.js +40 -0
- package/dist/assets/mcp-marketplace-page-f80Jmv37.js +1 -0
- package/dist/assets/model-config-DkD7vSQT.js +1 -0
- package/dist/assets/{notice-card-3a7FmAz-.js → notice-card-CD9Ok62G.js} +1 -1
- package/dist/assets/play-DHuSeKs8.js +1 -0
- package/dist/assets/plus-DrV685KC.js +1 -0
- package/dist/assets/{popover-BlJLguiZ.js → popover-CoBu3mbV.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-IMD4W0jD.js → provider-scoped-model-input-DZlmTCJU.js} +1 -1
- package/dist/assets/providers-list-CVkWiqIy.js +1 -0
- package/dist/assets/{react-BVjl1ZaK.js → react-C7HsxuKH.js} +1 -1
- package/dist/assets/{refresh-cw-DFR87zei.js → refresh-cw-CVo4nWpq.js} +1 -1
- package/dist/assets/remote-Bznw9iji.js +1 -0
- package/dist/assets/rotate-cw-CXDpRO6D.js +1 -0
- package/dist/assets/{runtime-config-page-DzbTVebD.js → runtime-config-page-5IycbIxs.js} +1 -1
- package/dist/assets/{save-BDmIlHvN.js → save-Z1QTovIt.js} +1 -1
- package/dist/assets/search-8nY6PGm0.js +1 -0
- package/dist/assets/{search-config-CJ6OPxAf.js → search-config-DEH80WkC.js} +1 -1
- package/dist/assets/{secrets-config-CYOHWL7P.js → secrets-config-Gzo5nDJG.js} +1 -1
- package/dist/assets/{status-dot-SVPjIyG5.js → status-dot-C1-FKgvv.js} +1 -1
- package/dist/assets/{tabs-Czd2mj6z.js → tabs-BqkKwx1R.js} +1 -1
- package/dist/assets/{tabs-custom-Ix4nkp2h.js → tabs-custom-DOkaJZmt.js} +1 -1
- package/dist/assets/{tag-chip-Cdwb3JSH.js → tag-chip-3kpiMTST.js} +1 -1
- package/dist/assets/{tooltip-CKGxLGnG.js → tooltip-Bw-mXQAM.js} +1 -1
- package/dist/assets/{trash-2-yLA8BEcE.js → trash-2-CmIwAJIT.js} +1 -1
- package/dist/assets/x-CSRTOZnM.js +1 -0
- package/dist/index.html +27 -25
- package/package.json +8 -8
- package/src/features/account/components/account-panel.tsx +2 -2
- package/src/features/account/managers/account.manager.ts +6 -5
- package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +45 -13
- package/src/features/chat/components/chat-session-workspace-panel.tsx +17 -8
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +69 -0
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +3 -0
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
- package/src/features/chat/managers/chat-session-list.manager.test.ts +4 -0
- package/src/features/chat/managers/chat-session-list.manager.ts +2 -0
- package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +153 -25
- package/src/features/chat/managers/ncp-chat-thread.manager.ts +153 -7
- package/src/features/chat/stores/chat-thread.store.test.ts +148 -0
- package/src/features/chat/stores/chat-thread.store.ts +287 -9
- package/src/features/marketplace/components/marketplace-catalog-grid.tsx +19 -1
- package/src/features/marketplace/components/marketplace-external-skill-source-action.tsx +14 -0
- package/src/features/marketplace/components/marketplace-page-parts.tsx +10 -2
- package/src/features/marketplace/components/marketplace-page.test.tsx +37 -1
- package/src/features/marketplace/components/marketplace-page.tsx +38 -16
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +1 -0
- package/src/features/marketplace/hooks/use-marketplace.test.tsx +109 -0
- package/src/features/marketplace/hooks/use-marketplace.ts +3 -5
- package/src/features/side-dock/components/side-dock.test.tsx +25 -0
- package/src/features/side-dock/components/side-dock.tsx +13 -0
- package/src/features/system-status/components/desktop-update-config.tsx +2 -1
- package/src/platforms/desktop/types/desktop-update.types.ts +9 -0
- package/src/shared/components/config/provider-form.tsx +2 -2
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +39 -32
- package/src/shared/components/ui/action-link.tsx +23 -1
- package/src/shared/lib/host-capabilities/host-capability.manager.test.ts +50 -0
- package/src/shared/lib/host-capabilities/host-capability.manager.ts +62 -0
- package/src/shared/lib/host-capabilities/index.ts +1 -0
- package/src/shared/lib/i18n/README.md +16 -3
- package/src/shared/lib/i18n/index.ts +93 -426
- package/src/shared/lib/i18n/locales/en-US/agents.json +49 -0
- package/src/shared/lib/i18n/locales/en-US/channel-auth.json +51 -0
- package/src/shared/lib/i18n/locales/en-US/channels.json +50 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +181 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +310 -0
- package/src/shared/lib/i18n/locales/en-US/cron.json +32 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +90 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +87 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +72 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +12 -0
- package/src/shared/lib/i18n/locales/en-US/pwa.json +26 -0
- package/src/shared/lib/i18n/locales/en-US/remote.json +168 -0
- package/src/shared/lib/i18n/locales/en-US/runtime-control.json +105 -0
- package/src/shared/lib/i18n/locales/en-US/search.json +28 -0
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +49 -0
- package/src/shared/lib/i18n/locales/zh-CN/channel-auth.json +51 -0
- package/src/shared/lib/i18n/locales/zh-CN/channels.json +50 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +181 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +310 -0
- package/src/shared/lib/i18n/locales/zh-CN/cron.json +32 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +90 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +87 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +72 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +12 -0
- package/src/shared/lib/i18n/locales/zh-CN/pwa.json +26 -0
- package/src/shared/lib/i18n/locales/zh-CN/remote.json +168 -0
- package/src/shared/lib/i18n/locales/zh-CN/runtime-control.json +105 -0
- package/src/shared/lib/i18n/locales/zh-CN/search.json +28 -0
- package/src/shared/lib/navigation-history/index.ts +1 -0
- package/src/shared/lib/navigation-history/navigation-history.utils.test.ts +60 -0
- package/src/shared/lib/navigation-history/navigation-history.utils.ts +105 -0
- package/dist/assets/app-presenter-provider-CDSjiqfE.js +0 -3
- package/dist/assets/arrow-left-Dd45Mvr1.js +0 -1
- package/dist/assets/chat-page-D1X8YNTC.js +0 -105
- package/dist/assets/desktop-update-config-DOi5R1fX.js +0 -1
- package/dist/assets/doc-browser-D5FJcAyz.js +0 -1
- package/dist/assets/index-Q891JiF_.css +0 -1
- package/dist/assets/index-ZDJP_uJc.js +0 -101
- package/dist/assets/loader-circle-Dz4FssYC.js +0 -1
- package/dist/assets/mcp-marketplace-page-LmMhKH5a.js +0 -1
- package/dist/assets/mcp-marketplace-page-RqISBSqO.js +0 -40
- package/dist/assets/model-config-JUsW1Khb.js +0 -1
- package/dist/assets/play-BziM-Y34.js +0 -1
- package/dist/assets/plus-DrpyArC8.js +0 -1
- package/dist/assets/providers-list-BsDpxNzy.js +0 -1
- package/dist/assets/remote-DjhVMpVd.js +0 -1
- package/dist/assets/rotate-cw-DMBnsYKm.js +0 -1
- package/dist/assets/search-CjHGYvTI.js +0 -1
- package/dist/assets/x-dpDISboO.js +0 -1
- package/src/shared/lib/i18n/agents.ts +0 -85
- package/src/shared/lib/i18n/channel-auth.constants.ts +0 -87
- package/src/shared/lib/i18n/channels.ts +0 -53
- package/src/shared/lib/i18n/chat-labels.utils.ts +0 -206
- package/src/shared/lib/i18n/cron-labels.utils.ts +0 -32
- package/src/shared/lib/i18n/desktop-update-labels.utils.ts +0 -165
- package/src/shared/lib/i18n/marketplace-labels.utils.ts +0 -78
- package/src/shared/lib/i18n/pwa.ts +0 -62
- package/src/shared/lib/i18n/remote.ts +0 -273
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +0 -120
- package/src/shared/lib/i18n/runtime/i18n.path-picker.ts +0 -12
- package/src/shared/lib/i18n/runtime-control-labels.utils.ts +0 -180
- package/src/shared/lib/i18n/search.ts +0 -37
- /package/dist/assets/{config-hints-Ceiol9x4.js → config-hints-fGnUjDe9.js} +0 -0
- /package/dist/assets/{provider-models-C_yOh6DE.js → provider-models-C9VRroK3.js} +0 -0
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
MarketplaceCuratedSceneView,
|
|
43
43
|
MarketplaceCuratedShelves,
|
|
44
44
|
} from "@/features/marketplace/components/curated-shelves/marketplace-curated-shelves";
|
|
45
|
+
import { MarketplaceExternalSkillSourceAction } from "@/features/marketplace/components/marketplace-external-skill-source-action";
|
|
45
46
|
import { useMarketplaceCuratedSceneRoute } from "@/features/marketplace/hooks/use-marketplace-curated-scene-route";
|
|
46
47
|
import { t } from "@/shared/lib/i18n";
|
|
47
48
|
import { PageLayout } from "@/app/components/layout/page-layout";
|
|
@@ -73,6 +74,8 @@ function getMarketplaceCopyKeys() {
|
|
|
73
74
|
emptyData: "marketplaceNoSkills",
|
|
74
75
|
emptyInstalled: "marketplaceNoInstalledSkills",
|
|
75
76
|
installedCountSuffix: "marketplaceInstalledSkillsCountSuffix",
|
|
77
|
+
allSkills: "marketplaceAllSkills",
|
|
78
|
+
refreshingResults: "marketplaceRefreshingResults",
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -201,6 +204,12 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
201
204
|
};
|
|
202
205
|
const showListSkeleton =
|
|
203
206
|
skeletonState.showCatalog || skeletonState.showInstalled;
|
|
207
|
+
const isRefreshingList =
|
|
208
|
+
scope === "all" &&
|
|
209
|
+
!showListSkeleton &&
|
|
210
|
+
Boolean(itemsQuery.data) &&
|
|
211
|
+
itemsQuery.isFetching &&
|
|
212
|
+
!itemsQuery.isFetchingNextPage;
|
|
204
213
|
|
|
205
214
|
const listSummary = useMemo(() => {
|
|
206
215
|
if (scope === "installed") {
|
|
@@ -424,23 +433,27 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
424
433
|
<PageLayout className="flex h-full min-h-0 flex-col pb-0">
|
|
425
434
|
{!curatedSceneRoute.isSceneRoute && (
|
|
426
435
|
<>
|
|
427
|
-
<
|
|
428
|
-
|
|
429
|
-
{
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
436
|
+
<div className="mb-3 flex items-start justify-between gap-3 border-b border-gray-200/60">
|
|
437
|
+
<Tabs
|
|
438
|
+
tabs={[
|
|
439
|
+
{ id: "all", label: t(copyKeys.tabMarketplace) },
|
|
440
|
+
{
|
|
441
|
+
id: "installed",
|
|
442
|
+
label: t(copyKeys.tabInstalled),
|
|
443
|
+
count: installedQuery.data?.total ?? 0,
|
|
444
|
+
},
|
|
445
|
+
]}
|
|
446
|
+
activeTab={scope}
|
|
447
|
+
onChange={(value) => setScope(value as ScopeType)}
|
|
448
|
+
className="mb-0 flex-1 border-b-0"
|
|
449
|
+
/>
|
|
450
|
+
<MarketplaceExternalSkillSourceAction />
|
|
451
|
+
</div>
|
|
440
452
|
|
|
441
453
|
<FilterPanel
|
|
442
454
|
scope={scope}
|
|
443
455
|
searchText={searchText}
|
|
456
|
+
isRefreshing={isRefreshingList}
|
|
444
457
|
searchPlaceholder={t(copyKeys.searchPlaceholder)}
|
|
445
458
|
sort={sort}
|
|
446
459
|
onSearchTextChange={setSearchText}
|
|
@@ -457,9 +470,16 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
457
470
|
<h3 className="text-[14px] font-semibold text-gray-900">
|
|
458
471
|
{scope === "installed"
|
|
459
472
|
? t(copyKeys.sectionInstalled)
|
|
460
|
-
:
|
|
473
|
+
: t(copyKeys.allSkills)}
|
|
461
474
|
</h3>
|
|
462
|
-
<
|
|
475
|
+
<div className="flex items-center gap-2 text-[12px] text-gray-500">
|
|
476
|
+
{isRefreshingList && (
|
|
477
|
+
<span className="font-medium text-primary">
|
|
478
|
+
{t(copyKeys.refreshingResults)}
|
|
479
|
+
</span>
|
|
480
|
+
)}
|
|
481
|
+
<span>{listSummary}</span>
|
|
482
|
+
</div>
|
|
463
483
|
</div>
|
|
464
484
|
)}
|
|
465
485
|
|
|
@@ -515,11 +535,13 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
515
535
|
title={
|
|
516
536
|
scope === "installed"
|
|
517
537
|
? t(copyKeys.sectionInstalled)
|
|
518
|
-
:
|
|
538
|
+
: t(copyKeys.allSkills)
|
|
519
539
|
}
|
|
520
540
|
summary={listSummary}
|
|
541
|
+
refreshingLabel={t(copyKeys.refreshingResults)}
|
|
521
542
|
showTitle={curatedSceneRoute.showShelves}
|
|
522
543
|
showListSkeleton={showListSkeleton}
|
|
544
|
+
isRefreshing={isRefreshingList}
|
|
523
545
|
skeletonCardCount={SKELETON_CARD_COUNT}
|
|
524
546
|
allItems={allItems}
|
|
525
547
|
installedEntries={installedEntries}
|
|
@@ -288,6 +288,7 @@ export function McpMarketplacePage() {
|
|
|
288
288
|
<FilterPanel
|
|
289
289
|
scope={scope === "catalog" ? "all" : "installed"}
|
|
290
290
|
searchText={searchText}
|
|
291
|
+
isRefreshing={false}
|
|
291
292
|
searchPlaceholder={t("marketplaceMcpSearchPlaceholder")}
|
|
292
293
|
sort={sort}
|
|
293
294
|
onSearchTextChange={setSearchText}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { useMarketplaceItems } from "@/features/marketplace/hooks/use-marketplace";
|
|
5
|
+
import type { MarketplaceListView } from "@/shared/lib/api";
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
fetchMarketplaceItems: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@/shared/lib/api", async () => {
|
|
12
|
+
const actual = await vi.importActual("@/shared/lib/api");
|
|
13
|
+
return {
|
|
14
|
+
...(actual as object),
|
|
15
|
+
fetchMarketplaceItems: mocks.fetchMarketplaceItems,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function createMarketplaceList(name: string): MarketplaceListView {
|
|
20
|
+
return {
|
|
21
|
+
total: 1,
|
|
22
|
+
page: 1,
|
|
23
|
+
pageSize: 20,
|
|
24
|
+
totalPages: 1,
|
|
25
|
+
sort: "relevance",
|
|
26
|
+
items: [
|
|
27
|
+
{
|
|
28
|
+
id: name,
|
|
29
|
+
slug: name,
|
|
30
|
+
type: "skill",
|
|
31
|
+
name,
|
|
32
|
+
summary: name,
|
|
33
|
+
summaryI18n: { en: name },
|
|
34
|
+
tags: [],
|
|
35
|
+
author: "NextClaw",
|
|
36
|
+
install: {
|
|
37
|
+
kind: "marketplace",
|
|
38
|
+
spec: `@nextclaw/${name}`,
|
|
39
|
+
command: `nextclaw skills install @nextclaw/${name}`,
|
|
40
|
+
},
|
|
41
|
+
updatedAt: "2026-06-08T00:00:00.000Z",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createWrapper() {
|
|
48
|
+
const queryClient = new QueryClient({
|
|
49
|
+
defaultOptions: {
|
|
50
|
+
queries: {
|
|
51
|
+
retry: false,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
57
|
+
return (
|
|
58
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("useMarketplaceItems", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
mocks.fetchMarketplaceItems.mockReset();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps previous marketplace items while search results refresh", async () => {
|
|
69
|
+
let resolveSearch:
|
|
70
|
+
| ((value: MarketplaceListView) => void)
|
|
71
|
+
| undefined;
|
|
72
|
+
mocks.fetchMarketplaceItems
|
|
73
|
+
.mockResolvedValueOnce(createMarketplaceList("Initial Skill"))
|
|
74
|
+
.mockReturnValueOnce(
|
|
75
|
+
new Promise<MarketplaceListView>((resolve) => {
|
|
76
|
+
resolveSearch = resolve;
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const { result, rerender } = renderHook(
|
|
81
|
+
({ q }: { q?: string }) =>
|
|
82
|
+
useMarketplaceItems({
|
|
83
|
+
type: "skill",
|
|
84
|
+
q,
|
|
85
|
+
sort: "relevance",
|
|
86
|
+
pageSize: 20,
|
|
87
|
+
}),
|
|
88
|
+
{
|
|
89
|
+
initialProps: {},
|
|
90
|
+
wrapper: createWrapper(),
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await waitFor(() => {
|
|
95
|
+
expect(result.current.data?.items[0]?.name).toBe("Initial Skill");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
rerender({ q: "search" });
|
|
99
|
+
|
|
100
|
+
expect(result.current.data?.items[0]?.name).toBe("Initial Skill");
|
|
101
|
+
expect(result.current.isFetching).toBe(true);
|
|
102
|
+
|
|
103
|
+
resolveSearch?.(createMarketplaceList("Search Skill"));
|
|
104
|
+
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(result.current.data?.items[0]?.name).toBe("Search Skill");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -38,7 +38,7 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const previousType = 'type' in previousParams ? previousParams.type : undefined;
|
|
41
|
-
return previousType === params.type &&
|
|
41
|
+
return previousType === params.type && hasSameMarketplaceListSurface(
|
|
42
42
|
previousParams as MarketplaceListParams,
|
|
43
43
|
params
|
|
44
44
|
)
|
|
@@ -56,14 +56,12 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function
|
|
59
|
+
function hasSameMarketplaceListSurface(
|
|
60
60
|
previousParams: MarketplaceListParams,
|
|
61
61
|
nextParams: MarketplaceListParams
|
|
62
62
|
): boolean {
|
|
63
|
-
return previousParams.
|
|
64
|
-
&& previousParams.tag === nextParams.tag
|
|
63
|
+
return previousParams.tag === nextParams.tag
|
|
65
64
|
&& previousParams.scene === nextParams.scene
|
|
66
|
-
&& previousParams.sort === nextParams.sort
|
|
67
65
|
&& previousParams.pageSize === nextParams.pageSize;
|
|
68
66
|
}
|
|
69
67
|
|
|
@@ -110,4 +110,29 @@ describe('SideDock', () => {
|
|
|
110
110
|
|
|
111
111
|
expect(screen.getByText('D')).toBeTruthy();
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
it('renders emoji icons as visual dock icons instead of small text labels', () => {
|
|
115
|
+
const manager = {
|
|
116
|
+
openItem: vi.fn(),
|
|
117
|
+
unpinItem: vi.fn(),
|
|
118
|
+
} as unknown as SideDockManager;
|
|
119
|
+
const docBrowserManager = new DocBrowserManager();
|
|
120
|
+
useSideDockStore.getState().setPinnedItems([
|
|
121
|
+
{
|
|
122
|
+
createdAt: '2026-06-02T00:00:00.000Z',
|
|
123
|
+
icon: { type: 'text', value: '🎨' },
|
|
124
|
+
id: 'pinned-panel-app-palette',
|
|
125
|
+
label: 'Palette App',
|
|
126
|
+
target: { type: 'right-panel-resource', uri: 'nextclaw://panel-app/palette' },
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
render(
|
|
131
|
+
<DocBrowserProvider manager={docBrowserManager}>
|
|
132
|
+
<SideDock manager={manager} />
|
|
133
|
+
</DocBrowserProvider>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(screen.getByText('🎨').className).toContain('text-[20px]');
|
|
137
|
+
});
|
|
113
138
|
});
|
|
@@ -34,11 +34,24 @@ const SIDE_DOCK_ICON_COMPONENTS: Record<SideDockIconName, LucideIcon> = {
|
|
|
34
34
|
'service-apps': AppWindow,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const SIDE_DOCK_EMOJI_ICON_PATTERN = /^\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?)*$/u;
|
|
38
|
+
|
|
39
|
+
function isSideDockEmojiIcon(value: string): boolean {
|
|
40
|
+
return SIDE_DOCK_EMOJI_ICON_PATTERN.test(value.trim());
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
function SideDockItemIconView({ icon }: { icon: SideDockItemIcon }) {
|
|
38
44
|
if (icon.type === 'url') {
|
|
39
45
|
return <img src={icon.url} alt="" className="h-5 w-5 rounded object-cover" />;
|
|
40
46
|
}
|
|
41
47
|
if (icon.type === 'text') {
|
|
48
|
+
if (isSideDockEmojiIcon(icon.value)) {
|
|
49
|
+
return (
|
|
50
|
+
<span className="flex h-7 w-7 items-center justify-center text-[20px] leading-none" aria-hidden="true">
|
|
51
|
+
{icon.value}
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
42
55
|
return (
|
|
43
56
|
<span className="max-w-7 truncate text-center text-[13px] font-semibold leading-none" aria-hidden="true">
|
|
44
57
|
{icon.value}
|
|
@@ -8,6 +8,7 @@ import { Switch } from '@/shared/components/ui/switch';
|
|
|
8
8
|
import { PageHeader, PageLayout } from '@/app/components/layout/page-layout';
|
|
9
9
|
import { formatDateTime, t } from '@/shared/lib/i18n';
|
|
10
10
|
import { cn } from '@/shared/lib/utils';
|
|
11
|
+
import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
|
|
11
12
|
import { Download, ExternalLink, RefreshCw, RotateCw } from 'lucide-react';
|
|
12
13
|
|
|
13
14
|
const STATUS_LABEL_KEYS: Record<string, string> = {
|
|
@@ -249,7 +250,7 @@ export function DesktopUpdateConfig() {
|
|
|
249
250
|
<RotateCw className={cn('mr-2 h-4 w-4', isApplying && 'animate-spin')} />
|
|
250
251
|
{t('runtimeUpdatesApplyNow')}
|
|
251
252
|
</Button>
|
|
252
|
-
{snapshot.releaseNotesUrl ? <Button variant="ghost" onClick={() =>
|
|
253
|
+
{snapshot.releaseNotesUrl ? <Button variant="ghost" onClick={() => void hostCapabilityManager.openExternalUrl(snapshot.releaseNotesUrl ?? '')}><ExternalLink className="mr-2 h-4 w-4" />{t('desktopUpdatesReleaseNotes')}</Button> : null}
|
|
253
254
|
</CardContent>
|
|
254
255
|
</Card>
|
|
255
256
|
</PageLayout>
|
|
@@ -52,6 +52,14 @@ export type DesktopWindowControlAction = 'minimize' | 'toggle-maximize' | 'close
|
|
|
52
52
|
|
|
53
53
|
export type DesktopWindowStateSnapshot = { isMaximized: boolean };
|
|
54
54
|
|
|
55
|
+
export type DesktopOpenExternalUrlResult =
|
|
56
|
+
| { opened: true }
|
|
57
|
+
| { opened: false; reason: 'unsupported-url' | 'popup-blocked' | 'bridge-failed' };
|
|
58
|
+
|
|
59
|
+
export type DesktopHostBridge = {
|
|
60
|
+
openExternalUrl: (url: string) => Promise<DesktopOpenExternalUrlResult>;
|
|
61
|
+
};
|
|
62
|
+
|
|
55
63
|
export type NextClawDesktopBridge = {
|
|
56
64
|
platform: string;
|
|
57
65
|
version: string;
|
|
@@ -69,6 +77,7 @@ export type NextClawDesktopBridge = {
|
|
|
69
77
|
setLocalePreference?: (language: DesktopUiLanguagePreference | null) => Promise<DesktopUiLanguagePreference | null>;
|
|
70
78
|
controlWindow?: (action: DesktopWindowControlAction) => Promise<void>;
|
|
71
79
|
getWindowState?: () => Promise<DesktopWindowStateSnapshot>;
|
|
80
|
+
host?: DesktopHostBridge;
|
|
72
81
|
onWindowStateChanged?: (listener: (snapshot: DesktopWindowStateSnapshot) => void) => () => void;
|
|
73
82
|
onUpdateStateChanged: (listener: (snapshot: DesktopUpdateSnapshot) => void) => () => void;
|
|
74
83
|
};
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
import { ProviderModelsSection } from './provider-models-section';
|
|
52
52
|
import type { PillSelectOption } from '@/shared/components/provider-pill-selector';
|
|
53
53
|
import { ProviderStatusBadge } from '@/shared/components/provider-status-badge';
|
|
54
|
-
|
|
54
|
+
import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
|
|
55
55
|
type ProviderFormProps = {
|
|
56
56
|
providerName?: string;
|
|
57
57
|
onProviderDeleted?: (providerName: string) => void;
|
|
@@ -488,7 +488,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
488
488
|
}
|
|
489
489
|
setAuthSessionId(result.sessionId);
|
|
490
490
|
setAuthStatusMessage(`${t('providerAuthOpenPrompt')}${result.userCode}${t('providerAuthOpenPromptSuffix')}`);
|
|
491
|
-
|
|
491
|
+
await hostCapabilityManager.openExternalUrl(result.verificationUri);
|
|
492
492
|
scheduleProviderAuthPoll(result.sessionId, result.intervalMs);
|
|
493
493
|
} catch (error) {
|
|
494
494
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -19,6 +19,11 @@ import {
|
|
|
19
19
|
} from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
|
|
20
20
|
import { DefaultDocBrowserRouteResolver } from '@/shared/components/doc-browser/utils/doc-browser-route-resolver.utils';
|
|
21
21
|
import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
|
|
22
|
+
import {
|
|
23
|
+
filterNavigationHistoryEntries,
|
|
24
|
+
pushNavigationHistoryEntry,
|
|
25
|
+
stepNavigationHistory,
|
|
26
|
+
} from '@/shared/lib/navigation-history';
|
|
22
27
|
|
|
23
28
|
const defaultDocBrowserRouteResolver = new DefaultDocBrowserRouteResolver();
|
|
24
29
|
|
|
@@ -74,15 +79,15 @@ function pushActiveHistory(
|
|
|
74
79
|
state: DocBrowserState,
|
|
75
80
|
entry: DocBrowserActiveHistoryEntry,
|
|
76
81
|
): DocBrowserState {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
const activeHistory = pushNavigationHistoryEntry(
|
|
83
|
+
{ entries: state.activeHistory, index: state.activeHistoryIndex },
|
|
84
|
+
entry,
|
|
85
|
+
(current, next) => areActiveHistoryEntriesEquivalent(routeResolver, current, next),
|
|
86
|
+
);
|
|
82
87
|
return {
|
|
83
88
|
...state,
|
|
84
|
-
activeHistory,
|
|
85
|
-
activeHistoryIndex: activeHistory.
|
|
89
|
+
activeHistory: [...activeHistory.entries],
|
|
90
|
+
activeHistoryIndex: activeHistory.index,
|
|
86
91
|
};
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -366,29 +371,35 @@ export class DocBrowserManager {
|
|
|
366
371
|
|
|
367
372
|
readonly goBack = (): void => {
|
|
368
373
|
this.setSnapshot((prev) => {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (!entry) {
|
|
374
|
+
const step = stepNavigationHistory(
|
|
375
|
+
{ entries: prev.activeHistory, index: prev.activeHistoryIndex },
|
|
376
|
+
'back',
|
|
377
|
+
);
|
|
378
|
+
if (!step) {
|
|
375
379
|
return prev;
|
|
376
380
|
}
|
|
377
|
-
return restoreActiveHistoryEntry(this.routeResolver, {
|
|
381
|
+
return restoreActiveHistoryEntry(this.routeResolver, {
|
|
382
|
+
...prev,
|
|
383
|
+
activeHistory: [...step.history.entries],
|
|
384
|
+
activeHistoryIndex: step.history.index,
|
|
385
|
+
}, step.entry);
|
|
378
386
|
});
|
|
379
387
|
};
|
|
380
388
|
|
|
381
389
|
readonly goForward = (): void => {
|
|
382
390
|
this.setSnapshot((prev) => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (!entry) {
|
|
391
|
+
const step = stepNavigationHistory(
|
|
392
|
+
{ entries: prev.activeHistory, index: prev.activeHistoryIndex },
|
|
393
|
+
'forward',
|
|
394
|
+
);
|
|
395
|
+
if (!step) {
|
|
389
396
|
return prev;
|
|
390
397
|
}
|
|
391
|
-
return restoreActiveHistoryEntry(this.routeResolver, {
|
|
398
|
+
return restoreActiveHistoryEntry(this.routeResolver, {
|
|
399
|
+
...prev,
|
|
400
|
+
activeHistory: [...step.history.entries],
|
|
401
|
+
activeHistoryIndex: step.history.index,
|
|
402
|
+
}, step.entry);
|
|
392
403
|
});
|
|
393
404
|
};
|
|
394
405
|
|
|
@@ -417,20 +428,16 @@ export class DocBrowserManager {
|
|
|
417
428
|
tabs: nextTabs,
|
|
418
429
|
activeTabId: nextActiveId,
|
|
419
430
|
};
|
|
420
|
-
const activeHistory = nextState.activeHistory.filter((entry) => entry.tabId !== tabId);
|
|
421
|
-
const activeHistoryBeforeIndex = nextState.activeHistory
|
|
422
|
-
.slice(0, nextState.activeHistoryIndex + 1)
|
|
423
|
-
.filter((entry) => entry.tabId !== tabId);
|
|
424
431
|
const nextActiveTab = nextTabs.find((tab) => tab.id === nextActiveId) ?? nextTabs[0];
|
|
432
|
+
const activeHistory = filterNavigationHistoryEntries(
|
|
433
|
+
{ entries: nextState.activeHistory, index: nextState.activeHistoryIndex },
|
|
434
|
+
(entry) => entry.tabId !== tabId,
|
|
435
|
+
createDocBrowserActiveHistoryEntry(nextActiveTab),
|
|
436
|
+
);
|
|
425
437
|
const reconciledState = {
|
|
426
438
|
...nextState,
|
|
427
|
-
activeHistory: activeHistory.
|
|
428
|
-
|
|
429
|
-
: [createDocBrowserActiveHistoryEntry(nextActiveTab)],
|
|
430
|
-
activeHistoryIndex: Math.min(
|
|
431
|
-
Math.max(0, activeHistoryBeforeIndex.length - 1),
|
|
432
|
-
Math.max(0, activeHistory.length - 1),
|
|
433
|
-
),
|
|
439
|
+
activeHistory: [...activeHistory.entries],
|
|
440
|
+
activeHistoryIndex: activeHistory.index,
|
|
434
441
|
};
|
|
435
442
|
return prev.activeTabId === tabId
|
|
436
443
|
? pushActiveHistory(this.routeResolver, reconciledState, createDocBrowserActiveHistoryEntry(nextActiveTab))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cn } from '@/shared/lib/utils';
|
|
2
|
-
import { ArrowRight } from 'lucide-react';
|
|
2
|
+
import { ArrowRight, ExternalLink } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
interface ActionLinkProps {
|
|
5
5
|
label: string;
|
|
@@ -7,6 +7,12 @@ interface ActionLinkProps {
|
|
|
7
7
|
onClick?: () => void;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
interface ExternalActionLinkProps {
|
|
11
|
+
label: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
onClick: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* Unified action link with arrow indicator.
|
|
12
18
|
* Used in card footers for "Configure →", "Enable →", etc.
|
|
@@ -25,3 +31,19 @@ export function ActionLink({ label, className, onClick }: ActionLinkProps) {
|
|
|
25
31
|
</span>
|
|
26
32
|
);
|
|
27
33
|
}
|
|
34
|
+
|
|
35
|
+
export function ExternalActionLink({ label, className, onClick }: ExternalActionLinkProps) {
|
|
36
|
+
return (
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className={cn(
|
|
40
|
+
'inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md px-1.5 text-[13px] font-medium text-primary transition-colors hover:text-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
onClick={onClick}
|
|
44
|
+
>
|
|
45
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
46
|
+
{label}
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { HostCapabilityManager } from "@/shared/lib/host-capabilities";
|
|
2
|
+
|
|
3
|
+
describe("HostCapabilityManager", () => {
|
|
4
|
+
it("uses the desktop host bridge when available", async () => {
|
|
5
|
+
const openExternalUrl = vi.fn(async () => ({ opened: true as const }));
|
|
6
|
+
const manager = new HostCapabilityManager(() => ({
|
|
7
|
+
open: vi.fn(),
|
|
8
|
+
nextclawDesktop: {
|
|
9
|
+
host: { openExternalUrl }
|
|
10
|
+
}
|
|
11
|
+
} as unknown as Window));
|
|
12
|
+
|
|
13
|
+
await expect(manager.openExternalUrl("https://skillhub.cn")).resolves.toEqual({ opened: true });
|
|
14
|
+
expect(openExternalUrl).toHaveBeenCalledWith("https://skillhub.cn/");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("falls back to window.open outside the desktop host", async () => {
|
|
18
|
+
const open = vi.fn(() => ({ closed: false }));
|
|
19
|
+
const manager = new HostCapabilityManager(() => ({
|
|
20
|
+
open
|
|
21
|
+
} as unknown as Window));
|
|
22
|
+
|
|
23
|
+
await expect(manager.openExternalUrl("https://skillhub.cn")).resolves.toEqual({ opened: true });
|
|
24
|
+
expect(open).toHaveBeenCalledWith("https://skillhub.cn/", "_blank", "noopener,noreferrer");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("reports unsupported URLs without opening them", async () => {
|
|
28
|
+
const open = vi.fn();
|
|
29
|
+
const manager = new HostCapabilityManager(() => ({
|
|
30
|
+
open
|
|
31
|
+
} as unknown as Window));
|
|
32
|
+
|
|
33
|
+
await expect(manager.openExternalUrl("javascript:alert(1)")).resolves.toEqual({
|
|
34
|
+
opened: false,
|
|
35
|
+
reason: "unsupported-url"
|
|
36
|
+
});
|
|
37
|
+
expect(open).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reports popup-blocked when the web fallback cannot open a window", async () => {
|
|
41
|
+
const manager = new HostCapabilityManager(() => ({
|
|
42
|
+
open: vi.fn(() => null)
|
|
43
|
+
} as unknown as Window));
|
|
44
|
+
|
|
45
|
+
await expect(manager.openExternalUrl("https://skillhub.cn")).resolves.toEqual({
|
|
46
|
+
opened: false,
|
|
47
|
+
reason: "popup-blocked"
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type OpenExternalUrlFailureReason =
|
|
2
|
+
| "unsupported-url"
|
|
3
|
+
| "popup-blocked"
|
|
4
|
+
| "bridge-failed";
|
|
5
|
+
|
|
6
|
+
export type OpenExternalUrlResult =
|
|
7
|
+
| { opened: true }
|
|
8
|
+
| { opened: false; reason: OpenExternalUrlFailureReason };
|
|
9
|
+
|
|
10
|
+
type HostWindow = Pick<Window, "open"> & {
|
|
11
|
+
nextclawDesktop?: {
|
|
12
|
+
host?: {
|
|
13
|
+
openExternalUrl?: (url: string) => Promise<OpenExternalUrlResult>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class HostCapabilityManager {
|
|
19
|
+
constructor(private readonly getWindow = (): HostWindow | null => {
|
|
20
|
+
return typeof window === "undefined" ? null : window;
|
|
21
|
+
}) {}
|
|
22
|
+
|
|
23
|
+
openExternalUrl = async (rawUrl: string): Promise<OpenExternalUrlResult> => {
|
|
24
|
+
const url = normalizeExternalHttpUrl(rawUrl);
|
|
25
|
+
if (!url) {
|
|
26
|
+
return { opened: false, reason: "unsupported-url" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const hostWindow = this.getWindow();
|
|
30
|
+
if (!hostWindow) {
|
|
31
|
+
return { opened: false, reason: "bridge-failed" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const desktopOpen = hostWindow.nextclawDesktop?.host?.openExternalUrl;
|
|
36
|
+
if (desktopOpen) {
|
|
37
|
+
return await desktopOpen(url);
|
|
38
|
+
}
|
|
39
|
+
const openedWindow = hostWindow.open(url, "_blank", "noopener,noreferrer");
|
|
40
|
+
if (!openedWindow) {
|
|
41
|
+
return { opened: false, reason: "popup-blocked" };
|
|
42
|
+
}
|
|
43
|
+
return { opened: true };
|
|
44
|
+
} catch {
|
|
45
|
+
return { opened: false, reason: "bridge-failed" };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const hostCapabilityManager = new HostCapabilityManager();
|
|
51
|
+
|
|
52
|
+
function normalizeExternalHttpUrl(rawUrl: string): string | null {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(rawUrl);
|
|
55
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return url.toString();
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './host-capability.manager';
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
# i18n owner
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This module owns the NextClaw UI language runtime and locale catalogs.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 文案维护方式
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- User-visible labels live in `locales/<locale>/*.json`.
|
|
8
|
+
- `index.ts` only owns catalog assembly, formatting helpers, and the stable `t(key)` API.
|
|
9
|
+
- Keep current flat keys when moving existing labels; semantic nested keys can migrate by domain during future feature work.
|
|
10
|
+
- When adding or editing copy, update both `zh-CN` and `en-US`.
|
|
11
|
+
|
|
12
|
+
## 验证
|
|
13
|
+
|
|
14
|
+
Run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node scripts/smoke/i18n/nextclaw-ui-i18n-check.mjs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The check verifies that both locales have the same JSON files, the same message keys, and matching `{placeholder}` names.
|