@nextclaw/ui 0.12.35-beta.6 → 0.12.35
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 +482 -0
- package/dist/assets/api-DruAcori.js +15 -0
- package/dist/assets/{book-open-qc0UVn6m.js → book-open-cPX0Za7Z.js} +1 -1
- package/dist/assets/channels-list-page-DXH1qYLs.js +8 -0
- package/dist/assets/chat-page-Cpo0AFHb.js +105 -0
- package/dist/assets/{config-split-page-DW8tP9Bc.js → config-split-page-BFh_Qixx.js} +1 -1
- package/dist/assets/confirm-dialog-B0TP-vPd.js +5 -0
- package/dist/assets/{createLucideIcon-CA0sgoMJ.js → createLucideIcon-Br1EFMEr.js} +1 -1
- package/dist/assets/{desktop-update-config-DARSwQG8.js → desktop-update-config-Be8x1jNy.js} +1 -1
- package/dist/assets/{dist-D5zsy7pX.js → dist-CpwDZDtv.js} +1 -1
- package/dist/assets/{dist-DNxIa3pe.js → dist-rzakR_bu.js} +1 -1
- package/dist/assets/doc-browser-BI7HSaMK.js +3 -0
- package/dist/assets/doc-browser-context-DB6TTHeE.js +1 -0
- package/dist/assets/doc-browser-route-registry.utils-x5ijvk0u.js +1 -0
- package/dist/assets/doc-browser-yTfN7UBH.js +1 -0
- package/dist/assets/{external-link-C9fbyi6k.js → external-link-C3usm3se.js} +1 -1
- package/dist/assets/index-GVTiCf1s.js +101 -0
- package/dist/assets/index-v0fAGXQW.css +1 -0
- package/dist/assets/{mcp-marketplace-page-BiExW5wq.js → mcp-marketplace-page-DDjeHd1I.js} +2 -2
- package/dist/assets/mcp-marketplace-page-WUkc7n94.js +1 -0
- package/dist/assets/model-config-BjXMpU9U.js +1 -0
- package/dist/assets/play-CqNi0u02.js +1 -0
- package/dist/assets/plus-BcIC8dG1.js +1 -0
- package/dist/assets/{provider-scoped-model-input-Bk8jlpRm.js → provider-scoped-model-input-t1tCP5y5.js} +1 -1
- package/dist/assets/providers-list-C1D5p9kK.js +1 -0
- package/dist/assets/react-B2X_ph0a.js +1 -0
- package/dist/assets/remote-BHeENKxL.js +1 -0
- package/dist/assets/runtime-config-page--O0Wq01S.js +1 -0
- package/dist/assets/{save-CG4Hb1u8.js → save-D10M5vQg.js} +1 -1
- package/dist/assets/{search-lWpOIHEn.js → search-CdhgFNwf.js} +1 -1
- package/dist/assets/search-config-Dzpizo-_.js +1 -0
- package/dist/assets/{secrets-config-ZbLzjIRx.js → secrets-config-D1AUSEXX.js} +1 -1
- package/dist/assets/{setting-row-CNfTKDjc.js → setting-row-8VkukJoH.js} +1 -1
- package/dist/assets/{tabs-custom-BbZoysAl.js → tabs-custom-BBXu1OGs.js} +1 -1
- package/dist/assets/{tag-chip-Dl8JbhC7.js → tag-chip-ix15P-r4.js} +1 -1
- package/dist/assets/wrench-qVIwShTF.js +1 -0
- package/dist/assets/x-Chb07O-Y.js +1 -0
- package/dist/index.html +17 -15
- package/package.json +8 -8
- package/src/features/panel-apps/components/panel-app-list-item.test.tsx +60 -0
- package/src/features/panel-apps/components/panel-app-list-item.tsx +120 -30
- package/src/features/panel-apps/components/panel-apps-list.tsx +8 -1
- package/src/features/panel-apps/hooks/use-panel-apps.ts +10 -0
- package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +153 -1
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +126 -9
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +7 -3
- package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +66 -0
- package/src/features/panel-apps/utils/panel-app-view.utils.ts +13 -2
- package/src/index.css +23 -0
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +170 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
- package/src/shared/components/doc-browser/doc-browser-home-page.tsx +95 -0
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +3 -97
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +117 -44
- package/src/shared/components/doc-browser/doc-browser.test.tsx +159 -1
- package/src/shared/components/doc-browser/doc-browser.tsx +46 -27
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +183 -71
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +52 -1
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +9 -0
- package/src/shared/components/doc-browser/utils/doc-browser-route-registry.utils.ts +232 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +2 -0
- package/src/shared/lib/api/types.ts +1 -0
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +10 -0
- package/dist/assets/api-BTZui7nj.js +0 -15
- package/dist/assets/channels-list-page-DBtU_M1Y.js +0 -8
- package/dist/assets/chat-page-BcoJYo91.js +0 -105
- package/dist/assets/doc-browser-B9skxzVo.js +0 -1
- package/dist/assets/doc-browser-CwyITRXx.js +0 -1
- package/dist/assets/doc-browser-context-Do2qSrB8.js +0 -1
- package/dist/assets/index-BYRP_BMj.css +0 -1
- package/dist/assets/index-DckPGTdM.js +0 -103
- package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +0 -1
- package/dist/assets/model-config-DbdY7fF1.js +0 -1
- package/dist/assets/notice-card-CyiVcu_Z.js +0 -5
- package/dist/assets/play-D-s0C4MN.js +0 -1
- package/dist/assets/plus-DOwjM_kF.js +0 -1
- package/dist/assets/providers-list-CJjNStDV.js +0 -1
- package/dist/assets/react-BaRotFd6.js +0 -1
- package/dist/assets/remote-gl8QckNc.js +0 -1
- package/dist/assets/runtime-config-page-BMFc1yzf.js +0 -1
- package/dist/assets/search-config-DUOR26if.js +0 -1
- package/dist/assets/x-D6GwT_8k.js +0 -1
- /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-CY1lg9IE.js} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { PanelAppListItem } from './panel-app-list-item';
|
|
5
|
+
|
|
6
|
+
const baseEntry = {
|
|
7
|
+
id: 'demo',
|
|
8
|
+
fileName: 'demo.panel.html',
|
|
9
|
+
title: 'Demo Panel',
|
|
10
|
+
description: 'A compact panel app with a longer description that should stay on one line.',
|
|
11
|
+
contentPath: '/api/panel-apps/demo/content',
|
|
12
|
+
createdAt: '2026-05-28T08:00:00.000Z',
|
|
13
|
+
updatedAt: '2026-05-28T09:00:00.000Z',
|
|
14
|
+
sizeBytes: 12,
|
|
15
|
+
favorite: false,
|
|
16
|
+
openCount: 0,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('PanelAppListItem', () => {
|
|
20
|
+
it('keeps panel app metadata compact below the icon-title row', () => {
|
|
21
|
+
const { container } = render(
|
|
22
|
+
<PanelAppListItem
|
|
23
|
+
deletePending={false}
|
|
24
|
+
entry={baseEntry}
|
|
25
|
+
favoritePending={false}
|
|
26
|
+
onDelete={vi.fn()}
|
|
27
|
+
onOpen={vi.fn()}
|
|
28
|
+
onToggleFavorite={vi.fn()}
|
|
29
|
+
/>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(screen.getByText(baseEntry.description).className).toContain('truncate');
|
|
33
|
+
expect(container.querySelector('.line-clamp-2')).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('opens delete from the more-actions menu only after confirmation', async () => {
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
const onDelete = vi.fn();
|
|
39
|
+
render(
|
|
40
|
+
<PanelAppListItem
|
|
41
|
+
deletePending={false}
|
|
42
|
+
entry={baseEntry}
|
|
43
|
+
favoritePending={false}
|
|
44
|
+
onDelete={onDelete}
|
|
45
|
+
onOpen={vi.fn()}
|
|
46
|
+
onToggleFavorite={vi.fn()}
|
|
47
|
+
/>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
|
|
51
|
+
await user.click(screen.getByRole('button', { name: 'Delete panel app' }));
|
|
52
|
+
|
|
53
|
+
expect(onDelete).not.toHaveBeenCalled();
|
|
54
|
+
expect(screen.getByText('Delete panel app?')).toBeTruthy();
|
|
55
|
+
|
|
56
|
+
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
|
57
|
+
|
|
58
|
+
expect(onDelete).toHaveBeenCalledTimes(1);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,60 +1,150 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { AppWindow, Star } from 'lucide-react';
|
|
1
|
+
import { useState, type MouseEvent } from 'react';
|
|
2
|
+
import { AppWindow, MoreVertical, Star, Trash2, type LucideIcon } from 'lucide-react';
|
|
3
3
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
4
|
-
import {
|
|
4
|
+
import { ConfirmDialog } from '@/shared/components/ui/confirm-dialog';
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
6
|
+
import { getLanguage, getLocale, t } from '@/shared/lib/i18n';
|
|
7
|
+
import { cn } from '@/shared/lib/utils';
|
|
5
8
|
|
|
6
9
|
export function PanelAppListItem({
|
|
10
|
+
deletePending,
|
|
7
11
|
entry,
|
|
8
12
|
favoritePending,
|
|
13
|
+
onDelete,
|
|
9
14
|
onOpen,
|
|
10
15
|
onToggleFavorite,
|
|
11
16
|
}: {
|
|
17
|
+
deletePending: boolean;
|
|
12
18
|
entry: PanelAppEntryView;
|
|
13
19
|
favoritePending: boolean;
|
|
20
|
+
onDelete: () => void;
|
|
14
21
|
onOpen: () => void;
|
|
15
22
|
onToggleFavorite: () => void;
|
|
16
23
|
}) {
|
|
24
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
25
|
+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
17
26
|
const favoriteLabel = entry.favorite ? t('panelAppsUnfavorite') : t('panelAppsFavorite');
|
|
18
27
|
const secondary = entry.lastOpenedAt
|
|
19
|
-
? `${t('panelAppsLastOpened')} ${
|
|
20
|
-
: `${t('panelAppsUpdated')} ${
|
|
28
|
+
? `${t('panelAppsLastOpened')} ${formatPanelAppTime(entry.lastOpenedAt)}`
|
|
29
|
+
: `${t('panelAppsUpdated')} ${formatPanelAppTime(entry.updatedAt)}`;
|
|
30
|
+
|
|
21
31
|
const handleFavorite = (event: MouseEvent<HTMLButtonElement>) => {
|
|
22
32
|
event.stopPropagation();
|
|
23
33
|
onToggleFavorite();
|
|
24
34
|
};
|
|
35
|
+
const openDeleteDialog = () => {
|
|
36
|
+
setIsMenuOpen(false);
|
|
37
|
+
setIsDeleteDialogOpen(true);
|
|
38
|
+
};
|
|
25
39
|
|
|
26
40
|
return (
|
|
27
|
-
<div className="group
|
|
28
|
-
<
|
|
29
|
-
type="button"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<span className="min-w-0 flex-1">
|
|
37
|
-
<span className="block truncate text-sm font-medium text-gray-900">{entry.title}</span>
|
|
41
|
+
<div className="group w-full min-w-0 rounded-lg border border-gray-200 bg-white px-2.5 py-2.5 transition-colors hover:border-amber-200 hover:bg-amber-50/50">
|
|
42
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
43
|
+
<button type="button" onClick={onOpen} className="min-w-0 flex-1 text-left">
|
|
44
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
45
|
+
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-amber-50 text-base text-amber-700">
|
|
46
|
+
<PanelAppIcon icon={entry.icon} title={entry.title} />
|
|
47
|
+
</span>
|
|
48
|
+
<span className="block min-w-0 flex-1 truncate text-sm font-medium text-gray-900">{entry.title}</span>
|
|
49
|
+
</span>
|
|
38
50
|
{entry.description ? (
|
|
39
|
-
<span className="mt-
|
|
51
|
+
<span className="mt-1.5 block truncate text-xs leading-5 text-gray-600">{entry.description}</span>
|
|
40
52
|
) : null}
|
|
41
|
-
<span className="mt-
|
|
42
|
-
</
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
<span className="mt-0.5 block truncate text-[11px] leading-4 text-gray-400">{secondary}</span>
|
|
54
|
+
</button>
|
|
55
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={handleFavorite}
|
|
59
|
+
disabled={favoritePending}
|
|
60
|
+
className="rounded-md p-1.5 text-gray-400 transition-colors hover:bg-white hover:text-amber-500 disabled:opacity-50"
|
|
61
|
+
title={favoriteLabel}
|
|
62
|
+
aria-label={favoriteLabel}
|
|
63
|
+
>
|
|
64
|
+
<Star className={entry.favorite ? 'h-4 w-4 fill-amber-400 text-amber-500' : 'h-4 w-4'} />
|
|
65
|
+
</button>
|
|
66
|
+
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
67
|
+
<PopoverTrigger asChild>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
className="rounded-md p-1.5 text-gray-400 transition-colors hover:bg-white hover:text-gray-700"
|
|
71
|
+
aria-label={t('panelAppsMoreActions')}
|
|
72
|
+
disabled={deletePending}
|
|
73
|
+
>
|
|
74
|
+
<MoreVertical className="h-4 w-4" />
|
|
75
|
+
</button>
|
|
76
|
+
</PopoverTrigger>
|
|
77
|
+
<PopoverContent align="end" className="w-48 rounded-xl p-1.5">
|
|
78
|
+
<PanelAppMenuItem
|
|
79
|
+
destructive
|
|
80
|
+
disabled={deletePending}
|
|
81
|
+
icon={Trash2}
|
|
82
|
+
label={t('panelAppsDelete')}
|
|
83
|
+
onClick={openDeleteDialog}
|
|
84
|
+
/>
|
|
85
|
+
</PopoverContent>
|
|
86
|
+
</Popover>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<ConfirmDialog
|
|
90
|
+
open={isDeleteDialogOpen}
|
|
91
|
+
onOpenChange={setIsDeleteDialogOpen}
|
|
92
|
+
title={t('panelAppsDeleteConfirmTitle')}
|
|
93
|
+
description={`${t('panelAppsDeleteConfirmDescription')} ${entry.fileName}`}
|
|
94
|
+
confirmLabel={t('delete')}
|
|
95
|
+
variant="destructive"
|
|
96
|
+
onConfirm={onDelete}
|
|
97
|
+
onCancel={() => undefined}
|
|
98
|
+
/>
|
|
54
99
|
</div>
|
|
55
100
|
);
|
|
56
101
|
}
|
|
57
102
|
|
|
103
|
+
function PanelAppMenuItem({
|
|
104
|
+
destructive = false,
|
|
105
|
+
disabled = false,
|
|
106
|
+
icon: Icon,
|
|
107
|
+
label,
|
|
108
|
+
onClick,
|
|
109
|
+
}: {
|
|
110
|
+
destructive?: boolean;
|
|
111
|
+
disabled?: boolean;
|
|
112
|
+
icon: LucideIcon;
|
|
113
|
+
label: string;
|
|
114
|
+
onClick: () => void;
|
|
115
|
+
}) {
|
|
116
|
+
return (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
className={cn(
|
|
120
|
+
'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',
|
|
121
|
+
destructive ? 'text-destructive hover:bg-destructive/10' : 'text-gray-700 hover:bg-gray-100',
|
|
122
|
+
)}
|
|
123
|
+
disabled={disabled}
|
|
124
|
+
onClick={onClick}
|
|
125
|
+
>
|
|
126
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
127
|
+
<span>{label}</span>
|
|
128
|
+
</button>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatPanelAppTime(value: string): string {
|
|
133
|
+
const date = new Date(value);
|
|
134
|
+
if (Number.isNaN(date.getTime())) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
const locale = getLocale(getLanguage());
|
|
138
|
+
const now = new Date();
|
|
139
|
+
if (date.toDateString() === now.toDateString()) {
|
|
140
|
+
return new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }).format(date);
|
|
141
|
+
}
|
|
142
|
+
if (date.getFullYear() === now.getFullYear()) {
|
|
143
|
+
return new Intl.DateTimeFormat(locale, { month: 'numeric', day: 'numeric' }).format(date);
|
|
144
|
+
}
|
|
145
|
+
return new Intl.DateTimeFormat(locale, { year: '2-digit', month: 'numeric', day: 'numeric' }).format(date);
|
|
146
|
+
}
|
|
147
|
+
|
|
58
148
|
function PanelAppIcon({ icon, title }: { icon?: string; title: string }) {
|
|
59
149
|
if (!icon) {
|
|
60
150
|
return <AppWindow className="h-4 w-4" />;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import { HelpCircle, RefreshCw } from 'lucide-react';
|
|
3
3
|
import { PanelAppListItem } from '@/features/panel-apps/components/panel-app-list-item';
|
|
4
|
-
import { usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
4
|
+
import { useDeletePanelApp, usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
5
5
|
import { getPanelAppViewEntries } from '@/features/panel-apps/utils/panel-app-view.utils';
|
|
6
6
|
import type { PanelAppViewMode } from '@/features/panel-apps/utils/panel-app-view.utils';
|
|
7
7
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
@@ -17,6 +17,7 @@ export function PanelAppsList({
|
|
|
17
17
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
18
18
|
}) {
|
|
19
19
|
const panelApps = usePanelApps();
|
|
20
|
+
const deletePanelApp = useDeletePanelApp();
|
|
20
21
|
const updatePreferences = useUpdatePanelAppPreferences();
|
|
21
22
|
const recordOpened = useRecordPanelAppOpened();
|
|
22
23
|
const [viewMode, setViewMode] = useState<PanelAppViewMode>('smart');
|
|
@@ -40,6 +41,10 @@ export function PanelAppsList({
|
|
|
40
41
|
});
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
const deleteEntry = (entry: PanelAppEntryView) => {
|
|
45
|
+
deletePanelApp.mutate(entry.id);
|
|
46
|
+
};
|
|
47
|
+
|
|
43
48
|
if (panelApps.isLoading) {
|
|
44
49
|
return (
|
|
45
50
|
<div className="flex h-full items-center justify-center text-sm text-gray-500">{t('panelAppsLoading')}</div>
|
|
@@ -100,7 +105,9 @@ export function PanelAppsList({
|
|
|
100
105
|
<PanelAppListItem
|
|
101
106
|
key={entry.id}
|
|
102
107
|
entry={entry}
|
|
108
|
+
deletePending={deletePanelApp.isPending}
|
|
103
109
|
favoritePending={updatePreferences.isPending}
|
|
110
|
+
onDelete={() => deleteEntry(entry)}
|
|
104
111
|
onOpen={() => void openPanelApp(entry)}
|
|
105
112
|
onToggleFavorite={() => toggleFavorite(entry)}
|
|
106
113
|
/>
|
|
@@ -37,3 +37,13 @@ export function useRecordPanelAppOpened() {
|
|
|
37
37
|
},
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
export function useDeletePanelApp() {
|
|
42
|
+
const queryClient = useQueryClient();
|
|
43
|
+
return useMutation({
|
|
44
|
+
mutationFn: (id: string) => nextclawClient.panelApps.deletePanelApp(id),
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
void queryClient.invalidateQueries({ queryKey: PANEL_APPS_QUERY_KEY });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { NextClawClientError } from '@nextclaw/client-sdk';
|
|
2
2
|
import { waitFor } from '@testing-library/react';
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { PanelAppBridgeManager } from '@/features/panel-apps/managers/panel-app-bridge.manager';
|
|
5
5
|
import type { ServiceActionAuthorizationManager } from '@/features/service-apps';
|
|
6
6
|
|
|
7
7
|
const mocks = vi.hoisted(() => ({
|
|
8
8
|
createBridgeSession: vi.fn(),
|
|
9
|
+
generateAgentObject: vi.fn(),
|
|
10
|
+
grantAgentCapability: vi.fn(),
|
|
9
11
|
grantServiceAction: vi.fn(),
|
|
10
12
|
invokeServiceAction: vi.fn(),
|
|
11
13
|
listServiceActions: vi.fn(),
|
|
14
|
+
sendAgentMessage: vi.fn(),
|
|
12
15
|
requestAuthorization: vi.fn(),
|
|
13
16
|
}));
|
|
14
17
|
|
|
@@ -16,6 +19,9 @@ vi.mock('@/shared/lib/api', () => ({
|
|
|
16
19
|
nextclawClient: {
|
|
17
20
|
panelApps: {
|
|
18
21
|
createBridgeSession: mocks.createBridgeSession,
|
|
22
|
+
generateAgentObject: mocks.generateAgentObject,
|
|
23
|
+
grantAgentCapability: mocks.grantAgentCapability,
|
|
24
|
+
sendAgentMessage: mocks.sendAgentMessage,
|
|
19
25
|
},
|
|
20
26
|
serviceApps: {
|
|
21
27
|
grantServiceAction: mocks.grantServiceAction,
|
|
@@ -26,6 +32,10 @@ vi.mock('@/shared/lib/api', () => ({
|
|
|
26
32
|
},
|
|
27
33
|
}));
|
|
28
34
|
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
29
39
|
describe('PanelAppBridgeManager', () => {
|
|
30
40
|
it('opens the authorization flow before retrying a protected service action', async () => {
|
|
31
41
|
const manager = new PanelAppBridgeManager({
|
|
@@ -77,6 +87,7 @@ describe('PanelAppBridgeManager', () => {
|
|
|
77
87
|
source: contentWindow,
|
|
78
88
|
} as MessageEvent,
|
|
79
89
|
iframe,
|
|
90
|
+
iframeInstanceId: 'tab-1:0:0',
|
|
80
91
|
tab: {
|
|
81
92
|
currentUrl: '/api/panel-apps/mood-calendar/content',
|
|
82
93
|
history: [],
|
|
@@ -108,4 +119,145 @@ describe('PanelAppBridgeManager', () => {
|
|
|
108
119
|
type: 'nextclaw:panel-app-service-actions:response',
|
|
109
120
|
}, '*');
|
|
110
121
|
});
|
|
122
|
+
|
|
123
|
+
it('opens the authorization flow before retrying a protected generateObject call', async () => {
|
|
124
|
+
const manager = new PanelAppBridgeManager({
|
|
125
|
+
requestAuthorization: mocks.requestAuthorization,
|
|
126
|
+
} as unknown as ServiceActionAuthorizationManager);
|
|
127
|
+
const postMessage = vi.fn();
|
|
128
|
+
const contentWindow = { postMessage } as unknown as Window;
|
|
129
|
+
const iframe = { contentWindow } as HTMLIFrameElement;
|
|
130
|
+
mocks.createBridgeSession.mockResolvedValue({
|
|
131
|
+
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
132
|
+
id: 'session-1',
|
|
133
|
+
panelAppId: 'mood-calendar',
|
|
134
|
+
tabId: 'tab-1',
|
|
135
|
+
token: 'token-1',
|
|
136
|
+
});
|
|
137
|
+
mocks.generateAgentObject
|
|
138
|
+
.mockRejectedValueOnce(new NextClawClientError({
|
|
139
|
+
code: 'AUTHORIZATION_REQUIRED',
|
|
140
|
+
message: 'authorization required',
|
|
141
|
+
}))
|
|
142
|
+
.mockResolvedValueOnce({ result: { summary: 'sunny' } });
|
|
143
|
+
mocks.requestAuthorization.mockResolvedValue(true);
|
|
144
|
+
mocks.grantAgentCapability.mockResolvedValue({
|
|
145
|
+
caller: { surface: 'panel-app', appId: 'mood-calendar' },
|
|
146
|
+
capability: 'agent:generateObject',
|
|
147
|
+
grantedAt: '2026-05-28T00:00:00.000Z',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
manager.handleIframeMessage({
|
|
151
|
+
event: {
|
|
152
|
+
data: {
|
|
153
|
+
method: 'agent.generateObject',
|
|
154
|
+
payload: {
|
|
155
|
+
input: {
|
|
156
|
+
peerId: 'mood-summary',
|
|
157
|
+
prompt: 'summarize',
|
|
158
|
+
schema: { type: 'object' },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
requestId: 'request-2',
|
|
162
|
+
type: 'nextclaw:panel-app-service-actions:request',
|
|
163
|
+
},
|
|
164
|
+
source: contentWindow,
|
|
165
|
+
} as MessageEvent,
|
|
166
|
+
iframe,
|
|
167
|
+
iframeInstanceId: 'tab-1:0:0',
|
|
168
|
+
tab: {
|
|
169
|
+
currentUrl: '/api/panel-apps/mood-calendar/content',
|
|
170
|
+
history: [],
|
|
171
|
+
historyIndex: 0,
|
|
172
|
+
id: 'tab-1',
|
|
173
|
+
kind: 'content',
|
|
174
|
+
navVersion: 0,
|
|
175
|
+
title: 'Mood Calendar',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await waitFor(() => expect(postMessage).toHaveBeenCalled());
|
|
180
|
+
|
|
181
|
+
expect(mocks.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
|
|
182
|
+
actionId: 'agent:generateObject',
|
|
183
|
+
panelAppId: 'mood-calendar',
|
|
184
|
+
risk: 'write',
|
|
185
|
+
}));
|
|
186
|
+
expect(mocks.grantAgentCapability).toHaveBeenCalledWith('agent:generateObject', {
|
|
187
|
+
bridgeSessionToken: 'token-1',
|
|
188
|
+
});
|
|
189
|
+
expect(mocks.generateAgentObject).toHaveBeenCalledTimes(2);
|
|
190
|
+
expect(postMessage).toHaveBeenCalledWith({
|
|
191
|
+
data: { result: { summary: 'sunny' } },
|
|
192
|
+
ok: true,
|
|
193
|
+
requestId: 'request-2',
|
|
194
|
+
type: 'nextclaw:panel-app-service-actions:response',
|
|
195
|
+
}, '*');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('creates a fresh bridge session for each iframe instance', async () => {
|
|
199
|
+
const manager = new PanelAppBridgeManager({
|
|
200
|
+
requestAuthorization: mocks.requestAuthorization,
|
|
201
|
+
} as unknown as ServiceActionAuthorizationManager);
|
|
202
|
+
const postMessage = vi.fn();
|
|
203
|
+
const contentWindow = { postMessage } as unknown as Window;
|
|
204
|
+
const iframe = { contentWindow } as HTMLIFrameElement;
|
|
205
|
+
mocks.createBridgeSession
|
|
206
|
+
.mockResolvedValueOnce({
|
|
207
|
+
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
208
|
+
id: 'session-1',
|
|
209
|
+
panelAppId: 'mood-calendar',
|
|
210
|
+
tabId: 'tab-1',
|
|
211
|
+
token: 'token-1',
|
|
212
|
+
})
|
|
213
|
+
.mockResolvedValueOnce({
|
|
214
|
+
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
215
|
+
id: 'session-2',
|
|
216
|
+
panelAppId: 'mood-calendar',
|
|
217
|
+
tabId: 'tab-1',
|
|
218
|
+
token: 'token-2',
|
|
219
|
+
});
|
|
220
|
+
mocks.listServiceActions.mockResolvedValue({ actions: [] });
|
|
221
|
+
|
|
222
|
+
for (const [requestId, iframeInstanceId] of [
|
|
223
|
+
['request-1', 'tab-1:0:0'],
|
|
224
|
+
['request-2', 'tab-1:0:0'],
|
|
225
|
+
['request-3', 'tab-1:0:1'],
|
|
226
|
+
]) {
|
|
227
|
+
manager.handleIframeMessage({
|
|
228
|
+
event: {
|
|
229
|
+
data: {
|
|
230
|
+
method: 'list',
|
|
231
|
+
requestId,
|
|
232
|
+
type: 'nextclaw:panel-app-service-actions:request',
|
|
233
|
+
},
|
|
234
|
+
source: contentWindow,
|
|
235
|
+
} as MessageEvent,
|
|
236
|
+
iframe,
|
|
237
|
+
iframeInstanceId,
|
|
238
|
+
tab: {
|
|
239
|
+
currentUrl: '/api/panel-apps/mood-calendar/content',
|
|
240
|
+
history: [],
|
|
241
|
+
historyIndex: 0,
|
|
242
|
+
id: 'tab-1',
|
|
243
|
+
kind: 'content',
|
|
244
|
+
navVersion: 0,
|
|
245
|
+
title: 'Mood Calendar',
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await waitFor(() => expect(postMessage).toHaveBeenCalledTimes(3));
|
|
251
|
+
|
|
252
|
+
expect(mocks.createBridgeSession).toHaveBeenCalledTimes(2);
|
|
253
|
+
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(1, {
|
|
254
|
+
bridgeSessionToken: 'token-1',
|
|
255
|
+
});
|
|
256
|
+
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(2, {
|
|
257
|
+
bridgeSessionToken: 'token-1',
|
|
258
|
+
});
|
|
259
|
+
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(3, {
|
|
260
|
+
bridgeSessionToken: 'token-2',
|
|
261
|
+
});
|
|
262
|
+
});
|
|
111
263
|
});
|