@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/assets/{api-CIJxdHmy.js → api-DhI2lb1f.js} +10 -5
  3. package/dist/assets/app-presenter-provider-BFbrQp8q.js +3 -0
  4. package/dist/assets/arrow-left-CI0Ocu9p.js +1 -0
  5. package/dist/assets/{channels-list-page-C_Xe0CNX.js → channels-list-page-B6CvmU9T.js} +1 -1
  6. package/dist/assets/{chat-page-CXRtoQR2.js → chat-page-Ch4ShlKB.js} +1 -1
  7. package/dist/assets/{config-split-page-DBAcw9mJ.js → config-split-page-bO0K6uQv.js} +1 -1
  8. package/dist/assets/{confirm-dialog-DwPTxq8C.js → confirm-dialog-DXRWJ5uB.js} +1 -1
  9. package/dist/assets/{desktop-update-config-BqABa_1C.js → desktop-update-config-DPk5V1Nl.js} +1 -1
  10. package/dist/assets/{dist-BfcibKyv.js → dist-DQoV-62W.js} +1 -1
  11. package/dist/assets/{dist-WjhKB-pc.js → dist-T6rf86Nf.js} +1 -1
  12. package/dist/assets/{doc-browser-8EEDwGCJ.js → doc-browser-CTFB6ACN.js} +1 -1
  13. package/dist/assets/{doc-browser-context-DLJOy6Am.js → doc-browser-context-CP2tI-Br.js} +1 -1
  14. package/dist/assets/doc-browser-yi02ASOL.js +1 -0
  15. package/dist/assets/{ellipsis-H-4aNwgt.js → ellipsis-B7CbNPNr.js} +1 -1
  16. package/dist/assets/{es2015-B5k1IpKX.js → es2015-BSPOGpH4.js} +1 -1
  17. package/dist/assets/{external-link-BN1wd-eI.js → external-link-CefT35zP.js} +1 -1
  18. package/dist/assets/index-CevK3jug.css +1 -0
  19. package/dist/assets/{index-WJt0X9wC.js → index-DZoXsgdz.js} +43 -43
  20. package/dist/assets/{key-round-D-7IaAQS.js → key-round-BpM3GZn4.js} +1 -1
  21. package/dist/assets/{loader-circle-CB38Q7Ws.js → loader-circle-_edjOFsw.js} +1 -1
  22. package/dist/assets/mcp-marketplace-page-9maf_9fL.js +1 -0
  23. package/dist/assets/{mcp-marketplace-page-ju1KH3TG.js → mcp-marketplace-page-WAgpy0hp.js} +1 -1
  24. package/dist/assets/{model-config-CgS6dp65.js → model-config-CixioI5U.js} +1 -1
  25. package/dist/assets/{notice-card-DW9BLKpG.js → notice-card-BLklZuEa.js} +1 -1
  26. package/dist/assets/play-DD0bg50i.js +1 -0
  27. package/dist/assets/plus-zZ2GC_AJ.js +1 -0
  28. package/dist/assets/{popover-ofN845gZ.js → popover-D_pJfOlM.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DaDqi8Zy.js → provider-scoped-model-input-BbWqk2yz.js} +1 -1
  30. package/dist/assets/{providers-list-CZNQXY4n.js → providers-list-C_JOSjYd.js} +1 -1
  31. package/dist/assets/{react-Bq34NL59.js → react-Bl8iJUcF.js} +1 -1
  32. package/dist/assets/{refresh-cw-DJttSXS8.js → refresh-cw-CqfS1EhK.js} +1 -1
  33. package/dist/assets/remote-qtmzkte2.js +1 -0
  34. package/dist/assets/rotate-cw-C7EvaSlb.js +1 -0
  35. package/dist/assets/{runtime-config-page-gUjM6Z_A.js → runtime-config-page-ByvKO6Yn.js} +1 -1
  36. package/dist/assets/{save-DtAFbS7x.js → save-CQERCODg.js} +1 -1
  37. package/dist/assets/search-C0b2yzmt.js +1 -0
  38. package/dist/assets/{search-config-C04PrmHj.js → search-config-Cl2_NgFY.js} +1 -1
  39. package/dist/assets/{secrets-config-DvxHdz9h.js → secrets-config-BkR72J4g.js} +1 -1
  40. package/dist/assets/{status-dot-D4jbKDLB.js → status-dot-DY0xUUvK.js} +1 -1
  41. package/dist/assets/{tabs-Ca2vw7YQ.js → tabs-C77i1w90.js} +1 -1
  42. package/dist/assets/{tabs-custom-DzGDjYxf.js → tabs-custom-CFpR54-m.js} +1 -1
  43. package/dist/assets/{tag-chip-CW-_pHjH.js → tag-chip-Kib_ljle.js} +1 -1
  44. package/dist/assets/{tooltip-Cq2mERPk.js → tooltip-fs7VQG7d.js} +1 -1
  45. package/dist/assets/{trash-2-rumaEu9l.js → trash-2-Bb8mTTLl.js} +1 -1
  46. package/dist/assets/x-BV4iiea8.js +1 -0
  47. package/dist/index.html +60 -33
  48. package/dist/sw.js +6 -31
  49. package/index.html +35 -8
  50. package/package.json +8 -8
  51. package/public/sw.js +6 -31
  52. package/src/app/index.tsx +0 -2
  53. package/src/features/panel-apps/components/panel-app-list-item.test.tsx +4 -0
  54. package/src/features/panel-apps/components/panel-apps-list.tsx +27 -1
  55. package/src/features/panel-apps/hooks/use-panel-apps.ts +10 -0
  56. package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +18 -40
  57. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +24 -36
  58. package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +4 -0
  59. package/src/features/pwa/components/pwa-install-entry.test.tsx +5 -46
  60. package/src/features/pwa/components/pwa-install-entry.tsx +1 -35
  61. package/src/features/pwa/index.ts +1 -1
  62. package/src/features/pwa/managers/pwa-install.manager.ts +0 -2
  63. package/src/features/pwa/managers/pwa-runtime.manager.ts +2 -120
  64. package/src/features/pwa/managers/pwa-service-worker-cache.manager.test.ts +7 -5
  65. package/src/features/pwa/stores/pwa.store.ts +1 -3
  66. package/src/features/pwa/types/pwa.types.ts +0 -2
  67. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +4 -0
  68. package/src/features/service-apps/components/service-apps-panel.tsx +3 -3
  69. package/src/shared/lib/api/index.ts +1 -1
  70. package/src/shared/lib/api/{ncp-session-query-cache.test.ts → ncp-session-query-cache.utils.test.ts} +23 -0
  71. package/src/shared/lib/api/{ncp-session-query-cache.ts → ncp-session-query-cache.utils.ts} +9 -11
  72. package/src/shared/lib/api/types.ts +4 -0
  73. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +5 -0
  74. package/dist/assets/app-presenter-provider-CKZqZfv5.js +0 -3
  75. package/dist/assets/arrow-left-DHcD-tbC.js +0 -1
  76. package/dist/assets/doc-browser-DlT90zQD.js +0 -1
  77. package/dist/assets/index-BCq7kbC8.css +0 -1
  78. package/dist/assets/mcp-marketplace-page-CmZwt64F.js +0 -1
  79. package/dist/assets/play-DGpH8jeI.js +0 -1
  80. package/dist/assets/plus-CkcQjwuH.js +0 -1
  81. package/dist/assets/remote-CYmsM9DD.js +0 -1
  82. package/dist/assets/rotate-cw-D_xslHna.js +0 -1
  83. package/dist/assets/search-CNMRwAFx.js +0 -1
  84. 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 !== SHELL_CACHE && key !== RUNTIME_CACHE).map((key) => caches.delete(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 (['script', 'style', 'image', 'font'].includes(request.destination) || url.pathname === '/manifest.webmanifest') {
52
- event.respondWith(handleStaticAsset(request));
37
+ if (SHELL_ASSETS.includes(url.pathname)) {
38
+ event.respondWith(readShellAsset(request));
53
39
  }
54
40
  });
55
41
 
56
- async function handleNavigation(request) {
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 runtimeCache = await caches.open(RUNTIME_CACHE);
75
- runtimeCache.put(request, response.clone());
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: { data, source: contentWindow } as MessageEvent,
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('creates a fresh bridge session for each iframe instance', async () => {
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, iframeInstanceId] of [
245
- ['request-1', 'tab-1:0:0'],
246
- ['request-2', 'tab-1:0:0'],
247
- ['request-3', 'tab-1:0:1'],
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
- }, iframeInstanceId);
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-1',
241
+ bridgeSessionToken: 'token-2',
264
242
  });
