@nextclaw/ui 0.13.9 → 0.13.10-beta.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.
- package/CHANGELOG.md +32 -0
- package/dist/assets/{api-CIJxdHmy.js → api-DhI2lb1f.js} +10 -5
- package/dist/assets/app-presenter-provider-BFbrQp8q.js +3 -0
- package/dist/assets/arrow-left-CI0Ocu9p.js +1 -0
- package/dist/assets/{channels-list-page-C_Xe0CNX.js → channels-list-page-B6CvmU9T.js} +1 -1
- package/dist/assets/{chat-page-CXRtoQR2.js → chat-page-Ch4ShlKB.js} +1 -1
- package/dist/assets/{config-split-page-DBAcw9mJ.js → config-split-page-bO0K6uQv.js} +1 -1
- package/dist/assets/{confirm-dialog-DwPTxq8C.js → confirm-dialog-DXRWJ5uB.js} +1 -1
- package/dist/assets/{desktop-update-config-BqABa_1C.js → desktop-update-config-DPk5V1Nl.js} +1 -1
- package/dist/assets/{dist-BfcibKyv.js → dist-DQoV-62W.js} +1 -1
- package/dist/assets/{dist-WjhKB-pc.js → dist-T6rf86Nf.js} +1 -1
- package/dist/assets/{doc-browser-8EEDwGCJ.js → doc-browser-CTFB6ACN.js} +1 -1
- package/dist/assets/{doc-browser-context-DLJOy6Am.js → doc-browser-context-CP2tI-Br.js} +1 -1
- package/dist/assets/doc-browser-yi02ASOL.js +1 -0
- package/dist/assets/{ellipsis-H-4aNwgt.js → ellipsis-B7CbNPNr.js} +1 -1
- package/dist/assets/{es2015-B5k1IpKX.js → es2015-BSPOGpH4.js} +1 -1
- package/dist/assets/{external-link-BN1wd-eI.js → external-link-CefT35zP.js} +1 -1
- package/dist/assets/index-CevK3jug.css +1 -0
- package/dist/assets/{index-WJt0X9wC.js → index-DZoXsgdz.js} +43 -43
- package/dist/assets/{key-round-D-7IaAQS.js → key-round-BpM3GZn4.js} +1 -1
- package/dist/assets/{loader-circle-CB38Q7Ws.js → loader-circle-_edjOFsw.js} +1 -1
- package/dist/assets/mcp-marketplace-page-9maf_9fL.js +1 -0
- package/dist/assets/{mcp-marketplace-page-ju1KH3TG.js → mcp-marketplace-page-WAgpy0hp.js} +1 -1
- package/dist/assets/{model-config-CgS6dp65.js → model-config-CixioI5U.js} +1 -1
- package/dist/assets/{notice-card-DW9BLKpG.js → notice-card-BLklZuEa.js} +1 -1
- package/dist/assets/play-DD0bg50i.js +1 -0
- package/dist/assets/plus-zZ2GC_AJ.js +1 -0
- package/dist/assets/{popover-ofN845gZ.js → popover-D_pJfOlM.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DaDqi8Zy.js → provider-scoped-model-input-BbWqk2yz.js} +1 -1
- package/dist/assets/{providers-list-CZNQXY4n.js → providers-list-C_JOSjYd.js} +1 -1
- package/dist/assets/{react-Bq34NL59.js → react-Bl8iJUcF.js} +1 -1
- package/dist/assets/{refresh-cw-DJttSXS8.js → refresh-cw-CqfS1EhK.js} +1 -1
- package/dist/assets/remote-qtmzkte2.js +1 -0
- package/dist/assets/rotate-cw-C7EvaSlb.js +1 -0
- package/dist/assets/{runtime-config-page-gUjM6Z_A.js → runtime-config-page-ByvKO6Yn.js} +1 -1
- package/dist/assets/{save-DtAFbS7x.js → save-CQERCODg.js} +1 -1
- package/dist/assets/search-C0b2yzmt.js +1 -0
- package/dist/assets/{search-config-C04PrmHj.js → search-config-Cl2_NgFY.js} +1 -1
- package/dist/assets/{secrets-config-DvxHdz9h.js → secrets-config-BkR72J4g.js} +1 -1
- package/dist/assets/{status-dot-D4jbKDLB.js → status-dot-DY0xUUvK.js} +1 -1
- package/dist/assets/{tabs-Ca2vw7YQ.js → tabs-C77i1w90.js} +1 -1
- package/dist/assets/{tabs-custom-DzGDjYxf.js → tabs-custom-CFpR54-m.js} +1 -1
- package/dist/assets/{tag-chip-CW-_pHjH.js → tag-chip-Kib_ljle.js} +1 -1
- package/dist/assets/{tooltip-Cq2mERPk.js → tooltip-fs7VQG7d.js} +1 -1
- package/dist/assets/{trash-2-rumaEu9l.js → trash-2-Bb8mTTLl.js} +1 -1
- package/dist/assets/x-BV4iiea8.js +1 -0
- package/dist/index.html +60 -33
- package/dist/sw.js +6 -31
- package/index.html +35 -8
- package/package.json +8 -8
- package/public/sw.js +6 -31
- package/src/app/index.tsx +0 -2
- package/src/features/panel-apps/components/panel-app-list-item.test.tsx +4 -0
- package/src/features/panel-apps/components/panel-apps-list.tsx +27 -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 +18 -40
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +24 -36
- package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +4 -0
- package/src/features/pwa/components/pwa-install-entry.test.tsx +5 -46
- package/src/features/pwa/components/pwa-install-entry.tsx +1 -35
- package/src/features/pwa/index.ts +1 -1
- package/src/features/pwa/managers/pwa-install.manager.ts +0 -2
- package/src/features/pwa/managers/pwa-runtime.manager.ts +2 -120
- package/src/features/pwa/managers/pwa-service-worker-cache.manager.test.ts +7 -5
- package/src/features/pwa/stores/pwa.store.ts +1 -3
- package/src/features/pwa/types/pwa.types.ts +0 -2
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +4 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +3 -3
- package/src/shared/lib/api/index.ts +1 -1
- package/src/shared/lib/api/{ncp-session-query-cache.test.ts → ncp-session-query-cache.utils.test.ts} +23 -0
- package/src/shared/lib/api/{ncp-session-query-cache.ts → ncp-session-query-cache.utils.ts} +9 -11
- package/src/shared/lib/api/types.ts +4 -0
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +5 -0
- package/dist/assets/app-presenter-provider-CKZqZfv5.js +0 -3
- package/dist/assets/arrow-left-DHcD-tbC.js +0 -1
- package/dist/assets/doc-browser-DlT90zQD.js +0 -1
- package/dist/assets/index-BCq7kbC8.css +0 -1
- package/dist/assets/mcp-marketplace-page-CmZwt64F.js +0 -1
- package/dist/assets/play-DGpH8jeI.js +0 -1
- package/dist/assets/plus-CkcQjwuH.js +0 -1
- package/dist/assets/remote-CYmsM9DD.js +0 -1
- package/dist/assets/rotate-cw-D_xslHna.js +0 -1
- package/dist/assets/search-CNMRwAFx.js +0 -1
- package/dist/assets/x-DGqjdMUt.js +0 -1
package/public/sw.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* global caches, self */
|
|
2
2
|
|
|
3
3
|
const SHELL_CACHE = 'nextclaw-ui-shell-v2';
|
|
4
|
-
const RUNTIME_CACHE = 'nextclaw-ui-runtime-v2';
|
|
5
4
|
const SHELL_ASSETS = ['/offline.html', '/manifest.webmanifest', '/logo.svg', '/pwa-192.png', '/pwa-512.png'];
|
|
6
5
|
|
|
7
6
|
self.addEventListener('install', (event) => {
|
|
@@ -10,24 +9,16 @@ self.addEventListener('install', (event) => {
|
|
|
10
9
|
return cache.addAll(SHELL_ASSETS);
|
|
11
10
|
})
|
|
12
11
|
);
|
|
13
|
-
self.skipWaiting();
|
|
14
12
|
});
|
|
15
13
|
|
|
16
14
|
self.addEventListener('activate', (event) => {
|
|
17
15
|
event.waitUntil(
|
|
18
16
|
caches.keys().then(async (keys) => {
|
|
19
|
-
await Promise.all(keys.filter((key) => key
|
|
20
|
-
await self.clients.claim();
|
|
17
|
+
await Promise.all(keys.filter((key) => key.startsWith('nextclaw-ui-') && key !== SHELL_CACHE).map((key) => caches.delete(key)));
|
|
21
18
|
})
|
|
22
19
|
);
|
|
23
20
|
});
|
|
24
21
|
|
|
25
|
-
self.addEventListener('message', (event) => {
|
|
26
|
-
if (event.data?.type === 'SKIP_WAIT') {
|
|
27
|
-
self.skipWaiting();
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
22
|
self.addEventListener('fetch', (event) => {
|
|
32
23
|
const { request } = event;
|
|
33
24
|
if (request.method !== 'GET') {
|
|
@@ -35,11 +26,6 @@ self.addEventListener('fetch', (event) => {
|
|
|
35
26
|
}
|
|
36
27
|
|
|
37
28
|
const url = new URL(request.url);
|
|
38
|
-
if (request.mode === 'navigate') {
|
|
39
|
-
event.respondWith(handleNavigation(request));
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
29
|
if (url.origin !== self.location.origin) {
|
|
44
30
|
return;
|
|
45
31
|
}
|
|
@@ -48,30 +34,19 @@ self.addEventListener('fetch', (event) => {
|
|
|
48
34
|
return;
|
|
49
35
|
}
|
|
50
36
|
|
|
51
|
-
if (
|
|
52
|
-
event.respondWith(
|
|
37
|
+
if (SHELL_ASSETS.includes(url.pathname)) {
|
|
38
|
+
event.respondWith(readShellAsset(request));
|
|
53
39
|
}
|
|
54
40
|
});
|
|
55
41
|
|
|
56
|
-
async function
|
|
57
|
-
try {
|
|
58
|
-
const response = await fetch(request);
|
|
59
|
-
const runtimeCache = await caches.open(RUNTIME_CACHE);
|
|
60
|
-
runtimeCache.put(request, response.clone());
|
|
61
|
-
return response;
|
|
62
|
-
} catch {
|
|
63
|
-
return caches.match('/offline.html');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function handleStaticAsset(request) {
|
|
42
|
+
async function readShellAsset(request) {
|
|
68
43
|
const cachedResponse = await caches.match(request);
|
|
69
44
|
if (cachedResponse) {
|
|
70
45
|
return cachedResponse;
|
|
71
46
|
}
|
|
72
47
|
|
|
73
48
|
const response = await fetch(request);
|
|
74
|
-
const
|
|
75
|
-
|
|
49
|
+
const shellCache = await caches.open(SHELL_CACHE);
|
|
50
|
+
shellCache.put(request, response.clone());
|
|
76
51
|
return response;
|
|
77
52
|
}
|
package/src/app/index.tsx
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
import { useAppEventConsumers } from "@/app/hooks/use-app-event-consumers";
|
|
20
20
|
import {
|
|
21
21
|
PwaInstallBanner,
|
|
22
|
-
PwaUpdateBanner,
|
|
23
22
|
} from "@/pwa/components/pwa-install-entry";
|
|
24
23
|
import { startNextClawPwa } from "@/pwa/register-pwa";
|
|
25
24
|
|
|
@@ -251,7 +250,6 @@ export default function AppContent() {
|
|
|
251
250
|
<QueryClientProvider client={appQueryClient}>
|
|
252
251
|
<AuthGate />
|
|
253
252
|
<PwaInstallBanner />
|
|
254
|
-
<PwaUpdateBanner />
|
|
255
253
|
<Toaster position="top-right" richColors offset={window.nextclawDesktop?.platform === "win32" ? 56 : undefined} mobileOffset={window.nextclawDesktop?.platform === "win32" ? 56 : undefined} />
|
|
256
254
|
</QueryClientProvider>
|
|
257
255
|
);
|
|
@@ -5,7 +5,9 @@ import { PanelAppListItem } from './panel-app-list-item';
|
|
|
5
5
|
|
|
6
6
|
const baseEntry = {
|
|
7
7
|
id: 'demo',
|
|
8
|
+
appId: 'demo',
|
|
8
9
|
fileName: 'demo.panel.html',
|
|
10
|
+
kind: 'single-file' as const,
|
|
9
11
|
title: 'Demo Panel',
|
|
10
12
|
description: 'A compact panel app with a longer description that should stay on one line.',
|
|
11
13
|
contentPath: '/api/panel-apps/demo/content',
|
|
@@ -13,6 +15,8 @@ const baseEntry = {
|
|
|
13
15
|
updatedAt: '2026-05-28T09:00:00.000Z',
|
|
14
16
|
sizeBytes: 12,
|
|
15
17
|
favorite: false,
|
|
18
|
+
clientDeclared: false,
|
|
19
|
+
clientGranted: false,
|
|
16
20
|
openCount: 0,
|
|
17
21
|
};
|
|
18
22
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import { HelpCircle, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { useAppPresenter } from '@/app/components/app-presenter-provider';
|
|
3
4
|
import { PanelAppListItem } from '@/features/panel-apps/components/panel-app-list-item';
|
|
4
|
-
import { useDeletePanelApp, usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
5
|
+
import { useDeletePanelApp, useGrantPanelAppClient, usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
5
6
|
import { getPanelAppViewEntries } from '@/features/panel-apps/utils/panel-app-view.utils';
|
|
6
7
|
import type { PanelAppViewMode } from '@/features/panel-apps/utils/panel-app-view.utils';
|
|
7
8
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
@@ -20,6 +21,8 @@ export function PanelAppsList({
|
|
|
20
21
|
const deletePanelApp = useDeletePanelApp();
|
|
21
22
|
const updatePreferences = useUpdatePanelAppPreferences();
|
|
22
23
|
const recordOpened = useRecordPanelAppOpened();
|
|
24
|
+
const grantClient = useGrantPanelAppClient();
|
|
25
|
+
const presenter = useAppPresenter();
|
|
23
26
|
const [viewMode, setViewMode] = useState<PanelAppViewMode>('smart');
|
|
24
27
|
const entries = useMemo(
|
|
25
28
|
() => getPanelAppViewEntries(panelApps.data?.entries ?? [], viewMode),
|
|
@@ -27,6 +30,9 @@ export function PanelAppsList({
|
|
|
27
30
|
);
|
|
28
31
|
|
|
29
32
|
const openPanelApp = async (entry: PanelAppEntryView) => {
|
|
33
|
+
if (!(await ensurePanelAppClientGrant(entry))) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
30
36
|
try {
|
|
31
37
|
onOpenPanelApp(await recordOpened.mutateAsync(entry.id));
|
|
32
38
|
} catch {
|
|
@@ -34,6 +40,26 @@ export function PanelAppsList({
|
|
|
34
40
|
}
|
|
35
41
|
};
|
|
36
42
|
|
|
43
|
+
const ensurePanelAppClientGrant = async (entry: PanelAppEntryView): Promise<boolean> => {
|
|
44
|
+
if (!entry.clientDeclared || entry.clientGranted) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const allowed = await presenter.serviceActionAuthorizationManager.requestAuthorization({
|
|
48
|
+
panelAppId: entry.appId,
|
|
49
|
+
actions: [{
|
|
50
|
+
actionId: 'nextclaw.client',
|
|
51
|
+
actionTitle: t('panelAppsClientGrantTitle'),
|
|
52
|
+
actionDescription: t('panelAppsClientGrantDescription'),
|
|
53
|
+
risk: 'dangerous',
|
|
54
|
+
}],
|
|
55
|
+
});
|
|
56
|
+
if (!allowed) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
await grantClient.mutateAsync(entry.appId);
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
|
|
37
63
|
const toggleFavorite = (entry: PanelAppEntryView) => {
|
|
38
64
|
updatePreferences.mutate({
|
|
39
65
|
id: entry.id,
|
|
@@ -38,6 +38,16 @@ export function useRecordPanelAppOpened() {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function useGrantPanelAppClient() {
|
|
42
|
+
const queryClient = useQueryClient();
|
|
43
|
+
return useMutation({
|
|
44
|
+
mutationFn: (appId: string) => nextclawClient.panelApps.grantClient(appId),
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
void queryClient.invalidateQueries({ queryKey: PANEL_APPS_QUERY_KEY });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
export function useDeletePanelApp() {
|
|
42
52
|
const queryClient = useQueryClient();
|
|
43
53
|
return useMutation({
|
|
@@ -5,7 +5,6 @@ import { PanelAppBridgeManager } from '@/features/panel-apps/managers/panel-app-
|
|
|
5
5
|
import type { ServiceActionAuthorizationManager } from '@/features/service-apps';
|
|
6
6
|
|
|
7
7
|
const mocks = vi.hoisted(() => ({
|
|
8
|
-
createBridgeSession: vi.fn(),
|
|
9
8
|
generateAgentObject: vi.fn(),
|
|
10
9
|
grantAgentCapability: vi.fn(),
|
|
11
10
|
grantServiceAction: vi.fn(),
|
|
@@ -19,7 +18,6 @@ const mocks = vi.hoisted(() => ({
|
|
|
19
18
|
vi.mock('@/shared/lib/api', () => ({
|
|
20
19
|
nextclawClient: {
|
|
21
20
|
panelApps: {
|
|
22
|
-
createBridgeSession: mocks.createBridgeSession,
|
|
23
21
|
generateAgentObject: mocks.generateAgentObject,
|
|
24
22
|
grantAgentCapability: mocks.grantAgentCapability,
|
|
25
23
|
sendAgentMessage: mocks.sendAgentMessage,
|
|
@@ -51,10 +49,17 @@ function createIframeHarness(manager: PanelAppBridgeManager) {
|
|
|
51
49
|
return {
|
|
52
50
|
postMessage,
|
|
53
51
|
send: (
|
|
54
|
-
data: unknown
|
|
52
|
+
data: Record<string, unknown>,
|
|
55
53
|
iframeInstanceId = 'tab-1:0:0',
|
|
56
54
|
) => manager.handleIframeMessage({
|
|
57
|
-
event: {
|
|
55
|
+
event: {
|
|
56
|
+
data: {
|
|
57
|
+
appId: 'mood-calendar',
|
|
58
|
+
runtimeToken: 'token-1',
|
|
59
|
+
...data,
|
|
60
|
+
},
|
|
61
|
+
source: contentWindow,
|
|
62
|
+
} as MessageEvent,
|
|
58
63
|
iframe,
|
|
59
64
|
iframeInstanceId,
|
|
60
65
|
tab: {
|
|
@@ -70,22 +75,11 @@ function createIframeHarness(manager: PanelAppBridgeManager) {
|
|
|
70
75
|
};
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
function mockBridgeSession(token = 'token-1') {
|
|
74
|
-
mocks.createBridgeSession.mockResolvedValue({
|
|
75
|
-
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
76
|
-
id: `session-${token}`,
|
|
77
|
-
panelAppId: 'mood-calendar',
|
|
78
|
-
tabId: 'tab-1',
|
|
79
|
-
token,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
78
|
describe('PanelAppBridgeManager', () => {
|
|
84
79
|
it('authorizes missing service actions in one flow before retrying a protected action', async () => {
|
|
85
80
|
const manager = createManager();
|
|
86
81
|
const { postMessage, send } = createIframeHarness(manager);
|
|
87
82
|
const actionId = 'mood-tracker.saveMood';
|
|
88
|
-
mockBridgeSession();
|
|
89
83
|
mocks.invokeServiceAction
|
|
90
84
|
.mockRejectedValueOnce(new NextClawClientError({
|
|
91
85
|
code: 'AUTHORIZATION_REQUIRED',
|
|
@@ -176,7 +170,6 @@ describe('PanelAppBridgeManager', () => {
|
|
|
176
170
|
it('opens the authorization flow before retrying a protected generateObject call', async () => {
|
|
177
171
|
const manager = createManager();
|
|
178
172
|
const { postMessage, send } = createIframeHarness(manager);
|
|
179
|
-
mockBridgeSession();
|
|
180
173
|
mocks.generateAgentObject
|
|
181
174
|
.mockRejectedValueOnce(new NextClawClientError({
|
|
182
175
|
code: 'AUTHORIZATION_REQUIRED',
|
|
@@ -221,49 +214,34 @@ describe('PanelAppBridgeManager', () => {
|
|
|
221
214
|
}, '*');
|
|
222
215
|
});
|
|
223
216
|
|
|
224
|
-
it('
|
|
217
|
+
it('uses the injected runtime token without creating a bridge session', async () => {
|
|
225
218
|
const manager = createManager();
|
|
226
219
|
const { postMessage, send } = createIframeHarness(manager);
|
|
227
|
-
mocks.createBridgeSession
|
|
228
|
-
.mockResolvedValueOnce({
|
|
229
|
-
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
230
|
-
id: 'session-1',
|
|
231
|
-
panelAppId: 'mood-calendar',
|
|
232
|
-
tabId: 'tab-1',
|
|
233
|
-
token: 'token-1',
|
|
234
|
-
})
|
|
235
|
-
.mockResolvedValueOnce({
|
|
236
|
-
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
237
|
-
id: 'session-2',
|
|
238
|
-
panelAppId: 'mood-calendar',
|
|
239
|
-
tabId: 'tab-1',
|
|
240
|
-
token: 'token-2',
|
|
241
|
-
});
|
|
242
220
|
mocks.listServiceActions.mockResolvedValue({ actions: [] });
|
|
243
221
|
|
|
244
|
-
for (const [requestId,
|
|
245
|
-
['request-1', '
|
|
246
|
-
['request-2', '
|
|
247
|
-
['request-3', '
|
|
222
|
+
for (const [requestId, runtimeToken] of [
|
|
223
|
+
['request-1', 'token-1'],
|
|
224
|
+
['request-2', 'token-2'],
|
|
225
|
+
['request-3', 'token-3'],
|
|
248
226
|
]) {
|
|
249
227
|
send({
|
|
250
228
|
method: 'list',
|
|
251
229
|
requestId,
|
|
230
|
+
runtimeToken,
|
|
252
231
|
type: 'nextclaw:panel-app-service-actions:request',
|
|
253
|
-
}
|
|
232
|
+
});
|
|
254
233
|
}
|
|
255
234
|
|
|
256
235
|
await waitFor(() => expect(postMessage).toHaveBeenCalledTimes(3));
|
|
257
236
|
|
|
258
|
-
expect(mocks.createBridgeSession).toHaveBeenCalledTimes(2);
|
|
259
237
|
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(1, {
|
|
260
238
|
bridgeSessionToken: 'token-1',
|
|
261
239
|
});
|
|
262
240
|
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(2, {
|
|
263
|
-
bridgeSessionToken: 'token-
|
|
241
|
+
bridgeSessionToken: 'token-2',
|
|
264
242
|
});
|
|
265
243
|
expect(mocks.listServiceActions).toHaveBeenNthCalledWith(3, {
|
|
266
|
-
bridgeSessionToken: 'token-
|
|
244
|
+
bridgeSessionToken: 'token-3',
|
|
267
245
|
});
|
|
268
246
|
});
|
|
269
247
|
});
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
PanelAppAgentGenerateObjectResultView,
|
|
6
6
|
PanelAppAgentSendRequestView,
|
|
7
7
|
PanelAppAgentSendResultView,
|
|
8
|
-
PanelAppBridgeSessionView,
|
|
9
8
|
PanelAppCapabilityGrantView,
|
|
10
9
|
ServiceActionGrantView,
|
|
11
10
|
ServiceActionInvokeResultView,
|
|
@@ -18,6 +17,8 @@ import { nextclawClient } from '@/shared/lib/api';
|
|
|
18
17
|
type PanelAppBridgeRequest = {
|
|
19
18
|
type: 'nextclaw:panel-app-service-actions:request';
|
|
20
19
|
requestId: string;
|
|
20
|
+
appId: string;
|
|
21
|
+
runtimeToken: string;
|
|
21
22
|
method:
|
|
22
23
|
| 'agent.generateObject'
|
|
23
24
|
| 'agent.send'
|
|
@@ -32,6 +33,11 @@ type PanelAppBridgeRequest = {
|
|
|
32
33
|
};
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
type PanelAppBridgeContext = {
|
|
37
|
+
appId: string;
|
|
38
|
+
token: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
35
41
|
type PanelAppBridgeResponse =
|
|
36
42
|
| {
|
|
37
43
|
type: 'nextclaw:panel-app-service-actions:response';
|
|
@@ -51,8 +57,6 @@ type PanelAppBridgeResponse =
|
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
export class PanelAppBridgeManager {
|
|
54
|
-
private readonly sessions = new Map<string, Promise<PanelAppBridgeSessionView>>();
|
|
55
|
-
|
|
56
60
|
constructor(private readonly authorizationManager: ServiceActionAuthorizationManager) {}
|
|
57
61
|
|
|
58
62
|
handleIframeMessage = ({ event, iframe, iframeInstanceId, tab }: DocBrowserIframeMessageParams): void => {
|
|
@@ -70,7 +74,7 @@ export class PanelAppBridgeManager {
|
|
|
70
74
|
request: PanelAppBridgeRequest,
|
|
71
75
|
): Promise<void> => {
|
|
72
76
|
try {
|
|
73
|
-
const session =
|
|
77
|
+
const session = this.getBridgeContext(request);
|
|
74
78
|
const data = await this.dispatchRequest(session, request);
|
|
75
79
|
this.postResponse(params, {
|
|
76
80
|
type: 'nextclaw:panel-app-service-actions:response',
|
|
@@ -89,7 +93,7 @@ export class PanelAppBridgeManager {
|
|
|
89
93
|
};
|
|
90
94
|
|
|
91
95
|
private dispatchRequest = async (
|
|
92
|
-
session:
|
|
96
|
+
session: PanelAppBridgeContext,
|
|
93
97
|
request: PanelAppBridgeRequest,
|
|
94
98
|
): Promise<
|
|
95
99
|
| PanelAppAgentGenerateObjectResultView
|
|
@@ -124,7 +128,7 @@ export class PanelAppBridgeManager {
|
|
|
124
128
|
};
|
|
125
129
|
|
|
126
130
|
private invokeWithAuthorization = async (
|
|
127
|
-
session:
|
|
131
|
+
session: PanelAppBridgeContext,
|
|
128
132
|
request: PanelAppBridgeRequest,
|
|
129
133
|
): Promise<ServiceActionInvokeResultView> => {
|
|
130
134
|
const actionId = this.requireActionId(request);
|
|
@@ -148,7 +152,7 @@ export class PanelAppBridgeManager {
|
|
|
148
152
|
};
|
|
149
153
|
|
|
150
154
|
private sendAgentMessageWithAuthorization = async (
|
|
151
|
-
session:
|
|
155
|
+
session: PanelAppBridgeContext,
|
|
152
156
|
request: PanelAppBridgeRequest,
|
|
153
157
|
): Promise<PanelAppAgentSendResultView> => {
|
|
154
158
|
try {
|
|
@@ -169,7 +173,7 @@ export class PanelAppBridgeManager {
|
|
|
169
173
|
};
|
|
170
174
|
|
|
171
175
|
private generateAgentObjectWithAuthorization = async (
|
|
172
|
-
session:
|
|
176
|
+
session: PanelAppBridgeContext,
|
|
173
177
|
request: PanelAppBridgeRequest,
|
|
174
178
|
): Promise<PanelAppAgentGenerateObjectResultView> => {
|
|
175
179
|
try {
|
|
@@ -190,11 +194,11 @@ export class PanelAppBridgeManager {
|
|
|
190
194
|
};
|
|
191
195
|
|
|
192
196
|
private confirmAndGrantAgentCapability = async (
|
|
193
|
-
session:
|
|
197
|
+
session: PanelAppBridgeContext,
|
|
194
198
|
capability: PanelAppAgentCapabilityView,
|
|
195
199
|
): Promise<PanelAppCapabilityGrantView> => {
|
|
196
200
|
const allowed = await this.authorizationManager.requestAuthorization({
|
|
197
|
-
panelAppId: session.
|
|
201
|
+
panelAppId: session.appId,
|
|
198
202
|
actions: [{
|
|
199
203
|
actionId: capability,
|
|
200
204
|
actionTitle: capability === 'agent:send' ? 'Send agent message' : 'Generate object',
|
|
@@ -217,13 +221,13 @@ export class PanelAppBridgeManager {
|
|
|
217
221
|
};
|
|
218
222
|
|
|
219
223
|
private confirmAndGrant = async (
|
|
220
|
-
session:
|
|
224
|
+
session: PanelAppBridgeContext,
|
|
221
225
|
actionId: string,
|
|
222
226
|
input?: Record<string, unknown>,
|
|
223
227
|
): Promise<ServiceActionGrantView> => {
|
|
224
228
|
const actions = await this.listGrantCandidateActions(session, actionId);
|
|
225
229
|
const allowed = await this.authorizationManager.requestAuthorization({
|
|
226
|
-
panelAppId: session.
|
|
230
|
+
panelAppId: session.appId,
|
|
227
231
|
actions: actions.map((action) => ({
|
|
228
232
|
actionId: action.id,
|
|
229
233
|
actionTitle: action.title,
|
|
@@ -250,7 +254,7 @@ export class PanelAppBridgeManager {
|
|
|
250
254
|
};
|
|
251
255
|
|
|
252
256
|
private listGrantCandidateActions = async (
|
|
253
|
-
session:
|
|
257
|
+
session: PanelAppBridgeContext,
|
|
254
258
|
actionId: string,
|
|
255
259
|
): Promise<Array<ServiceActionListView['actions'][number]>> => {
|
|
256
260
|
const actionList = await nextclawClient.serviceApps.listServiceActions({
|
|
@@ -294,29 +298,11 @@ export class PanelAppBridgeManager {
|
|
|
294
298
|
}
|
|
295
299
|
};
|
|
296
300
|
|
|
297
|
-
private
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
let session = this.sessions.get(key);
|
|
303
|
-
if (!session) {
|
|
304
|
-
session = nextclawClient.panelApps.createBridgeSession({
|
|
305
|
-
panelAppId,
|
|
306
|
-
tabId: params.tab.id,
|
|
307
|
-
});
|
|
308
|
-
this.sessions.set(key, session);
|
|
309
|
-
}
|
|
310
|
-
return await session;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
private readPanelAppId = (url: string): string => {
|
|
314
|
-
const parsed = new URL(url, window.location.origin);
|
|
315
|
-
const match = /^\/api\/panel-apps\/([^/]+)\/content$/.exec(parsed.pathname);
|
|
316
|
-
if (!match?.[1]) {
|
|
317
|
-
throw new Error('Current tab is not a Panel App.');
|
|
318
|
-
}
|
|
319
|
-
return decodeURIComponent(match[1]);
|
|
301
|
+
private getBridgeContext = (request: PanelAppBridgeRequest): PanelAppBridgeContext => {
|
|
302
|
+
return {
|
|
303
|
+
appId: request.appId,
|
|
304
|
+
token: request.runtimeToken,
|
|
305
|
+
};
|
|
320
306
|
};
|
|
321
307
|
|
|
322
308
|
private requireActionId = (request: PanelAppBridgeRequest): string => {
|
|
@@ -383,6 +369,8 @@ export class PanelAppBridgeManager {
|
|
|
383
369
|
return (
|
|
384
370
|
candidate.type === 'nextclaw:panel-app-service-actions:request' &&
|
|
385
371
|
typeof candidate.requestId === 'string' &&
|
|
372
|
+
typeof candidate.runtimeToken === 'string' &&
|
|
373
|
+
typeof candidate.appId === 'string' &&
|
|
386
374
|
(candidate.method === 'invoke' ||
|
|
387
375
|
candidate.method === 'agent.send' ||
|
|
388
376
|
candidate.method === 'agent.generateObject' ||
|
|
@@ -7,13 +7,17 @@ function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelA
|
|
|
7
7
|
const updatedAt = overrides.updatedAt ?? createdAt;
|
|
8
8
|
return {
|
|
9
9
|
id: overrides.id ?? overrides.fileName ?? 'demo.panel.html',
|
|
10
|
+
appId: overrides.appId ?? 'demo',
|
|
10
11
|
fileName: overrides.fileName ?? 'demo.panel.html',
|
|
12
|
+
kind: overrides.kind ?? 'single-file',
|
|
11
13
|
title: overrides.title ?? 'Demo',
|
|
12
14
|
contentPath: overrides.contentPath ?? '/api/panel-apps/demo/content',
|
|
13
15
|
createdAt,
|
|
14
16
|
updatedAt,
|
|
15
17
|
sizeBytes: overrides.sizeBytes ?? 12,
|
|
16
18
|
favorite: overrides.favorite ?? false,
|
|
19
|
+
clientDeclared: overrides.clientDeclared ?? false,
|
|
20
|
+
clientGranted: overrides.clientGranted ?? false,
|
|
17
21
|
openCount: overrides.openCount ?? 0,
|
|
18
22
|
...overrides,
|
|
19
23
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { PwaInstallBanner, PwaInstallCard
|
|
3
|
+
import { PwaInstallBanner, PwaInstallCard } from '@/features/pwa';
|
|
4
4
|
import { usePwaStore, createInitialPwaState } from '@/features/pwa/stores/pwa.store';
|
|
5
5
|
|
|
6
6
|
describe('PwaInstallCard', () => {
|
|
@@ -14,9 +14,7 @@ describe('PwaInstallCard', () => {
|
|
|
14
14
|
installability: 'available',
|
|
15
15
|
installMethod: 'prompt',
|
|
16
16
|
blockedReason: null,
|
|
17
|
-
dismissedInstallPrompt: false
|
|
18
|
-
updateAvailable: false,
|
|
19
|
-
registrationFailed: false
|
|
17
|
+
dismissedInstallPrompt: false
|
|
20
18
|
});
|
|
21
19
|
|
|
22
20
|
render(<PwaInstallCard />);
|
|
@@ -31,9 +29,7 @@ describe('PwaInstallCard', () => {
|
|
|
31
29
|
installability: 'suppressed',
|
|
32
30
|
installMethod: 'none',
|
|
33
31
|
blockedReason: 'desktop-host',
|
|
34
|
-
dismissedInstallPrompt: false
|
|
35
|
-
updateAvailable: false,
|
|
36
|
-
registrationFailed: false
|
|
32
|
+
dismissedInstallPrompt: false
|
|
37
33
|
});
|
|
38
34
|
|
|
39
35
|
render(<PwaInstallCard />);
|
|
@@ -42,48 +38,13 @@ describe('PwaInstallCard', () => {
|
|
|
42
38
|
expect(screen.getByText(/already running inside the Electron desktop host/i)).toBeTruthy();
|
|
43
39
|
});
|
|
44
40
|
|
|
45
|
-
it('does not render update banner before installation', () => {
|
|
46
|
-
usePwaStore.setState({
|
|
47
|
-
initialized: true,
|
|
48
|
-
installability: 'available',
|
|
49
|
-
installMethod: 'manual',
|
|
50
|
-
blockedReason: null,
|
|
51
|
-
dismissedInstallPrompt: false,
|
|
52
|
-
updateAvailable: true,
|
|
53
|
-
registrationFailed: false
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const { container } = render(<PwaUpdateBanner />);
|
|
57
|
-
|
|
58
|
-
expect(container.textContent).toBe('');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('renders update banner only for installed pwa', () => {
|
|
62
|
-
usePwaStore.setState({
|
|
63
|
-
initialized: true,
|
|
64
|
-
installability: 'installed',
|
|
65
|
-
installMethod: 'none',
|
|
66
|
-
blockedReason: null,
|
|
67
|
-
dismissedInstallPrompt: false,
|
|
68
|
-
updateAvailable: true,
|
|
69
|
-
registrationFailed: false
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
render(<PwaUpdateBanner />);
|
|
73
|
-
|
|
74
|
-
expect(screen.getByText('NextClaw Update Ready')).toBeTruthy();
|
|
75
|
-
expect(screen.getByRole('button', { name: 'Refresh Now' })).toBeTruthy();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
41
|
it('renders install banner when prompt install is available and not dismissed', () => {
|
|
79
42
|
usePwaStore.setState({
|
|
80
43
|
initialized: true,
|
|
81
44
|
installability: 'available',
|
|
82
45
|
installMethod: 'prompt',
|
|
83
46
|
blockedReason: null,
|
|
84
|
-
dismissedInstallPrompt: false
|
|
85
|
-
updateAvailable: false,
|
|
86
|
-
registrationFailed: false
|
|
47
|
+
dismissedInstallPrompt: false
|
|
87
48
|
});
|
|
88
49
|
|
|
89
50
|
render(<PwaInstallBanner />);
|
|
@@ -98,9 +59,7 @@ describe('PwaInstallCard', () => {
|
|
|
98
59
|
installability: 'available',
|
|
99
60
|
installMethod: 'prompt',
|
|
100
61
|
blockedReason: null,
|
|
101
|
-
dismissedInstallPrompt: true
|
|
102
|
-
updateAvailable: false,
|
|
103
|
-
registrationFailed: false
|
|
62
|
+
dismissedInstallPrompt: true
|
|
104
63
|
});
|
|
105
64
|
|
|
106
65
|
const { container } = render(<PwaInstallBanner />);
|
|
@@ -2,10 +2,9 @@ import { Button } from '@/shared/components/ui/button';
|
|
|
2
2
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
|
3
3
|
import { usePwaStore } from '@/features/pwa/stores/pwa.store';
|
|
4
4
|
import { pwaInstallManager } from '@/features/pwa/managers/pwa-install.manager';
|
|
5
|
-
import { pwaRuntimeManager } from '@/features/pwa/managers/pwa-runtime.manager';
|
|
6
5
|
import { t } from '@/shared/lib/i18n';
|
|
7
6
|
import { cn } from '@/shared/lib/utils';
|
|
8
|
-
import { Download,
|
|
7
|
+
import { Download, Smartphone, X } from 'lucide-react';
|
|
9
8
|
|
|
10
9
|
function InstallStatusBadge() {
|
|
11
10
|
const installability = usePwaStore((state) => state.installability);
|
|
@@ -170,36 +169,3 @@ export function PwaInstallBanner() {
|
|
|
170
169
|
</div>
|
|
171
170
|
);
|
|
172
171
|
}
|
|
173
|
-
|
|
174
|
-
export function PwaUpdateBanner() {
|
|
175
|
-
const updateAvailable = usePwaStore((state) => state.updateAvailable);
|
|
176
|
-
const installability = usePwaStore((state) => state.installability);
|
|
177
|
-
|
|
178
|
-
if (!updateAvailable || installability !== 'installed') {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return (
|
|
183
|
-
<div className="fixed top-5 right-5 z-50 w-[min(420px,calc(100vw-2rem))] rounded-[26px] border border-gray-200 bg-white/95 p-5 shadow-2xl backdrop-blur-xl">
|
|
184
|
-
<div className="flex items-start justify-between gap-4">
|
|
185
|
-
<div className="space-y-1">
|
|
186
|
-
<p className="text-sm font-semibold text-gray-900">{t('pwaUpdateBannerTitle')}</p>
|
|
187
|
-
<p className="text-sm leading-6 text-gray-600">{t('pwaUpdateBannerDescription')}</p>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
<div className="mt-4">
|
|
191
|
-
<Button
|
|
192
|
-
type="button"
|
|
193
|
-
size="sm"
|
|
194
|
-
className="gap-2"
|
|
195
|
-
onClick={() => {
|
|
196
|
-
void pwaRuntimeManager.applyUpdate();
|
|
197
|
-
}}
|
|
198
|
-
>
|
|
199
|
-
<RefreshCw className="h-4 w-4" />
|
|
200
|
-
{t('pwaUpdateAction')}
|
|
201
|
-
</Button>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
);
|
|
205
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { PwaInstallBanner, PwaInstallCard
|
|
1
|
+
export { PwaInstallBanner, PwaInstallCard } from './components/pwa-install-entry';
|
|
2
2
|
export { startNextClawPwa } from './managers/pwa-bootstrap.manager';
|
|
3
3
|
export { pwaInstallManager } from './managers/pwa-install.manager';
|
|
4
4
|
export { pwaShellThemeManager } from './managers/pwa-shell-theme.manager';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { usePwaStore, createInitialPwaState } from '@/features/pwa/stores/pwa.store';
|
|
2
2
|
import type { PwaInstallBlockedReason, PwaInstallMethod, PwaInstallPromptOutcome, PwaInstallabilityState } from '@/features/pwa/types/pwa.types';
|
|
3
|
-
import { pwaRuntimeManager } from '@/features/pwa/managers/pwa-runtime.manager';
|
|
4
3
|
import {
|
|
5
4
|
clearPwaInstallBannerDismissal,
|
|
6
5
|
dismissPwaInstallBanner,
|
|
@@ -68,7 +67,6 @@ export class PwaInstallManager {
|
|
|
68
67
|
blockedReason: resolution.blockedReason,
|
|
69
68
|
dismissedInstallPrompt: isPwaInstallBannerDismissed()
|
|
70
69
|
}));
|
|
71
|
-
void pwaRuntimeManager.syncUpdateAvailability();
|
|
72
70
|
};
|
|
73
71
|
|
|
74
72
|
dismissInstallPrompt = () => {
|