@nextclaw/ui 0.15.26 → 0.15.28
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 +23 -0
- package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
- package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-KEYKkxHu.js} +1 -1
- package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-eXLXPgKT.js} +2 -2
- package/dist/assets/{chat-page-D4wehRTz.js → chat-page-jfImLdsq.js} +1 -1
- package/dist/assets/{config-split-page-CceNFvlY.js → config-split-page-ps1Aod3g.js} +1 -1
- package/dist/assets/{confirm-dialog-CWeNYWZY.js → confirm-dialog-DJSUNG33.js} +2 -2
- package/dist/assets/desktop-update-config-C20U71k1.js +1 -0
- package/dist/assets/{dist-D9xtGgdL.js → dist-BkTYNzm_.js} +1 -1
- package/dist/assets/{dist-AYxkFx2B.js → dist-DkR7_Dpl.js} +1 -1
- package/dist/assets/doc-browser-DahzJEWj.js +1 -0
- package/dist/assets/doc-browser-Q67TOGVS.js +1 -0
- package/dist/assets/{doc-browser-context-Cb7Z0j90.js → doc-browser-context-BQim8SqD.js} +1 -1
- package/dist/assets/form-actions-B2-bpFie.js +1 -0
- package/dist/assets/index-BP0Hd2jI.js +109 -0
- package/dist/assets/index-DSLK7zgR.css +1 -0
- package/dist/assets/loader-circle-DyXZU92l.js +1 -0
- package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +1 -0
- package/dist/assets/mcp-marketplace-page-w8panjZG.js +9 -0
- package/dist/assets/{model-config-page-DfOLyQqR.js → model-config-page-CJCnaKOh.js} +1 -1
- package/dist/assets/{navigation-link-MiiyoIjH.js → navigation-link-mpHFXwRr.js} +2 -2
- package/dist/assets/plus-Dhb4KuMH.js +1 -0
- package/dist/assets/{provider-scoped-model-input-DXzjr5l0.js → provider-scoped-model-input-Dqu-ZoWG.js} +1 -1
- package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-C6fRN2rJ.js} +1 -1
- package/dist/assets/react-BlXwvOLo.js +8 -0
- package/dist/assets/{refresh-cw-a2VVeeAF.js → refresh-cw-JFuyDj_P.js} +1 -1
- package/dist/assets/remote-Xyv2c_W9.js +1 -0
- package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
- package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-CtypMgLd.js} +1 -1
- package/dist/assets/{save-CGm23Yww.js → save-BL8P8CLX.js} +1 -1
- package/dist/assets/{search-CH2t--sH.js → search-bZL75Pgc.js} +1 -1
- package/dist/assets/{search-config-page-BAmYqoAz.js → search-config-page-DhSFjYj4.js} +1 -1
- package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-BLGcyb1D.js} +2 -2
- package/dist/assets/security-config-DmJw9QZA.js +1 -0
- package/dist/assets/{select-DnxxDpcV.js → select-rUZ8nIZu.js} +2 -2
- package/dist/assets/{settings-2-agnMYc8a.js → settings-2-BI4SCkxQ.js} +1 -1
- package/dist/assets/skeleton-CVmEPG43.js +1 -0
- package/dist/assets/{tag-chip-CPOc8b2N.js → tag-chip-BdI_yIOA.js} +1 -1
- package/dist/assets/x-BcEayhV6.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +6 -6
- package/src/app/components/layout/app-layout.tsx +38 -2
- package/src/app/hooks/__tests__/use-app-event-consumers.test.ts +20 -0
- package/src/app/hooks/use-app-event-consumers.ts +21 -0
- package/src/app/presenters/app.presenter.ts +6 -0
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +116 -14
- package/src/features/apps/components/__tests__/apps-panel.test.tsx +58 -0
- package/src/features/apps/components/app-artwork.tsx +49 -0
- package/src/features/apps/components/app-marketplace-catalog.tsx +332 -0
- package/src/features/apps/components/app-marketplace-cover.tsx +64 -0
- package/src/features/apps/components/app-marketplace-detail.tsx +161 -0
- package/src/features/apps/components/app-marketplace-dialog.tsx +107 -217
- package/src/features/apps/components/app-marketplace-operation.tsx +84 -0
- package/src/features/apps/components/app-package-card.tsx +201 -52
- package/src/features/apps/components/app-packages-panel.tsx +120 -42
- package/src/features/apps/components/apps-panel.tsx +28 -27
- package/src/features/apps/hooks/use-app-marketplace.test.ts +144 -0
- package/src/features/apps/hooks/use-app-marketplace.ts +55 -85
- package/src/features/apps/hooks/use-app-packages.ts +59 -4
- package/src/features/apps/index.ts +1 -0
- package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +81 -0
- package/src/features/apps/managers/app-package-operation-settlement.manager.ts +80 -0
- package/src/features/apps/services/app-marketplace.service.ts +223 -0
- package/src/features/apps/types/app-marketplace.types.ts +75 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +64 -3
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +24 -14
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +53 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +18 -0
- package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +4 -1
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +126 -32
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +12 -2
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +31 -0
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
- package/src/features/chat/features/input/utils/chat-inline-token-composer.utils.ts +14 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +25 -0
- package/src/features/chat/features/message/hooks/use-chat-inline-token-actions.ts +8 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +77 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +56 -50
- package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +33 -11
- package/src/features/chat/features/workspace/components/project-files/workspace-project-tree-view.tsx +93 -0
- package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts +38 -0
- package/src/features/chat/features/workspace/stores/workspace-project-tree.store.ts +160 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +29 -0
- package/src/features/chat/managers/chat-composer-intent.manager.ts +29 -2
- package/src/features/chat/presenters/chat.presenter.ts +2 -2
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +1 -1
- package/src/features/panel-apps/components/panel-apps-list.tsx +1 -6
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +34 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +29 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +3 -17
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -0
- package/src/platforms/mobile/components/mobile-app-shell.tsx +9 -1
- package/src/shared/components/actions/__tests__/navigation-link.test.tsx +12 -0
- package/src/shared/components/actions/navigation-link.tsx +6 -3
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +5 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +11 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +13 -0
- package/src/shared/components/ui/tabs.tsx +24 -1
- package/src/shared/hooks/use-server-path-watch.ts +42 -0
- package/src/shared/lib/api/server-path/server-path.types.ts +10 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +10 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +49 -7
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +49 -7
- package/vite.config.ts +32 -1
- package/dist/assets/desktop-update-config-Dirc3bpt.js +0 -1
- package/dist/assets/doc-browser-4Sud6dWw.js +0 -1
- package/dist/assets/doc-browser-D4x_yH5F.js +0 -1
- package/dist/assets/form-actions-B2q4jgjE.js +0 -1
- package/dist/assets/index-CdyTsSBu.css +0 -1
- package/dist/assets/index-Das_bw2h.js +0 -107
- package/dist/assets/loader-circle-CpmBdaon.js +0 -1
- package/dist/assets/mcp-marketplace-page-CdOY3Sop.js +0 -1
- package/dist/assets/mcp-marketplace-page-DvjrfqEt.js +0 -9
- package/dist/assets/plus-C8FkjpSZ.js +0 -1
- package/dist/assets/react-C8XFFvuz.js +0 -8
- package/dist/assets/remote-BJHXa4mB.js +0 -1
- package/dist/assets/rotate-cw-DJ7UF3F2.js +0 -1
- package/dist/assets/security-config-CbDiY2xr.js +0 -1
- package/dist/assets/skeleton-XpqeWL62.js +0 -1
- package/dist/assets/x-odTHAtLZ.js +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import type { AppPackageView } from '@nextclaw/client-sdk';
|
|
2
|
+
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
3
3
|
import {
|
|
4
|
+
AlertCircle,
|
|
4
5
|
AppWindow,
|
|
5
6
|
Bookmark,
|
|
6
7
|
CalendarDays,
|
|
8
|
+
Check,
|
|
7
9
|
CheckSquare2,
|
|
8
10
|
ChevronRight,
|
|
9
11
|
MoreHorizontal,
|
|
@@ -12,12 +14,22 @@ import {
|
|
|
12
14
|
RotateCcw,
|
|
13
15
|
Server,
|
|
14
16
|
Trash2,
|
|
17
|
+
LoaderCircle,
|
|
15
18
|
type LucideIcon,
|
|
16
19
|
} from 'lucide-react';
|
|
20
|
+
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
21
|
+
import { isAppPackageOperationActive } from '@/features/apps/hooks/use-app-packages';
|
|
17
22
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
18
23
|
import { Button } from '@/shared/components/ui/button';
|
|
19
24
|
import { Card } from '@/shared/components/ui/card';
|
|
20
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
Dialog,
|
|
27
|
+
DialogContent,
|
|
28
|
+
DialogDescription,
|
|
29
|
+
DialogFooter,
|
|
30
|
+
DialogHeader,
|
|
31
|
+
DialogTitle,
|
|
32
|
+
} from '@/shared/components/ui/dialog';
|
|
21
33
|
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
22
34
|
import { getLanguage, t } from '@/shared/lib/i18n';
|
|
23
35
|
import { cn } from '@/shared/lib/utils';
|
|
@@ -25,6 +37,7 @@ import { cn } from '@/shared/lib/utils';
|
|
|
25
37
|
export function AppPackageCard({
|
|
26
38
|
appPackage,
|
|
27
39
|
isPending,
|
|
40
|
+
operation,
|
|
28
41
|
onDisable,
|
|
29
42
|
onEnable,
|
|
30
43
|
onOpenPanelApp,
|
|
@@ -35,33 +48,35 @@ export function AppPackageCard({
|
|
|
35
48
|
}: {
|
|
36
49
|
appPackage: AppPackageView;
|
|
37
50
|
isPending: boolean;
|
|
51
|
+
operation?: AppPackageOperationView;
|
|
38
52
|
onDisable: () => void;
|
|
39
53
|
onEnable: () => void;
|
|
40
54
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
41
55
|
onRollback: (version: string) => void;
|
|
42
|
-
onUninstall: () => void;
|
|
56
|
+
onUninstall: (purgeData: boolean) => void;
|
|
43
57
|
onUpdate: () => void;
|
|
44
58
|
panelApps: PanelAppEntryView[];
|
|
45
59
|
}) {
|
|
46
60
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
47
61
|
const [uninstallOpen, setUninstallOpen] = useState(false);
|
|
62
|
+
const [purgeData, setPurgeData] = useState(false);
|
|
48
63
|
const panelComponents = appPackage.components.filter((component) => component.kind === 'panel');
|
|
49
64
|
const serviceCount = appPackage.components.length - panelComponents.length;
|
|
50
65
|
const rollbackVersions = appPackage.installedVersions.filter(
|
|
51
66
|
(version) => version !== appPackage.activeVersion,
|
|
52
67
|
);
|
|
53
|
-
const displayName = readLocalizedText(appPackage.name, appPackage.nameI18n);
|
|
68
|
+
const displayName = readLocalizedText(appPackage.name, appPackage.nameI18n) ?? appPackage.id;
|
|
54
69
|
const displayDescription = readLocalizedText(
|
|
55
70
|
appPackage.description,
|
|
56
71
|
appPackage.descriptionI18n,
|
|
57
72
|
);
|
|
73
|
+
const operationActive = operation ? isAppPackageOperationActive(operation.status) : false;
|
|
74
|
+
const pending = isPending || operationActive;
|
|
58
75
|
|
|
59
76
|
return (
|
|
60
|
-
<Card surface="flat" hover={false} className="overflow-hidden">
|
|
61
|
-
<div className="flex items-start gap-3 px-
|
|
62
|
-
<
|
|
63
|
-
<AppWindow className="h-5 w-5" />
|
|
64
|
-
</div>
|
|
77
|
+
<Card surface="flat" hover={false} className="overflow-hidden border-border/60 shadow-[0_1px_2px_rgba(0,0,0,0.025)]">
|
|
78
|
+
<div className="flex items-start gap-3.5 px-4 pb-3.5 pt-4">
|
|
79
|
+
<AppArtwork icon={appPackage.icon} name={displayName} />
|
|
65
80
|
<div className="min-w-0 flex-1">
|
|
66
81
|
<div className="flex min-w-0 items-center gap-2">
|
|
67
82
|
<h2 className="truncate text-sm font-semibold text-foreground">{displayName}</h2>
|
|
@@ -95,39 +110,37 @@ export function AppPackageCard({
|
|
|
95
110
|
type="button"
|
|
96
111
|
size="sm"
|
|
97
112
|
variant={appPackage.enabled ? 'outline' : 'default'}
|
|
98
|
-
disabled={
|
|
113
|
+
disabled={pending}
|
|
99
114
|
onClick={appPackage.enabled ? onDisable : onEnable}
|
|
100
115
|
>
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
{renderPrimaryActionLabel({
|
|
117
|
+
enabled: appPackage.enabled,
|
|
118
|
+
isPending,
|
|
119
|
+
operation,
|
|
120
|
+
operationActive,
|
|
121
|
+
})}
|
|
106
122
|
</Button>
|
|
107
|
-
{
|
|
108
|
-
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
|
|
123
|
+
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
|
|
109
124
|
<PopoverTrigger asChild>
|
|
110
125
|
<button
|
|
111
126
|
type="button"
|
|
112
127
|
aria-label={t('appPackagesMoreActions')}
|
|
113
128
|
title={t('appPackagesMoreActions')}
|
|
114
|
-
disabled={
|
|
129
|
+
disabled={pending}
|
|
115
130
|
className="rounded-full p-2 text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:opacity-50"
|
|
116
131
|
>
|
|
117
132
|
<MoreHorizontal className="h-4 w-4" />
|
|
118
133
|
</button>
|
|
119
134
|
</PopoverTrigger>
|
|
120
135
|
<PopoverContent align="end" className="w-52 rounded-xl p-1.5">
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/>
|
|
130
|
-
) : null}
|
|
136
|
+
<AppPackageMenuItem
|
|
137
|
+
icon={RefreshCw}
|
|
138
|
+
label={t('appPackagesCheckUpdate')}
|
|
139
|
+
onClick={() => {
|
|
140
|
+
setMenuOpen(false);
|
|
141
|
+
onUpdate();
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
131
144
|
{rollbackVersions.map((version) => (
|
|
132
145
|
<AppPackageMenuItem
|
|
133
146
|
key={version}
|
|
@@ -139,23 +152,24 @@ export function AppPackageCard({
|
|
|
139
152
|
}}
|
|
140
153
|
/>
|
|
141
154
|
))}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
/>
|
|
152
|
-
) : null}
|
|
155
|
+
<AppPackageMenuItem
|
|
156
|
+
destructive
|
|
157
|
+
icon={Trash2}
|
|
158
|
+
label={t('appPackagesUninstall')}
|
|
159
|
+
onClick={() => {
|
|
160
|
+
setMenuOpen(false);
|
|
161
|
+
setUninstallOpen(true);
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
153
164
|
</PopoverContent>
|
|
154
165
|
</Popover>
|
|
155
|
-
) : null}
|
|
156
166
|
</div>
|
|
157
167
|
</div>
|
|
158
168
|
|
|
169
|
+
{operation && (operationActive || operation.status === 'failed' || operation.status === 'interrupted') ? (
|
|
170
|
+
<PackageOperationStatus operation={operation} />
|
|
171
|
+
) : null}
|
|
172
|
+
|
|
159
173
|
{panelComponents.length > 0 ? (
|
|
160
174
|
<div className="border-t border-border/60 bg-muted/20 p-2">
|
|
161
175
|
<div className="grid grid-cols-2 gap-1.5">
|
|
@@ -168,7 +182,7 @@ export function AppPackageCard({
|
|
|
168
182
|
<button
|
|
169
183
|
key={component.id}
|
|
170
184
|
type="button"
|
|
171
|
-
disabled={!appPackage.enabled || !panelApp ||
|
|
185
|
+
disabled={!appPackage.enabled || !panelApp || pending}
|
|
172
186
|
onClick={() => panelApp && onOpenPanelApp({
|
|
173
187
|
...panelApp,
|
|
174
188
|
title: componentTitle,
|
|
@@ -196,20 +210,108 @@ export function AppPackageCard({
|
|
|
196
210
|
</div>
|
|
197
211
|
) : null}
|
|
198
212
|
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
<Dialog open={uninstallOpen} onOpenChange={setUninstallOpen}>
|
|
214
|
+
<DialogContent className="max-w-md [&>:last-child]:hidden">
|
|
215
|
+
<DialogHeader>
|
|
216
|
+
<DialogTitle>{t('appPackagesUninstallTitle')}</DialogTitle>
|
|
217
|
+
<DialogDescription>{t('appPackagesUninstallDescription')}</DialogDescription>
|
|
218
|
+
</DialogHeader>
|
|
219
|
+
<div className="mt-4 grid gap-2">
|
|
220
|
+
<DataChoice
|
|
221
|
+
checked={!purgeData}
|
|
222
|
+
description={t('appPackagesKeepDataDescription')}
|
|
223
|
+
label={t('appPackagesKeepData')}
|
|
224
|
+
onClick={() => setPurgeData(false)}
|
|
225
|
+
/>
|
|
226
|
+
<DataChoice
|
|
227
|
+
checked={purgeData}
|
|
228
|
+
destructive
|
|
229
|
+
description={t('appPackagesDeleteDataDescription')}
|
|
230
|
+
label={t('appPackagesDeleteData')}
|
|
231
|
+
onClick={() => setPurgeData(true)}
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
<DialogFooter className="mt-5 gap-2 sm:gap-0">
|
|
235
|
+
<Button type="button" variant="outline" onClick={() => setUninstallOpen(false)}>
|
|
236
|
+
{t('cancel')}
|
|
237
|
+
</Button>
|
|
238
|
+
<Button
|
|
239
|
+
type="button"
|
|
240
|
+
variant="destructive"
|
|
241
|
+
onClick={() => {
|
|
242
|
+
onUninstall(purgeData);
|
|
243
|
+
setUninstallOpen(false);
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
{t('appPackagesUninstall')}
|
|
247
|
+
</Button>
|
|
248
|
+
</DialogFooter>
|
|
249
|
+
</DialogContent>
|
|
250
|
+
</Dialog>
|
|
209
251
|
</Card>
|
|
210
252
|
);
|
|
211
253
|
}
|
|
212
254
|
|
|
255
|
+
function renderPrimaryActionLabel({
|
|
256
|
+
enabled,
|
|
257
|
+
isPending,
|
|
258
|
+
operation,
|
|
259
|
+
operationActive,
|
|
260
|
+
}: {
|
|
261
|
+
enabled: boolean;
|
|
262
|
+
isPending: boolean;
|
|
263
|
+
operation?: AppPackageOperationView;
|
|
264
|
+
operationActive: boolean;
|
|
265
|
+
}) {
|
|
266
|
+
if (operationActive) {
|
|
267
|
+
return (
|
|
268
|
+
<>
|
|
269
|
+
<LoaderCircle className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
270
|
+
{operation ? operationLabel(operation) : t('appPackagesWorking')}
|
|
271
|
+
</>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (isPending) return t('appPackagesWorking');
|
|
275
|
+
return enabled ? t('appPackagesDisable') : t('appPackagesEnable');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function DataChoice({
|
|
279
|
+
checked,
|
|
280
|
+
description,
|
|
281
|
+
destructive = false,
|
|
282
|
+
label,
|
|
283
|
+
onClick,
|
|
284
|
+
}: {
|
|
285
|
+
checked: boolean;
|
|
286
|
+
description: string;
|
|
287
|
+
destructive?: boolean;
|
|
288
|
+
label: string;
|
|
289
|
+
onClick: () => void;
|
|
290
|
+
}) {
|
|
291
|
+
return (
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
aria-pressed={checked}
|
|
295
|
+
onClick={onClick}
|
|
296
|
+
className={cn(
|
|
297
|
+
'flex items-start gap-3 rounded-xl border px-3 py-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border',
|
|
298
|
+
checked ? 'border-foreground/25 bg-muted/55' : 'border-border/60 hover:bg-muted/35',
|
|
299
|
+
)}
|
|
300
|
+
>
|
|
301
|
+
<span className={cn(
|
|
302
|
+
'mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border',
|
|
303
|
+
checked ? 'border-foreground bg-foreground text-background' : 'border-border',
|
|
304
|
+
)}>
|
|
305
|
+
{checked ? <Check className="h-2.5 w-2.5" /> : null}
|
|
306
|
+
</span>
|
|
307
|
+
<span>
|
|
308
|
+
<span className={cn('block text-sm font-medium', destructive ? 'text-destructive' : 'text-foreground')}>{label}</span>
|
|
309
|
+
<span className="mt-0.5 block text-xs leading-5 text-muted-foreground">{description}</span>
|
|
310
|
+
</span>
|
|
311
|
+
</button>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
213
315
|
function AppPackageMenuItem({
|
|
214
316
|
destructive = false,
|
|
215
317
|
icon: Icon,
|
|
@@ -245,12 +347,59 @@ function ComponentIcon({
|
|
|
245
347
|
fallback: LucideIcon;
|
|
246
348
|
icon?: string;
|
|
247
349
|
}) {
|
|
248
|
-
if (icon &&
|
|
350
|
+
if (icon && (
|
|
351
|
+
icon.startsWith('data:') ||
|
|
352
|
+
icon.startsWith('/') ||
|
|
353
|
+
icon.startsWith('http://') ||
|
|
354
|
+
icon.startsWith('https://')
|
|
355
|
+
)) {
|
|
356
|
+
return <img src={icon} alt="" className="h-4 w-4 rounded object-cover" />;
|
|
357
|
+
}
|
|
358
|
+
if (icon) {
|
|
249
359
|
return <span className="text-sm leading-none">{icon}</span>;
|
|
250
360
|
}
|
|
251
361
|
return <Fallback className="h-3.5 w-3.5" />;
|
|
252
362
|
}
|
|
253
363
|
|
|
364
|
+
function PackageOperationStatus({ operation }: { operation: AppPackageOperationView }) {
|
|
365
|
+
const failed = operation.status === 'failed' || operation.status === 'interrupted';
|
|
366
|
+
const progress = Math.max(4, Math.round((operation.completedSteps / operation.totalSteps) * 100));
|
|
367
|
+
return (
|
|
368
|
+
<div className="border-t border-border/50 px-4 py-2.5" role={failed ? 'alert' : 'status'} aria-live="polite">
|
|
369
|
+
<div className="flex items-center gap-2 text-[11px]">
|
|
370
|
+
{failed ? <AlertCircle className="h-3.5 w-3.5 text-destructive" /> : <LoaderCircle className="h-3.5 w-3.5 animate-spin text-primary" />}
|
|
371
|
+
<span className={failed ? 'text-destructive' : 'text-muted-foreground'}>
|
|
372
|
+
{failed ? operation.error ?? t('appPackagesActionFailed') : operationLabel(operation)}
|
|
373
|
+
</span>
|
|
374
|
+
</div>
|
|
375
|
+
<div className="mt-2 h-1 overflow-hidden rounded-full bg-muted">
|
|
376
|
+
<div
|
|
377
|
+
className={cn('h-full rounded-full transition-[width] duration-300', failed ? 'bg-destructive' : 'bg-primary')}
|
|
378
|
+
style={{ width: failed ? '100%' : `${progress}%` }}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function operationLabel(operation: AppPackageOperationView): string {
|
|
386
|
+
switch (operation.status) {
|
|
387
|
+
case 'queued': return t('appPackagesPreparing');
|
|
388
|
+
case 'resolving': return t('appPackagesResolving');
|
|
389
|
+
case 'downloading': return t('appPackagesDownloading');
|
|
390
|
+
case 'verifying': return t('appPackagesVerifying');
|
|
391
|
+
case 'installing': return operation.action === 'rollback'
|
|
392
|
+
? `${t('appPackagesSwitchingVersion')} ${operation.targetVersion ? `v${operation.targetVersion}` : ''}`.trim()
|
|
393
|
+
: operation.action === 'uninstall'
|
|
394
|
+
? t('appPackagesUninstalling')
|
|
395
|
+
: t('appPackagesInstalling');
|
|
396
|
+
case 'finalizing': return t('appPackagesFinalizing');
|
|
397
|
+
case 'succeeded': return t('appPackagesCompleted');
|
|
398
|
+
case 'failed':
|
|
399
|
+
case 'interrupted': return t('appPackagesActionFailed');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
254
403
|
function resolveComponentIcon(componentId: string): LucideIcon {
|
|
255
404
|
if (componentId.endsWith('-todos')) return CheckSquare2;
|
|
256
405
|
if (componentId.endsWith('-notes')) return NotebookText;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { useState, type FormEvent
|
|
2
|
-
import type { AppPackageView } from '@nextclaw/client-sdk';
|
|
3
|
-
import {
|
|
1
|
+
import { useMemo, useState, type FormEvent } from 'react';
|
|
2
|
+
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
3
|
+
import { LoaderCircle, PackagePlus, RefreshCw, ShieldCheck, Store } from 'lucide-react';
|
|
4
|
+
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
4
5
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
5
6
|
import { useAppPresenter } from '@/app/components/app-presenter-provider';
|
|
6
7
|
import { AppMarketplaceDialog } from '@/features/apps/components/app-marketplace-dialog';
|
|
7
8
|
import { AppPackageCard } from '@/features/apps/components/app-package-card';
|
|
8
9
|
import {
|
|
9
10
|
useAppPackageMutation,
|
|
11
|
+
useAppPackageOperationSettlement,
|
|
12
|
+
useAppPackageOperations,
|
|
10
13
|
useAppPackages,
|
|
14
|
+
isAppPackageOperationActive,
|
|
11
15
|
type AppPackageMutationInput,
|
|
12
16
|
} from '@/features/apps/hooks/use-app-packages';
|
|
13
17
|
import {
|
|
@@ -29,13 +33,12 @@ import { Skeleton } from '@/shared/components/ui/skeleton';
|
|
|
29
33
|
import { t } from '@/shared/lib/i18n';
|
|
30
34
|
|
|
31
35
|
export function AppPackagesPanel({
|
|
32
|
-
headerContent,
|
|
33
36
|
onOpenPanelApp,
|
|
34
37
|
}: {
|
|
35
|
-
headerContent?: ReactNode;
|
|
36
38
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
37
39
|
}) {
|
|
38
40
|
const appPackages = useAppPackages();
|
|
41
|
+
const appPackageOperations = useAppPackageOperations();
|
|
39
42
|
const panelApps = usePanelApps();
|
|
40
43
|
const lifecycle = useAppPackageMutation();
|
|
41
44
|
const recordOpened = useRecordPanelAppOpened();
|
|
@@ -45,6 +48,19 @@ export function AppPackagesPanel({
|
|
|
45
48
|
const [installSource, setInstallSource] = useState('');
|
|
46
49
|
const [marketplaceOpen, setMarketplaceOpen] = useState(false);
|
|
47
50
|
|
|
51
|
+
const operations = useMemo(
|
|
52
|
+
() => appPackageOperations.data?.entries ?? [],
|
|
53
|
+
[appPackageOperations.data?.entries],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
useAppPackageOperationSettlement({
|
|
57
|
+
isLoaded: Boolean(appPackageOperations.data),
|
|
58
|
+
operations,
|
|
59
|
+
refetchPackages: appPackages.refetch,
|
|
60
|
+
refetchPanels: panelApps.refetch,
|
|
61
|
+
settlementManager: presenter.appPackageOperationSettlementManager,
|
|
62
|
+
});
|
|
63
|
+
|
|
48
64
|
const runMutation = (input: AppPackageMutationInput, onSuccess?: () => void) => {
|
|
49
65
|
lifecycle.reset();
|
|
50
66
|
lifecycle.mutate(input, { onSuccess });
|
|
@@ -83,14 +99,13 @@ export function AppPackagesPanel({
|
|
|
83
99
|
|
|
84
100
|
const refetch = () => {
|
|
85
101
|
void appPackages.refetch();
|
|
102
|
+
void appPackageOperations.refetch();
|
|
86
103
|
void panelApps.refetch();
|
|
87
104
|
};
|
|
88
105
|
|
|
89
106
|
return (
|
|
90
107
|
<div className="@container flex h-full min-h-0 flex-col bg-card text-card-foreground">
|
|
91
|
-
<div className="flex
|
|
92
|
-
<div className="w-full min-w-0 overflow-x-auto @[42rem]:flex-1">{headerContent}</div>
|
|
93
|
-
<div className="ml-auto flex shrink-0 items-center gap-1">
|
|
108
|
+
<div className="flex min-h-12 shrink-0 items-center justify-end gap-1 border-b border-border/70 px-4 py-2">
|
|
94
109
|
<button
|
|
95
110
|
type="button"
|
|
96
111
|
onClick={refetch}
|
|
@@ -124,23 +139,15 @@ export function AppPackagesPanel({
|
|
|
124
139
|
<PackagePlus className="mr-1.5 h-3.5 w-3.5" />
|
|
125
140
|
{t('appPackagesInstall')}
|
|
126
141
|
</Button>
|
|
127
|
-
</div>
|
|
128
142
|
</div>
|
|
129
143
|
|
|
130
144
|
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto">
|
|
131
|
-
<div className="mx-auto w-full max-w-
|
|
132
|
-
<div className="mb-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<div>
|
|
138
|
-
<h1 className="text-sm font-semibold text-foreground">{t('appPackagesLibraryTitle')}</h1>
|
|
139
|
-
<p className="mt-0.5 text-xs leading-5 text-muted-foreground">
|
|
140
|
-
{t('appPackagesLibraryDescription')}
|
|
141
|
-
</p>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
145
|
+
<div className="mx-auto w-full max-w-3xl p-3 sm:p-5">
|
|
146
|
+
<div className="mb-4 px-1">
|
|
147
|
+
<h1 className="text-base font-semibold tracking-tight text-foreground">{t('appPackagesLibraryTitle')}</h1>
|
|
148
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
149
|
+
{t('appPackagesLibraryDescription')}
|
|
150
|
+
</p>
|
|
144
151
|
</div>
|
|
145
152
|
|
|
146
153
|
{lifecycle.isError ? (
|
|
@@ -158,6 +165,7 @@ export function AppPackagesPanel({
|
|
|
158
165
|
: undefined}
|
|
159
166
|
packages={appPackages.data?.entries ?? []}
|
|
160
167
|
panelApps={panelApps.data?.entries ?? []}
|
|
168
|
+
operations={operations}
|
|
161
169
|
onInstall={() => setInstallOpen(true)}
|
|
162
170
|
onMutate={runMutation}
|
|
163
171
|
onOpenPanelApp={(entry) => void openPanelApp(entry)}
|
|
@@ -168,17 +176,19 @@ export function AppPackagesPanel({
|
|
|
168
176
|
<AppMarketplaceDialog
|
|
169
177
|
error={lifecycle.error instanceof Error ? lifecycle.error : null}
|
|
170
178
|
installedPackages={appPackages.data?.entries ?? []}
|
|
171
|
-
|
|
179
|
+
startingSource={lifecycle.variables?.action === 'install'
|
|
172
180
|
? lifecycle.variables.source
|
|
173
181
|
: undefined}
|
|
174
|
-
|
|
182
|
+
isStarting={lifecycle.isPending}
|
|
183
|
+
operations={operations}
|
|
175
184
|
open={marketplaceOpen}
|
|
176
185
|
onOpenChange={setMarketplaceOpen}
|
|
177
186
|
onInstall={(source, registryUrl) => runMutation({
|
|
178
187
|
action: 'install',
|
|
179
188
|
source,
|
|
180
189
|
registryUrl,
|
|
181
|
-
}
|
|
190
|
+
})}
|
|
191
|
+
onUpdate={(appId) => runMutation({ action: 'update', appId })}
|
|
182
192
|
/>
|
|
183
193
|
|
|
184
194
|
<Dialog open={installOpen} onOpenChange={setInstallOpen}>
|
|
@@ -231,6 +241,7 @@ function AppPackageLibrary({
|
|
|
231
241
|
onOpenPanelApp,
|
|
232
242
|
packages,
|
|
233
243
|
panelApps,
|
|
244
|
+
operations,
|
|
234
245
|
}: {
|
|
235
246
|
error?: string;
|
|
236
247
|
isLoading: boolean;
|
|
@@ -241,6 +252,7 @@ function AppPackageLibrary({
|
|
|
241
252
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
242
253
|
packages: AppPackageView[];
|
|
243
254
|
panelApps: PanelAppEntryView[];
|
|
255
|
+
operations: AppPackageOperationView[];
|
|
244
256
|
}) {
|
|
245
257
|
if (isLoading) {
|
|
246
258
|
return (
|
|
@@ -258,6 +270,13 @@ function AppPackageLibrary({
|
|
|
258
270
|
);
|
|
259
271
|
}
|
|
260
272
|
if (packages.length === 0) {
|
|
273
|
+
const activeInstalls = operations.filter((entry) =>
|
|
274
|
+
entry.action === 'install' && isAppPackageOperationActive(entry.status));
|
|
275
|
+
if (activeInstalls.length > 0) {
|
|
276
|
+
return <div className="space-y-2.5">{activeInstalls.map((operation) => (
|
|
277
|
+
<PendingInstallCard key={operation.id} operation={operation} />
|
|
278
|
+
))}</div>;
|
|
279
|
+
}
|
|
261
280
|
return (
|
|
262
281
|
<div className="flex flex-col items-center rounded-2xl border border-dashed border-border px-6 py-10 text-center">
|
|
263
282
|
<PackagePlus className="h-7 w-7 text-muted-foreground" />
|
|
@@ -273,28 +292,87 @@ function AppPackageLibrary({
|
|
|
273
292
|
}
|
|
274
293
|
return (
|
|
275
294
|
<div className="space-y-2.5">
|
|
276
|
-
{
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
onEnable={() => onMutate({ action: 'enable', appId: appPackage.id })}
|
|
283
|
-
onDisable={() => onMutate({ action: 'disable', appId: appPackage.id })}
|
|
284
|
-
onUpdate={() => onMutate({ action: 'update', appId: appPackage.id })}
|
|
285
|
-
onRollback={(version) => onMutate({
|
|
286
|
-
action: 'rollback',
|
|
287
|
-
appId: appPackage.id,
|
|
288
|
-
version,
|
|
289
|
-
})}
|
|
290
|
-
onUninstall={() => onMutate({ action: 'uninstall', appId: appPackage.id })}
|
|
291
|
-
onOpenPanelApp={onOpenPanelApp}
|
|
292
|
-
/>
|
|
295
|
+
{operations.filter((entry) =>
|
|
296
|
+
entry.action === 'install' &&
|
|
297
|
+
isAppPackageOperationActive(entry.status) &&
|
|
298
|
+
!packages.some((appPackage) => appPackage.id === entry.appId),
|
|
299
|
+
).map((operation) => (
|
|
300
|
+
<PendingInstallCard key={operation.id} operation={operation} />
|
|
293
301
|
))}
|
|
302
|
+
{packages.map((appPackage) => {
|
|
303
|
+
const latestOperation = operations.find((entry) => entry.appId === appPackage.id);
|
|
304
|
+
const visibleOperation = latestOperation && (
|
|
305
|
+
isAppPackageOperationActive(latestOperation.status) ||
|
|
306
|
+
latestOperation.status === 'failed' ||
|
|
307
|
+
latestOperation.status === 'interrupted'
|
|
308
|
+
)
|
|
309
|
+
? latestOperation
|
|
310
|
+
: undefined;
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<AppPackageCard
|
|
314
|
+
key={appPackage.id}
|
|
315
|
+
appPackage={appPackage}
|
|
316
|
+
panelApps={panelApps.filter((entry) => entry.packageId === appPackage.id)}
|
|
317
|
+
isPending={isPending && mutationAppId === appPackage.id}
|
|
318
|
+
operation={visibleOperation}
|
|
319
|
+
onEnable={() => onMutate({ action: 'enable', appId: appPackage.id })}
|
|
320
|
+
onDisable={() => onMutate({ action: 'disable', appId: appPackage.id })}
|
|
321
|
+
onUpdate={() => onMutate({ action: 'update', appId: appPackage.id })}
|
|
322
|
+
onRollback={(version) => onMutate({
|
|
323
|
+
action: 'rollback',
|
|
324
|
+
appId: appPackage.id,
|
|
325
|
+
version,
|
|
326
|
+
})}
|
|
327
|
+
onUninstall={(purgeData) => onMutate({
|
|
328
|
+
action: 'uninstall',
|
|
329
|
+
appId: appPackage.id,
|
|
330
|
+
purgeData,
|
|
331
|
+
})}
|
|
332
|
+
onOpenPanelApp={onOpenPanelApp}
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
})}
|
|
294
336
|
</div>
|
|
295
337
|
);
|
|
296
338
|
}
|
|
297
339
|
|
|
340
|
+
function PendingInstallCard({ operation }: { operation: AppPackageOperationView }) {
|
|
341
|
+
const label = operation.appId ?? operation.source ?? t('appPackagesInstall');
|
|
342
|
+
const progress = Math.max(4, Math.round((operation.completedSteps / operation.totalSteps) * 100));
|
|
343
|
+
return (
|
|
344
|
+
<div className="rounded-xl border border-border/60 bg-card px-4 py-3.5 shadow-[0_1px_2px_rgba(0,0,0,0.025)]" role="status" aria-live="polite">
|
|
345
|
+
<div className="flex items-center gap-3">
|
|
346
|
+
<AppArtwork name={label} />
|
|
347
|
+
<div className="min-w-0 flex-1">
|
|
348
|
+
<p className="truncate text-sm font-semibold text-foreground">{label}</p>
|
|
349
|
+
<p className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
350
|
+
<LoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
|
351
|
+
{operationProgressLabel(operation)}
|
|
352
|
+
</p>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
<div className="ml-14 mt-2 h-1 overflow-hidden rounded-full bg-muted">
|
|
356
|
+
<div className="h-full rounded-full bg-primary transition-[width] duration-300" style={{ width: `${progress}%` }} />
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function operationProgressLabel(operation: AppPackageOperationView): string {
|
|
363
|
+
switch (operation.status) {
|
|
364
|
+
case 'queued': return t('appPackagesPreparing');
|
|
365
|
+
case 'resolving': return t('appPackagesResolving');
|
|
366
|
+
case 'downloading': return t('appPackagesDownloading');
|
|
367
|
+
case 'verifying': return t('appPackagesVerifying');
|
|
368
|
+
case 'installing': return t('appPackagesInstalling');
|
|
369
|
+
case 'finalizing': return t('appPackagesFinalizing');
|
|
370
|
+
case 'succeeded': return t('appPackagesCompleted');
|
|
371
|
+
case 'failed':
|
|
372
|
+
case 'interrupted': return operation.error ?? t('appPackagesActionFailed');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
298
376
|
function getPackageLoadError(...errors: unknown[]): string | undefined {
|
|
299
377
|
const error = errors.find((candidate): candidate is Error => candidate instanceof Error);
|
|
300
378
|
return error?.message || (errors.some(Boolean) ? t('appPackagesLoadFailed') : undefined);
|