@nextclaw/ui 0.17.0 → 0.17.1
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 +8 -0
- package/dist/assets/{api-CVjdSvJk.js → api-DJ6ZKWFY.js} +2 -2
- package/dist/assets/{appearance-settings-page-BOorfaUx.js → appearance-settings-page-B-juoJ6F.js} +1 -1
- package/dist/assets/{channels-list-page-DuZJvYB6.js → channels-list-page-mVHYa7xW.js} +1 -1
- package/dist/assets/{chat-page-8PTMgI0t.js → chat-page-DsMYGNee.js} +1 -1
- package/dist/assets/{config-split-page-UuhsVcQD.js → config-split-page-BaqLcRxk.js} +1 -1
- package/dist/assets/{confirm-dialog-Bnosa8UF.js → confirm-dialog-DasFoaCT.js} +1 -1
- package/dist/assets/{desktop-update-config-CM4lsX4R.js → desktop-update-config-CoEvKrpB.js} +1 -1
- package/dist/assets/{dist-D17RZAsz.js → dist-C741xUwt.js} +1 -1
- package/dist/assets/{dist-_zZ8P38T.js → dist-zOKNc7QQ.js} +1 -1
- package/dist/assets/doc-browser-BbirIYbj.js +1 -0
- package/dist/assets/{doc-browser-CiLvybSC.js → doc-browser-Crqog5cn.js} +1 -1
- package/dist/assets/{doc-browser-context-CerkiK_N.js → doc-browser-context-BT8NWQyM.js} +1 -1
- package/dist/assets/{form-actions-Cmv23dN4.js → form-actions-BcukaMyR.js} +1 -1
- package/dist/assets/index-1f4Pv2q3.css +1 -0
- package/dist/assets/index-BYWLjYFQ.js +109 -0
- package/dist/assets/loader-circle-LVHz84Xj.js +1 -0
- package/dist/assets/mcp-marketplace-page-2SsrbaiQ.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DN_fqcLd.js → mcp-marketplace-page-XgYLOAkK.js} +1 -1
- package/dist/assets/{model-config-page-BxaeCMbJ.js → model-config-page-DL8ljJK-.js} +1 -1
- package/dist/assets/{navigation-link-k9rsmtC4.js → navigation-link-BoMNAKyw.js} +1 -1
- package/dist/assets/plus-BeGZHC3f.js +1 -0
- package/dist/assets/{provider-scoped-model-input-DLSAB6a5.js → provider-scoped-model-input-CB-vojwC.js} +1 -1
- package/dist/assets/{providers-config-page-DM5N0aeS.js → providers-config-page-C80nti5H.js} +1 -1
- package/dist/assets/{react-BgRLh3ee.js → react-DND3vEAM.js} +1 -1
- package/dist/assets/{refresh-cw-axvJLmRG.js → refresh-cw-DZ_Y89ld.js} +1 -1
- package/dist/assets/remote-Codkr9SX.js +1 -0
- package/dist/assets/rotate-cw-CP3pQ4R8.js +1 -0
- package/dist/assets/{runtime-config-page-WNzZyjMW.js → runtime-config-page-Um1XDqXJ.js} +1 -1
- package/dist/assets/{save-DbR4SGk8.js → save-BgMfxs5u.js} +1 -1
- package/dist/assets/{search-M9lrb8id.js → search-C7ruoV7u.js} +1 -1
- package/dist/assets/{search-config-page-DmsID8FQ.js → search-config-page-BTjfOZ6q.js} +1 -1
- package/dist/assets/{secrets-config-page-C10OwI0b.js → secrets-config-page-BFjiSz73.js} +1 -1
- package/dist/assets/security-config-Dx-wMb4q.js +1 -0
- package/dist/assets/{select-D_YBzMdv.js → select-BMX2ooE-.js} +1 -1
- package/dist/assets/{settings-2-zy40UwTo.js → settings-2-BNQF6hed.js} +1 -1
- package/dist/assets/skeleton-CWZ0IO2D.js +1 -0
- package/dist/assets/{tag-chip-D7QRdcph.js → tag-chip-BL-UXlVO.js} +1 -1
- package/dist/assets/x-BSu1xwPK.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +7 -7
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +43 -10
- package/src/features/apps/components/__tests__/apps-panel.test.tsx +18 -2
- package/src/features/apps/components/app-package-card.tsx +58 -4
- package/src/features/apps/components/app-packages-panel.tsx +60 -22
- package/src/features/apps/components/apps-panel.tsx +14 -3
- package/src/features/apps/hooks/use-app-packages.ts +5 -2
- package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +5 -0
- package/src/features/apps/managers/app-package-operation-settlement.manager.ts +8 -2
- package/src/features/service-apps/components/__tests__/service-apps-panel.test.tsx +32 -4
- package/src/features/service-apps/components/service-app-list-item.tsx +26 -12
- package/src/features/service-apps/components/service-apps-panel.tsx +23 -1
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +3 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +3 -0
- package/dist/assets/doc-browser-z7vdH3Ez.js +0 -1
- package/dist/assets/index-CWdgBpSc.css +0 -1
- package/dist/assets/index-DlL8uKXu.js +0 -109
- package/dist/assets/loader-circle-vUsFtTfb.js +0 -1
- package/dist/assets/mcp-marketplace-page-BV8WB3_L.js +0 -1
- package/dist/assets/plus-Dc760kHj.js +0 -1
- package/dist/assets/remote-Cj1GBMaN.js +0 -1
- package/dist/assets/rotate-cw-CFByP2O8.js +0 -1
- package/dist/assets/security-config-CvU5JxDT.js +0 -1
- package/dist/assets/skeleton-BmAtzjDM.js +0 -1
- package/dist/assets/x-D9Y9hf_r.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useMemo, useRef, useState, type FormEvent } from 'react';
|
|
2
|
-
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
|
2
|
+
import type { AppDataEntry, AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
3
3
|
import { LoaderCircle, PackagePlus, RefreshCw, ShieldCheck, Store } from 'lucide-react';
|
|
4
4
|
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
5
5
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
@@ -34,8 +34,10 @@ import { Skeleton } from '@/shared/components/ui/skeleton';
|
|
|
34
34
|
import { t } from '@/shared/lib/i18n';
|
|
35
35
|
|
|
36
36
|
export function AppPackagesPanel({
|
|
37
|
+
focusedPackageId,
|
|
37
38
|
onOpenPanelApp,
|
|
38
39
|
}: {
|
|
40
|
+
focusedPackageId?: string;
|
|
39
41
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
40
42
|
}) {
|
|
41
43
|
const appPackages = useAppPackages();
|
|
@@ -56,11 +58,19 @@ export function AppPackagesPanel({
|
|
|
56
58
|
[appPackageOperations.data?.entries],
|
|
57
59
|
);
|
|
58
60
|
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!focusedPackageId || appPackages.isLoading) return;
|
|
63
|
+
const target = document.getElementById(`app-package-${focusedPackageId}`);
|
|
64
|
+
target?.scrollIntoView({ block: 'nearest' });
|
|
65
|
+
target?.focus({ preventScroll: true });
|
|
66
|
+
}, [appPackages.data, appPackages.isLoading, focusedPackageId]);
|
|
67
|
+
|
|
59
68
|
useAppPackageOperationSettlement({
|
|
60
69
|
isLoaded: Boolean(appPackageOperations.data),
|
|
61
70
|
operations,
|
|
62
71
|
refetchPackages: appPackages.refetch,
|
|
63
72
|
refetchPanels: panelApps.refetch,
|
|
73
|
+
refetchAppData: appData.refetch,
|
|
64
74
|
settlementManager: presenter.appPackageOperationSettlementManager,
|
|
65
75
|
});
|
|
66
76
|
|
|
@@ -167,7 +177,11 @@ export function AppPackagesPanel({
|
|
|
167
177
|
) : null}
|
|
168
178
|
|
|
169
179
|
<AppPackageLibrary
|
|
180
|
+
appDataEntries={appData.data?.entries ?? []}
|
|
181
|
+
appDataLoading={appData.isLoading}
|
|
182
|
+
appDataUnavailable={appData.isError}
|
|
170
183
|
error={getPackageLoadError(appPackages.error, panelApps.error)}
|
|
184
|
+
focusedPackageId={focusedPackageId}
|
|
171
185
|
isLoading={appPackages.isLoading || panelApps.isLoading}
|
|
172
186
|
isPending={lifecycle.isPending}
|
|
173
187
|
mutationAppId={'appId' in (lifecycle.variables ?? {})
|
|
@@ -249,7 +263,11 @@ export function AppPackagesPanel({
|
|
|
249
263
|
}
|
|
250
264
|
|
|
251
265
|
function AppPackageLibrary({
|
|
266
|
+
appDataEntries,
|
|
267
|
+
appDataLoading,
|
|
268
|
+
appDataUnavailable,
|
|
252
269
|
error,
|
|
270
|
+
focusedPackageId,
|
|
253
271
|
isLoading,
|
|
254
272
|
isPending,
|
|
255
273
|
mutationAppId,
|
|
@@ -260,7 +278,11 @@ function AppPackageLibrary({
|
|
|
260
278
|
panelApps,
|
|
261
279
|
operations,
|
|
262
280
|
}: {
|
|
281
|
+
appDataEntries: AppDataEntry[];
|
|
282
|
+
appDataLoading: boolean;
|
|
283
|
+
appDataUnavailable: boolean;
|
|
263
284
|
error?: string;
|
|
285
|
+
focusedPackageId?: string;
|
|
264
286
|
isLoading: boolean;
|
|
265
287
|
isPending: boolean;
|
|
266
288
|
mutationAppId?: string;
|
|
@@ -327,27 +349,43 @@ function AppPackageLibrary({
|
|
|
327
349
|
: undefined;
|
|
328
350
|
|
|
329
351
|
return (
|
|
330
|
-
<
|
|
352
|
+
<section
|
|
331
353
|
key={appPackage.id}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
354
|
+
id={`app-package-${appPackage.id}`}
|
|
355
|
+
aria-label={appPackage.name}
|
|
356
|
+
tabIndex={-1}
|
|
357
|
+
className={focusedPackageId === appPackage.id
|
|
358
|
+
? 'rounded-xl outline-none ring-2 ring-primary/35 ring-offset-2 ring-offset-background'
|
|
359
|
+
: 'rounded-xl outline-none'}
|
|
360
|
+
>
|
|
361
|
+
<AppPackageCard
|
|
362
|
+
appPackage={appPackage}
|
|
363
|
+
storageUsage={appDataEntries.find((entry) =>
|
|
364
|
+
entry.source === 'package' &&
|
|
365
|
+
entry.lifecycle === 'active' &&
|
|
366
|
+
entry.appId === appPackage.id &&
|
|
367
|
+
entry.instanceId === appPackage.instanceId)?.usage ?? appPackage.storageUsage}
|
|
368
|
+
storageUsageLoading={appDataLoading}
|
|
369
|
+
storageUsageUnavailable={appDataUnavailable}
|
|
370
|
+
panelApps={panelApps.filter((entry) => entry.packageId === appPackage.id)}
|
|
371
|
+
isPending={isPending && mutationAppId === appPackage.id}
|
|
372
|
+
operation={visibleOperation}
|
|
373
|
+
onEnable={() => onMutate({ action: 'enable', appId: appPackage.id })}
|
|
374
|
+
onDisable={() => onMutate({ action: 'disable', appId: appPackage.id })}
|
|
375
|
+
onUpdate={() => onMutate({ action: 'update', appId: appPackage.id })}
|
|
376
|
+
onRollback={(version) => onMutate({
|
|
377
|
+
action: 'rollback',
|
|
378
|
+
appId: appPackage.id,
|
|
379
|
+
version,
|
|
380
|
+
})}
|
|
381
|
+
onUninstall={(purgeData) => onMutate({
|
|
382
|
+
action: 'uninstall',
|
|
383
|
+
appId: appPackage.id,
|
|
384
|
+
purgeData,
|
|
385
|
+
})}
|
|
386
|
+
onOpenPanelApp={onOpenPanelApp}
|
|
387
|
+
/>
|
|
388
|
+
</section>
|
|
351
389
|
);
|
|
352
390
|
})}
|
|
353
391
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import { AppWindow, Boxes, Server } from 'lucide-react';
|
|
2
3
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
3
4
|
import { AppPackagesPanel } from '@/features/apps/components/app-packages-panel';
|
|
@@ -17,10 +18,20 @@ export function AppsPanel({
|
|
|
17
18
|
onActiveTabChange: (tab: AppsPanelTab) => void;
|
|
18
19
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
19
20
|
}) {
|
|
21
|
+
const [focusedPackageId, setFocusedPackageId] = useState<string>();
|
|
22
|
+
const changeTab = (tab: AppsPanelTab) => {
|
|
23
|
+
setFocusedPackageId(undefined);
|
|
24
|
+
onActiveTabChange(tab);
|
|
25
|
+
};
|
|
26
|
+
const managePackage = (packageId: string) => {
|
|
27
|
+
setFocusedPackageId(packageId);
|
|
28
|
+
onActiveTabChange('apps');
|
|
29
|
+
};
|
|
30
|
+
|
|
20
31
|
return (
|
|
21
32
|
<div className="flex h-full min-h-0 flex-col bg-card text-card-foreground">
|
|
22
33
|
<nav className="shrink-0 border-b border-border/70 px-4 py-3" aria-label={t('appsTitle')}>
|
|
23
|
-
<Tabs value={activeTab} onValueChange={(value) =>
|
|
34
|
+
<Tabs value={activeTab} onValueChange={(value) => changeTab(value as AppsPanelTab)}>
|
|
24
35
|
<TabsList className="grid h-auto w-full max-w-[390px] grid-cols-3 rounded-lg bg-muted/70 p-0.5">
|
|
25
36
|
<TabsTrigger value="apps" className="min-w-0 gap-1.5 px-2 py-1.5 text-xs">
|
|
26
37
|
<Boxes className="h-3.5 w-3.5 shrink-0" />
|
|
@@ -39,11 +50,11 @@ export function AppsPanel({
|
|
|
39
50
|
</nav>
|
|
40
51
|
<div className="min-h-0 flex-1">
|
|
41
52
|
{activeTab === 'apps' ? (
|
|
42
|
-
<AppPackagesPanel onOpenPanelApp={onOpenPanelApp} />
|
|
53
|
+
<AppPackagesPanel focusedPackageId={focusedPackageId} onOpenPanelApp={onOpenPanelApp} />
|
|
43
54
|
) : activeTab === 'panel-apps' ? (
|
|
44
55
|
<PanelAppsList onOpenPanelApp={onOpenPanelApp} />
|
|
45
56
|
) : (
|
|
46
|
-
<ServiceAppsPanel />
|
|
57
|
+
<ServiceAppsPanel onManagePackage={managePackage} />
|
|
47
58
|
)}
|
|
48
59
|
</div>
|
|
49
60
|
</div>
|
|
@@ -31,7 +31,7 @@ export type AppPackageMutationInput =
|
|
|
31
31
|
export function useAppPackages() {
|
|
32
32
|
return useQuery({
|
|
33
33
|
queryKey: APP_PACKAGES_QUERY_KEY,
|
|
34
|
-
queryFn: () => nextclawClient.appPackages.list(),
|
|
34
|
+
queryFn: () => nextclawClient.appPackages.list({ includeStorageUsage: false }),
|
|
35
35
|
staleTime: 0,
|
|
36
36
|
});
|
|
37
37
|
}
|
|
@@ -57,6 +57,7 @@ export function useAppPackageOperationSettlement({
|
|
|
57
57
|
operations,
|
|
58
58
|
refetchPackages,
|
|
59
59
|
refetchPanels,
|
|
60
|
+
refetchAppData,
|
|
60
61
|
settlementManager,
|
|
61
62
|
}: {
|
|
62
63
|
isLoaded: boolean;
|
|
@@ -65,6 +66,7 @@ export function useAppPackageOperationSettlement({
|
|
|
65
66
|
refetchPanels: () => Promise<{
|
|
66
67
|
data?: { entries: Array<{ id: string; packageId?: string }> };
|
|
67
68
|
}>;
|
|
69
|
+
refetchAppData: () => Promise<unknown>;
|
|
68
70
|
settlementManager: AppPackageOperationSettlementManager;
|
|
69
71
|
}) {
|
|
70
72
|
useEffect(() => {
|
|
@@ -73,8 +75,9 @@ export function useAppPackageOperationSettlement({
|
|
|
73
75
|
operations,
|
|
74
76
|
refetchPackages,
|
|
75
77
|
refetchPanels,
|
|
78
|
+
refetchAppData,
|
|
76
79
|
});
|
|
77
|
-
}, [isLoaded, operations, refetchPackages, refetchPanels, settlementManager]);
|
|
80
|
+
}, [isLoaded, operations, refetchPackages, refetchPanels, refetchAppData, settlementManager]);
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
export function useAppPackageMutation() {
|
package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts
CHANGED
|
@@ -36,6 +36,7 @@ describe('AppPackageOperationSettlementManager', () => {
|
|
|
36
36
|
it('ignores historical terminal operations and settles each new operation once', async () => {
|
|
37
37
|
const reloadByDedupeKeys = vi.fn();
|
|
38
38
|
const refetchPackages = vi.fn(async () => undefined);
|
|
39
|
+
const refetchAppData = vi.fn(async () => undefined);
|
|
39
40
|
const refetchPanels = vi.fn(async () => ({
|
|
40
41
|
data: {
|
|
41
42
|
entries: [
|
|
@@ -54,6 +55,7 @@ describe('AppPackageOperationSettlementManager', () => {
|
|
|
54
55
|
operations: [historical],
|
|
55
56
|
refetchPackages,
|
|
56
57
|
refetchPanels,
|
|
58
|
+
refetchAppData,
|
|
57
59
|
});
|
|
58
60
|
expect(mocks.success).not.toHaveBeenCalled();
|
|
59
61
|
|
|
@@ -63,17 +65,20 @@ describe('AppPackageOperationSettlementManager', () => {
|
|
|
63
65
|
operations: [completed, historical],
|
|
64
66
|
refetchPackages,
|
|
65
67
|
refetchPanels,
|
|
68
|
+
refetchAppData,
|
|
66
69
|
});
|
|
67
70
|
manager.settle({
|
|
68
71
|
isLoaded: true,
|
|
69
72
|
operations: [completed, historical],
|
|
70
73
|
refetchPackages,
|
|
71
74
|
refetchPanels,
|
|
75
|
+
refetchAppData,
|
|
72
76
|
});
|
|
73
77
|
|
|
74
78
|
await vi.waitFor(() => {
|
|
75
79
|
expect(refetchPackages).toHaveBeenCalledTimes(1);
|
|
76
80
|
expect(refetchPanels).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(refetchAppData).toHaveBeenCalledTimes(1);
|
|
77
82
|
expect(reloadByDedupeKeys).toHaveBeenCalledWith(['panel-app:todos']);
|
|
78
83
|
});
|
|
79
84
|
expect(mocks.success).toHaveBeenCalledTimes(1);
|
|
@@ -19,11 +19,13 @@ export class AppPackageOperationSettlementManager {
|
|
|
19
19
|
operations,
|
|
20
20
|
refetchPackages,
|
|
21
21
|
refetchPanels,
|
|
22
|
+
refetchAppData,
|
|
22
23
|
}: {
|
|
23
24
|
isLoaded: boolean;
|
|
24
25
|
operations: AppPackageOperationView[];
|
|
25
26
|
refetchPackages: () => Promise<unknown>;
|
|
26
27
|
refetchPanels: RefetchPanels;
|
|
28
|
+
refetchAppData: () => Promise<unknown>;
|
|
27
29
|
}): void => {
|
|
28
30
|
if (!isLoaded) return;
|
|
29
31
|
const terminal = operations.filter(
|
|
@@ -37,7 +39,7 @@ export class AppPackageOperationSettlementManager {
|
|
|
37
39
|
terminal.forEach((operation) => {
|
|
38
40
|
if (this.knownTerminalOperationIds.has(operation.id)) return;
|
|
39
41
|
this.knownTerminalOperationIds.add(operation.id);
|
|
40
|
-
this.settleOperation(operation, refetchPackages, refetchPanels);
|
|
42
|
+
this.settleOperation(operation, refetchPackages, refetchPanels, refetchAppData);
|
|
41
43
|
});
|
|
42
44
|
};
|
|
43
45
|
|
|
@@ -45,10 +47,13 @@ export class AppPackageOperationSettlementManager {
|
|
|
45
47
|
operation: AppPackageOperationView,
|
|
46
48
|
refetchPackages: () => Promise<unknown>,
|
|
47
49
|
refetchPanels: RefetchPanels,
|
|
50
|
+
refetchAppData: () => Promise<unknown>,
|
|
48
51
|
): void => {
|
|
49
52
|
if (operation.status === 'succeeded') {
|
|
50
53
|
toast.success(operationSuccessLabel(operation));
|
|
51
|
-
void
|
|
54
|
+
void refetchPackages();
|
|
55
|
+
void refetchAppData();
|
|
56
|
+
void refetchPanels().then((panelsResult) => {
|
|
52
57
|
if (operation.action !== 'rollback' && operation.action !== 'update') return;
|
|
53
58
|
this.docBrowserManager.reloadByDedupeKeys(
|
|
54
59
|
panelsResult.data?.entries
|
|
@@ -61,6 +66,7 @@ export class AppPackageOperationSettlementManager {
|
|
|
61
66
|
if (operation.status === 'failed' || operation.status === 'interrupted') {
|
|
62
67
|
void refetchPackages();
|
|
63
68
|
void refetchPanels();
|
|
69
|
+
void refetchAppData();
|
|
64
70
|
toast.error(operation.error ?? t('appPackagesActionFailed'));
|
|
65
71
|
}
|
|
66
72
|
};
|
|
@@ -95,9 +95,24 @@ vi.mock('@/features/service-apps/hooks/use-service-apps', () => ({
|
|
|
95
95
|
id: 'notes',
|
|
96
96
|
manifestPath: '/workspace/service-apps/notes/service-app.json',
|
|
97
97
|
protocol: 'mcp',
|
|
98
|
+
sourceKind: 'workspace',
|
|
98
99
|
status: 'idle',
|
|
99
100
|
title: 'Notes',
|
|
101
|
+
}, {
|
|
102
|
+
args: ['server.mjs'],
|
|
103
|
+
command: 'node',
|
|
104
|
+
cwd: '/apps/personal-organizer/service',
|
|
105
|
+
dirPath: '/apps/personal-organizer/service',
|
|
106
|
+
enabled: true,
|
|
107
|
+
id: 'organizer-service',
|
|
108
|
+
manifestPath: '/apps/personal-organizer/service/service-app.json',
|
|
109
|
+
packageId: 'nextclaw.personal-organizer',
|
|
110
|
+
protocol: 'mcp',
|
|
111
|
+
sourceKind: 'package',
|
|
112
|
+
status: 'idle',
|
|
113
|
+
title: 'Organizer Service',
|
|
100
114
|
}],
|
|
115
|
+
diagnostics: [],
|
|
101
116
|
serviceAppsPath: '/workspace/service-apps',
|
|
102
117
|
workspacePath: '/workspace',
|
|
103
118
|
},
|
|
@@ -111,11 +126,13 @@ describe('ServiceAppsPanel', () => {
|
|
|
111
126
|
it('keeps service actions progressive and deletes through a confirm dialog', async () => {
|
|
112
127
|
const user = userEvent.setup();
|
|
113
128
|
|
|
114
|
-
render(<ServiceAppsPanel />);
|
|
129
|
+
render(<ServiceAppsPanel onManagePackage={vi.fn()} />);
|
|
115
130
|
|
|
116
|
-
expect(screen.
|
|
131
|
+
expect(screen.getAllByText('Not connected')).toHaveLength(2);
|
|
117
132
|
expect(screen.queryByText('idle')).toBeNull();
|
|
118
|
-
const discoverButton = screen.
|
|
133
|
+
const discoverButton = screen.getAllByRole('button', {
|
|
134
|
+
name: 'Connect and discover actions',
|
|
135
|
+
})[0]!;
|
|
119
136
|
expect(discoverButton).toBeTruthy();
|
|
120
137
|
await user.hover(discoverButton);
|
|
121
138
|
await waitFor(() => {
|
|
@@ -125,7 +142,7 @@ describe('ServiceAppsPanel', () => {
|
|
|
125
142
|
await user.click(discoverButton);
|
|
126
143
|
expect(discoverServiceAppActionsMutateAsync).toHaveBeenCalledWith('notes');
|
|
127
144
|
|
|
128
|
-
fireEvent.click(screen.
|
|
145
|
+
fireEvent.click(screen.getAllByRole('button', { name: 'More actions' })[0]!);
|
|
129
146
|
expect((screen.getByRole('button', { name: 'Disconnect runtime' }) as HTMLButtonElement).disabled).toBe(true);
|
|
130
147
|
fireEvent.click(screen.getByRole('button', { name: 'Delete service app' }));
|
|
131
148
|
await waitFor(() => {
|
|
@@ -143,4 +160,15 @@ describe('ServiceAppsPanel', () => {
|
|
|
143
160
|
{ onSuccess: expect.any(Function) },
|
|
144
161
|
);
|
|
145
162
|
});
|
|
163
|
+
|
|
164
|
+
it('routes package-managed services back to Apps without offering standalone deletion', () => {
|
|
165
|
+
const onManagePackage = vi.fn();
|
|
166
|
+
render(<ServiceAppsPanel onManagePackage={onManagePackage} />);
|
|
167
|
+
|
|
168
|
+
fireEvent.click(screen.getAllByRole('button', { name: 'More actions' })[1]!);
|
|
169
|
+
expect(screen.queryByRole('button', { name: 'Delete service app' })).toBeNull();
|
|
170
|
+
fireEvent.click(screen.getByRole('button', { name: 'Manage in Apps' }));
|
|
171
|
+
|
|
172
|
+
expect(onManagePackage).toHaveBeenCalledWith('nextclaw.personal-organizer');
|
|
173
|
+
});
|
|
146
174
|
});
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "@nextclaw/client-sdk";
|
|
8
8
|
import {
|
|
9
9
|
AlertTriangle,
|
|
10
|
+
Boxes,
|
|
10
11
|
CheckCircle2,
|
|
11
12
|
ChevronDown,
|
|
12
13
|
CircleDashed,
|
|
@@ -51,6 +52,7 @@ export function ServiceAppListItem({
|
|
|
51
52
|
isDiscovering,
|
|
52
53
|
onActionsOpenChange,
|
|
53
54
|
onDiscover,
|
|
55
|
+
onManagePackage,
|
|
54
56
|
onDelete,
|
|
55
57
|
onResetDelete,
|
|
56
58
|
onRestart,
|
|
@@ -67,6 +69,7 @@ export function ServiceAppListItem({
|
|
|
67
69
|
isDiscovering: boolean;
|
|
68
70
|
onActionsOpenChange: (open: boolean) => void;
|
|
69
71
|
onDiscover: (appId: string) => void;
|
|
72
|
+
onManagePackage: (packageId: string) => void;
|
|
70
73
|
onDelete: (appId: string, purgeData: boolean, onSuccess: () => void) => void;
|
|
71
74
|
onResetDelete: () => void;
|
|
72
75
|
onRestart: (appId: string) => void;
|
|
@@ -76,10 +79,9 @@ export function ServiceAppListItem({
|
|
|
76
79
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
77
80
|
const [isDiagnosticsOpen, setIsDiagnosticsOpen] = useState(false);
|
|
78
81
|
const [purgeData, setPurgeData] = useState(false);
|
|
79
|
-
const canConnectAndDiscover =
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
app.status === "running" || app.status === "failed";
|
|
82
|
+
const canConnectAndDiscover = app.status !== "starting" && app.status !== "stopped";
|
|
83
|
+
const canDisconnectRuntime = app.status === "running" || app.status === "failed";
|
|
84
|
+
const managedPackageId = app.sourceKind === "package" ? app.packageId : undefined;
|
|
83
85
|
const diagnostics = getServiceAppDiagnostics(app);
|
|
84
86
|
const openDeleteDialog = () => {
|
|
85
87
|
setIsMenuOpen(false);
|
|
@@ -87,7 +89,6 @@ export function ServiceAppListItem({
|
|
|
87
89
|
onResetDelete();
|
|
88
90
|
setIsDeleteDialogOpen(true);
|
|
89
91
|
};
|
|
90
|
-
|
|
91
92
|
return (
|
|
92
93
|
<TooltipProvider delayDuration={250}>
|
|
93
94
|
<section className="group bg-card transition-colors hover:bg-muted/25">
|
|
@@ -168,13 +169,24 @@ export function ServiceAppListItem({
|
|
|
168
169
|
onRestart(app.id);
|
|
169
170
|
}}
|
|
170
171
|
/>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
{managedPackageId ? (
|
|
173
|
+
<ServiceAppMenuItem
|
|
174
|
+
icon={Boxes}
|
|
175
|
+
label={t("serviceAppsManagePackage")}
|
|
176
|
+
onClick={() => {
|
|
177
|
+
setIsMenuOpen(false);
|
|
178
|
+
onManagePackage(managedPackageId);
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
) : (
|
|
182
|
+
<ServiceAppMenuItem
|
|
183
|
+
destructive
|
|
184
|
+
disabled={deletePending}
|
|
185
|
+
icon={Trash2}
|
|
186
|
+
label={t("serviceAppsDelete")}
|
|
187
|
+
onClick={openDeleteDialog}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
178
190
|
</PopoverContent>
|
|
179
191
|
</Popover>
|
|
180
192
|
</div>
|
|
@@ -245,6 +257,7 @@ export function ServiceAppListItem({
|
|
|
245
257
|
) : null}
|
|
246
258
|
</div>
|
|
247
259
|
|
|
260
|
+
{app.sourceKind !== "package" ? (
|
|
248
261
|
<ServiceAppDeleteDialog
|
|
249
262
|
appId={app.id}
|
|
250
263
|
appTitle={app.title}
|
|
@@ -258,6 +271,7 @@ export function ServiceAppListItem({
|
|
|
258
271
|
onOpenChange={setIsDeleteDialogOpen}
|
|
259
272
|
onPurgeDataChange={setPurgeData}
|
|
260
273
|
/>
|
|
274
|
+
) : null}
|
|
261
275
|
</section>
|
|
262
276
|
</TooltipProvider>
|
|
263
277
|
);
|
|
@@ -27,7 +27,11 @@ import { t } from "@/shared/lib/i18n";
|
|
|
27
27
|
|
|
28
28
|
type ServiceActionView = ServiceActionListView["actions"][number];
|
|
29
29
|
|
|
30
|
-
export function ServiceAppsPanel(
|
|
30
|
+
export function ServiceAppsPanel({
|
|
31
|
+
onManagePackage,
|
|
32
|
+
}: {
|
|
33
|
+
onManagePackage: (packageId: string) => void;
|
|
34
|
+
}) {
|
|
31
35
|
const serviceApps = useServiceApps();
|
|
32
36
|
const appData = useAppData();
|
|
33
37
|
const serviceActions = useServiceActions();
|
|
@@ -77,6 +81,7 @@ export function ServiceAppsPanel() {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
const apps: ServiceAppRecordView[] = serviceApps.data?.entries ?? [];
|
|
84
|
+
const diagnostics = serviceApps.data?.diagnostics ?? [];
|
|
80
85
|
const actions: ServiceActionView[] = serviceActions.data?.actions ?? [];
|
|
81
86
|
const grants: ServiceActionGrantView[] =
|
|
82
87
|
serviceActionGrants.data?.grants ?? [];
|
|
@@ -115,6 +120,22 @@ export function ServiceAppsPanel() {
|
|
|
115
120
|
</TooltipProvider>
|
|
116
121
|
</div>
|
|
117
122
|
|
|
123
|
+
{diagnostics.length > 0 ? (
|
|
124
|
+
<div
|
|
125
|
+
role="alert"
|
|
126
|
+
className="mx-3 mt-3 rounded-xl border border-destructive/20 bg-destructive/5 px-3 py-2.5 text-xs text-destructive sm:mx-4"
|
|
127
|
+
>
|
|
128
|
+
<div className="font-medium">{t("serviceAppsLoadFailed")}</div>
|
|
129
|
+
<ul className="mt-1 space-y-1">
|
|
130
|
+
{diagnostics.map((diagnostic) => (
|
|
131
|
+
<li key={diagnostic.stagedPath} className="break-words">
|
|
132
|
+
{diagnostic.appId}: {diagnostic.message}
|
|
133
|
+
</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
</div>
|
|
137
|
+
) : null}
|
|
138
|
+
|
|
118
139
|
{apps.length === 0 ? (
|
|
119
140
|
<div className="flex flex-1 items-center justify-center px-6 py-8 text-center">
|
|
120
141
|
<div className="w-full max-w-xs">
|
|
@@ -154,6 +175,7 @@ export function ServiceAppsPanel() {
|
|
|
154
175
|
}))
|
|
155
176
|
}
|
|
156
177
|
onDiscover={discover}
|
|
178
|
+
onManagePackage={onManagePackage}
|
|
157
179
|
onDelete={(appId, purgeData, onSuccess) => void deleteServiceApp.mutate(
|
|
158
180
|
{ appId, purgeData },
|
|
159
181
|
{ onSuccess },
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"appPackagesLibraryTitle": "Your apps",
|
|
35
35
|
"appPackagesLibraryDescription": "Open and manage the apps in NextClaw. Installs continue in the background, and personal data stays local.",
|
|
36
36
|
"appPackagesLoading": "Loading apps…",
|
|
37
|
+
"appPackagesDataUsageUnavailable": "Size unavailable",
|
|
38
|
+
"appPackagesNoDataYet": "No data yet",
|
|
37
39
|
"appPackagesLoadFailed": "Failed to load apps",
|
|
38
40
|
"appPackagesRefresh": "Refresh apps",
|
|
39
41
|
"appPackagesBrowse": "Add apps",
|
|
@@ -182,6 +184,7 @@
|
|
|
182
184
|
"serviceAppsDisconnectRuntimeHint": "Close the current runtime connection and return the service app to not connected.",
|
|
183
185
|
"serviceAppsMoreActions": "More actions",
|
|
184
186
|
"serviceAppsDelete": "Delete service app",
|
|
187
|
+
"serviceAppsManagePackage": "Manage in Apps",
|
|
185
188
|
"serviceAppsDeleteConfirmTitle": "Delete service app?",
|
|
186
189
|
"serviceAppsDeleteConfirmDescription": "The service source will be deleted. Its private data is kept by default.",
|
|
187
190
|
"serviceAppsKeepDataDescription": "Delete the service source but keep its private data for a future copy of the same service.",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"appPackagesLibraryTitle": "你的应用",
|
|
35
35
|
"appPackagesLibraryDescription": "打开和管理装进 NextClaw 的应用。安装会在后台继续,个人数据保存在本机。",
|
|
36
36
|
"appPackagesLoading": "加载应用中…",
|
|
37
|
+
"appPackagesDataUsageUnavailable": "大小不可用",
|
|
38
|
+
"appPackagesNoDataYet": "尚无数据",
|
|
37
39
|
"appPackagesLoadFailed": "应用加载失败",
|
|
38
40
|
"appPackagesRefresh": "刷新应用",
|
|
39
41
|
"appPackagesBrowse": "添加应用",
|
|
@@ -182,6 +184,7 @@
|
|
|
182
184
|
"serviceAppsDisconnectRuntimeHint": "断开当前运行时连接,并将服务应用恢复为未连接状态。",
|
|
183
185
|
"serviceAppsMoreActions": "更多操作",
|
|
184
186
|
"serviceAppsDelete": "删除服务应用",
|
|
187
|
+
"serviceAppsManagePackage": "在应用中管理",
|
|
185
188
|
"serviceAppsDeleteConfirmTitle": "删除服务应用?",
|
|
186
189
|
"serviceAppsDeleteConfirmDescription": "服务源码会被删除,私有数据默认保留。",
|
|
187
190
|
"serviceAppsKeepDataDescription": "删除服务源码,但保留私有数据,之后放回同一服务仍可继续使用。",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./doc-browser-CiLvybSC.js";export{e as DocBrowser};
|