@nextclaw/ui 0.13.10-beta.0 → 0.13.11-beta.0

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 (45) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/assets/{api-CA2udGyv.js → api-C0F4luAh.js} +9 -4
  3. package/dist/assets/{app-presenter-provider-B9ChYida.js → app-presenter-provider-3K24vYTD.js} +2 -2
  4. package/dist/assets/{channels-list-page-PrT4a9g4.js → channels-list-page-DT8gjnFw.js} +1 -1
  5. package/dist/assets/{chat-page-CImtIhmN.js → chat-page-By1I_JyH.js} +1 -1
  6. package/dist/assets/{confirm-dialog-l4FYdzga.js → confirm-dialog-a7YIzprP.js} +1 -1
  7. package/dist/assets/{desktop-update-config-p3rJIgRg.js → desktop-update-config-CbJZyLND.js} +1 -1
  8. package/dist/assets/index-CevK3jug.css +1 -0
  9. package/dist/assets/{index-DFC_DdT1.js → index-DBOXf-UA.js} +43 -43
  10. package/dist/assets/mcp-marketplace-page-C2O8-sSE.js +1 -0
  11. package/dist/assets/{mcp-marketplace-page-DhqcnhZc.js → mcp-marketplace-page-IhNyQ9Hh.js} +1 -1
  12. package/dist/assets/{model-config-C9ydDDFh.js → model-config-Z7oAQmlP.js} +1 -1
  13. package/dist/assets/{notice-card-BfXlhPjX.js → notice-card-DhE1Fafy.js} +1 -1
  14. package/dist/assets/{provider-scoped-model-input-CkeEyTfO.js → provider-scoped-model-input-BfhiS3GD.js} +1 -1
  15. package/dist/assets/{providers-list-9eOBQFUn.js → providers-list-DpAMol1o.js} +1 -1
  16. package/dist/assets/remote-hKy7hvBh.js +1 -0
  17. package/dist/assets/{runtime-config-page-B30PC6bx.js → runtime-config-page-BvLypM3f.js} +1 -1
  18. package/dist/assets/{search-config-RN01uPJQ.js → search-config-mD9kAsXy.js} +1 -1
  19. package/dist/assets/{secrets-config-CTZ_TGoA.js → secrets-config-Dvd4VEDw.js} +1 -1
  20. package/dist/assets/{tag-chip-Dnb2VuWh.js → tag-chip-BFTpu75w.js} +1 -1
  21. package/dist/index.html +41 -14
  22. package/dist/sw.js +6 -31
  23. package/index.html +35 -8
  24. package/package.json +8 -8
  25. package/public/sw.js +6 -31
  26. package/src/app/index.tsx +0 -2
  27. package/src/features/panel-apps/components/panel-app-toolbar.test.tsx +24 -0
  28. package/src/features/panel-apps/components/panel-app-toolbar.tsx +11 -2
  29. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -1
  30. package/src/features/pwa/components/pwa-install-entry.test.tsx +5 -46
  31. package/src/features/pwa/components/pwa-install-entry.tsx +1 -35
  32. package/src/features/pwa/index.ts +1 -1
  33. package/src/features/pwa/managers/pwa-install.manager.ts +0 -2
  34. package/src/features/pwa/managers/pwa-runtime.manager.ts +2 -120
  35. package/src/features/pwa/managers/pwa-service-worker-cache.manager.test.ts +7 -5
  36. package/src/features/pwa/stores/pwa.store.ts +1 -3
  37. package/src/features/pwa/types/pwa.types.ts +0 -2
  38. package/src/shared/hooks/use-config.ts +2 -2
  39. package/src/shared/lib/api/ncp-session-query-cache.utils.test.ts +35 -0
  40. package/src/shared/lib/api/ncp-session-query-cache.utils.ts +9 -1
  41. package/src/shared/lib/api/utils/ncp-session.utils.ts +2 -2
  42. package/vite.config.ts +8 -0
  43. package/dist/assets/index-BCq7kbC8.css +0 -1
  44. package/dist/assets/mcp-marketplace-page-DNHSmfDf.js +0 -1
  45. package/dist/assets/remote-fGlxrJI_.js +0 -1
package/dist/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/index.html CHANGED
@@ -43,7 +43,6 @@
43
43
  </script>
44
44
  <script>
