@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
|
@@ -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
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AppWindow, BookOpen, Boxes, Plus } from 'lucide-react';
|
|
1
|
+
import { AppWindow, BookOpen, Boxes, Plus, X } from 'lucide-react';
|
|
2
2
|
import type { LucideIcon } from 'lucide-react';
|
|
3
3
|
import type { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
|
|
4
|
-
import {
|
|
4
|
+
import { getSideDockBuiltInItems } from '@/features/side-dock/configs/side-dock-built-in-items.config';
|
|
5
5
|
import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
|
|
6
6
|
import type {
|
|
7
7
|
SideDockIconName,
|
|
@@ -30,6 +30,7 @@ const SIDE_DOCK_ICON_COMPONENTS: Record<SideDockIconName, LucideIcon> = {
|
|
|
30
30
|
apps: Boxes,
|
|
31
31
|
docs: BookOpen,
|
|
32
32
|
'new-tab': Plus,
|
|
33
|
+
'panel-app': AppWindow,
|
|
33
34
|
'service-apps': AppWindow,
|
|
34
35
|
};
|
|
35
36
|
|
|
@@ -37,6 +38,13 @@ function SideDockItemIconView({ icon }: { icon: SideDockItemIcon }) {
|
|
|
37
38
|
if (icon.type === 'url') {
|
|
38
39
|
return <img src={icon.url} alt="" className="h-5 w-5 rounded object-cover" />;
|
|
39
40
|
}
|
|
41
|
+
if (icon.type === 'text') {
|
|
42
|
+
return (
|
|
43
|
+
<span className="max-w-7 truncate text-center text-[13px] font-semibold leading-none" aria-hidden="true">
|
|
44
|
+
{icon.value}
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
40
48
|
|
|
41
49
|
const Icon = SIDE_DOCK_ICON_COMPONENTS[icon.name];
|
|
42
50
|
return <Icon className="h-5 w-5" aria-hidden="true" />;
|
|
@@ -46,56 +54,100 @@ function SideDockButton({
|
|
|
46
54
|
active,
|
|
47
55
|
item,
|
|
48
56
|
onOpen,
|
|
57
|
+
onUnpin,
|
|
49
58
|
}: {
|
|
50
59
|
active: boolean;
|
|
51
60
|
item: SideDockItem;
|
|
52
61
|
onOpen: (item: SideDockItem) => void;
|
|
62
|
+
onUnpin: (item: SideDockItem) => void;
|
|
53
63
|
}) {
|
|
54
|
-
const label =
|
|
64
|
+
const { label } = item;
|
|
55
65
|
|
|
56
66
|
return (
|
|
57
67
|
<Tooltip>
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
<div className="group relative">
|
|
69
|
+
<TooltipTrigger asChild>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
aria-label={label}
|
|
73
|
+
title={label}
|
|
74
|
+
data-side-dock-item-id={item.id}
|
|
75
|
+
onClick={() => onOpen(item)}
|
|
76
|
+
className={cn(
|
|
77
|
+
'flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors',
|
|
78
|
+
'hover:bg-primary/10 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
79
|
+
active
|
|
80
|
+
? 'bg-primary/10 text-primary'
|
|
81
|
+
: 'bg-transparent',
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<SideDockItemIconView icon={item.icon} />
|
|
85
|
+
</button>
|
|
86
|
+
</TooltipTrigger>
|
|
87
|
+
{item.removable ? (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
aria-label={t('sideDockUnpinItem')}
|
|
91
|
+
title={t('sideDockUnpinItem')}
|
|
92
|
+
onClick={(event) => {
|
|
93
|
+
event.stopPropagation();
|
|
94
|
+
onUnpin(item);
|
|
95
|
+
}}
|
|
96
|
+
className="absolute -right-1 -top-1 hidden h-4 w-4 items-center justify-center rounded-full bg-background text-muted-foreground shadow-sm ring-1 ring-border/70 transition-colors hover:bg-muted hover:text-foreground group-hover:flex focus-visible:flex focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
97
|
+
>
|
|
98
|
+
<X className="h-3 w-3" aria-hidden="true" />
|
|
99
|
+
</button>
|
|
100
|
+
) : null}
|
|
101
|
+
</div>
|
|
76
102
|
<TooltipContent side="left">{label}</TooltipContent>
|
|
77
103
|
</Tooltip>
|
|
78
104
|
);
|
|
79
105
|
}
|
|
80
106
|
|
|
107
|
+
function normalizeSideDockUri(uri?: string): string {
|
|
108
|
+
return uri?.trim() ?? '';
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
function isSideDockItemActive(
|
|
82
112
|
item: SideDockItem,
|
|
83
113
|
isDocBrowserOpen: boolean,
|
|
84
114
|
currentTab?: DocBrowserTab,
|
|
115
|
+
hasExactActiveItem = false,
|
|
85
116
|
): boolean {
|
|
86
117
|
if (!isDocBrowserOpen || !currentTab) {
|
|
87
118
|
return false;
|
|
88
119
|
}
|
|
89
|
-
|
|
120
|
+
const itemUri = normalizeSideDockUri(item.target.uri);
|
|
121
|
+
const currentResourceUri = normalizeSideDockUri(currentTab.resourceUri);
|
|
122
|
+
if (
|
|
123
|
+
(currentResourceUri && currentResourceUri === itemUri)
|
|
124
|
+
|| normalizeSideDockUri(currentTab.currentUrl) === itemUri
|
|
125
|
+
) {
|
|
90
126
|
return true;
|
|
91
127
|
}
|
|
128
|
+
if (hasExactActiveItem) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
92
131
|
return item.id === 'docs' && currentTab.kind === 'docs';
|
|
93
132
|
}
|
|
94
133
|
|
|
134
|
+
function hasExactActiveSideDockItem(items: SideDockItem[], currentTab?: DocBrowserTab): boolean {
|
|
135
|
+
if (!currentTab) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const currentResourceUri = normalizeSideDockUri(currentTab.resourceUri);
|
|
139
|
+
const currentUrl = normalizeSideDockUri(currentTab.currentUrl);
|
|
140
|
+
return items.some((item) => {
|
|
141
|
+
const itemUri = normalizeSideDockUri(item.target.uri);
|
|
142
|
+
return itemUri === currentResourceUri || itemUri === currentUrl;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
95
146
|
export function SideDock({ manager }: SideDockProps) {
|
|
96
147
|
const pinnedItems = useSideDockStore((state) => state.pinnedItems);
|
|
97
148
|
const { currentTab, isOpen } = useDocBrowser();
|
|
98
|
-
const items = mergeSideDockItems(
|
|
149
|
+
const items = mergeSideDockItems(getSideDockBuiltInItems(), pinnedItems);
|
|
150
|
+
const hasExactActiveItem = hasExactActiveSideDockItem(items, currentTab);
|
|
99
151
|
|
|
100
152
|
return (
|
|
101
153
|
<TooltipProvider delayDuration={250}>
|
|
@@ -106,9 +158,10 @@ export function SideDock({ manager }: SideDockProps) {
|
|
|
106
158
|
{items.map((item) => (
|
|
107
159
|
<SideDockButton
|
|
108
160
|
key={item.id}
|
|
109
|
-
active={isSideDockItemActive(item, isOpen, currentTab)}
|
|
161
|
+
active={isSideDockItemActive(item, isOpen, currentTab, hasExactActiveItem)}
|
|
110
162
|
item={item}
|
|
111
163
|
onOpen={manager.openItem}
|
|
164
|
+
onUnpin={(dockItem) => manager.unpinItem(dockItem.id)}
|
|
112
165
|
/>
|
|
113
166
|
))}
|
|
114
167
|
</aside>
|