@nextclaw/ui 0.13.4 → 0.13.5
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 +64 -0
- package/dist/assets/api-BAyyGaoz.js +15 -0
- package/dist/assets/app-presenter-provider-DqHHfC1K.js +3 -0
- package/dist/assets/arrow-left-Bn-3Pj3p.js +1 -0
- package/dist/assets/{boxes-BvA-I63l.js → boxes-ClVuotcz.js} +1 -1
- package/dist/assets/channels-list-page-BnGxv6ma.js +8 -0
- package/dist/assets/chat-page-DInyWLb4.js +105 -0
- package/dist/assets/config-split-page-CWN2YG9U.js +1 -0
- package/dist/assets/confirm-dialog-CVbgKuHp.js +5 -0
- package/dist/assets/{createLucideIcon-Br1EFMEr.js → createLucideIcon-CjPUPAAK.js} +1 -1
- package/dist/assets/desktop-update-config-abmYzHSy.js +1 -0
- package/dist/assets/{dist-rzakR_bu.js → dist-hkrb6Ynt.js} +1 -1
- package/dist/assets/doc-browser-BlrpUKVJ.js +1 -0
- package/dist/assets/doc-browser-context-Cb1sEUT0.js +1 -0
- package/dist/assets/doc-browser-kJ2ClG0H.js +1 -0
- package/dist/assets/ellipsis-CdkxFkVZ.js +1 -0
- package/dist/assets/{external-link-DHjFdCpy.js → external-link-CT9_qFwN.js} +1 -1
- package/dist/assets/index-BGXTR0F_.css +1 -0
- package/dist/assets/index-Ccbswzt9.js +101 -0
- package/dist/assets/{key-round-D41Oe35x.js → key-round-xJfR4iRe.js} +1 -1
- package/dist/assets/{book-open-CRotBOF2.js → loader-circle-BI6HJDg5.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-Dvi8mCBd.js → mcp-marketplace-page-CmUKUeno.js} +2 -2
- package/dist/assets/mcp-marketplace-page-DH11bSVx.js +1 -0
- package/dist/assets/model-config-Dbo3zWDl.js +1 -0
- package/dist/assets/notice-card-CFTFtJky.js +1 -0
- package/dist/assets/play-DyBWW2j4.js +1 -0
- package/dist/assets/plus-BlnBmcKE.js +1 -0
- package/dist/assets/popover-DPqa_Mfb.js +1 -0
- package/dist/assets/provider-models-CXUgDxGR.js +1 -0
- package/dist/assets/provider-scoped-model-input-C6xL7zSJ.js +1 -0
- package/dist/assets/providers-list-DOj_7GL8.js +1 -0
- package/dist/assets/react-oAHNoVya.js +1 -0
- package/dist/assets/refresh-cw-o8NSmIYV.js +1 -0
- package/dist/assets/remote-fCE5fLsj.js +1 -0
- package/dist/assets/rotate-cw-DA8EqNAH.js +1 -0
- package/dist/assets/runtime-config-page-Dg8aEgWx.js +1 -0
- package/dist/assets/{save-aYdSdK97.js → save-D4-g0EAq.js} +1 -1
- package/dist/assets/search-G2cMn3M5.js +1 -0
- package/dist/assets/{search-config-G2O1fcRb.js → search-config-DAibVFGR.js} +1 -1
- package/dist/assets/{secrets-config-TenzxYbE.js → secrets-config-CoJzy13_.js} +2 -2
- package/dist/assets/{status-dot-BSMG7ZaC.js → status-dot-D8xx5ZR8.js} +1 -1
- package/dist/assets/{tabs-D1CjpWNX.js → tabs-DxmND60Q.js} +1 -1
- package/dist/assets/{tabs-custom-CRSN1ekg.js → tabs-custom-Cjo0bHw8.js} +1 -1
- package/dist/assets/{tag-chip-DEashiw6.js → tag-chip-DnNWFRMu.js} +1 -1
- package/dist/assets/{trash-2-DSWTsARW.js → trash-2-C9tc-9sQ.js} +1 -1
- package/dist/assets/x-DmLl2yG6.js +1 -0
- package/dist/index.html +24 -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/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 +25 -2
- package/src/shared/components/doc-browser/doc-browser.test.tsx +42 -0
- 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/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/desktop-update-config-CehWxSSg.js +0 -1
- package/dist/assets/dist-CpwDZDtv.js +0 -1
- 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/index-DORmh0Cy.css +0 -1
- package/dist/assets/index-DcFpvnge.js +0 -101
- 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/search-CdhgFNwf.js +0 -1
- package/dist/assets/setting-row-BNu0qhOM.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-TmNgpFHv.js} +0 -0
|
@@ -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
|
+
});
|
|
@@ -5,15 +5,26 @@ import type {
|
|
|
5
5
|
ServiceAppRecordView,
|
|
6
6
|
} from '@nextclaw/client-sdk';
|
|
7
7
|
import {
|
|
8
|
+
AlertTriangle,
|
|
9
|
+
CheckCircle2,
|
|
10
|
+
CircleDashed,
|
|
11
|
+
LoaderCircle,
|
|
12
|
+
MoreVertical,
|
|
13
|
+
PauseCircle,
|
|
14
|
+
Radar,
|
|
8
15
|
RefreshCw,
|
|
9
|
-
RotateCw,
|
|
10
|
-
Search,
|
|
11
16
|
Server,
|
|
12
17
|
ShieldCheck,
|
|
13
18
|
Trash2,
|
|
19
|
+
Unplug,
|
|
20
|
+
type LucideIcon,
|
|
14
21
|
Wrench,
|
|
15
22
|
} from 'lucide-react';
|
|
23
|
+
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog';
|
|
24
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
25
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shared/components/ui/tooltip';
|
|
16
26
|
import {
|
|
27
|
+
useDeleteServiceApp,
|
|
17
28
|
useDiscoverServiceAppActions,
|
|
18
29
|
useRestartServiceApp,
|
|
19
30
|
useRevokeServiceActionGrant,
|
|
@@ -22,8 +33,10 @@ import {
|
|
|
22
33
|
useServiceApps,
|
|
23
34
|
} from '@/features/service-apps/hooks/use-service-apps';
|
|
24
35
|
import { t } from '@/shared/lib/i18n';
|
|
36
|
+
import { cn } from '@/shared/lib/utils';
|
|
25
37
|
|
|
26
38
|
type ServiceActionView = ServiceActionListView['actions'][number];
|
|
39
|
+
type ServiceAppStatus = ServiceAppRecordView['status'];
|
|
27
40
|
|
|
28
41
|
export function ServiceAppsPanel({
|
|
29
42
|
headerContent,
|
|
@@ -33,6 +46,7 @@ export function ServiceAppsPanel({
|
|
|
33
46
|
const serviceApps = useServiceApps();
|
|
34
47
|
const serviceActions = useServiceActions();
|
|
35
48
|
const serviceActionGrants = useServiceActionGrants();
|
|
49
|
+
const deleteServiceApp = useDeleteServiceApp();
|
|
36
50
|
const restartServiceApp = useRestartServiceApp();
|
|
37
51
|
const discoverServiceAppActions = useDiscoverServiceAppActions();
|
|
38
52
|
const revokeServiceActionGrant = useRevokeServiceActionGrant();
|
|
@@ -86,15 +100,13 @@ export function ServiceAppsPanel({
|
|
|
86
100
|
</>
|
|
87
101
|
)}
|
|
88
102
|
</div>
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
>
|
|
96
|
-
<RefreshCw className="h-3.5 w-3.5" />
|
|
97
|
-
</button>
|
|
103
|
+
<TooltipProvider delayDuration={250}>
|
|
104
|
+
<ServiceAppIconButton
|
|
105
|
+
icon={RefreshCw}
|
|
106
|
+
label={t('serviceAppsRefresh')}
|
|
107
|
+
onClick={refetch}
|
|
108
|
+
/>
|
|
109
|
+
</TooltipProvider>
|
|
98
110
|
</div>
|
|
99
111
|
|
|
100
112
|
{apps.length === 0 ? (
|
|
@@ -110,8 +122,10 @@ export function ServiceAppsPanel({
|
|
|
110
122
|
app={app}
|
|
111
123
|
actions={discoveredActionsByApp[app.id] ?? actions.filter((action) => action.appId === app.id)}
|
|
112
124
|
grants={grants}
|
|
125
|
+
deletePending={deleteServiceApp.isPending}
|
|
113
126
|
isDiscovering={discoverServiceAppActions.isPending}
|
|
114
127
|
onDiscover={discover}
|
|
128
|
+
onDelete={(appId) => void deleteServiceApp.mutate(appId)}
|
|
115
129
|
onRestart={(appId) => void restartServiceApp.mutate(appId)}
|
|
116
130
|
onRevoke={(grant) => void revokeServiceActionGrant.mutate({
|
|
117
131
|
actionId: grant.actionId,
|
|
@@ -130,68 +144,244 @@ function ServiceAppCard({
|
|
|
130
144
|
app,
|
|
131
145
|
actions,
|
|
132
146
|
grants,
|
|
147
|
+
deletePending,
|
|
133
148
|
isDiscovering,
|
|
134
149
|
onDiscover,
|
|
150
|
+
onDelete,
|
|
135
151
|
onRestart,
|
|
136
152
|
onRevoke,
|
|
137
153
|
}: {
|
|
138
154
|
app: ServiceAppRecordView;
|
|
139
155
|
actions: ServiceActionView[];
|
|
140
156
|
grants: ServiceActionGrantView[];
|
|
157
|
+
deletePending: boolean;
|
|
141
158
|
isDiscovering: boolean;
|
|
142
159
|
onDiscover: (appId: string) => void;
|
|
160
|
+
onDelete: (appId: string) => void;
|
|
143
161
|
onRestart: (appId: string) => void;
|
|
144
162
|
onRevoke: (grant: ServiceActionGrantView) => void;
|
|
145
163
|
}) {
|
|
164
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
165
|
+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
166
|
+
const canConnectAndDiscover = app.status !== 'starting' && app.status !== 'stopped';
|
|
167
|
+
const canDisconnectRuntime = app.status === 'running' || app.status === 'failed';
|
|
168
|
+
const openDeleteDialog = () => {
|
|
169
|
+
setIsMenuOpen(false);
|
|
170
|
+
setIsDeleteDialogOpen(true);
|
|
171
|
+
};
|
|
172
|
+
|
|
146
173
|
return (
|
|
147
|
-
<
|
|
148
|
-
<
|
|
149
|
-
<div className="
|
|
150
|
-
<div className="
|
|
151
|
-
<div className="
|
|
152
|
-
|
|
174
|
+
<TooltipProvider delayDuration={250}>
|
|
175
|
+
<section className="rounded-lg border border-gray-200 bg-white p-3">
|
|
176
|
+
<div className="flex items-start justify-between gap-3">
|
|
177
|
+
<div className="min-w-0">
|
|
178
|
+
<div className="flex items-center gap-2">
|
|
179
|
+
<div className="truncate text-sm font-semibold text-gray-900">{app.title}</div>
|
|
180
|
+
<ServiceAppStatusBadge status={app.status} />
|
|
181
|
+
</div>
|
|
182
|
+
{app.description ? (
|
|
183
|
+
<div className="mt-1 line-clamp-2 text-xs text-gray-500">{app.description}</div>
|
|
184
|
+
) : null}
|
|
185
|
+
{app.lastError ? (
|
|
186
|
+
<div className="mt-2 rounded bg-rose-50 px-2 py-1 text-xs text-rose-700">{app.lastError}</div>
|
|
187
|
+
) : null}
|
|
188
|
+
</div>
|
|
189
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
190
|
+
<ServiceAppIconButton
|
|
191
|
+
disabled={isDiscovering || !canConnectAndDiscover}
|
|
192
|
+
icon={Radar}
|
|
193
|
+
label={t('serviceAppsDiscoverActions')}
|
|
194
|
+
tooltip={t('serviceAppsDiscoverActionsHint')}
|
|
195
|
+
onClick={() => onDiscover(app.id)}
|
|
196
|
+
/>
|
|
197
|
+
<ServiceAppIconButton
|
|
198
|
+
disabled={!canDisconnectRuntime}
|
|
199
|
+
icon={Unplug}
|
|
200
|
+
label={t('serviceAppsDisconnectRuntime')}
|
|
201
|
+
tooltip={t('serviceAppsDisconnectRuntimeHint')}
|
|
202
|
+
onClick={() => onRestart(app.id)}
|
|
203
|
+
/>
|
|
204
|
+
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
205
|
+
<Tooltip>
|
|
206
|
+
<TooltipTrigger asChild>
|
|
207
|
+
<PopoverTrigger asChild>
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 disabled:opacity-50"
|
|
211
|
+
aria-label={t('serviceAppsMoreActions')}
|
|
212
|
+
disabled={deletePending}
|
|
213
|
+
>
|
|
214
|
+
<MoreVertical className="h-3.5 w-3.5" />
|
|
215
|
+
</button>
|
|
216
|
+
</PopoverTrigger>
|
|
217
|
+
</TooltipTrigger>
|
|
218
|
+
<TooltipContent side="bottom" className="text-xs">{t('serviceAppsMoreActions')}</TooltipContent>
|
|
219
|
+
</Tooltip>
|
|
220
|
+
<PopoverContent align="end" className="w-48 rounded-xl p-1.5">
|
|
221
|
+
<ServiceAppMenuItem
|
|
222
|
+
destructive
|
|
223
|
+
disabled={deletePending}
|
|
224
|
+
icon={Trash2}
|
|
225
|
+
label={t('serviceAppsDelete')}
|
|
226
|
+
onClick={openDeleteDialog}
|
|
227
|
+
/>
|
|
228
|
+
</PopoverContent>
|
|
229
|
+
</Popover>
|
|
153
230
|
</div>
|
|
154
|
-
{app.description ? (
|
|
155
|
-
<div className="mt-1 line-clamp-2 text-xs text-gray-500">{app.description}</div>
|
|
156
|
-
) : null}
|
|
157
|
-
{app.lastError ? (
|
|
158
|
-
<div className="mt-2 rounded bg-rose-50 px-2 py-1 text-xs text-rose-700">{app.lastError}</div>
|
|
159
|
-
) : null}
|
|
160
231
|
</div>
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
</button>
|
|
172
|
-
<button
|
|
173
|
-
type="button"
|
|
174
|
-
onClick={() => onRestart(app.id)}
|
|
175
|
-
className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800"
|
|
176
|
-
title={t('serviceAppsRestart')}
|
|
177
|
-
aria-label={t('serviceAppsRestart')}
|
|
178
|
-
>
|
|
179
|
-
<RotateCw className="h-3.5 w-3.5" />
|
|
180
|
-
</button>
|
|
232
|
+
<ServiceAppDiagnostics app={app} />
|
|
233
|
+
<div className="mt-3 space-y-1">
|
|
234
|
+
{actions.map((action) => (
|
|
235
|
+
<ServiceActionRow
|
|
236
|
+
key={action.id}
|
|
237
|
+
action={action}
|
|
238
|
+
grants={grants.filter((grant) => grant.actionId === action.id)}
|
|
239
|
+
onRevoke={onRevoke}
|
|
240
|
+
/>
|
|
241
|
+
))}
|
|
181
242
|
</div>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
<ConfirmDialog
|
|
244
|
+
open={isDeleteDialogOpen}
|
|
245
|
+
onOpenChange={setIsDeleteDialogOpen}
|
|
246
|
+
title={t('serviceAppsDeleteConfirmTitle')}
|
|
247
|
+
description={`${t('serviceAppsDeleteConfirmDescription')} ${app.title}`}
|
|
248
|
+
confirmLabel={t('delete')}
|
|
249
|
+
variant="destructive"
|
|
250
|
+
onConfirm={() => onDelete(app.id)}
|
|
251
|
+
onCancel={() => undefined}
|
|
252
|
+
/>
|
|
253
|
+
</section>
|
|
254
|
+
</TooltipProvider>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ServiceAppStatusBadge({ status }: { status: ServiceAppStatus }) {
|
|
259
|
+
const view = getServiceAppStatusView(status);
|
|
260
|
+
const Icon = view.icon;
|
|
261
|
+
return (
|
|
262
|
+
<Tooltip>
|
|
263
|
+
<TooltipTrigger asChild>
|
|
264
|
+
<span
|
|
265
|
+
className={cn(
|
|
266
|
+
'inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium',
|
|
267
|
+
view.className,
|
|
268
|
+
)}
|
|
269
|
+
aria-label={view.label}
|
|
270
|
+
>
|
|
271
|
+
<Icon className={cn('h-3 w-3', view.iconClassName)} />
|
|
272
|
+
<span>{view.label}</span>
|
|
273
|
+
</span>
|
|
274
|
+
</TooltipTrigger>
|
|
275
|
+
<TooltipContent side="bottom" className="text-xs">{view.tooltip}</TooltipContent>
|
|
276
|
+
</Tooltip>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getServiceAppStatusView(status: ServiceAppStatus): {
|
|
281
|
+
className: string;
|
|
282
|
+
icon: LucideIcon;
|
|
283
|
+
iconClassName?: string;
|
|
284
|
+
label: string;
|
|
285
|
+
tooltip: string;
|
|
286
|
+
} {
|
|
287
|
+
switch (status) {
|
|
288
|
+
case 'running':
|
|
289
|
+
return {
|
|
290
|
+
className: 'bg-emerald-50 text-emerald-700',
|
|
291
|
+
icon: CheckCircle2,
|
|
292
|
+
label: t('serviceAppsStatus_running'),
|
|
293
|
+
tooltip: t('serviceAppsStatusHint_running'),
|
|
294
|
+
};
|
|
295
|
+
case 'starting':
|
|
296
|
+
return {
|
|
297
|
+
className: 'bg-blue-50 text-blue-700',
|
|
298
|
+
icon: LoaderCircle,
|
|
299
|
+
iconClassName: 'animate-spin',
|
|
300
|
+
label: t('serviceAppsStatus_starting'),
|
|
301
|
+
tooltip: t('serviceAppsStatusHint_starting'),
|
|
302
|
+
};
|
|
303
|
+
case 'failed':
|
|
304
|
+
return {
|
|
305
|
+
className: 'bg-rose-50 text-rose-700',
|
|
306
|
+
icon: AlertTriangle,
|
|
307
|
+
label: t('serviceAppsStatus_failed'),
|
|
308
|
+
tooltip: t('serviceAppsStatusHint_failed'),
|
|
309
|
+
};
|
|
310
|
+
case 'stopped':
|
|
311
|
+
return {
|
|
312
|
+
className: 'bg-gray-100 text-gray-600',
|
|
313
|
+
icon: PauseCircle,
|
|
314
|
+
label: t('serviceAppsStatus_stopped'),
|
|
315
|
+
tooltip: t('serviceAppsStatusHint_stopped'),
|
|
316
|
+
};
|
|
317
|
+
case 'idle':
|
|
318
|
+
default:
|
|
319
|
+
return {
|
|
320
|
+
className: 'bg-slate-100 text-slate-600',
|
|
321
|
+
icon: CircleDashed,
|
|
322
|
+
label: t('serviceAppsStatus_idle'),
|
|
323
|
+
tooltip: t('serviceAppsStatusHint_idle'),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function ServiceAppIconButton({
|
|
329
|
+
disabled = false,
|
|
330
|
+
icon: Icon,
|
|
331
|
+
label,
|
|
332
|
+
tooltip = label,
|
|
333
|
+
onClick,
|
|
334
|
+
}: {
|
|
335
|
+
disabled?: boolean;
|
|
336
|
+
icon: LucideIcon;
|
|
337
|
+
label: string;
|
|
338
|
+
tooltip?: string;
|
|
339
|
+
onClick: () => void;
|
|
340
|
+
}) {
|
|
341
|
+
return (
|
|
342
|
+
<Tooltip>
|
|
343
|
+
<TooltipTrigger asChild>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={onClick}
|
|
347
|
+
disabled={disabled}
|
|
348
|
+
className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-800 disabled:cursor-not-allowed disabled:text-gray-300 disabled:opacity-60 disabled:hover:bg-transparent"
|
|
349
|
+
aria-label={label}
|
|
350
|
+
>
|
|
351
|
+
<Icon className="h-3.5 w-3.5" />
|
|
352
|
+
</button>
|
|
353
|
+
</TooltipTrigger>
|
|
354
|
+
<TooltipContent side="bottom" className="max-w-56 text-xs">{tooltip}</TooltipContent>
|
|
355
|
+
</Tooltip>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ServiceAppMenuItem({
|
|
360
|
+
destructive = false,
|
|
361
|
+
disabled = false,
|
|
362
|
+
icon: Icon,
|
|
363
|
+
label,
|
|
364
|
+
onClick,
|
|
365
|
+
}: {
|
|
366
|
+
destructive?: boolean;
|
|
367
|
+
disabled?: boolean;
|
|
368
|
+
icon: LucideIcon;
|
|
369
|
+
label: string;
|
|
370
|
+
onClick: () => void;
|
|
371
|
+
}) {
|
|
372
|
+
return (
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
className={cn(
|
|
376
|
+
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
|
377
|
+
destructive ? 'text-destructive hover:bg-destructive/10' : 'text-gray-700 hover:bg-gray-100',
|
|
378
|
+
)}
|
|
379
|
+
disabled={disabled}
|
|
380
|
+
onClick={onClick}
|
|
381
|
+
>
|
|
382
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
383
|
+
<span>{label}</span>
|
|
384
|
+
</button>
|
|
195
385
|
);
|
|
196
386
|
}
|
|
197
387
|
|
|
@@ -39,6 +39,20 @@ export function useRestartServiceApp() {
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export function useDeleteServiceApp() {
|
|
43
|
+
const queryClient = useQueryClient();
|
|
44
|
+
return useMutation({
|
|
45
|
+
mutationFn: (appId: string) => nextclawClient.serviceApps.deleteServiceApp(appId),
|
|
46
|
+
onSuccess: async () => {
|
|
47
|
+
await Promise.all([
|
|
48
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_APPS_QUERY_KEY }),
|
|
49
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_ACTIONS_QUERY_KEY }),
|
|
50
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_ACTION_GRANTS_QUERY_KEY }),
|
|
51
|
+
]);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
export function useDiscoverServiceAppActions() {
|
|
43
57
|
const queryClient = useQueryClient();
|
|
44
58
|
return useMutation({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import { getSideDockBuiltInItems } from '@/features/side-dock/configs/side-dock-built-in-items.config';
|
|
4
4
|
import { SideDock } from '@/features/side-dock/components/side-dock';
|
|
5
5
|
import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
|
|
6
6
|
import type { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
|
|
@@ -28,7 +28,8 @@ describe('SideDock', () => {
|
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
expect(screen.getByTestId('side-dock')).toBeTruthy();
|
|
31
|
-
|
|
31
|
+
const builtInItems = getSideDockBuiltInItems();
|
|
32
|
+
builtInItems.forEach((item) => {
|
|
32
33
|
expect(document.querySelector(`[data-side-dock-item-id="${item.id}"]`)).toBeTruthy();
|
|
33
34
|
});
|
|
34
35
|
|
|
@@ -36,7 +37,7 @@ describe('SideDock', () => {
|
|
|
36
37
|
expect(appsButton).toBeTruthy();
|
|
37
38
|
fireEvent.click(appsButton as Element);
|
|
38
39
|
|
|
39
|
-
expect(openItem).toHaveBeenCalledWith(
|
|
40
|
+
expect(openItem).toHaveBeenCalledWith(builtInItems[0]);
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
it('does not highlight the default docs tab while DocBrowser is closed', () => {
|
|
@@ -55,4 +56,58 @@ describe('SideDock', () => {
|
|
|
55
56
|
expect(docsButton?.className).toContain('bg-transparent');
|
|
56
57
|
expect(docsButton?.className).not.toContain('shadow-sm');
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
it('renders removable pinned entries and calls manager unpin by item id', () => {
|
|
61
|
+
const manager = {
|
|
62
|
+
openItem: vi.fn(),
|
|
63
|
+
unpinItem: vi.fn(),
|
|
64
|
+
} as unknown as SideDockManager;
|
|
65
|
+
const docBrowserManager = new DocBrowserManager();
|
|
66
|
+
useSideDockStore.getState().setPinnedItems([
|
|
67
|
+
{
|
|
68
|
+
createdAt: '2026-06-02T00:00:00.000Z',
|
|
69
|
+
icon: { type: 'builtin', name: 'docs' },
|
|
70
|
+
id: 'pinned-docs-custom',
|
|
71
|
+
label: 'Custom Docs',
|
|
72
|
+
target: { type: 'right-panel-resource', uri: 'nextclaw://docs/custom' },
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
render(
|
|
77
|
+
<DocBrowserProvider manager={docBrowserManager}>
|
|
78
|
+
<SideDock manager={manager} />
|
|
79
|
+
</DocBrowserProvider>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(document.querySelector('[data-side-dock-item-id="pinned-docs-custom"]')).toBeTruthy();
|
|
83
|
+
|
|
84
|
+
fireEvent.click(screen.getByTitle('Remove shortcut'));
|
|
85
|
+
|
|
86
|
+
expect(manager.unpinItem).toHaveBeenCalledWith('pinned-docs-custom');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('renders text icons for pinned entries', () => {
|
|
90
|
+
const manager = {
|
|
91
|
+
openItem: vi.fn(),
|
|
92
|
+
unpinItem: vi.fn(),
|
|
93
|
+
} as unknown as SideDockManager;
|
|
94
|
+
const docBrowserManager = new DocBrowserManager();
|
|
95
|
+
useSideDockStore.getState().setPinnedItems([
|
|
96
|
+
{
|
|
97
|
+
createdAt: '2026-06-02T00:00:00.000Z',
|
|
98
|
+
icon: { type: 'text', value: 'D' },
|
|
99
|
+
id: 'pinned-panel-app-demo',
|
|
100
|
+
label: 'Demo App',
|
|
101
|
+
target: { type: 'right-panel-resource', uri: 'nextclaw://panel-app/demo' },
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
render(
|
|
106
|
+
<DocBrowserProvider manager={docBrowserManager}>
|
|
107
|
+
<SideDock manager={manager} />
|
|
108
|
+
</DocBrowserProvider>,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText('D')).toBeTruthy();
|
|
112
|
+
});
|
|
58
113
|
});
|