45
45
  (() => {
46
- const devResetKey = 'nextclaw-pwa-dev-sw-reset';
47
46
  const isViteDevServer = '%MODE%' === 'development';
48
47
  if (!isViteDevServer || !('serviceWorker' in navigator)) {
49
48
  return;
@@ -57,7 +56,6 @@
57
56
  });
58
57
 
59
58
  if (nextclawRegistrations.length === 0) {
60
- window.sessionStorage.removeItem(devResetKey);
61
59
  return;
62
60
  }
63
61
 
@@ -66,14 +64,43 @@
66
64
  const keys = await caches.keys();
67
65
  await Promise.all(keys.filter((key) => key.startsWith('nextclaw-ui-')).map(async (key) => await caches.delete(key)));
68
66
  }
69
-
70
- if (navigator.serviceWorker.controller && window.sessionStorage.getItem(devResetKey) !== '1') {
71
- window.sessionStorage.setItem(devResetKey, '1');
72
- window.location.reload();
73
- return;
67
+ });
68
+ })();
69
+ </script>
70
+ <script>
71
+ (() => {
72
+ const cleanupKey = 'nextclaw-pwa-legacy-runtime-cache-cleaned-v1';
73
+ const hasCleaned = () => {
74
+ try {
75
+ return window.localStorage.getItem(cleanupKey) === '1';
76
+ } catch {
77
+ return false;
78
+ }
79
+ };
80
+ const markCleaned = () => {
81
+ try {
82
+ window.localStorage.setItem(cleanupKey, '1');
83
+ } catch {
84
+ // ignore storage failures; cache cleanup is still safe to attempt once
74
85
  }
86
+ };
87
+ if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller || hasCleaned()) {
88
+ return;
89
+ }
75
90
 
76
- window.sessionStorage.removeItem(devResetKey);
91
+ markCleaned();
92
+ window.stop();
93
+ void Promise.all([
94
+ navigator.serviceWorker.getRegistrations().then((registrations) =>
95
+ Promise.all(registrations.map((registration) => registration.unregister()))
96
+ ),
97
+ 'caches' in window
98
+ ? caches.keys().then((keys) =>
99
+ Promise.all(keys.filter((key) => key.startsWith('nextclaw-ui-')).map((key) => caches.delete(key)))
100
+ )
101
+ : Promise.resolve()
102
+ ]).finally(() => {
103
+ window.location.reload();
77
104
  });
78
105
  })();
79
106
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.13.10-beta.0",
3
+ "version": "0.13.11-beta.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,13 +28,13 @@
28
28
  "tailwind-merge": "^2.5.4",
29
29
  "zod": "^3.23.8",
30
30
  "zustand": "^5.0.2",
31
- "@nextclaw/agent-chat": "0.2.9-beta.0",
32
- "@nextclaw/agent-chat-ui": "0.4.9-beta.0",
33
- "@nextclaw/client-sdk": "0.2.10-beta.0",
34
- "@nextclaw/ncp-http-agent-client": "0.3.41-beta.0",
35
- "@nextclaw/ncp": "0.5.29-beta.0",
36
- "@nextclaw/ncp-react": "0.4.49-beta.0",
37
- "@nextclaw/shared": "0.2.9-beta.0"
31
+ "@nextclaw/ncp": "0.6.0-beta.0",
32
+ "@nextclaw/client-sdk": "0.4.0-beta.0",
33
+ "@nextclaw/ncp-react": "0.4.50-beta.0",
34
+ "@nextclaw/agent-chat-ui": "0.4.10-beta.0",
35
+ "@nextclaw/ncp-http-agent-client": "0.3.42-beta.0",
36
+ "@nextclaw/shared": "0.2.10-beta.0",
37
+ "@nextclaw/agent-chat": "0.2.10-beta.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@testing-library/react": "^16.3.0",
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
  );
