@nextclaw/ui 0.13.10-beta.0 → 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 +16 -0
- package/dist/assets/{api-CA2udGyv.js → api-DhI2lb1f.js} +9 -4
- package/dist/assets/{app-presenter-provider-B9ChYida.js → app-presenter-provider-BFbrQp8q.js} +1 -1
- package/dist/assets/{channels-list-page-PrT4a9g4.js → channels-list-page-B6CvmU9T.js} +1 -1
- package/dist/assets/{chat-page-CImtIhmN.js → chat-page-Ch4ShlKB.js} +1 -1
- package/dist/assets/{confirm-dialog-l4FYdzga.js → confirm-dialog-DXRWJ5uB.js} +1 -1
- package/dist/assets/{desktop-update-config-p3rJIgRg.js → desktop-update-config-DPk5V1Nl.js} +1 -1
- package/dist/assets/index-CevK3jug.css +1 -0
- package/dist/assets/{index-DFC_DdT1.js → index-DZoXsgdz.js} +43 -43
- package/dist/assets/mcp-marketplace-page-9maf_9fL.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DhqcnhZc.js → mcp-marketplace-page-WAgpy0hp.js} +1 -1
- package/dist/assets/{model-config-C9ydDDFh.js → model-config-CixioI5U.js} +1 -1
- package/dist/assets/{notice-card-BfXlhPjX.js → notice-card-BLklZuEa.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-CkeEyTfO.js → provider-scoped-model-input-BbWqk2yz.js} +1 -1
- package/dist/assets/{providers-list-9eOBQFUn.js → providers-list-C_JOSjYd.js} +1 -1
- package/dist/assets/remote-qtmzkte2.js +1 -0
- package/dist/assets/{runtime-config-page-B30PC6bx.js → runtime-config-page-ByvKO6Yn.js} +1 -1
- package/dist/assets/{search-config-RN01uPJQ.js → search-config-Cl2_NgFY.js} +1 -1
- package/dist/assets/{secrets-config-CTZ_TGoA.js → secrets-config-BkR72J4g.js} +1 -1
- package/dist/assets/{tag-chip-Dnb2VuWh.js → tag-chip-Kib_ljle.js} +1 -1
- package/dist/index.html +41 -14
- 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/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/dist/assets/index-BCq7kbC8.css +0 -1
- package/dist/assets/mcp-marketplace-page-DNHSmfDf.js +0 -1
- 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
|
|
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/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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.13.10-beta.1",
|
|
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/
|
|
32
|
-
"@nextclaw/
|
|
33
|
-
"@nextclaw/client
|
|
34
|
-
"@nextclaw/
|
|
35
|
-
"@nextclaw/ncp": "0.
|
|
36
|
-
"@nextclaw/
|
|
37
|
-
"@nextclaw/shared": "0.2.9-beta.
|
|
31
|
+
"@nextclaw/client-sdk": "0.3.0-beta.1",
|
|
32
|
+
"@nextclaw/ncp": "0.5.29-beta.1",
|
|
33
|
+
"@nextclaw/ncp-http-agent-client": "0.3.41-beta.1",
|
|
34
|
+
"@nextclaw/agent-chat": "0.2.9-beta.1",
|
|
35
|
+
"@nextclaw/ncp-react": "0.4.49-beta.1",
|
|
36
|
+
"@nextclaw/agent-chat-ui": "0.4.9-beta.1",
|
|
37
|
+
"@nextclaw/shared": "0.2.9-beta.1"
|
|
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
|
|
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
|
);
|
|
@@ -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 = () => {
|
|
@@ -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
|
-
|
|
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();
|
|
@@ -6,11 +6,13 @@ describe('NextClaw service worker cache policy', () => {
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
it('does not fall back to stale navigation responses', () => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
expect(serviceWorkerSource).not.toContain("request.mode === 'navigate'");
|
|
10
|
+
expect(serviceWorkerSource).not.toContain('handleNavigation');
|
|
11
|
+
expect(serviceWorkerSource).toContain('SHELL_ASSETS.includes(url.pathname)');
|
|
12
|
+
});
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
it('does not force service worker activation or page control', () => {
|
|
15
|
+
expect(serviceWorkerSource).not.toContain('skipWaiting');
|
|
16
|
+
expect(serviceWorkerSource).not.toContain('clients.claim');
|
|
15
17
|
});
|
|
16
18
|
});
|
|
@@ -8,9 +8,7 @@ export function createInitialPwaState(): PwaInstallStateSnapshot {
|
|
|
8
8
|
installability: 'unsupported',
|
|
9
9
|
installMethod: 'none',
|
|
10
10
|
blockedReason: 'missing-browser-support',
|
|
11
|
-
dismissedInstallPrompt: isPwaInstallBannerDismissed()
|
|
12
|
-
updateAvailable: false,
|
|
13
|
-
registrationFailed: false
|
|
11
|
+
dismissedInstallPrompt: isPwaInstallBannerDismissed()
|
|
14
12
|
};
|
|
15
13
|
}
|
|
16
14
|
|