@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/assets/{api-lwyw9j7i.js → api-BGd3rgv_.js} +5 -5
  3. package/dist/assets/app-manager-provider-BuJ_U9eC.js +1 -0
  4. package/dist/assets/{app-navigation.config-DgiR0c5_.js → app-navigation.config-BTdUuqXS.js} +1 -1
  5. package/dist/assets/{book-open-DgLqYpNY.js → book-open-DDlN5MvX.js} +1 -1
  6. package/dist/assets/{channels-list-page-Dl839n02.js → channels-list-page-BrwymXPe.js} +2 -2
  7. package/dist/assets/{chat-DwUf7AKR.js → chat-DGM6K3Qs.js} +5 -5
  8. package/dist/assets/{chat-page-B-FvPmA7.js → chat-page-DpmXMWNS.js} +1 -1
  9. package/dist/assets/{chunk-JZWAC4HX-u4uYphxM.js → chunk-JZWAC4HX-Kydj4yEz.js} +1 -1
  10. package/dist/assets/{config-split-page-BMRGuCJQ.js → config-split-page-DIOCjj2Q.js} +1 -1
  11. package/dist/assets/{createLucideIcon-BZkY6emz.js → createLucideIcon-BLMK3QUd.js} +1 -1
  12. package/dist/assets/{desktop-update-config-D5g_gPak.js → desktop-update-config-BGKiqc6q.js} +1 -1
  13. package/dist/assets/{dialog-CdtCU2xX.js → dialog-dxsKz7jJ.js} +1 -1
  14. package/dist/assets/{dist-CuqvE--P.js → dist-DsYTOyq7.js} +1 -1
  15. package/dist/assets/{doc-browser-BUlCkZo2.js → doc-browser-C8FM5fC0.js} +1 -1
  16. package/dist/assets/doc-browser-RJUOL_GO.js +1 -0
  17. package/dist/assets/{doc-browser-context-DfLHAWbG.js → doc-browser-context-BJuMaI3o.js} +1 -1
  18. package/dist/assets/{doc-browser-CzCV73NJ.js → doc-browser-p82AdNO-.js} +1 -1
  19. package/dist/assets/{es2015-yYU5Ad5w.js → es2015-V75WQJ2s.js} +1 -1
  20. package/dist/assets/{external-link-Sw3ah_JD.js → external-link-DwfSfTLB.js} +1 -1
  21. package/dist/assets/{folder-D7-VTnkz.js → folder-CeJKPx5P.js} +1 -1
  22. package/dist/assets/{hash-zajSTDXZ.js → hash-BqxRTZW5.js} +1 -1
  23. package/dist/assets/i18n-DnTGDIRw.js +1 -0
  24. package/dist/assets/{index-Doxyk7L2.js → index-BrEdR78s.js} +2 -2
  25. package/dist/assets/{key-round-CnI1mc9F.js → key-round-CJ5gDAAG.js} +1 -1
  26. package/dist/assets/loader-circle-fd-vQKtW.js +1 -0
  27. package/dist/assets/{logo-badge-BQgKnVtz.js → logo-badge-KAe-7d8c.js} +1 -1
  28. package/dist/assets/{logos-CqVm0q0W.js → logos-C4sYP1Vl.js} +1 -1
  29. package/dist/assets/marketplace-page-B2Pm2RDJ.js +1 -0
  30. package/dist/assets/{marketplace-page-CawcdL6Y.js → marketplace-page-CPHxlYL8.js} +1 -1
  31. package/dist/assets/mcp-marketplace-page-BcjVmw36.js +1 -0
  32. package/dist/assets/{mcp-marketplace-page-DEGfJ_70.js → mcp-marketplace-page-CswPXSjf.js} +1 -1
  33. package/dist/assets/message-square-z_osm9c0.js +1 -0
  34. package/dist/assets/{model-config-r-1RPSrZ.js → model-config-Cmruiqdx.js} +1 -1
  35. package/dist/assets/{notice-card-BPtCVEKW.js → notice-card-D1RNsTn_.js} +1 -1
  36. package/dist/assets/play-Dv6Nr1Ew.js +1 -0
  37. package/dist/assets/plus-D8eKFY7h.js +1 -0
  38. package/dist/assets/{popover-jbfQhYQh.js → popover-BMyiifTA.js} +1 -1
  39. package/dist/assets/{provider-scoped-model-input-gdk2lmRi.js → provider-scoped-model-input-D7ACiMAO.js} +1 -1
  40. package/dist/assets/{providers-list-DpISIr3M.js → providers-list-gg7LrfuB.js} +1 -1
  41. package/dist/assets/{refresh-ccw-Bii4w8aB.js → refresh-ccw-ByVwmnN_.js} +1 -1
  42. package/dist/assets/{refresh-cw-BxojR62w.js → refresh-cw-PcqoYB3K.js} +1 -1
  43. package/dist/assets/remote-Db2M39Cv.js +1 -0
  44. package/dist/assets/{rotate-cw-1Xqa7LZ8.js → rotate-cw-BZ2JObNs.js} +1 -1
  45. package/dist/assets/runtime-config-page-BT_VV41p.js +1 -0
  46. package/dist/assets/{save--BVI5wZX.js → save-euRxl8pI.js} +1 -1
  47. package/dist/assets/{search-vChioOoe.js → search-CLd7m0M7.js} +1 -1
  48. package/dist/assets/{search-config-BWqz8nqY.js → search-config-0VTPpz-w.js} +1 -1
  49. package/dist/assets/{secrets-config-CjzSNg0Y.js → secrets-config-DwQbLLEy.js} +1 -1
  50. package/dist/assets/{select-Cw5Zkb1w.js → select-DTdzR8j8.js} +1 -1
  51. package/dist/assets/{sessions-config-page-beoDPtII.js → sessions-config-page-CAG7Zevv.js} +1 -1
  52. package/dist/assets/{setting-row-Cjl2d40s.js → setting-row-CvKngoNI.js} +1 -1
  53. package/dist/assets/{settings-CiRChctQ.js → settings-drbWqzA4.js} +1 -1
  54. package/dist/assets/skeleton-BK1SOSRA.js +1 -0
  55. package/dist/assets/{sparkles-D1ZKWdm4.js → sparkles-DVfeSVJQ.js} +1 -1
  56. package/dist/assets/{status-dot-Dv_hiUVa.js → status-dot-ChvPCib9.js} +1 -1
  57. package/dist/assets/{tabs-custom-CsACkVji.js → tabs-custom-Hia_ong0.js} +1 -1
  58. package/dist/assets/{tag-chip-CoWHxYJj.js → tag-chip-BywQeHJj.js} +1 -1
  59. package/dist/assets/theme-provider-COAwWFv8.js +2 -0
  60. package/dist/assets/{tooltip-GYzH-Hfq.js → tooltip-BOYp8Ue7.js} +1 -1
  61. package/dist/assets/{trash-2-rY9ZteZX.js → trash-2-CBsHCfqq.js} +1 -1
  62. package/dist/assets/{use-config-BhJHD3-G.js → use-config-DTwhNDQE.js} +1 -1
  63. package/dist/assets/{use-confirm-dialog-Bqgy3Gi-.js → use-confirm-dialog-oeSqhmrx.js} +1 -1
  64. package/dist/assets/{use-infinite-scroll-loader-BfexitoF.js → use-infinite-scroll-loader-X3KGuME8.js} +1 -1
  65. package/dist/assets/{use-viewport-layout-D33zVbr5.js → use-viewport-layout-C0NJAVXs.js} +1 -1
  66. package/dist/assets/x-CM-XDMpk.js +1 -0
  67. package/dist/index.html +39 -39
  68. package/package.json +6 -6
  69. package/src/features/account/hooks/use-auth.test.ts +7 -5
  70. package/src/features/account/hooks/use-auth.ts +23 -20
  71. package/src/features/system-status/hooks/use-system-status.ts +6 -28
  72. package/src/features/system-status/index.ts +2 -1
  73. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +14 -4
  74. package/src/features/system-status/managers/system-status.manager.test.ts +2 -8
  75. package/src/features/system-status/managers/system-status.manager.ts +20 -30
  76. package/src/shared/components/common/brand-header.test.tsx +84 -3
  77. package/src/shared/components/common/brand-header.tsx +37 -39
  78. package/src/shared/lib/api/managers/client.manager.ts +30 -2
  79. package/src/shared/lib/api/utils/config.utils.ts +6 -4
  80. package/src/shared/lib/i18n/desktop-update-labels.utils.ts +3 -1
  81. package/src/shared/lib/transport/index.ts +1 -0
  82. package/src/shared/lib/transport/transport.types.ts +20 -0
  83. package/dist/assets/app-manager-provider-C0ONQxUg.js +0 -1
  84. package/dist/assets/doc-browser-Doh2541x.js +0 -1
  85. package/dist/assets/i18n-C5Mibli1.js +0 -1
  86. package/dist/assets/loader-circle-B5i8oMMY.js +0 -1
  87. package/dist/assets/marketplace-page-BRHkZaO5.js +0 -1
  88. package/dist/assets/mcp-marketplace-page-CL7BF4dD.js +0 -1
  89. package/dist/assets/message-square-D6Z4NwpG.js +0 -1
  90. package/dist/assets/play-D8WJLnJe.js +0 -1
  91. package/dist/assets/plus-Di0KAkiO.js +0 -1
  92. package/dist/assets/remote-BnRNqMlb.js +0 -1
  93. package/dist/assets/runtime-config-page-DQ8YY8Lc.js +0 -1
  94. package/dist/assets/skeleton-CFQRIUzt.js +0 -1
  95. package/dist/assets/theme-provider-B5XReW_-.js +0 -1
  96. 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-Doxyk7L2.js"></script>
