@nextclaw/ui 0.17.0 → 0.17.1

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