@nextclaw/ui 0.12.22 → 0.12.23
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 +6 -0
- package/dist/assets/{api-lwyw9j7i.js → api-BGd3rgv_.js} +5 -5
- package/dist/assets/app-manager-provider-BuJ_U9eC.js +1 -0
- package/dist/assets/{app-navigation.config-DgiR0c5_.js → app-navigation.config-BTdUuqXS.js} +1 -1
- package/dist/assets/{book-open-DgLqYpNY.js → book-open-DDlN5MvX.js} +1 -1
- package/dist/assets/{channels-list-page-Dl839n02.js → channels-list-page-BrwymXPe.js} +2 -2
- package/dist/assets/{chat-DwUf7AKR.js → chat-DGM6K3Qs.js} +5 -5
- package/dist/assets/{chat-page-B-FvPmA7.js → chat-page-DpmXMWNS.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-u4uYphxM.js → chunk-JZWAC4HX-Kydj4yEz.js} +1 -1
- package/dist/assets/{config-split-page-BMRGuCJQ.js → config-split-page-DIOCjj2Q.js} +1 -1
- package/dist/assets/{createLucideIcon-BZkY6emz.js → createLucideIcon-BLMK3QUd.js} +1 -1
- package/dist/assets/{desktop-update-config-D5g_gPak.js → desktop-update-config-BGKiqc6q.js} +1 -1
- package/dist/assets/{dialog-CdtCU2xX.js → dialog-dxsKz7jJ.js} +1 -1
- package/dist/assets/{dist-CuqvE--P.js → dist-DsYTOyq7.js} +1 -1
- package/dist/assets/{doc-browser-BUlCkZo2.js → doc-browser-C8FM5fC0.js} +1 -1
- package/dist/assets/doc-browser-RJUOL_GO.js +1 -0
- package/dist/assets/{doc-browser-context-DfLHAWbG.js → doc-browser-context-BJuMaI3o.js} +1 -1
- package/dist/assets/{doc-browser-CzCV73NJ.js → doc-browser-p82AdNO-.js} +1 -1
- package/dist/assets/{es2015-yYU5Ad5w.js → es2015-V75WQJ2s.js} +1 -1
- package/dist/assets/{external-link-Sw3ah_JD.js → external-link-DwfSfTLB.js} +1 -1
- package/dist/assets/{folder-D7-VTnkz.js → folder-CeJKPx5P.js} +1 -1
- package/dist/assets/{hash-zajSTDXZ.js → hash-BqxRTZW5.js} +1 -1
- package/dist/assets/i18n-DnTGDIRw.js +1 -0
- package/dist/assets/{index-Doxyk7L2.js → index-BrEdR78s.js} +2 -2
- package/dist/assets/{key-round-CnI1mc9F.js → key-round-CJ5gDAAG.js} +1 -1
- package/dist/assets/loader-circle-fd-vQKtW.js +1 -0
- package/dist/assets/{logo-badge-BQgKnVtz.js → logo-badge-KAe-7d8c.js} +1 -1
- package/dist/assets/{logos-CqVm0q0W.js → logos-C4sYP1Vl.js} +1 -1
- package/dist/assets/marketplace-page-B2Pm2RDJ.js +1 -0
- package/dist/assets/{marketplace-page-CawcdL6Y.js → marketplace-page-CPHxlYL8.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BcjVmw36.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DEGfJ_70.js → mcp-marketplace-page-CswPXSjf.js} +1 -1
- package/dist/assets/message-square-z_osm9c0.js +1 -0
- package/dist/assets/{model-config-r-1RPSrZ.js → model-config-Cmruiqdx.js} +1 -1
- package/dist/assets/{notice-card-BPtCVEKW.js → notice-card-D1RNsTn_.js} +1 -1
- package/dist/assets/play-Dv6Nr1Ew.js +1 -0
- package/dist/assets/plus-D8eKFY7h.js +1 -0
- package/dist/assets/{popover-jbfQhYQh.js → popover-BMyiifTA.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-gdk2lmRi.js → provider-scoped-model-input-D7ACiMAO.js} +1 -1
- package/dist/assets/{providers-list-DpISIr3M.js → providers-list-gg7LrfuB.js} +1 -1
- package/dist/assets/{refresh-ccw-Bii4w8aB.js → refresh-ccw-ByVwmnN_.js} +1 -1
- package/dist/assets/{refresh-cw-BxojR62w.js → refresh-cw-PcqoYB3K.js} +1 -1
- package/dist/assets/remote-Db2M39Cv.js +1 -0
- package/dist/assets/{rotate-cw-1Xqa7LZ8.js → rotate-cw-BZ2JObNs.js} +1 -1
- package/dist/assets/runtime-config-page-BT_VV41p.js +1 -0
- package/dist/assets/{save--BVI5wZX.js → save-euRxl8pI.js} +1 -1
- package/dist/assets/{search-vChioOoe.js → search-CLd7m0M7.js} +1 -1
- package/dist/assets/{search-config-BWqz8nqY.js → search-config-0VTPpz-w.js} +1 -1
- package/dist/assets/{secrets-config-CjzSNg0Y.js → secrets-config-DwQbLLEy.js} +1 -1
- package/dist/assets/{select-Cw5Zkb1w.js → select-DTdzR8j8.js} +1 -1
- package/dist/assets/{sessions-config-page-beoDPtII.js → sessions-config-page-CAG7Zevv.js} +1 -1
- package/dist/assets/{setting-row-Cjl2d40s.js → setting-row-CvKngoNI.js} +1 -1
- package/dist/assets/{settings-CiRChctQ.js → settings-drbWqzA4.js} +1 -1
- package/dist/assets/skeleton-BK1SOSRA.js +1 -0
- package/dist/assets/{sparkles-D1ZKWdm4.js → sparkles-DVfeSVJQ.js} +1 -1
- package/dist/assets/{status-dot-Dv_hiUVa.js → status-dot-ChvPCib9.js} +1 -1
- package/dist/assets/{tabs-custom-CsACkVji.js → tabs-custom-Hia_ong0.js} +1 -1
- package/dist/assets/{tag-chip-CoWHxYJj.js → tag-chip-BywQeHJj.js} +1 -1
- package/dist/assets/theme-provider-COAwWFv8.js +2 -0
- package/dist/assets/{tooltip-GYzH-Hfq.js → tooltip-BOYp8Ue7.js} +1 -1
- package/dist/assets/{trash-2-rY9ZteZX.js → trash-2-CBsHCfqq.js} +1 -1
- package/dist/assets/{use-config-BhJHD3-G.js → use-config-DTwhNDQE.js} +1 -1
- package/dist/assets/{use-confirm-dialog-Bqgy3Gi-.js → use-confirm-dialog-oeSqhmrx.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-BfexitoF.js → use-infinite-scroll-loader-X3KGuME8.js} +1 -1
- package/dist/assets/{use-viewport-layout-D33zVbr5.js → use-viewport-layout-C0NJAVXs.js} +1 -1
- package/dist/assets/x-CM-XDMpk.js +1 -0
- package/dist/index.html +39 -39
- package/package.json +6 -6
- package/src/features/account/hooks/use-auth.test.ts +7 -5
- package/src/features/account/hooks/use-auth.ts +23 -20
- package/src/features/system-status/hooks/use-system-status.ts +6 -28
- package/src/features/system-status/index.ts +2 -1
- package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +14 -4
- package/src/features/system-status/managers/system-status.manager.test.ts +2 -8
- package/src/features/system-status/managers/system-status.manager.ts +20 -30
- package/src/shared/components/common/brand-header.test.tsx +84 -3
- package/src/shared/components/common/brand-header.tsx +37 -39
- package/src/shared/lib/api/managers/client.manager.ts +30 -2
- package/src/shared/lib/api/utils/config.utils.ts +6 -4
- package/src/shared/lib/i18n/desktop-update-labels.utils.ts +3 -1
- package/src/shared/lib/transport/index.ts +1 -0
- package/src/shared/lib/transport/transport.types.ts +20 -0
- package/dist/assets/app-manager-provider-C0ONQxUg.js +0 -1
- package/dist/assets/doc-browser-Doh2541x.js +0 -1
- package/dist/assets/i18n-C5Mibli1.js +0 -1
- package/dist/assets/loader-circle-B5i8oMMY.js +0 -1
- package/dist/assets/marketplace-page-BRHkZaO5.js +0 -1
- package/dist/assets/mcp-marketplace-page-CL7BF4dD.js +0 -1
- package/dist/assets/message-square-D6Z4NwpG.js +0 -1
- package/dist/assets/play-D8WJLnJe.js +0 -1
- package/dist/assets/plus-Di0KAkiO.js +0 -1
- package/dist/assets/remote-BnRNqMlb.js +0 -1
- package/dist/assets/runtime-config-page-DQ8YY8Lc.js +0 -1
- package/dist/assets/skeleton-CFQRIUzt.js +0 -1
- package/dist/assets/theme-provider-B5XReW_-.js +0 -1
- package/dist/assets/x-DpTzXQcX.js +0 -1
package/dist/index.html
CHANGED
|
@@ -78,45 +78,45 @@
|
|
|
78
78
|
})();
|
|
79
79
|
</script>
|
|
80
80
|
<title>NextClaw</title>
|
|
81
|
-
<script type="module" crossorigin src="/assets/index-
|
|
82
|
-
<link rel="modulepreload" crossorigin href="/assets/i18n-
|
|
83
|
-
<link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-
|
|
84
|
-
<link rel="modulepreload" crossorigin href="/assets/api-
|
|
85
|
-
<link rel="modulepreload" crossorigin href="/assets/es2015-
|
|
86
|
-
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-
|
|
87
|
-
<link rel="modulepreload" crossorigin href="/assets/select-
|
|
88
|
-
<link rel="modulepreload" crossorigin href="/assets/dist-
|
|
89
|
-
<link rel="modulepreload" crossorigin href="/assets/x-
|
|
90
|
-
<link rel="modulepreload" crossorigin href="/assets/dialog-
|
|
91
|
-
<link rel="modulepreload" crossorigin href="/assets/popover-
|
|
92
|
-
<link rel="modulepreload" crossorigin href="/assets/tooltip-
|
|
93
|
-
<link rel="modulepreload" crossorigin href="/assets/refresh-cw-
|
|
94
|
-
<link rel="modulepreload" crossorigin href="/assets/use-config-
|
|
95
|
-
<link rel="modulepreload" crossorigin href="/assets/theme-provider-
|
|
96
|
-
<link rel="modulepreload" crossorigin href="/assets/search-
|
|
97
|
-
<link rel="modulepreload" crossorigin href="/assets/book-open-
|
|
98
|
-
<link rel="modulepreload" crossorigin href="/assets/external-link-
|
|
99
|
-
<link rel="modulepreload" crossorigin href="/assets/folder-
|
|
100
|
-
<link rel="modulepreload" crossorigin href="/assets/logos-
|
|
101
|
-
<link rel="modulepreload" crossorigin href="/assets/loader-circle-
|
|
102
|
-
<link rel="modulepreload" crossorigin href="/assets/plus-
|
|
103
|
-
<link rel="modulepreload" crossorigin href="/assets/refresh-ccw-
|
|
104
|
-
<link rel="modulepreload" crossorigin href="/assets/settings-
|
|
105
|
-
<link rel="modulepreload" crossorigin href="/assets/sparkles-
|
|
106
|
-
<link rel="modulepreload" crossorigin href="/assets/trash-2-
|
|
107
|
-
<link rel="modulepreload" crossorigin href="/assets/doc-browser-context-
|
|
108
|
-
<link rel="modulepreload" crossorigin href="/assets/doc-browser-
|
|
109
|
-
<link rel="modulepreload" crossorigin href="/assets/doc-browser-
|
|
110
|
-
<link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-
|
|
111
|
-
<link rel="modulepreload" crossorigin href="/assets/logo-badge-
|
|
112
|
-
<link rel="modulepreload" crossorigin href="/assets/skeleton-
|
|
113
|
-
<link rel="modulepreload" crossorigin href="/assets/chat-
|
|
114
|
-
<link rel="modulepreload" crossorigin href="/assets/key-round-
|
|
115
|
-
<link rel="modulepreload" crossorigin href="/assets/message-square-
|
|
116
|
-
<link rel="modulepreload" crossorigin href="/assets/app-navigation.config-
|
|
117
|
-
<link rel="modulepreload" crossorigin href="/assets/notice-card-
|
|
118
|
-
<link rel="modulepreload" crossorigin href="/assets/status-dot-
|
|
119
|
-
<link rel="modulepreload" crossorigin href="/assets/app-manager-provider-
|
|
81
|
+
<script type="module" crossorigin src="/assets/index-BrEdR78s.js"></script>
|
|
82
|
+
<link rel="modulepreload" crossorigin href="/assets/i18n-DnTGDIRw.js">
|
|
83
|
+
<link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-Kydj4yEz.js">
|
|
84
|
+
<link rel="modulepreload" crossorigin href="/assets/api-BGd3rgv_.js">
|
|
85
|
+
<link rel="modulepreload" crossorigin href="/assets/es2015-V75WQJ2s.js">
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BLMK3QUd.js">
|
|
87
|
+
<link rel="modulepreload" crossorigin href="/assets/select-DTdzR8j8.js">
|
|
88
|
+
<link rel="modulepreload" crossorigin href="/assets/dist-DsYTOyq7.js">
|
|
89
|
+
<link rel="modulepreload" crossorigin href="/assets/x-CM-XDMpk.js">
|
|
90
|
+
<link rel="modulepreload" crossorigin href="/assets/dialog-dxsKz7jJ.js">
|
|
91
|
+
<link rel="modulepreload" crossorigin href="/assets/popover-BMyiifTA.js">
|
|
92
|
+
<link rel="modulepreload" crossorigin href="/assets/tooltip-BOYp8Ue7.js">
|
|
93
|
+
<link rel="modulepreload" crossorigin href="/assets/refresh-cw-PcqoYB3K.js">
|
|
94
|
+
<link rel="modulepreload" crossorigin href="/assets/use-config-DTwhNDQE.js">
|
|
95
|
+
<link rel="modulepreload" crossorigin href="/assets/theme-provider-COAwWFv8.js">
|
|
96
|
+
<link rel="modulepreload" crossorigin href="/assets/search-CLd7m0M7.js">
|
|
97
|
+
<link rel="modulepreload" crossorigin href="/assets/book-open-DDlN5MvX.js">
|
|
98
|
+
<link rel="modulepreload" crossorigin href="/assets/external-link-DwfSfTLB.js">
|
|
99
|
+
<link rel="modulepreload" crossorigin href="/assets/folder-CeJKPx5P.js">
|
|
100
|
+
<link rel="modulepreload" crossorigin href="/assets/logos-C4sYP1Vl.js">
|
|
101
|
+
<link rel="modulepreload" crossorigin href="/assets/loader-circle-fd-vQKtW.js">
|
|
102
|
+
<link rel="modulepreload" crossorigin href="/assets/plus-D8eKFY7h.js">
|
|
103
|
+
<link rel="modulepreload" crossorigin href="/assets/refresh-ccw-ByVwmnN_.js">
|
|
104
|
+
<link rel="modulepreload" crossorigin href="/assets/settings-drbWqzA4.js">
|
|
105
|
+
<link rel="modulepreload" crossorigin href="/assets/sparkles-DVfeSVJQ.js">
|
|
106
|
+
<link rel="modulepreload" crossorigin href="/assets/trash-2-CBsHCfqq.js">
|
|
107
|
+
<link rel="modulepreload" crossorigin href="/assets/doc-browser-context-BJuMaI3o.js">
|
|
108
|
+
<link rel="modulepreload" crossorigin href="/assets/doc-browser-p82AdNO-.js">
|
|
109
|
+
<link rel="modulepreload" crossorigin href="/assets/doc-browser-C8FM5fC0.js">
|
|
110
|
+
<link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-C0NJAVXs.js">
|
|
111
|
+
<link rel="modulepreload" crossorigin href="/assets/logo-badge-KAe-7d8c.js">
|
|
112
|
+
<link rel="modulepreload" crossorigin href="/assets/skeleton-BK1SOSRA.js">
|
|
113
|
+
<link rel="modulepreload" crossorigin href="/assets/chat-DGM6K3Qs.js">
|
|
114
|
+
<link rel="modulepreload" crossorigin href="/assets/key-round-CJ5gDAAG.js">
|
|
115
|
+
<link rel="modulepreload" crossorigin href="/assets/message-square-z_osm9c0.js">
|
|
116
|
+
<link rel="modulepreload" crossorigin href="/assets/app-navigation.config-BTdUuqXS.js">
|
|
117
|
+
<link rel="modulepreload" crossorigin href="/assets/notice-card-D1RNsTn_.js">
|
|
118
|
+
<link rel="modulepreload" crossorigin href="/assets/status-dot-ChvPCib9.js">
|
|
119
|
+
<link rel="modulepreload" crossorigin href="/assets/app-manager-provider-BuJ_U9eC.js">
|
|
120
120
|
<link rel="stylesheet" crossorigin href="/assets/index-D8MKmXtO.css">
|
|
121
121
|
</head>
|
|
122
122
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.23",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
"zod": "^3.23.8",
|
|
30
30
|
"zustand": "^5.0.2",
|
|
31
31
|
"@nextclaw/agent-chat": "0.1.13",
|
|
32
|
-
"@nextclaw/agent-chat-ui": "0.3.15",
|
|
33
32
|
"@nextclaw/client-sdk": "0.1.3",
|
|
34
|
-
"@nextclaw/
|
|
35
|
-
"@nextclaw/ncp-react": "0.4.28",
|
|
36
|
-
"@nextclaw/server": "0.12.15",
|
|
33
|
+
"@nextclaw/agent-chat-ui": "0.3.15",
|
|
37
34
|
"@nextclaw/shared": "0.1.2",
|
|
38
|
-
"@nextclaw/ncp-
|
|
35
|
+
"@nextclaw/ncp-react": "0.4.28",
|
|
36
|
+
"@nextclaw/ncp-http-agent-client": "0.3.20",
|
|
37
|
+
"@nextclaw/ncp": "0.5.8",
|
|
38
|
+
"@nextclaw/server": "0.12.15"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@testing-library/react": "^16.3.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
|
-
AUTH_STATUS_BOOTSTRAP_RETRY_DELAY_MS,
|
|
4
3
|
isTransientAuthStatusBootstrapError,
|
|
4
|
+
resolveAuthStatusBootstrapRetryDelay,
|
|
5
5
|
shouldRetryAuthStatusBootstrap
|
|
6
6
|
} from './use-auth';
|
|
7
7
|
|
|
@@ -23,11 +23,13 @@ describe('auth status bootstrap retry policy', () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it('stops retrying after the bootstrap retry budget is exhausted', () => {
|
|
26
|
-
expect(shouldRetryAuthStatusBootstrap(
|
|
27
|
-
expect(shouldRetryAuthStatusBootstrap(
|
|
26
|
+
expect(shouldRetryAuthStatusBootstrap(7, new Error('Failed to fetch'))).toBe(true);
|
|
27
|
+
expect(shouldRetryAuthStatusBootstrap(8, new Error('Failed to fetch'))).toBe(false);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it('
|
|
31
|
-
expect(
|
|
30
|
+
it('backs off retry delay without becoming sluggish', () => {
|
|
31
|
+
expect(resolveAuthStatusBootstrapRetryDelay(1)).toBe(500);
|
|
32
|
+
expect(resolveAuthStatusBootstrapRetryDelay(2)).toBe(1000);
|
|
33
|
+
expect(resolveAuthStatusBootstrapRetryDelay(4)).toBe(3000);
|
|
32
34
|
});
|
|
33
35
|
});
|
|
@@ -11,46 +11,49 @@ import {
|
|
|
11
11
|
import type { AuthStatusView } from '@/shared/lib/api';
|
|
12
12
|
import { toast } from 'sonner';
|
|
13
13
|
import { t } from '@/shared/lib/i18n';
|
|
14
|
+
import { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const AUTH_STATUS_BOOTSTRAP_PROBE_POLICY = {
|
|
17
|
+
maxRetries: 8,
|
|
18
|
+
startupTimeoutMs: 2_000,
|
|
19
|
+
settledTimeoutMs: 5_000,
|
|
20
|
+
retryBaseDelayMs: 500,
|
|
21
|
+
retryMaxDelayMs: 3_000,
|
|
22
|
+
} as const;
|
|
18
23
|
|
|
19
24
|
export function isTransientAuthStatusBootstrapError(error: unknown): boolean {
|
|
20
25
|
if (!(error instanceof Error)) {
|
|
21
26
|
return false;
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
if (!message) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
return (
|
|
28
|
-
message.includes('failed to fetch') ||
|
|
29
|
-
message.includes('networkerror') ||
|
|
30
|
-
message.includes('network request failed') ||
|
|
31
|
-
message.includes('load failed') ||
|
|
32
|
-
message.includes('request timed out') ||
|
|
33
|
-
message.includes('timed out waiting for remote request response') ||
|
|
34
|
-
message.includes('remote transport connection closed')
|
|
35
|
-
);
|
|
28
|
+
return isTransientRuntimeConnectionErrorMessage(error.message);
|
|
36
29
|
}
|
|
37
30
|
|
|
38
31
|
export function shouldRetryAuthStatusBootstrap(failureCount: number, error: unknown): boolean {
|
|
39
|
-
if (failureCount >=
|
|
32
|
+
if (failureCount >= AUTH_STATUS_BOOTSTRAP_PROBE_POLICY.maxRetries) {
|
|
40
33
|
return false;
|
|
41
34
|
}
|
|
42
35
|
return isTransientAuthStatusBootstrapError(error);
|
|
43
36
|
}
|
|
44
37
|
|
|
38
|
+
export function resolveAuthStatusBootstrapRetryDelay(failureCount: number): number {
|
|
39
|
+
return Math.min(
|
|
40
|
+
AUTH_STATUS_BOOTSTRAP_PROBE_POLICY.retryMaxDelayMs,
|
|
41
|
+
AUTH_STATUS_BOOTSTRAP_PROBE_POLICY.retryBaseDelayMs * 2 ** Math.max(0, failureCount - 1)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
45
|
export function useAuthStatus() {
|
|
46
46
|
const [bootstrapSettled, setBootstrapSettled] = useState(false);
|
|
47
47
|
const query = useQuery<AuthStatusView>({
|
|
48
48
|
queryKey: ['auth-status'],
|
|
49
|
-
queryFn: () => fetchAuthStatus({
|
|
49
|
+
queryFn: () => fetchAuthStatus({
|
|
50
|
+
timeoutMs: bootstrapSettled
|
|
51
|
+
? AUTH_STATUS_BOOTSTRAP_PROBE_POLICY.settledTimeoutMs
|
|
52
|
+
: AUTH_STATUS_BOOTSTRAP_PROBE_POLICY.startupTimeoutMs,
|
|
53
|
+
}),
|
|
50
54
|
staleTime: 5_000,
|
|
51
55
|
retry: shouldRetryAuthStatusBootstrap,
|
|
52
|
-
retryDelay:
|
|
53
|
-
refetchOnWindowFocus: true
|
|
56
|
+
retryDelay: resolveAuthStatusBootstrapRetryDelay
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
useEffect(() => {
|
|
@@ -10,46 +10,24 @@ import {
|
|
|
10
10
|
import { systemStatusManager } from '@/features/system-status/managers/system-status.manager';
|
|
11
11
|
import { useSystemStatusStore } from '@/features/system-status/stores/system-status.store';
|
|
12
12
|
|
|
13
|
-
function createPendingBootstrapStatus(): BootstrapStatusView {
|
|
14
|
-
return {
|
|
15
|
-
phase: 'kernel-starting',
|
|
16
|
-
ncpAgent: {
|
|
17
|
-
state: 'pending',
|
|
18
|
-
},
|
|
19
|
-
pluginHydration: {
|
|
20
|
-
state: 'pending',
|
|
21
|
-
loadedPluginCount: 0,
|
|
22
|
-
totalPluginCount: 0,
|
|
23
|
-
},
|
|
24
|
-
channels: {
|
|
25
|
-
state: 'pending',
|
|
26
|
-
enabled: [],
|
|
27
|
-
},
|
|
28
|
-
remote: {
|
|
29
|
-
state: 'pending',
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
13
|
export function useSystemStatusSources() {
|
|
35
|
-
const runtimeBootstrapStatus = useQuery({
|
|
14
|
+
const runtimeBootstrapStatus = useQuery<BootstrapStatusView>({
|
|
36
15
|
queryKey: ['runtime-bootstrap-status'],
|
|
37
|
-
queryFn: fetchBootstrapStatus
|
|
38
|
-
|
|
16
|
+
queryFn: () => fetchBootstrapStatus({
|
|
17
|
+
timeoutMs: 5_000,
|
|
18
|
+
}),
|
|
39
19
|
refetchInterval: (query) => {
|
|
40
20
|
return systemStatusManager.getRuntimeBootstrapPollInterval(
|
|
41
|
-
query.state.data
|
|
21
|
+
query.state.data,
|
|
22
|
+
query.state.fetchFailureCount
|
|
42
23
|
);
|
|
43
24
|
},
|
|
44
|
-
refetchIntervalInBackground: true,
|
|
45
25
|
retry: false,
|
|
46
|
-
refetchOnWindowFocus: true,
|
|
47
26
|
});
|
|
48
27
|
const runtimeControl = useQuery({
|
|
49
28
|
queryKey: ['runtime-control'],
|
|
50
29
|
queryFn: async () => await systemStatusManager.getRuntimeControl(),
|
|
51
30
|
staleTime: 5_000,
|
|
52
|
-
refetchOnWindowFocus: true,
|
|
53
31
|
});
|
|
54
32
|
|
|
55
33
|
useEffect(() => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { useRuntimeControlPanelView, useRuntimeStatusBadgeView, useSystemStatus, useSystemStatusSources } from './hooks/use-system-status';
|
|
2
|
-
export {
|
|
2
|
+
export { systemStatusManager } from './managers/system-status.manager';
|
|
3
|
+
export { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
3
4
|
export { runtimeUpdateManager } from './managers/runtime-update.manager';
|
|
4
5
|
export type { SystemStatusState, SystemStatusView } from './types/system-status.types';
|
|
5
6
|
export { useSystemStatusStore } from './stores/system-status.store';
|
|
@@ -16,7 +16,7 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
16
16
|
|
|
17
17
|
it('keeps polling while bootstrap status is missing', () => {
|
|
18
18
|
expect(systemStatusManager.getRuntimeBootstrapPollInterval(undefined)).toBe(
|
|
19
|
-
|
|
19
|
+
1000
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -40,7 +40,7 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
40
40
|
state: 'pending',
|
|
41
41
|
},
|
|
42
42
|
})
|
|
43
|
-
).toBe(
|
|
43
|
+
).toBe(1000);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it('continues polling even when bootstrap status reports an ncp agent error', () => {
|
|
@@ -65,7 +65,7 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
65
65
|
},
|
|
66
66
|
lastError: 'startup failed',
|
|
67
67
|
})
|
|
68
|
-
).toBe(
|
|
68
|
+
).toBe(2000);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
it('stops polling once the ncp agent is ready', () => {
|
|
@@ -121,6 +121,16 @@ describe('getRuntimeBootstrapPollInterval', () => {
|
|
|
121
121
|
state: 'disabled',
|
|
122
122
|
},
|
|
123
123
|
})
|
|
124
|
-
).toBe(
|
|
124
|
+
).toBe(1000);
|
|
125
125
|
});
|
|
126
|
+
|
|
127
|
+
it('backs off polling after transport failures', () => {
|
|
128
|
+
expect(
|
|
129
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(undefined, 1)
|
|
130
|
+
).toBe(2000);
|
|
131
|
+
expect(
|
|
132
|
+
systemStatusManager.getRuntimeBootstrapPollInterval(undefined, 3)
|
|
133
|
+
).toBe(5000);
|
|
134
|
+
});
|
|
135
|
+
|
|
126
136
|
});
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type { BootstrapStatusView } from '@/shared/lib/api';
|
|
3
3
|
import { appQueryClient } from '@/app-query-client';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
systemStatusManager,
|
|
7
|
-
} from './system-status.manager';
|
|
4
|
+
import { systemStatusManager } from './system-status.manager';
|
|
5
|
+
import { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
8
6
|
import { useSystemStatusStore } from '@/features/system-status/stores/system-status.store';
|
|
9
7
|
|
|
10
8
|
const readyBootstrapStatus: BootstrapStatusView = {
|
|
@@ -76,9 +74,6 @@ describe('systemStatusManager', () => {
|
|
|
76
74
|
});
|
|
77
75
|
|
|
78
76
|
it('enters recovering only after the page has previously reached ready', async () => {
|
|
79
|
-
const invalidateQueriesSpy = vi
|
|
80
|
-
.spyOn(appQueryClient, 'invalidateQueries')
|
|
81
|
-
.mockResolvedValue(undefined as never);
|
|
82
77
|
const refetchQueriesSpy = vi
|
|
83
78
|
.spyOn(appQueryClient, 'refetchQueries')
|
|
84
79
|
.mockResolvedValue(undefined as never);
|
|
@@ -93,7 +88,6 @@ describe('systemStatusManager', () => {
|
|
|
93
88
|
systemStatusManager.reportBootstrapStatus(readyBootstrapStatus);
|
|
94
89
|
|
|
95
90
|
expect(useSystemStatusStore.getState().state.lifecyclePhase).toBe('ready');
|
|
96
|
-
expect(invalidateQueriesSpy).toHaveBeenCalled();
|
|
97
91
|
expect(refetchQueriesSpy).toHaveBeenCalledWith({ type: 'active' });
|
|
98
92
|
});
|
|
99
93
|
|
|
@@ -21,8 +21,14 @@ import {
|
|
|
21
21
|
initialSystemStatusState,
|
|
22
22
|
useSystemStatusStore,
|
|
23
23
|
} from '@/features/system-status/stores/system-status.store';
|
|
24
|
+
import { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
24
25
|
|
|
25
26
|
const RECOVERY_TIMEOUT_MS = 30_000;
|
|
27
|
+
const RUNTIME_BOOTSTRAP_PROBE_POLICY = {
|
|
28
|
+
activePollIntervalMs: 1_000,
|
|
29
|
+
errorPollIntervalMs: 2_000,
|
|
30
|
+
maxErrorPollIntervalMs: 5_000,
|
|
31
|
+
} as const;
|
|
26
32
|
|
|
27
33
|
function getErrorMessage(error: unknown): string {
|
|
28
34
|
if (error instanceof Error) {
|
|
@@ -53,46 +59,34 @@ function resolveActionHelp(action: RuntimeControlAction): string {
|
|
|
53
59
|
return t('runtimeControlRestartingAppHelp');
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
export function isTransientRuntimeConnectionErrorMessage(
|
|
57
|
-
message: string
|
|
58
|
-
): boolean {
|
|
59
|
-
const normalized = message.trim().toLowerCase();
|
|
60
|
-
if (!normalized) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
return (
|
|
64
|
-
normalized.includes('failed to fetch') ||
|
|
65
|
-
normalized.includes('networkerror') ||
|
|
66
|
-
normalized.includes('network request failed') ||
|
|
67
|
-
normalized.includes('load failed') ||
|
|
68
|
-
normalized.includes('request timed out') ||
|
|
69
|
-
normalized.includes('timed out waiting for remote request response') ||
|
|
70
|
-
normalized.includes('remote transport connection closed') ||
|
|
71
|
-
normalized.includes('websocket error') ||
|
|
72
|
-
normalized.includes('fetch failed on ') ||
|
|
73
|
-
normalized.includes('stream request failed for ') ||
|
|
74
|
-
normalized.includes('ncp fetch failed for ')
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
62
|
export class SystemStatusManager {
|
|
79
63
|
private recoveryTimeoutId: number | null = null;
|
|
80
64
|
|
|
81
65
|
getRuntimeBootstrapPollInterval = (
|
|
82
|
-
status: BootstrapStatusView | null | undefined
|
|
66
|
+
status: BootstrapStatusView | null | undefined,
|
|
67
|
+
fetchFailureCount = 0
|
|
83
68
|
): number | false => {
|
|
84
69
|
const { lifecyclePhase, activeSystemAction } = this.getState();
|
|
70
|
+
if (fetchFailureCount > 0) {
|
|
71
|
+
return Math.min(
|
|
72
|
+
RUNTIME_BOOTSTRAP_PROBE_POLICY.maxErrorPollIntervalMs,
|
|
73
|
+
RUNTIME_BOOTSTRAP_PROBE_POLICY.errorPollIntervalMs * fetchFailureCount
|
|
74
|
+
);
|
|
75
|
+
}
|
|
85
76
|
if (
|
|
86
77
|
lifecyclePhase === 'recovering' ||
|
|
87
78
|
lifecyclePhase === 'stalled' ||
|
|
88
79
|
activeSystemAction?.lifecycle === 'recovering'
|
|
89
80
|
) {
|
|
90
|
-
return
|
|
81
|
+
return RUNTIME_BOOTSTRAP_PROBE_POLICY.activePollIntervalMs;
|
|
91
82
|
}
|
|
92
83
|
if (status?.ncpAgent.state === 'ready') {
|
|
93
84
|
return false;
|
|
94
85
|
}
|
|
95
|
-
|
|
86
|
+
if (status?.ncpAgent.state === 'error' || status?.phase === 'error') {
|
|
87
|
+
return RUNTIME_BOOTSTRAP_PROBE_POLICY.errorPollIntervalMs;
|
|
88
|
+
}
|
|
89
|
+
return RUNTIME_BOOTSTRAP_PROBE_POLICY.activePollIntervalMs;
|
|
96
90
|
};
|
|
97
91
|
|
|
98
92
|
getRuntimeControl = async (): Promise<RuntimeControlView> => {
|
|
@@ -343,10 +337,7 @@ export class SystemStatusManager {
|
|
|
343
337
|
});
|
|
344
338
|
|
|
345
339
|
if (shouldRefreshQueries) {
|
|
346
|
-
void
|
|
347
|
-
appQueryClient.invalidateQueries(),
|
|
348
|
-
appQueryClient.refetchQueries({ type: 'active' }),
|
|
349
|
-
]);
|
|
340
|
+
void appQueryClient.refetchQueries({ type: 'active' });
|
|
350
341
|
}
|
|
351
342
|
};
|
|
352
343
|
|
|
@@ -413,7 +404,6 @@ export class SystemStatusManager {
|
|
|
413
404
|
const view = await this.getRuntimeControl();
|
|
414
405
|
this.syncRuntimeControlQueryCache(view);
|
|
415
406
|
this.reportRuntimeControlView(view);
|
|
416
|
-
await appQueryClient.invalidateQueries({ queryKey: ['runtime-control'] });
|
|
417
407
|
} catch (error) {
|
|
418
408
|
this.reportRuntimeControlError(error);
|
|
419
409
|
}
|
|
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
3
3
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
4
|
import { MemoryRouter } from 'react-router-dom';
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import type * as SystemStatusModule from '@/features/system-status';
|
|
6
7
|
import { useRuntimeUpdateStore } from '@/features/system-status';
|
|
7
8
|
import { BrandHeader } from '@/shared/components/common/brand-header';
|
|
8
9
|
import { setLanguage } from '@/shared/lib/i18n';
|
|
@@ -13,9 +14,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
13
14
|
}));
|
|
14
15
|
|
|
15
16
|
vi.mock('@/features/system-status', async () => {
|
|
16
|
-
const actual = await vi.importActual<typeof
|
|
17
|
-
'@/features/system-status'
|
|
18
|
-
);
|
|
17
|
+
const actual = await vi.importActual<typeof SystemStatusModule>('@/features/system-status');
|
|
19
18
|
return {
|
|
20
19
|
...actual,
|
|
21
20
|
runtimeUpdateManager: {
|
|
@@ -144,4 +143,86 @@ describe('BrandHeader', () => {
|
|
|
144
143
|
expect(mocks.applyDownloadedUpdate).toHaveBeenCalledTimes(1);
|
|
145
144
|
expect(mocks.downloadUpdate).not.toHaveBeenCalled();
|
|
146
145
|
});
|
|
146
|
+
|
|
147
|
+
it('shows a warning icon with the blocked update reason instead of a visible failure label', async () => {
|
|
148
|
+
useRuntimeUpdateStore.setState({
|
|
149
|
+
supported: true,
|
|
150
|
+
initialized: true,
|
|
151
|
+
busyAction: null,
|
|
152
|
+
snapshot: {
|
|
153
|
+
status: 'blocked',
|
|
154
|
+
installationKind: 'npm-runtime-bundle',
|
|
155
|
+
channel: 'stable',
|
|
156
|
+
hostVersion: '0.19.4',
|
|
157
|
+
currentVersion: '0.19.4',
|
|
158
|
+
availableVersion: null,
|
|
159
|
+
downloadedVersion: null,
|
|
160
|
+
minimumHostVersion: null,
|
|
161
|
+
releaseNotesUrl: null,
|
|
162
|
+
lastCheckedAt: null,
|
|
163
|
+
progress: null,
|
|
164
|
+
canAutoDownload: true,
|
|
165
|
+
canApplyInApp: false,
|
|
166
|
+
requiresRestart: false,
|
|
167
|
+
blockReason: 'signature-verification-unavailable',
|
|
168
|
+
recoveryCommand: 'Set NEXTCLAW_UPDATE_BUNDLE_PUBLIC_KEY',
|
|
169
|
+
errorMessage: 'Runtime bundle updates require a configured update public key.',
|
|
170
|
+
preferences: {
|
|
171
|
+
automaticChecks: true,
|
|
172
|
+
autoDownload: true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
renderBrandHeader();
|
|
178
|
+
|
|
179
|
+
expect(screen.queryByText('更新异常')).toBeNull();
|
|
180
|
+
const issueIcon = screen.getByLabelText('更新被阻塞');
|
|
181
|
+
|
|
182
|
+
expect(issueIcon.textContent).toBe('!');
|
|
183
|
+
expect(issueIcon.getAttribute('title')).toContain('更新被阻塞');
|
|
184
|
+
expect(issueIcon.getAttribute('title')).toContain('根因:缺少更新签名公钥,无法验证更新包来源');
|
|
185
|
+
expect(issueIcon.getAttribute('title')).toContain('Runtime bundle updates require a configured update public key.');
|
|
186
|
+
expect(issueIcon.getAttribute('title')).toContain('Set NEXTCLAW_UPDATE_BUNDLE_PUBLIC_KEY');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('uses the failed update wording only for failed snapshots', async () => {
|
|
190
|
+
useRuntimeUpdateStore.setState({
|
|
191
|
+
supported: true,
|
|
192
|
+
initialized: true,
|
|
193
|
+
busyAction: null,
|
|
194
|
+
snapshot: {
|
|
195
|
+
status: 'failed',
|
|
196
|
+
installationKind: 'npm-runtime-bundle',
|
|
197
|
+
channel: 'stable',
|
|
198
|
+
hostVersion: '0.19.4',
|
|
199
|
+
currentVersion: '0.19.3',
|
|
200
|
+
availableVersion: '0.19.4',
|
|
201
|
+
downloadedVersion: null,
|
|
202
|
+
minimumHostVersion: null,
|
|
203
|
+
releaseNotesUrl: null,
|
|
204
|
+
lastCheckedAt: null,
|
|
205
|
+
progress: null,
|
|
206
|
+
canAutoDownload: true,
|
|
207
|
+
canApplyInApp: false,
|
|
208
|
+
requiresRestart: false,
|
|
209
|
+
blockReason: null,
|
|
210
|
+
recoveryCommand: null,
|
|
211
|
+
errorMessage: 'runtime bundle sha256 mismatch',
|
|
212
|
+
preferences: {
|
|
213
|
+
automaticChecks: true,
|
|
214
|
+
autoDownload: true
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
renderBrandHeader();
|
|
220
|
+
|
|
221
|
+
const issueIcon = screen.getByLabelText('更新失败');
|
|
222
|
+
|
|
223
|
+
expect(issueIcon.textContent).toBe('!');
|
|
224
|
+
expect(issueIcon.getAttribute('title')).toContain('更新失败');
|
|
225
|
+
expect(issueIcon.getAttribute('title')).toContain('runtime bundle sha256 mismatch');
|
|
226
|
+
expect(screen.queryByText('更新被阻塞')).toBeNull();
|
|
227
|
+
});
|
|
147
228
|
});
|