82
- <link rel="modulepreload" crossorigin href="/assets/i18n-C5Mibli1.js">
83
- <link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-u4uYphxM.js">
84
- <link rel="modulepreload" crossorigin href="/assets/api-lwyw9j7i.js">
85
- <link rel="modulepreload" crossorigin href="/assets/es2015-yYU5Ad5w.js">
86
- <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BZkY6emz.js">
87
- <link rel="modulepreload" crossorigin href="/assets/select-Cw5Zkb1w.js">
88
- <link rel="modulepreload" crossorigin href="/assets/dist-CuqvE--P.js">
89
- <link rel="modulepreload" crossorigin href="/assets/x-DpTzXQcX.js">
90
- <link rel="modulepreload" crossorigin href="/assets/dialog-CdtCU2xX.js">
91
- <link rel="modulepreload" crossorigin href="/assets/popover-jbfQhYQh.js">
92
- <link rel="modulepreload" crossorigin href="/assets/tooltip-GYzH-Hfq.js">
93
- <link rel="modulepreload" crossorigin href="/assets/refresh-cw-BxojR62w.js">
94
- <link rel="modulepreload" crossorigin href="/assets/use-config-BhJHD3-G.js">
95
- <link rel="modulepreload" crossorigin href="/assets/theme-provider-B5XReW_-.js">
96
- <link rel="modulepreload" crossorigin href="/assets/search-vChioOoe.js">
97
- <link rel="modulepreload" crossorigin href="/assets/book-open-DgLqYpNY.js">
98
- <link rel="modulepreload" crossorigin href="/assets/external-link-Sw3ah_JD.js">
99
- <link rel="modulepreload" crossorigin href="/assets/folder-D7-VTnkz.js">
100
- <link rel="modulepreload" crossorigin href="/assets/logos-CqVm0q0W.js">
101
- <link rel="modulepreload" crossorigin href="/assets/loader-circle-B5i8oMMY.js">
102
- <link rel="modulepreload" crossorigin href="/assets/plus-Di0KAkiO.js">
103
- <link rel="modulepreload" crossorigin href="/assets/refresh-ccw-Bii4w8aB.js">
104
- <link rel="modulepreload" crossorigin href="/assets/settings-CiRChctQ.js">
105
- <link rel="modulepreload" crossorigin href="/assets/sparkles-D1ZKWdm4.js">
106
- <link rel="modulepreload" crossorigin href="/assets/trash-2-rY9ZteZX.js">
107
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-context-DfLHAWbG.js">
108
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-CzCV73NJ.js">
109
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-BUlCkZo2.js">
110
- <link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-D33zVbr5.js">
111
- <link rel="modulepreload" crossorigin href="/assets/logo-badge-BQgKnVtz.js">
112
- <link rel="modulepreload" crossorigin href="/assets/skeleton-CFQRIUzt.js">
113
- <link rel="modulepreload" crossorigin href="/assets/chat-DwUf7AKR.js">
114
- <link rel="modulepreload" crossorigin href="/assets/key-round-CnI1mc9F.js">
115
- <link rel="modulepreload" crossorigin href="/assets/message-square-D6Z4NwpG.js">
116
- <link rel="modulepreload" crossorigin href="/assets/app-navigation.config-DgiR0c5_.js">
117
- <link rel="modulepreload" crossorigin href="/assets/notice-card-BPtCVEKW.js">
118
- <link rel="modulepreload" crossorigin href="/assets/status-dot-Dv_hiUVa.js">
119
- <link rel="modulepreload" crossorigin href="/assets/app-manager-provider-C0ONQxUg.js">
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.22",
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/ncp": "0.5.8",
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-http-agent-client": "0.3.20"
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(39, new Error('Failed to fetch'))).toBe(true);
27
- expect(shouldRetryAuthStatusBootstrap(40, new Error('Failed to fetch'))).toBe(false);
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('keeps the retry delay short and predictable', () => {
31
- expect(AUTH_STATUS_BOOTSTRAP_RETRY_DELAY_MS).toBe(250);
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 AUTH_STATUS_BOOTSTRAP_MAX_RETRIES = 40;
16
- const AUTH_STATUS_BOOTSTRAP_TIMEOUT_MS = 400;
17
- export const AUTH_STATUS_BOOTSTRAP_RETRY_DELAY_MS = 250;
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
- const message = error.message.trim().toLowerCase();
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 >= AUTH_STATUS_BOOTSTRAP_MAX_RETRIES) {
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({ timeoutMs: bootstrapSettled ? 5_000 : AUTH_STATUS_BOOTSTRAP_TIMEOUT_MS }),
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: AUTH_STATUS_BOOTSTRAP_RETRY_DELAY_MS,
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
- placeholderData: createPendingBootstrapStatus,
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 { isTransientRuntimeConnectionErrorMessage, systemStatusManager } from './managers/system-status.manager';
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
- 500
19
+ 1000
20
20
  );
21
21
  });
22
22
 
@@ -40,7 +40,7 @@ describe('getRuntimeBootstrapPollInterval', () => {
40
40
  state: 'pending',
41
41
  },
42
42
  })
43
- ).toBe(500);
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(500);
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(500);
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
- isTransientRuntimeConnectionErrorMessage,
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 500;
81
+ return RUNTIME_BOOTSTRAP_PROBE_POLICY.activePollIntervalMs;
91
82
  }
92
83
  if (status?.ncpAgent.state === 'ready') {
93
84
  return false;
94
85
  }
95
- return 500;
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 Promise.all([
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 import('@/features/system-status')>(
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
  });