265
243
  expect(mocks.listServiceActions).toHaveBeenNthCalledWith(3, {
266
- bridgeSessionToken: 'token-2',
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 = await this.getSession(params);
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: PanelAppBridgeSessionView,
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: PanelAppBridgeSessionView,
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: PanelAppBridgeSessionView,
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: PanelAppBridgeSessionView,
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: PanelAppBridgeSessionView,
197
+ session: PanelAppBridgeContext,
194
198
  capability: PanelAppAgentCapabilityView,
195
199
  ): Promise<PanelAppCapabilityGrantView> => {
196
200
  const allowed = await this.authorizationManager.requestAuthorization({
197
- panelAppId: session.panelAppId,
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: PanelAppBridgeSessionView,
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.panelAppId,
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: PanelAppBridgeSessionView,
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 getSession = async (
298
- params: DocBrowserIframeMessageParams,
299
- ): Promise<PanelAppBridgeSessionView> => {
300
- const panelAppId = this.readPanelAppId(params.tab.currentUrl);
301
- const key = `${params.iframeInstanceId}:${panelAppId}`;
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, PwaUpdateBanner } from '@/features/pwa';
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, RefreshCw, Smartphone, X } from 'lucide-react';
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, PwaUpdateBanner } from './components/pwa-install-entry';
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 = () => {