@nextclaw/ui 0.12.10 → 0.12.11

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 (169) hide show
  1. package/CHANGELOG.md +51 -10
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-DMfr0Oow.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-BXydqby-.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-hO7tY7hE.js → LogoBadge-DP8Ye7wJ.js} +1 -1
  7. package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
  8. package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
  9. package/dist/assets/ProvidersList-BCupBayq.js +1 -0
  10. package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
  11. package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
  12. package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
  13. package/dist/assets/SessionsConfig-CvjxU40H.js +2 -0
  14. package/dist/assets/{book-open-DzdUViDm.js → book-open-BE8M56IM.js} +1 -1
  15. package/dist/assets/chat-page-JKC6ln-y.js +58 -0
  16. package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
  17. package/dist/assets/{chunk-JZWAC4HX-C5dEc8hV.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/{config-split-page-BUout_Ak.js → config-split-page-BAGSzUR3.js} +1 -1
  20. package/dist/assets/{createLucideIcon-dy5ie7Ox.js → createLucideIcon-CCiTGX8L.js} +1 -1
  21. package/dist/assets/desktop-DfkLlkG2.js +1 -0
  22. package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
  23. package/dist/assets/dialog-BghZFPch.js +5 -0
  24. package/dist/assets/{dist-Cy7_j6hA.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BD0ETkB-.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-kZSAO8nT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-BHJC2Ovu.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-CpTZLchQ.js → i18n-DvzXOGQX.js} +1 -1
  31. package/dist/assets/index-DvVTC9FF.css +1 -0
  32. package/dist/assets/index-lr6rQUSd.js +2 -0
  33. package/dist/assets/key-round-BLe9D8ND.js +1 -0
  34. package/dist/assets/loader-circle-wj7kARHv.js +1 -0
  35. package/dist/assets/{logos-B7gRObP8.js → logos-_v5b2SdG.js} +1 -1
  36. package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
  37. package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
  38. package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
  39. package/dist/assets/play-o6NmwGTi.js +1 -0
  40. package/dist/assets/plus-I9pBS4Fl.js +1 -0
  41. package/dist/assets/{refresh-cw-Bcv40SXy.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-EqJPOF0G.js → save-D4bObrmH.js} +1 -1
  44. package/dist/assets/search-DxmL3IWE.js +1 -0
  45. package/dist/assets/security-config-BUm6FFfl.js +1 -0
  46. package/dist/assets/select-BILPf7zs.js +1 -0
  47. package/dist/assets/setting-row-BATDgg4r.js +1 -0
  48. package/dist/assets/skeleton-COKMAnJy.js +1 -0
  49. package/dist/assets/{switch-CM29eCAR.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-YcZUWn3o.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-mJT6oWa2.js → trash-2-CQUgYyRn.js} +1 -1
  53. package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
  54. package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
  55. package/dist/assets/{useMutation-CNcz2fgt.js → useMutation-__AYv-Pz.js} +1 -1
  56. package/dist/assets/x-BHUGQIUv.js +1 -0
  57. package/dist/index.html +22 -22
  58. package/module-structure.config.json +7 -0
  59. package/package.json +5 -5
  60. package/src/api/config.ts +10 -0
  61. package/src/api/raw-client.test.ts +1 -1
  62. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  63. package/src/api/types.ts +40 -0
  64. package/src/app/components/app-manager-provider.tsx +20 -0
  65. package/src/app/managers/app.manager.ts +12 -0
  66. package/src/app.tsx +8 -8
  67. package/src/components/chat/chat-conversation-panel.test.tsx +10 -0
  68. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  69. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  70. package/src/components/chat/chat-page-shell.tsx +1 -1
  71. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  72. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  73. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +13 -37
  74. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  75. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  76. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  77. package/src/components/chat/ncp/ncp-chat-page.tsx +21 -2
  78. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  79. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  80. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  81. package/src/components/config/desktop-update-config.test.tsx +10 -4
  82. package/src/components/config/desktop-update-config.tsx +5 -3
  83. package/src/components/config/runtime-control-card.test.tsx +119 -197
  84. package/src/components/config/runtime-control-card.tsx +20 -70
  85. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  86. package/src/components/config/runtime-presence-card.tsx +7 -5
  87. package/src/components/layout/Sidebar.tsx +4 -4
  88. package/src/components/layout/runtime-status-entry.test.tsx +45 -101
  89. package/src/components/layout/runtime-status-entry.tsx +15 -63
  90. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  91. package/src/{account → features/account}/components/account-panel.tsx +13 -13
  92. package/src/features/account/index.ts +6 -0
  93. package/src/{account → features/account}/managers/account.manager.ts +3 -3
  94. package/src/{components/remote → features/remote/components}/remote-access-page.test.tsx +4 -5
  95. package/src/{components/remote → features/remote/components}/remote-access-page.tsx +15 -13
  96. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  97. package/src/features/remote/index.ts +27 -0
  98. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  99. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  100. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  101. package/src/features/system-status/index.ts +12 -0
  102. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  103. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  104. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  105. package/src/features/system-status/stores/system-status.store.ts +32 -0
  106. package/src/features/system-status/types/system-status.types.ts +73 -0
  107. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  108. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  109. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  110. package/src/lib/i18n.chat.ts +8 -0
  111. package/src/platforms/desktop/index.ts +20 -0
  112. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  113. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  114. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  115. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  116. package/src/stores/ui.store.ts +0 -9
  117. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  118. package/src/transport/app-client.test.ts +9 -5
  119. package/src/transport/index.ts +1 -1
  120. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  121. package/dist/assets/ChannelsList-M9FTK1Ak.js +0 -8
  122. package/dist/assets/DocBrowser-CH7-GxlL.js +0 -1
  123. package/dist/assets/ModelConfig-CNIgLf0e.js +0 -1
  124. package/dist/assets/ProviderScopedModelInput-B3HWP4oz.js +0 -1
  125. package/dist/assets/ProvidersList-CHjMnRhX.js +0 -1
  126. package/dist/assets/RuntimeConfig-psp8nMSG.js +0 -1
  127. package/dist/assets/SearchConfig-CSoKip1f.js +0 -1
  128. package/dist/assets/SecretsConfig-MEt6MjuD.js +0 -3
  129. package/dist/assets/SessionsConfig-DifCiXwR.js +0 -2
  130. package/dist/assets/app-query-client-9jNewezV.js +0 -1
  131. package/dist/assets/chat-page-CLp0UV0Y.js +0 -58
  132. package/dist/assets/chat-session-display-DsYHx0RZ.js +0 -1
  133. package/dist/assets/client-C-8fH7-c.js +0 -7
  134. package/dist/assets/config-CBScxsdV.js +0 -1
  135. package/dist/assets/desktop-update-config-2BS6BMkW.js +0 -1
  136. package/dist/assets/dist-BruyLa92.js +0 -9
  137. package/dist/assets/index-mW8W2FUu.css +0 -1
  138. package/dist/assets/index-zDZfXoI4.js +0 -6
  139. package/dist/assets/infiniteQueryBehavior-CyER9hv0.js +0 -1
  140. package/dist/assets/loader-circle-Bc2gCU33.js +0 -1
  141. package/dist/assets/marketplace-page-3qVMnF3d.js +0 -1
  142. package/dist/assets/marketplace-page-BhFIeQzI.js +0 -49
  143. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +0 -40
  144. package/dist/assets/page-layout-0UcO9H9Z.js +0 -1
  145. package/dist/assets/play-CKDjSQFL.js +0 -1
  146. package/dist/assets/plus-CG0QrVY_.js +0 -1
  147. package/dist/assets/refresh-ccw-COVhNHtN.js +0 -1
  148. package/dist/assets/remote-access-page-CWHG-sug.js +0 -1
  149. package/dist/assets/rotate-cw-oHMKJMC8.js +0 -1
  150. package/dist/assets/search-BCAlB8nz.js +0 -1
  151. package/dist/assets/security-config-Slh0Mayz.js +0 -1
  152. package/dist/assets/select-CVz0t7MF.js +0 -41
  153. package/dist/assets/setting-row-CbVHAuQt.js +0 -1
  154. package/dist/assets/skeleton-D5rdKvzy.js +0 -1
  155. package/dist/assets/status-dot-DpPtVzQT.js +0 -1
  156. package/dist/assets/tag-chip-DMXdnLcj.js +0 -1
  157. package/dist/assets/use-infinite-scroll-loader-DJ1L81Dz.js +0 -1
  158. package/dist/assets/useConfirmDialog-BsVuqu1x.js +0 -1
  159. package/dist/assets/x-Czwxm82I.js +0 -1
  160. package/src/hooks/use-runtime-control.ts +0 -24
  161. package/src/presenter/app-presenter-context.tsx +0 -20
  162. package/src/presenter/app.presenter.ts +0 -12
  163. package/src/runtime-control/runtime-control.manager.ts +0 -118
  164. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  165. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  166. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  167. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  168. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  169. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -0,0 +1,511 @@
1
+ import type { BootstrapStatusView } from '@/api/types';
2
+ import {
3
+ fetchRuntimeControl,
4
+ restartRuntimeService,
5
+ startRuntimeService,
6
+ stopRuntimeService,
7
+ } from '@/api/runtime-control';
8
+ import type {
9
+ RuntimeControlAction,
10
+ RuntimeControlActionResult,
11
+ RuntimeControlView,
12
+ } from '@/api/runtime-control.types';
13
+ import { appQueryClient } from '@/app-query-client';
14
+ import type { NextClawDesktopBridge } from '@/platforms/desktop';
15
+ import { t } from '@/lib/i18n';
16
+ import {
17
+ buildActiveSystemActionState,
18
+ resolveChatRuntimeMessage,
19
+ toSystemStatusView,
20
+ } from '@/features/system-status/utils/system-status.utils';
21
+ import {
22
+ initialSystemStatusState,
23
+ useSystemStatusStore,
24
+ } from '@/features/system-status/stores/system-status.store';
25
+
26
+ const RECOVERY_TIMEOUT_MS = 30_000;
27
+
28
+ function getErrorMessage(error: unknown): string {
29
+ if (error instanceof Error) {
30
+ return error.message;
31
+ }
32
+ return String(error ?? '');
33
+ }
34
+
35
+ function resolveBootstrapStatusError(
36
+ status: BootstrapStatusView | null | undefined
37
+ ): string | null {
38
+ if (!status) {
39
+ return null;
40
+ }
41
+ return status.ncpAgent.error?.trim() || status.lastError?.trim() || null;
42
+ }
43
+
44
+ function resolveActionHelp(action: RuntimeControlAction): string {
45
+ if (action === 'start-service') {
46
+ return t('runtimeControlStartingServiceHelp');
47
+ }
48
+ if (action === 'restart-service') {
49
+ return t('runtimeControlRestartingServiceHelp');
50
+ }
51
+ if (action === 'stop-service') {
52
+ return t('runtimeControlStoppingServiceHelp');
53
+ }
54
+ return t('runtimeControlRestartingAppHelp');
55
+ }
56
+
57
+ export function isTransientRuntimeConnectionErrorMessage(
58
+ message: string
59
+ ): boolean {
60
+ const normalized = message.trim().toLowerCase();
61
+ if (!normalized) {
62
+ return false;
63
+ }
64
+ return (
65
+ normalized.includes('failed to fetch') ||
66
+ normalized.includes('networkerror') ||
67
+ normalized.includes('network request failed') ||
68
+ normalized.includes('load failed') ||
69
+ normalized.includes('request timed out') ||
70
+ normalized.includes('timed out waiting for remote request response') ||
71
+ normalized.includes('remote transport connection closed') ||
72
+ normalized.includes('websocket error') ||
73
+ normalized.includes('fetch failed on ') ||
74
+ normalized.includes('stream request failed for ') ||
75
+ normalized.includes('ncp fetch failed for ')
76
+ );
77
+ }
78
+
79
+ export class SystemStatusManager {
80
+ private recoveryTimeoutId: number | null = null;
81
+
82
+ getRuntimeBootstrapPollInterval = (
83
+ status: BootstrapStatusView | null | undefined
84
+ ): number | false => {
85
+ const { lifecyclePhase, activeSystemAction } = this.getState();
86
+ if (
87
+ lifecyclePhase === 'recovering' ||
88
+ lifecyclePhase === 'stalled' ||
89
+ activeSystemAction?.lifecycle === 'recovering'
90
+ ) {
91
+ return 500;
92
+ }
93
+ if (status?.ncpAgent.state === 'ready') {
94
+ return false;
95
+ }
96
+ return 500;
97
+ };
98
+
99
+ getRuntimeControl = async (): Promise<RuntimeControlView> => {
100
+ return this.decorateForCurrentEnvironment(await fetchRuntimeControl());
101
+ };
102
+
103
+ reportBootstrapStatus = (status: BootstrapStatusView): void => {
104
+ const state = this.getState();
105
+ const statusError = resolveBootstrapStatusError(status);
106
+
107
+ if (status.ncpAgent.state === 'ready') {
108
+ this.transitionToReady(status);
109
+ return;
110
+ }
111
+
112
+ if (!state.hasReachedReady) {
113
+ if (status.ncpAgent.state === 'error' || status.phase === 'error') {
114
+ this.transitionToStartupFailed(statusError, status);
115
+ return;
116
+ }
117
+ this.transitionToColdStarting(status);
118
+ return;
119
+ }
120
+
121
+ this.transitionToRecovering(statusError, status);
122
+ };
123
+
124
+ reportBootstrapQueryError = (error: unknown): void => {
125
+ const message = getErrorMessage(error).trim();
126
+ if (!message) {
127
+ return;
128
+ }
129
+ if (this.reportTransportFailure(message)) {
130
+ return;
131
+ }
132
+ const state = this.getState();
133
+ if (state.hasReachedReady) {
134
+ this.transitionToRecovering(message);
135
+ return;
136
+ }
137
+ this.transitionToStartupFailed(message);
138
+ };
139
+
140
+ reportTransportFailure = (error: unknown): boolean => {
141
+ const message = getErrorMessage(error).trim();
142
+ if (!isTransientRuntimeConnectionErrorMessage(message)) {
143
+ return false;
144
+ }
145
+ const state = this.getState();
146
+ if (!state.hasReachedReady) {
147
+ this.patchState({
148
+ lastTransportError: message,
149
+ });
150
+ return true;
151
+ }
152
+ this.transitionToRecovering(message);
153
+ return true;
154
+ };
155
+
156
+ handleConnectionInterrupted = (message?: string | null): void => {
157
+ const state = this.getState();
158
+ const normalizedMessage = message?.trim() || null;
159
+ if (!state.hasReachedReady) {
160
+ if (normalizedMessage) {
161
+ this.patchState({
162
+ lastTransportError: normalizedMessage,
163
+ });
164
+ }
165
+ return;
166
+ }
167
+ this.transitionToRecovering(normalizedMessage);
168
+ };
169
+
170
+ handleConnectionRestored = (): void => {
171
+ const state = this.getState();
172
+ if (state.bootstrapStatus?.ncpAgent.state === 'ready') {
173
+ this.transitionToReady(state.bootstrapStatus);
174
+ }
175
+ };
176
+
177
+ reportRuntimeControlView = (view: RuntimeControlView): void => {
178
+ this.patchState({
179
+ runtimeControlView: view,
180
+ runtimeControlError: null,
181
+ });
182
+ };
183
+
184
+ reportRuntimeControlError = (error: unknown): void => {
185
+ const message = getErrorMessage(error).trim();
186
+ if (!message) {
187
+ return;
188
+ }
189
+ this.patchState({
190
+ runtimeControlError: message,
191
+ });
192
+ };
193
+
194
+ runRuntimeControlAction = async (
195
+ action: RuntimeControlAction
196
+ ): Promise<RuntimeControlActionResult> => {
197
+ this.patchState({
198
+ activeSystemAction: buildActiveSystemActionState({
199
+ action,
200
+ message: resolveActionHelp(action),
201
+ }),
202
+ lastSystemActionError: null,
203
+ });
204
+
205
+ try {
206
+ const result = await this.executeRuntimeControlAction(action);
207
+ if (action === 'restart-app') {
208
+ return result;
209
+ }
210
+
211
+ if (action === 'stop-service') {
212
+ await this.refreshRuntimeControlView();
213
+ this.clearActiveSystemAction();
214
+ return result;
215
+ }
216
+
217
+ this.patchState({
218
+ activeSystemAction: {
219
+ action,
220
+ lifecycle: 'recovering',
221
+ serviceState: null,
222
+ message: t('runtimeControlRecoveringHelp'),
223
+ },
224
+ });
225
+
226
+ const recoveredView = await this.waitForRecovery();
227
+ this.syncRuntimeControlQueryCache(recoveredView);
228
+ this.reportRuntimeControlView(recoveredView);
229
+ this.clearActiveSystemAction();
230
+ return result;
231
+ } catch (error) {
232
+ const message =
233
+ error instanceof Error ? error.message : t('runtimeControlActionFailed');
234
+ this.patchState({
235
+ activeSystemAction: {
236
+ action,
237
+ lifecycle: 'failed',
238
+ serviceState:
239
+ action === 'stop-service' ? 'running' : action === 'restart-app' ? null : 'unknown',
240
+ message,
241
+ },
242
+ lastSystemActionError: message,
243
+ });
244
+ throw error;
245
+ }
246
+ };
247
+
248
+ isChatInteractionBlocked = (): boolean => {
249
+ return toSystemStatusView(this.getState()).isChatBlocked;
250
+ };
251
+
252
+ getDisplayMessage = (message: string | null | undefined): string | null => {
253
+ if (!message?.trim()) {
254
+ return resolveChatRuntimeMessage(this.getState());
255
+ }
256
+ const { phase } = toSystemStatusView(this.getState());
257
+ if (
258
+ phase === 'service-transitioning' &&
259
+ this.getState().activeSystemAction?.message?.trim()
260
+ ) {
261
+ return this.getState().activeSystemAction?.message?.trim() ?? null;
262
+ }
263
+ if (
264
+ phase === 'recovering' &&
265
+ isTransientRuntimeConnectionErrorMessage(message)
266
+ ) {
267
+ return t('runtimeControlRecoveringHelp');
268
+ }
269
+ if (
270
+ phase === 'stalled' &&
271
+ isTransientRuntimeConnectionErrorMessage(message)
272
+ ) {
273
+ return null;
274
+ }
275
+ return message;
276
+ };
277
+
278
+ resetForTests = (): void => {
279
+ this.clearRecoveryTimeout();
280
+ useSystemStatusStore.setState({
281
+ state: initialSystemStatusState,
282
+ });
283
+ };
284
+
285
+ private getState = () => {
286
+ return useSystemStatusStore.getState().state;
287
+ };
288
+
289
+ private patchState = (patch: Partial<(typeof initialSystemStatusState)>) => {
290
+ useSystemStatusStore.getState().patchState(patch);
291
+ };
292
+
293
+ private transitionToColdStarting = (
294
+ bootstrapStatus: BootstrapStatusView
295
+ ): void => {
296
+ this.clearRecoveryTimeout();
297
+ this.patchState({
298
+ lifecyclePhase: 'cold-starting',
299
+ recoveryStartedAt: null,
300
+ bootstrapStatus,
301
+ lastError: null,
302
+ });
303
+ };
304
+
305
+ private transitionToStartupFailed = (
306
+ errorMessage: string | null,
307
+ bootstrapStatus?: BootstrapStatusView
308
+ ): void => {
309
+ this.clearRecoveryTimeout();
310
+ this.patchState({
311
+ lifecyclePhase: 'startup-failed',
312
+ recoveryStartedAt: null,
313
+ bootstrapStatus: bootstrapStatus ?? this.getState().bootstrapStatus,
314
+ lastError: errorMessage,
315
+ });
316
+ };
317
+
318
+ private transitionToRecovering = (
319
+ errorMessage: string | null,
320
+ bootstrapStatus?: BootstrapStatusView
321
+ ): void => {
322
+ const state = this.getState();
323
+ if (!state.hasReachedReady) {
324
+ this.patchState({
325
+ lastTransportError: errorMessage?.trim() || state.lastTransportError,
326
+ });
327
+ return;
328
+ }
329
+
330
+ if (
331
+ state.lifecyclePhase !== 'recovering' &&
332
+ state.lifecyclePhase !== 'stalled'
333
+ ) {
334
+ this.clearRecoveryTimeout();
335
+ this.recoveryTimeoutId = window.setTimeout(() => {
336
+ this.recoveryTimeoutId = null;
337
+ const current = this.getState();
338
+ if (current.lifecyclePhase === 'recovering') {
339
+ this.patchState({
340
+ lifecyclePhase: 'stalled',
341
+ });
342
+ }
343
+ }, RECOVERY_TIMEOUT_MS);
344
+ }
345
+
346
+ this.patchState({
347
+ lifecyclePhase:
348
+ state.lifecyclePhase === 'stalled' ? 'stalled' : 'recovering',
349
+ recoveryStartedAt: state.recoveryStartedAt ?? Date.now(),
350
+ bootstrapStatus:
351
+ bootstrapStatus ??
352
+ (state.lifecyclePhase === 'ready' ? null : state.bootstrapStatus),
353
+ lastError: errorMessage?.trim() || state.lastError,
354
+ lastTransportError: errorMessage?.trim() || state.lastTransportError,
355
+ });
356
+ };
357
+
358
+ private transitionToReady = (
359
+ bootstrapStatus: BootstrapStatusView | null
360
+ ): void => {
361
+ const state = this.getState();
362
+ const shouldRefreshQueries =
363
+ state.lifecyclePhase === 'recovering' || state.lifecyclePhase === 'stalled';
364
+
365
+ this.clearRecoveryTimeout();
366
+ this.patchState({
367
+ lifecyclePhase: 'ready',
368
+ hasReachedReady: true,
369
+ lastReadyAt: Date.now(),
370
+ recoveryStartedAt: null,
371
+ bootstrapStatus,
372
+ lastError: null,
373
+ lastTransportError: null,
374
+ });
375
+
376
+ if (shouldRefreshQueries) {
377
+ void Promise.all([
378
+ appQueryClient.invalidateQueries(),
379
+ appQueryClient.refetchQueries({ type: 'active' }),
380
+ ]);
381
+ }
382
+ };
383
+
384
+ private executeRuntimeControlAction = async (
385
+ action: RuntimeControlAction
386
+ ): Promise<RuntimeControlActionResult> => {
387
+ const desktopBridge = this.getDesktopBridge();
388
+ if (
389
+ action === 'restart-service' &&
390
+ desktopBridge &&
391
+ typeof desktopBridge.restartService === 'function'
392
+ ) {
393
+ const result = await desktopBridge.restartService();
394
+ return {
395
+ accepted: result.accepted,
396
+ action: 'restart-service',
397
+ lifecycle: result.lifecycle,
398
+ message: result.message,
399
+ };
400
+ }
401
+ if (action === 'start-service') {
402
+ return await startRuntimeService();
403
+ }
404
+ if (action === 'stop-service') {
405
+ return await stopRuntimeService();
406
+ }
407
+ if (action === 'restart-app') {
408
+ if (!desktopBridge || typeof desktopBridge.restartApp !== 'function') {
409
+ throw new Error(t('runtimeRestartAppUnavailable'));
410
+ }
411
+ const result = await desktopBridge.restartApp();
412
+ return {
413
+ accepted: result.accepted,
414
+ action: 'restart-app',
415
+ lifecycle: result.lifecycle,
416
+ message: result.message,
417
+ };
418
+ }
419
+ return await restartRuntimeService();
420
+ };
421
+
422
+ private waitForRecovery = async (): Promise<RuntimeControlView> => {
423
+ const deadline = Date.now() + 25_000;
424
+ let lastError: unknown = null;
425
+
426
+ while (Date.now() < deadline) {
427
+ try {
428
+ return await this.getRuntimeControl();
429
+ } catch (error) {
430
+ lastError = error;
431
+ await new Promise<void>((resolve) => {
432
+ window.setTimeout(resolve, 1_500);
433
+ });
434
+ }
435
+ }
436
+
437
+ throw lastError instanceof Error
438
+ ? lastError
439
+ : new Error(t('runtimeRecoveryTimedOut'));
440
+ };
441
+
442
+ private refreshRuntimeControlView = async (): Promise<void> => {
443
+ try {
444
+ const view = await this.getRuntimeControl();
445
+ this.syncRuntimeControlQueryCache(view);
446
+ this.reportRuntimeControlView(view);
447
+ await appQueryClient.invalidateQueries({ queryKey: ['runtime-control'] });
448
+ } catch (error) {
449
+ this.reportRuntimeControlError(error);
450
+ }
451
+ };
452
+
453
+ private syncRuntimeControlQueryCache = (view: RuntimeControlView): void => {
454
+ appQueryClient.setQueryData(['runtime-control'], view);
455
+ };
456
+
457
+ private clearActiveSystemAction = (): void => {
458
+ this.patchState({
459
+ activeSystemAction: null,
460
+ lastSystemActionError: null,
461
+ });
462
+ };
463
+
464
+ private clearRecoveryTimeout = (): void => {
465
+ if (this.recoveryTimeoutId !== null) {
466
+ window.clearTimeout(this.recoveryTimeoutId);
467
+ this.recoveryTimeoutId = null;
468
+ }
469
+ };
470
+
471
+ private decorateForCurrentEnvironment = (
472
+ view: RuntimeControlView
473
+ ): RuntimeControlView => {
474
+ const desktopBridge = this.getDesktopBridge();
475
+ if (!desktopBridge || typeof desktopBridge.restartApp !== 'function') {
476
+ return view;
477
+ }
478
+
479
+ return {
480
+ ...view,
481
+ environment: 'desktop-embedded',
482
+ serviceState: 'running',
483
+ canStartService: {
484
+ available: false,
485
+ requiresConfirmation: false,
486
+ impact: 'none',
487
+ },
488
+ canStopService: {
489
+ available: false,
490
+ requiresConfirmation: true,
491
+ impact: 'brief-ui-disconnect',
492
+ },
493
+ canRestartApp: {
494
+ available: true,
495
+ requiresConfirmation: true,
496
+ impact: 'full-app-relaunch',
497
+ },
498
+ ownerLabel: t('runtimeControlEnvironmentDesktop'),
499
+ managementHint: t('runtimeControlDesktopServiceHint'),
500
+ };
501
+ };
502
+
503
+ private getDesktopBridge = (): NextClawDesktopBridge | null => {
504
+ if (typeof window === 'undefined') {
505
+ return null;
506
+ }
507
+ return window.nextclawDesktop ?? null;
508
+ };
509
+ }
510
+
511
+ export const systemStatusManager = new SystemStatusManager();
@@ -0,0 +1,32 @@
1
+ import { create } from 'zustand';
2
+ import type { SystemStatusState } from '@/features/system-status/types/system-status.types';
3
+
4
+ type SystemStatusStore = {
5
+ state: SystemStatusState;
6
+ patchState: (patch: Partial<SystemStatusState>) => void;
7
+ };
8
+
9
+ export const initialSystemStatusState: SystemStatusState = {
10
+ lifecyclePhase: 'cold-starting',
11
+ hasReachedReady: false,
12
+ lastReadyAt: null,
13
+ recoveryStartedAt: null,
14
+ bootstrapStatus: null,
15
+ lastError: null,
16
+ lastTransportError: null,
17
+ runtimeControlView: null,
18
+ runtimeControlError: null,
19
+ activeSystemAction: null,
20
+ lastSystemActionError: null,
21
+ };
22
+
23
+ export const useSystemStatusStore = create<SystemStatusStore>((set) => ({
24
+ state: initialSystemStatusState,
25
+ patchState: (patch) =>
26
+ set((current) => ({
27
+ state: {
28
+ ...current.state,
29
+ ...patch,
30
+ },
31
+ })),
32
+ }));
@@ -0,0 +1,73 @@
1
+ import type { BootstrapStatusView } from '@/api/types';
2
+ import type {
3
+ RuntimeControlAction,
4
+ RuntimeControlView,
5
+ RuntimeLifecycleState,
6
+ RuntimeServiceState,
7
+ } from '@/api/runtime-control.types';
8
+
9
+ export type SystemStatusLifecyclePhase =
10
+ | 'cold-starting'
11
+ | 'ready'
12
+ | 'recovering'
13
+ | 'stalled'
14
+ | 'startup-failed';
15
+
16
+ export type SystemStatusPhase =
17
+ | SystemStatusLifecyclePhase
18
+ | 'service-transitioning';
19
+
20
+ export type SystemConnectionStatus =
21
+ | 'connected'
22
+ | 'disconnected'
23
+ | 'connecting';
24
+
25
+ export type ActiveSystemActionState = {
26
+ action: RuntimeControlAction;
27
+ lifecycle: RuntimeLifecycleState;
28
+ serviceState: RuntimeServiceState | null;
29
+ message: string | null;
30
+ };
31
+
32
+ export type SystemStatusState = {
33
+ lifecyclePhase: SystemStatusLifecyclePhase;
34
+ hasReachedReady: boolean;
35
+ lastReadyAt: number | null;
36
+ recoveryStartedAt: number | null;
37
+ bootstrapStatus: BootstrapStatusView | null;
38
+ lastError: string | null;
39
+ lastTransportError: string | null;
40
+ runtimeControlView: RuntimeControlView | null;
41
+ runtimeControlError: string | null;
42
+ activeSystemAction: ActiveSystemActionState | null;
43
+ lastSystemActionError: string | null;
44
+ };
45
+
46
+ export type SystemStatusView = SystemStatusState & {
47
+ phase: SystemStatusPhase;
48
+ connectionStatus: SystemConnectionStatus;
49
+ isChatBlocked: boolean;
50
+ chatMessage: string | null;
51
+ };
52
+
53
+ export type RuntimeStatusTone = 'healthy' | 'attention' | 'inactive';
54
+
55
+ export type RuntimeStatusBadgeView = {
56
+ actionLabel: string | null;
57
+ description: string;
58
+ reasonLines: string[];
59
+ title: string;
60
+ tone: RuntimeStatusTone;
61
+ isBusy: boolean;
62
+ };
63
+
64
+ export type RuntimeControlPanelView = {
65
+ controlView: RuntimeControlView | null;
66
+ visibleLifecycle: RuntimeLifecycleState;
67
+ visibleServiceState: RuntimeServiceState;
68
+ visibleMessage: string;
69
+ busyAction: RuntimeControlAction | null;
70
+ busy: boolean;
71
+ pendingRestart: RuntimeControlView['pendingRestart'] | null;
72
+ errorMessage: string | null;
73
+ };