@@ -0,0 +1,24 @@
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 { PanelAppToolbar } from './panel-app-toolbar';
5
+
6
+ describe('PanelAppToolbar', () => {
7
+ it('shows the current panel app title in the detail header', async () => {
8
+ const user = userEvent.setup();
9
+ const onOpenApps = vi.fn();
10
+ render(
11
+ <PanelAppToolbar
12
+ appTitle="墨爪助手"
13
+ onOpenApps={onOpenApps}
14
+ onRefresh={vi.fn()}
15
+ />,
16
+ );
17
+
18
+ expect(screen.getByText('墨爪助手')).toBeTruthy();
19
+
20
+ await user.click(screen.getByRole('button', { name: 'Apps' }));
21
+
22
+ expect(onOpenApps).toHaveBeenCalledTimes(1);
23
+ });
24
+ });
@@ -2,11 +2,13 @@ import { ArrowLeft, RefreshCw } from 'lucide-react';
2
2
  import { t } from '@/shared/lib/i18n';
3
3
 
4
4
  type PanelAppToolbarProps = {
5
+ appTitle: string;
5
6
  onOpenApps: () => void;
6
7
  onRefresh: () => void;
7
8
  };
8
9
 
9
10
  export function PanelAppToolbar({
11
+ appTitle,
10
12
  onOpenApps,
11
13
  onRefresh,
12
14
  }: PanelAppToolbarProps) {
@@ -15,11 +17,18 @@ export function PanelAppToolbar({
15
17
  <button
16
18
  type="button"
17
19
  onClick={onOpenApps}
18
- className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
20
+ className="inline-flex shrink-0 items-center rounded-md p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
21
+ title={t('appsTitle')}
22
+ aria-label={t('appsTitle')}
19
23
  >
20
24
  <ArrowLeft className="h-3.5 w-3.5" />
21
- {t('appsTitle')}
22
25
  </button>
26
+ <span
27
+ className="min-w-0 flex-1 truncate text-xs font-medium text-gray-600"
28
+ title={appTitle}
29
+ >
30
+ {appTitle}
31
+ </span>
23
32
  <button
24
33
  type="button"
25
34
  onClick={onRefresh}
@@ -80,8 +80,9 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
80
80
  getTitle: (tab) => tab.title || t('panelAppsTitle'),
81
81
  onIframeMessage: (params) => getPresenter().panelAppBridgeManager.handleIframeMessage(params),
82
82
  renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
83
- renderToolbar: ({ open, refreshIframe }) => (
83
+ renderToolbar: ({ open, refreshIframe, tab }) => (
84
84
  <PanelAppToolbar
85
+ appTitle={tab.title || t('panelAppsTitle')}
85
86
  onOpenApps={() => openApps({ open })}
86
87
  onRefresh={refreshIframe}
87
88
  />
@@ -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 = () => {
@@ -1,11 +1,6 @@
1
- import { usePwaStore } from '@/features/pwa/stores/pwa.store';
2
-
3
- const PWA_DEV_RESET_SESSION_KEY = 'nextclaw-pwa-dev-sw-reset';
4
-
5
1
  export class PwaRuntimeManager {
6
2
  private started = false;
7
3
  private registration: ServiceWorkerRegistration | null = null;
8
- private reloadWhenControllerChanges = false;
9
4
 
10
5
  start = async () => {
11
6
  if (this.started || typeof window === 'undefined') {
@@ -25,55 +20,9 @@ export class PwaRuntimeManager {
25
20
  try {
26
21
  const registration = await navigator.serviceWorker.register('/sw.js');
27
22
  this.registration = registration;
28
- this.bindRegistration(registration);
29
- navigator.serviceWorker.addEventListener('controllerchange', this.handleControllerChange);
30
- await this.syncUpdateAvailability();
31
23
  } catch {
32
- usePwaStore.setState({
33
- registrationFailed: true
34
- });
35
- }
36
- };
37
-
38
- syncUpdateAvailability = async () => {
39
- if (
40
- typeof window === 'undefined' ||
41
- !('serviceWorker' in navigator) ||
42
- typeof navigator.serviceWorker.getRegistration !== 'function'
43
- ) {
44
- usePwaStore.setState({
45
- updateAvailable: false
46
- });
47
24
  return;
48
25
  }
49
-
50
- const registration = this.registration ?? (await navigator.serviceWorker.getRegistration('/sw.js')) ?? null;
51
- usePwaStore.setState({
52
- updateAvailable: this.shouldSurfaceUpdate(registration)
53
- });
54
- };
55
-
56
- applyUpdate = async () => {
57
- const registration = this.registration ?? (await navigator.serviceWorker.getRegistration('/sw.js')) ?? null;
58
- if (!registration) {
59
- window.location.reload();
60
- return;
61
- }
62
-
63
- if (!registration.waiting) {
64
- await registration.update();
65
- }
66
-
67
- const waitingWorker = registration.waiting;
68
- if (!waitingWorker) {
69
- window.location.reload();
70
- return;
71
- }
72
-
73
- this.reloadWhenControllerChanges = true;
74
- waitingWorker.postMessage({
75
- type: 'SKIP_WAIT'
76
- });
77
26
  };
78
27
 
79
28
  stop = () => {
@@ -81,34 +30,8 @@ export class PwaRuntimeManager {
81
30
  return;
82
31
  }
83
32
 
84
- navigator.serviceWorker.removeEventListener('controllerchange', this.handleControllerChange);
85
33
  this.started = false;
86
34
  this.registration = null;
87
- this.reloadWhenControllerChanges = false;
88
- };
89
-
90
- private bindRegistration = (registration: ServiceWorkerRegistration) => {
91
- registration.addEventListener('updatefound', () => {
92
- const installingWorker = registration.installing;
93
- if (!installingWorker) {
94
- return;
95
- }
96
-
97
- installingWorker.addEventListener('statechange', () => {
98
- if (installingWorker.state === 'installed') {
99
- void this.syncUpdateAvailability();
100
- }
101
- });
102
- });
103
- };
104
-
105
- private handleControllerChange = () => {
106
- if (!this.reloadWhenControllerChanges) {
107
- return;
108
- }
109
-
110
- this.reloadWhenControllerChanges = false;
111
- window.location.reload();
112
35
  };
113
36
 
114
37
  private isEligibleInstallContext = (): boolean => {
@@ -123,13 +46,6 @@ export class PwaRuntimeManager {
123
46
  return import.meta.env.DEV && !import.meta.env.VITEST;
124
47
  };
125
48
 
126
- private shouldSurfaceUpdate = (registration: ServiceWorkerRegistration | null): boolean => {
127
- if (!registration?.waiting || !navigator.serviceWorker.controller) {
128
- return false;
129
- }
130
- return usePwaStore.getState().installability === 'installed';
131
- };
132
-
133
49
  private cleanupDevelopmentRegistrations = async () => {
134
50
  if (typeof navigator.serviceWorker.getRegistrations !== 'function') {
135
51
  return;
@@ -142,21 +58,10 @@ export class PwaRuntimeManager {
142
58
  .some((scriptUrl) => scriptUrl.endsWith('/sw.js'))
143
59
  );
144
60
 
145
- if (nextclawRegistrations.length === 0) {
146
- this.clearDevResetMarker();
147
- return;
148
- }
149
-
150
61
  await Promise.all(nextclawRegistrations.map(async (registration) => await registration.unregister()));
151
- await this.clearNextClawCaches();
152
-
153
- if (navigator.serviceWorker.controller && !this.hasDevResetMarker()) {
154
- this.setDevResetMarker();
155
- window.location.reload();
156
- return;
62
+ if (nextclawRegistrations.length > 0) {
63
+ await this.clearNextClawCaches();
157
64
  }
158
-
159
- this.clearDevResetMarker();
160
65
  };
161
66
 
162
67
  private clearNextClawCaches = async () => {
@@ -168,29 +73,6 @@ export class PwaRuntimeManager {
168
73
  await Promise.all(keys.filter((key) => key.startsWith('nextclaw-ui-')).map(async (key) => await caches.delete(key)));
169
74
  };
170
75
 
171
- private hasDevResetMarker = (): boolean => {
172
- try {
173
- return window.sessionStorage.getItem(PWA_DEV_RESET_SESSION_KEY) === '1';
174
- } catch {
175
- return false;
176
- }
177
- };
178
-
179
- private setDevResetMarker = () => {
180
- try {
181
- window.sessionStorage.setItem(PWA_DEV_RESET_SESSION_KEY, '1');
182
- } catch {
183
- // ignore sessionStorage availability failures in dev cleanup
184
- }
185
- };
186
-
187
- private clearDevResetMarker = () => {
188
- try {
189
- window.sessionStorage.removeItem(PWA_DEV_RESET_SESSION_KEY);
190
- } catch {
191
- // ignore sessionStorage availability failures in dev cleanup
192
- }
193
- };
194
76
  }
195
77
 
196
78
  export const pwaRuntimeManager = new PwaRuntimeManager();