@nextclaw/ui 0.13.4 → 0.13.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 +128 -0
- package/dist/assets/api-BCDb13Uv.js +8 -0
- package/dist/assets/app-presenter-provider-C-QMjK7m.js +3 -0
- package/dist/assets/arrow-left-rIXFA2Zy.js +1 -0
- package/dist/assets/channels-list-page-DnqwsKGe.js +8 -0
- package/dist/assets/chat-page-CsQx20xL.js +105 -0
- package/dist/assets/config-split-page-BwhGqmUS.js +1 -0
- package/dist/assets/confirm-dialog-DBkKcQ1q.js +5 -0
- package/dist/assets/desktop-update-config-BE9KjiXi.js +1 -0
- package/dist/assets/{dist-CpwDZDtv.js → dist-BI0bX8jv.js} +1 -1
- package/dist/assets/dist-C8JRt-tf.js +1 -0
- package/dist/assets/doc-browser-context-VHUOIUXL.js +1 -0
- package/dist/assets/doc-browser-eqdNEerh.js +1 -0
- package/dist/assets/doc-browser-lpF_pyx4.js +1 -0
- package/dist/assets/ellipsis-C9VB2CxT.js +1 -0
- package/dist/assets/es2015-BCw3Q7nY.js +41 -0
- package/dist/assets/external-link-Bp7_4s9t.js +1 -0
- package/dist/assets/index-BCq7kbC8.css +1 -0
- package/dist/assets/index-DTdPDqto.js +101 -0
- package/dist/assets/key-round-Ccy6oND3.js +1 -0
- package/dist/assets/loader-circle-OpH4EErP.js +1 -0
- package/dist/assets/{mcp-marketplace-page-Dvi8mCBd.js → mcp-marketplace-page-B-GxKOqS.js} +2 -2
- package/dist/assets/mcp-marketplace-page-Dm-0-w51.js +1 -0
- package/dist/assets/model-config-BXPnV_7n.js +1 -0
- package/dist/assets/notice-card-Dsbkl7u7.js +1 -0
- package/dist/assets/play-CvgYhNbo.js +1 -0
- package/dist/assets/plus-DHcP-K1U.js +1 -0
- package/dist/assets/popover-BTLZE5Iz.js +1 -0
- package/dist/assets/provider-models-C_yOh6DE.js +1 -0
- package/dist/assets/provider-scoped-model-input-DKI9KN7N.js +1 -0
- package/dist/assets/providers-list-Bko6B7dE.js +1 -0
- package/dist/assets/react-BqS8oMyM.js +8 -0
- package/dist/assets/refresh-cw-BIaj7txb.js +1 -0
- package/dist/assets/remote-DHn_b8QU.js +1 -0
- package/dist/assets/rotate-cw-DMqSkRJQ.js +1 -0
- package/dist/assets/runtime-config-page-DX_wSK1J.js +1 -0
- package/dist/assets/save-BEoORten.js +1 -0
- package/dist/assets/search-COFh_SFI.js +1 -0
- package/dist/assets/{search-config-G2O1fcRb.js → search-config-DA5By-tE.js} +1 -1
- package/dist/assets/{secrets-config-TenzxYbE.js → secrets-config-eDxG7iN4.js} +2 -2
- package/dist/assets/{status-dot-BSMG7ZaC.js → status-dot-oWBjTovo.js} +1 -1
- package/dist/assets/{tabs-D1CjpWNX.js → tabs-CGaY0BVO.js} +1 -1
- package/dist/assets/{tabs-custom-CRSN1ekg.js → tabs-custom-D2fPYb4G.js} +1 -1
- package/dist/assets/{tag-chip-DEashiw6.js → tag-chip-TVkgofS1.js} +1 -1
- package/dist/assets/tooltip-BwHPKBv9.js +1 -0
- package/dist/assets/trash-2-D3psAGrz.js +1 -0
- package/dist/assets/x-CHAxzyp2.js +1 -0
- package/dist/index.html +25 -23
- package/dist/logos/mimo.svg +9 -0
- package/package.json +8 -8
- package/public/logos/mimo.svg +9 -0
- package/src/app/components/layout/app-layout.tsx +10 -2
- package/src/features/agents/components/agents-page.tsx +6 -4
- package/src/features/chat/components/chat-welcome.test.tsx +15 -0
- package/src/features/chat/components/chat-welcome.tsx +4 -7
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +17 -10
- package/src/features/marketplace/components/marketplace-list-card.tsx +22 -1
- package/src/features/marketplace/components/marketplace-page.tsx +3 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +3 -1
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +8 -0
- package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +70 -6
- package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +25 -1
- package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +1 -1
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +42 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +22 -0
- package/src/features/service-apps/components/service-apps-panel.test.tsx +88 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +246 -56
- package/src/features/service-apps/hooks/use-service-apps.ts +14 -0
- package/src/features/side-dock/components/side-dock.test.tsx +58 -3
- package/src/features/side-dock/components/side-dock.tsx +77 -24
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +37 -34
- package/src/features/side-dock/index.ts +2 -0
- package/src/features/side-dock/managers/side-dock.manager.test.ts +84 -3
- package/src/features/side-dock/managers/side-dock.manager.ts +135 -12
- package/src/features/side-dock/types/side-dock.types.ts +11 -4
- package/src/features/side-dock/utils/side-dock-item.utils.test.ts +8 -8
- package/src/features/side-dock/utils/side-dock-item.utils.ts +13 -5
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
- package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
- package/src/shared/components/config/provider-form-support.test.ts +21 -0
- package/src/shared/components/config/provider-form-support.ts +38 -78
- package/src/shared/components/config/provider-form.tsx +54 -67
- package/src/shared/components/config/providers-list.tsx +211 -73
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +73 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +184 -98
- package/src/shared/components/doc-browser/doc-browser.test.tsx +168 -43
- package/src/shared/components/doc-browser/doc-browser.tsx +38 -7
- package/src/shared/components/doc-browser/index.ts +1 -0
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +30 -6
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +29 -0
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +23 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +7 -0
- package/src/shared/components/model-config.tsx +5 -4
- package/src/shared/components/provider-auth-section.tsx +2 -2
- package/src/shared/components/ui/switch.tsx +5 -2
- package/src/shared/hooks/use-config.ts +27 -6
- package/src/shared/lib/api/marketplace.types.ts +193 -0
- package/src/shared/lib/api/types.ts +23 -200
- package/src/shared/lib/api/utils/config.utils.ts +17 -7
- package/src/shared/lib/i18n/index.ts +10 -12
- package/src/shared/lib/i18n/{marketplace.ts → marketplace-labels.utils.ts} +3 -0
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +46 -2
- package/src/shared/lib/provider-models/index.test.ts +93 -15
- package/src/shared/lib/provider-models/index.ts +35 -20
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
- package/dist/assets/api-DruAcori.js +0 -15
- package/dist/assets/app-presenter-provider-DIcnNB9s.js +0 -3
- package/dist/assets/book-open-CRotBOF2.js +0 -1
- package/dist/assets/boxes-BvA-I63l.js +0 -1
- package/dist/assets/channels-list-page-wK3l7p5R.js +0 -8
- package/dist/assets/chat-page-DgehvdJT.js +0 -105
- package/dist/assets/config-split-page-BnTO1mAM.js +0 -1
- package/dist/assets/confirm-dialog-CGrKHbAW.js +0 -5
- package/dist/assets/createLucideIcon-Br1EFMEr.js +0 -1
- package/dist/assets/desktop-update-config-CehWxSSg.js +0 -1
- package/dist/assets/dist-rzakR_bu.js +0 -41
- package/dist/assets/doc-browser-BgxmVsTx.js +0 -1
- package/dist/assets/doc-browser-CZn5-Qbu.js +0 -1
- package/dist/assets/doc-browser-context-B8gk7HMr.js +0 -1
- package/dist/assets/external-link-DHjFdCpy.js +0 -1
- package/dist/assets/index-DORmh0Cy.css +0 -1
- package/dist/assets/index-DcFpvnge.js +0 -101
- package/dist/assets/key-round-D41Oe35x.js +0 -1
- package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +0 -1
- package/dist/assets/model-config-B-EJXGtO.js +0 -1
- package/dist/assets/play-C-0vo7hi.js +0 -1
- package/dist/assets/plus-Cr_akLp2.js +0 -1
- package/dist/assets/popover-YieElO8c.js +0 -1
- package/dist/assets/provider-scoped-model-input-BPRNGqYn.js +0 -1
- package/dist/assets/providers-list-CtBpL1ZY.js +0 -1
- package/dist/assets/react-B2X_ph0a.js +0 -1
- package/dist/assets/remote-BGHbHoHf.js +0 -1
- package/dist/assets/rotate-cw-jueVCNJQ.js +0 -1
- package/dist/assets/runtime-config-page-C1Hv7mad.js +0 -1
- package/dist/assets/save-aYdSdK97.js +0 -1
- package/dist/assets/search-CdhgFNwf.js +0 -1
- package/dist/assets/setting-row-BNu0qhOM.js +0 -1
- package/dist/assets/trash-2-DSWTsARW.js +0 -1
- package/dist/assets/x-D1v_vb54.js +0 -1
- package/src/shared/components/provider-enabled-field.tsx +0 -23
- /package/dist/assets/{config-hints-A7G_jMTU.js → config-hints-Ceiol9x4.js} +0 -0
|
@@ -19,6 +19,7 @@ import { cn } from "@/shared/lib/utils";
|
|
|
19
19
|
import {
|
|
20
20
|
CheckCircle2,
|
|
21
21
|
Download,
|
|
22
|
+
RefreshCw,
|
|
22
23
|
Trash2,
|
|
23
24
|
} from "lucide-react";
|
|
24
25
|
|
|
@@ -35,6 +36,7 @@ type MarketplaceListCardActionProps = {
|
|
|
35
36
|
record?: MarketplaceInstalledRecord;
|
|
36
37
|
isInstalling: boolean;
|
|
37
38
|
isDisabled: boolean;
|
|
39
|
+
canUpdate: boolean;
|
|
38
40
|
canUninstall: boolean;
|
|
39
41
|
busyAction?: MarketplaceManageAction;
|
|
40
42
|
busyForRecord: boolean;
|
|
@@ -96,6 +98,7 @@ function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
|
|
|
96
98
|
record,
|
|
97
99
|
isInstalling,
|
|
98
100
|
isDisabled,
|
|
101
|
+
canUpdate,
|
|
99
102
|
canUninstall,
|
|
100
103
|
busyAction,
|
|
101
104
|
busyForRecord,
|
|
@@ -103,7 +106,7 @@ function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
|
|
|
103
106
|
onInstall,
|
|
104
107
|
onManage,
|
|
105
108
|
} = props;
|
|
106
|
-
const hasActions = Boolean((item && !record) || (record && canUninstall));
|
|
109
|
+
const hasActions = Boolean((item && !record) || (record && (canUpdate || canUninstall)));
|
|
107
110
|
|
|
108
111
|
return (
|
|
109
112
|
<div
|
|
@@ -148,6 +151,22 @@ function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
|
|
|
148
151
|
</button>
|
|
149
152
|
)}
|
|
150
153
|
|
|
154
|
+
{record && canUpdate && (
|
|
155
|
+
<button
|
|
156
|
+
disabled={busyForRecord}
|
|
157
|
+
onClick={(event) => {
|
|
158
|
+
event.stopPropagation();
|
|
159
|
+
onManage("update", record);
|
|
160
|
+
}}
|
|
161
|
+
className="inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-xl border border-blue-200/80 bg-white px-3 text-xs font-medium text-blue-600 transition-colors hover:border-blue-300 hover:bg-blue-50 disabled:opacity-50"
|
|
162
|
+
>
|
|
163
|
+
<RefreshCw className="h-3.5 w-3.5" />
|
|
164
|
+
{busyAction === "update"
|
|
165
|
+
? t("marketplaceUpdating")
|
|
166
|
+
: t("marketplaceUpdate")}
|
|
167
|
+
</button>
|
|
168
|
+
)}
|
|
169
|
+
|
|
151
170
|
{record && canUninstall && (
|
|
152
171
|
<button
|
|
153
172
|
disabled={busyForRecord}
|
|
@@ -241,6 +260,7 @@ export function MarketplaceListCard(props: {
|
|
|
241
260
|
: undefined;
|
|
242
261
|
const busyForRecord = Boolean(busyAction);
|
|
243
262
|
const canUninstall = record?.type === "skill" && record.source === "workspace";
|
|
263
|
+
const canUpdate = record?.type === "skill" && record.origin === "marketplace";
|
|
244
264
|
const isDisabled = record
|
|
245
265
|
? record.enabled === false || record.runtimeStatus === "disabled"
|
|
246
266
|
: false;
|
|
@@ -269,6 +289,7 @@ export function MarketplaceListCard(props: {
|
|
|
269
289
|
record={record}
|
|
270
290
|
isInstalling={isInstalling}
|
|
271
291
|
isDisabled={isDisabled}
|
|
292
|
+
canUpdate={canUpdate}
|
|
272
293
|
canUninstall={canUninstall}
|
|
273
294
|
busyAction={busyAction}
|
|
274
295
|
busyForRecord={busyForRecord}
|
|
@@ -282,7 +282,9 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
|
|
|
282
282
|
action: MarketplaceManageAction,
|
|
283
283
|
record: MarketplaceInstalledRecord,
|
|
284
284
|
) => {
|
|
285
|
-
const targetId =
|
|
285
|
+
const targetId = action === "update"
|
|
286
|
+
? record.catalogSlug || record.id || record.spec
|
|
287
|
+
: record.id || record.spec;
|
|
286
288
|
if (!targetId) {
|
|
287
289
|
return;
|
|
288
290
|
}
|
|
@@ -178,7 +178,9 @@ export function useManageMarketplaceItem() {
|
|
|
178
178
|
? t('marketplaceEnableSuccess')
|
|
179
179
|
: result.action === 'disable'
|
|
180
180
|
? t('marketplaceDisableSuccess')
|
|
181
|
-
:
|
|
181
|
+
: result.action === 'update'
|
|
182
|
+
? t('marketplaceUpdateSuccess')
|
|
183
|
+
: t('marketplaceUninstallSuccess');
|
|
182
184
|
toast.success(result.message || fallback);
|
|
183
185
|
},
|
|
184
186
|
onError: (error: Error) => {
|
|
@@ -133,6 +133,14 @@ export function applyManageResultToInstalledView(params: {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
if (params.result.action === 'update') {
|
|
137
|
+
return {
|
|
138
|
+
...record,
|
|
139
|
+
installedAt: new Date().toISOString(),
|
|
140
|
+
origin: record.origin ?? 'marketplace'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
136
144
|
return {
|
|
137
145
|
...record,
|
|
138
146
|
enabled: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t } from '@/shared/lib/i18n';
|
|
2
|
-
import type
|
|
2
|
+
import { parseResourceUri, type ParsedResourceUri, type ResourceUriRouteDefinition } from '@/shared/lib/resource-uri';
|
|
3
3
|
import {
|
|
4
4
|
getDefaultDocsUrl,
|
|
5
5
|
inferTabTitle,
|
|
@@ -32,9 +32,67 @@ function resolveNextclawDocsUrl(uri: ParsedResourceUri): string {
|
|
|
32
32
|
return normalizeUrlByKind(rawPath, 'docs');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function createDocsResourceUriFromSegments(pathSegments: string[]): string {
|
|
36
|
+
const path = pathSegments
|
|
37
|
+
.filter((segment) => segment.trim().length > 0)
|
|
38
|
+
.map((segment) => encodeURIComponent(decodeURIComponent(segment)))
|
|
39
|
+
.join('/');
|
|
40
|
+
return path ? `nextclaw://docs/${path}` : 'nextclaw://docs';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createDocsResourceUriFromUrl(url: string): string {
|
|
44
|
+
const normalized = normalizeDocUrl(url);
|
|
45
|
+
const pathname = normalized.startsWith('/')
|
|
46
|
+
? normalized
|
|
47
|
+
: (() => {
|
|
48
|
+
try {
|
|
49
|
+
return new URL(url).pathname;
|
|
50
|
+
} catch {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
if (!pathname) {
|
|
55
|
+
return 'nextclaw://docs';
|
|
56
|
+
}
|
|
57
|
+
return createDocsResourceUriFromSegments(pathname.split('/'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getPanelAppId(uri: ParsedResourceUri): string {
|
|
61
|
+
if (uri.scheme === 'nextclaw' && uri.authority === 'panel-app') {
|
|
62
|
+
return uri.pathSegments[0] ?? '';
|
|
63
|
+
}
|
|
64
|
+
const [apiSegment, collectionSegment, appId, contentSegment] = uri.pathSegments;
|
|
65
|
+
return apiSegment === 'api'
|
|
66
|
+
&& collectionSegment === 'panel-apps'
|
|
67
|
+
&& appId !== undefined
|
|
68
|
+
&& appId.length > 0
|
|
69
|
+
&& contentSegment === 'content'
|
|
70
|
+
? appId
|
|
71
|
+
: '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isPanelAppContentUri(uri: ParsedResourceUri): boolean {
|
|
75
|
+
return getPanelAppId(uri).length > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
35
78
|
function resolvePanelAppPlaceholderUrl(uri: ParsedResourceUri): string {
|
|
36
|
-
const appId = uri
|
|
37
|
-
return appId ?
|
|
79
|
+
const appId = getPanelAppId(uri);
|
|
80
|
+
return appId ? `/api/panel-apps/${encodeURIComponent(decodeURIComponent(appId))}/content` : 'nextclaw://panel-app';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createPanelAppResourceUri(uri: ParsedResourceUri): string {
|
|
84
|
+
const appId = getPanelAppId(uri);
|
|
85
|
+
return appId ? `nextclaw://panel-app/${encodeURIComponent(decodeURIComponent(appId))}` : 'nextclaw://panel-app';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function arePanelAppUrlsEquivalent(left: string, right: string): boolean {
|
|
89
|
+
const leftUri = parseResourceUri(left);
|
|
90
|
+
const rightUri = parseResourceUri(right);
|
|
91
|
+
const leftAppId = getPanelAppId(leftUri);
|
|
92
|
+
const rightAppId = getPanelAppId(rightUri);
|
|
93
|
+
return leftAppId.length > 0 && rightAppId.length > 0
|
|
94
|
+
? decodeURIComponent(leftAppId) === decodeURIComponent(rightAppId)
|
|
95
|
+
: left === right;
|
|
38
96
|
}
|
|
39
97
|
|
|
40
98
|
export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefinition[] = [
|
|
@@ -46,6 +104,7 @@ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefi
|
|
|
46
104
|
resolve: () => ({
|
|
47
105
|
historyPolicy: 'managed',
|
|
48
106
|
kind: RIGHT_PANEL_HOME_TAB_KIND,
|
|
107
|
+
resourceUri: RIGHT_PANEL_HOME_URL,
|
|
49
108
|
title: t('docBrowserHomeTitle'),
|
|
50
109
|
url: RIGHT_PANEL_HOME_URL,
|
|
51
110
|
}),
|
|
@@ -61,6 +120,7 @@ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefi
|
|
|
61
120
|
return {
|
|
62
121
|
historyPolicy: 'managed',
|
|
63
122
|
kind: 'docs',
|
|
123
|
+
resourceUri: createDocsResourceUriFromUrl(url),
|
|
64
124
|
title: t('docBrowserHelp'),
|
|
65
125
|
url,
|
|
66
126
|
};
|
|
@@ -77,6 +137,7 @@ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefi
|
|
|
77
137
|
return {
|
|
78
138
|
historyPolicy: 'managed',
|
|
79
139
|
kind: 'docs',
|
|
140
|
+
resourceUri: createDocsResourceUriFromSegments(uri.pathSegments),
|
|
80
141
|
title: t('docBrowserHelp'),
|
|
81
142
|
url,
|
|
82
143
|
};
|
|
@@ -94,6 +155,7 @@ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefi
|
|
|
94
155
|
dedupeKey: 'apps',
|
|
95
156
|
historyPolicy: 'managed',
|
|
96
157
|
kind: RIGHT_PANEL_APPS_TAB_KIND,
|
|
158
|
+
resourceUri: url,
|
|
97
159
|
title: url === RIGHT_PANEL_APPS_URL ? t('appsTitle') : t('serviceAppsTitle'),
|
|
98
160
|
url,
|
|
99
161
|
};
|
|
@@ -104,19 +166,20 @@ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefi
|
|
|
104
166
|
defaultUrl: () => 'nextclaw://panel-app',
|
|
105
167
|
id: 'panel-app',
|
|
106
168
|
kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
|
|
107
|
-
match: (uri) => uri.scheme === 'nextclaw' && uri.authority === 'panel-app',
|
|
169
|
+
match: (uri) => (uri.scheme === 'nextclaw' && uri.authority === 'panel-app') || isPanelAppContentUri(uri),
|
|
108
170
|
resolve: (uri) => {
|
|
109
171
|
const url = resolvePanelAppPlaceholderUrl(uri);
|
|
110
|
-
const appId = uri
|
|
172
|
+
const appId = getPanelAppId(uri);
|
|
111
173
|
return {
|
|
112
174
|
dedupeKey: appId ? `panel-app:${decodeURIComponent(appId)}` : undefined,
|
|
113
175
|
historyPolicy: 'managed',
|
|
114
176
|
kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
|
|
177
|
+
resourceUri: createPanelAppResourceUri(uri),
|
|
115
178
|
title: t('panelAppsTitle'),
|
|
116
179
|
url,
|
|
117
180
|
};
|
|
118
181
|
},
|
|
119
|
-
areEquivalent:
|
|
182
|
+
areEquivalent: arePanelAppUrlsEquivalent,
|
|
120
183
|
},
|
|
121
184
|
{
|
|
122
185
|
defaultUrl: () => 'about:blank',
|
|
@@ -126,6 +189,7 @@ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefi
|
|
|
126
189
|
resolve: (uri) => ({
|
|
127
190
|
historyPolicy: 'managed',
|
|
128
191
|
kind: 'content',
|
|
192
|
+
resourceUri: uri.raw || 'about:blank',
|
|
129
193
|
title: inferTabTitle(uri.raw, 'content', 'Detail'),
|
|
130
194
|
url: uri.raw || 'about:blank',
|
|
131
195
|
}),
|
package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts
CHANGED
|
@@ -8,11 +8,13 @@ describe('RightPanelResourceRouteResolver', () => {
|
|
|
8
8
|
expect(resolver.resolve('nextclaw://apps')).toMatchObject({
|
|
9
9
|
dedupeKey: 'apps',
|
|
10
10
|
kind: 'apps',
|
|
11
|
+
resourceUri: 'nextclaw://apps',
|
|
11
12
|
url: 'nextclaw://apps',
|
|
12
13
|
});
|
|
13
14
|
expect(resolver.resolve('nextclaw://apps?tab=service-apps')).toMatchObject({
|
|
14
15
|
dedupeKey: 'apps',
|
|
15
16
|
kind: 'apps',
|
|
17
|
+
resourceUri: 'nextclaw://apps?tab=service-apps',
|
|
16
18
|
url: 'nextclaw://apps?tab=service-apps',
|
|
17
19
|
});
|
|
18
20
|
});
|
|
@@ -22,6 +24,7 @@ describe('RightPanelResourceRouteResolver', () => {
|
|
|
22
24
|
const target = resolver.resolve('nextclaw://docs/guide/getting-started');
|
|
23
25
|
|
|
24
26
|
expect(target.kind).toBe('docs');
|
|
27
|
+
expect(target.resourceUri).toBe('nextclaw://docs/guide/getting-started');
|
|
25
28
|
expect(target.title).toBe('Help Docs');
|
|
26
29
|
expect(target.url).toContain('/guide/getting-started');
|
|
27
30
|
});
|
|
@@ -32,7 +35,28 @@ describe('RightPanelResourceRouteResolver', () => {
|
|
|
32
35
|
expect(resolver.resolve('nextclaw://panel-app/timer')).toMatchObject({
|
|
33
36
|
dedupeKey: 'panel-app:timer',
|
|
34
37
|
kind: 'panel-app',
|
|
35
|
-
|
|
38
|
+
resourceUri: 'nextclaw://panel-app/timer',
|
|
39
|
+
url: '/api/panel-apps/timer/content',
|
|
36
40
|
});
|
|
37
41
|
});
|
|
42
|
+
|
|
43
|
+
it('keeps panel app content URLs idempotent under panel app kind', () => {
|
|
44
|
+
const resolver = new RightPanelResourceRouteResolver();
|
|
45
|
+
|
|
46
|
+
expect(resolver.resolveOpenTarget({
|
|
47
|
+
kind: 'panel-app',
|
|
48
|
+
url: '/api/panel-apps/timer/content',
|
|
49
|
+
})).toMatchObject({
|
|
50
|
+
dedupeKey: 'panel-app:timer',
|
|
51
|
+
kind: 'panel-app',
|
|
52
|
+
resourceUri: 'nextclaw://panel-app/timer',
|
|
53
|
+
url: '/api/panel-apps/timer/content',
|
|
54
|
+
});
|
|
55
|
+
expect(resolver.areUrlsEquivalent(
|
|
56
|
+
'nextclaw://panel-app/timer',
|
|
57
|
+
'/api/panel-apps/timer/content',
|
|
58
|
+
'panel-app',
|
|
59
|
+
'panel-app',
|
|
60
|
+
)).toBe(true);
|
|
61
|
+
});
|
|
38
62
|
});
|
package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts
CHANGED
|
@@ -21,7 +21,7 @@ function inferFallbackKind(url?: string): DocBrowserTabKind {
|
|
|
21
21
|
export class RightPanelResourceRouteResolver implements DocBrowserRouteResolver {
|
|
22
22
|
private readonly resourceUriResolver = new ResourceUriResolver<RightPanelResourceTarget>(
|
|
23
23
|
RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS_FOR_RESOURCE_URI,
|
|
24
|
-
{ getNormalizedUri: (target) => target.url },
|
|
24
|
+
{ getNormalizedUri: (target) => target.resourceUri ?? target.url },
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
resolve = (uri: string): RightPanelResourceTarget => this.resourceUriResolver.resolve(uri);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
3
|
+
import { createPanelAppRightPanelResourceTarget } from '@/features/right-panel-resources/utils/right-panel-resource-uri.utils';
|
|
4
|
+
|
|
5
|
+
function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
|
|
6
|
+
return {
|
|
7
|
+
contentPath: '/api/panel-apps/demo/content',
|
|
8
|
+
createdAt: '2026-06-02T00:00:00.000Z',
|
|
9
|
+
favorite: false,
|
|
10
|
+
fileName: 'demo.html',
|
|
11
|
+
id: 'demo',
|
|
12
|
+
openCount: 0,
|
|
13
|
+
sizeBytes: 1024,
|
|
14
|
+
title: 'Demo App',
|
|
15
|
+
updatedAt: '2026-06-02T00:00:00.000Z',
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('right-panel-resource uri utils', () => {
|
|
21
|
+
it('keeps panel app image icons as dock image icons', () => {
|
|
22
|
+
const target = createPanelAppRightPanelResourceTarget(createPanelAppEntry({
|
|
23
|
+
icon: '/api/panel-apps/demo/assets/icon.png',
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
expect(target.dockIcon).toEqual({
|
|
27
|
+
type: 'url',
|
|
28
|
+
url: '/api/panel-apps/demo/assets/icon.png',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('keeps panel app text icons as dock text icons', () => {
|
|
33
|
+
const target = createPanelAppRightPanelResourceTarget(createPanelAppEntry({
|
|
34
|
+
icon: '🧭',
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
expect(target.dockIcon).toEqual({
|
|
38
|
+
type: 'text',
|
|
39
|
+
value: '🧭',
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
2
|
+
import type { DocBrowserDockIcon } from '@/shared/components/doc-browser';
|
|
2
3
|
import type { RightPanelResourceTarget } from '@/features/right-panel-resources/types/right-panel-resource.types';
|
|
3
4
|
|
|
4
5
|
export const RIGHT_PANEL_HOME_TAB_KIND = 'home';
|
|
@@ -37,11 +38,32 @@ export function createPanelAppResourceUri(appId: string): string {
|
|
|
37
38
|
return `nextclaw://panel-app/${encodeURIComponent(appId)}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function isPanelAppImageIcon(icon: string): boolean {
|
|
42
|
+
return (
|
|
43
|
+
icon.startsWith('data:image/') ||
|
|
44
|
+
icon.startsWith('http://') ||
|
|
45
|
+
icon.startsWith('https://') ||
|
|
46
|
+
icon.startsWith('/')
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createPanelAppDockIcon(entry: PanelAppEntryView): DocBrowserDockIcon | undefined {
|
|
51
|
+
const icon = entry.icon?.trim();
|
|
52
|
+
if (!icon) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return isPanelAppImageIcon(icon)
|
|
56
|
+
? { type: 'url', url: icon }
|
|
57
|
+
: { type: 'text', value: icon };
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
export function createPanelAppRightPanelResourceTarget(entry: PanelAppEntryView): RightPanelResourceTarget {
|
|
41
61
|
return {
|
|
42
62
|
dedupeKey: `panel-app:${entry.id}`,
|
|
63
|
+
dockIcon: createPanelAppDockIcon(entry),
|
|
43
64
|
historyPolicy: 'managed',
|
|
44
65
|
kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
|
|
66
|
+
resourceUri: createPanelAppResourceUri(entry.id),
|
|
45
67
|
title: entry.title,
|
|
46
68
|
url: entry.contentPath,
|
|
47
69
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ServiceAppsPanel } from '@/features/service-apps/components/service-apps-panel';
|
|
5
|
+
|
|
6
|
+
const deleteServiceAppMutate = vi.fn();
|
|
7
|
+
const discoverServiceAppActionsMutateAsync = vi.fn(async () => ({ actions: [] }));
|
|
8
|
+
const refetchServiceActions = vi.fn();
|
|
9
|
+
const refetchServiceActionGrants = vi.fn();
|
|
10
|
+
const refetchServiceApps = vi.fn();
|
|
11
|
+
const restartServiceAppMutate = vi.fn();
|
|
12
|
+
const revokeServiceActionGrantMutate = vi.fn();
|
|
13
|
+
|
|
14
|
+
vi.mock('@/features/service-apps/hooks/use-service-apps', () => ({
|
|
15
|
+
useDeleteServiceApp: () => ({
|
|
16
|
+
isPending: false,
|
|
17
|
+
mutate: deleteServiceAppMutate,
|
|
18
|
+
}),
|
|
19
|
+
useDiscoverServiceAppActions: () => ({
|
|
20
|
+
isPending: false,
|
|
21
|
+
mutateAsync: discoverServiceAppActionsMutateAsync,
|
|
22
|
+
}),
|
|
23
|
+
useRestartServiceApp: () => ({
|
|
24
|
+
mutate: restartServiceAppMutate,
|
|
25
|
+
}),
|
|
26
|
+
useRevokeServiceActionGrant: () => ({
|
|
27
|
+
mutate: revokeServiceActionGrantMutate,
|
|
28
|
+
}),
|
|
29
|
+
useServiceActionGrants: () => ({
|
|
30
|
+
data: { grants: [] },
|
|
31
|
+
isError: false,
|
|
32
|
+
isLoading: false,
|
|
33
|
+
refetch: refetchServiceActionGrants,
|
|
34
|
+
}),
|
|
35
|
+
useServiceActions: () => ({
|
|
36
|
+
data: { actions: [] },
|
|
37
|
+
isError: false,
|
|
38
|
+
isLoading: false,
|
|
39
|
+
refetch: refetchServiceActions,
|
|
40
|
+
}),
|
|
41
|
+
useServiceApps: () => ({
|
|
42
|
+
data: {
|
|
43
|
+
entries: [{
|
|
44
|
+
args: ['server.mjs'],
|
|
45
|
+
command: 'node',
|
|
46
|
+
cwd: '/workspace/service-apps/notes',
|
|
47
|
+
dirPath: '/workspace/service-apps/notes',
|
|
48
|
+
enabled: true,
|
|
49
|
+
id: 'notes',
|
|
50
|
+
manifestPath: '/workspace/service-apps/notes/service-app.json',
|
|
51
|
+
protocol: 'mcp',
|
|
52
|
+
status: 'idle',
|
|
53
|
+
title: 'Notes',
|
|
54
|
+
}],
|
|
55
|
+
serviceAppsPath: '/workspace/service-apps',
|
|
56
|
+
workspacePath: '/workspace',
|
|
57
|
+
},
|
|
58
|
+
isError: false,
|
|
59
|
+
isLoading: false,
|
|
60
|
+
refetch: refetchServiceApps,
|
|
61
|
+
}),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
describe('ServiceAppsPanel', () => {
|
|
65
|
+
it('labels service app icon actions and deletes through a confirm dialog', async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
|
|
68
|
+
render(<ServiceAppsPanel />);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByText('Not connected')).toBeTruthy();
|
|
71
|
+
expect(screen.queryByText('idle')).toBeNull();
|
|
72
|
+
const discoverButton = screen.getByRole('button', { name: 'Connect and discover actions' });
|
|
73
|
+
expect(discoverButton).toBeTruthy();
|
|
74
|
+
const disconnectButton = screen.getByRole('button', { name: 'Disconnect runtime' });
|
|
75
|
+
expect(disconnectButton).toBeTruthy();
|
|
76
|
+
expect((disconnectButton as HTMLButtonElement).disabled).toBe(true);
|
|
77
|
+
await user.hover(discoverButton);
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(screen.queryAllByText('Connect to the service app runtime and discover its available actions.').length).toBeGreaterThan(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
fireEvent.click(screen.getByRole('button', { name: 'More actions' }));
|
|
83
|
+
fireEvent.click(screen.getByRole('button', { name: 'Delete service app' }));
|
|
84
|
+
fireEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
|
85
|
+
|
|
86
|
+
expect(deleteServiceAppMutate).toHaveBeenCalledWith('notes');
|
|
87
|
+
});
|
|
88
|
+
});
|