@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,132 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { t } from '@/lib/i18n';
3
+ import {
4
+ resolveChatRuntimeMessage,
5
+ resolveSystemConnectionStatus,
6
+ toSystemStatusView,
7
+ } from './system-status.utils';
8
+
9
+ describe('resolveSystemConnectionStatus', () => {
10
+ it('maps cold-starting to connecting', () => {
11
+ expect(resolveSystemConnectionStatus('cold-starting')).toBe('connecting');
12
+ });
13
+
14
+ it('maps ready to connected', () => {
15
+ expect(resolveSystemConnectionStatus('ready')).toBe('connected');
16
+ });
17
+
18
+ it('maps stalled to disconnected', () => {
19
+ expect(resolveSystemConnectionStatus('stalled')).toBe('disconnected');
20
+ });
21
+
22
+ it('maps service-transitioning to connecting', () => {
23
+ expect(resolveSystemConnectionStatus('service-transitioning')).toBe(
24
+ 'connecting'
25
+ );
26
+ });
27
+ });
28
+
29
+ describe('resolveChatRuntimeMessage', () => {
30
+ it('uses the startup message during cold start', () => {
31
+ expect(
32
+ resolveChatRuntimeMessage({
33
+ lifecyclePhase: 'cold-starting',
34
+ hasReachedReady: false,
35
+ lastReadyAt: null,
36
+ recoveryStartedAt: null,
37
+ bootstrapStatus: null,
38
+ lastError: null,
39
+ lastTransportError: null,
40
+ runtimeControlView: null,
41
+ runtimeControlError: null,
42
+ activeSystemAction: null,
43
+ lastSystemActionError: null,
44
+ })
45
+ ).toBe(t('chatRuntimeInitializing'));
46
+ });
47
+
48
+ it('uses the bootstrap error when startup failed', () => {
49
+ expect(
50
+ resolveChatRuntimeMessage({
51
+ lifecyclePhase: 'startup-failed',
52
+ hasReachedReady: false,
53
+ lastReadyAt: null,
54
+ recoveryStartedAt: null,
55
+ bootstrapStatus: {
56
+ phase: 'error',
57
+ ncpAgent: {
58
+ state: 'error',
59
+ error: 'boom',
60
+ },
61
+ pluginHydration: {
62
+ state: 'pending',
63
+ loadedPluginCount: 0,
64
+ totalPluginCount: 0,
65
+ },
66
+ channels: {
67
+ state: 'pending',
68
+ enabled: [],
69
+ },
70
+ remote: {
71
+ state: 'pending',
72
+ },
73
+ lastError: 'boom',
74
+ },
75
+ lastError: null,
76
+ lastTransportError: null,
77
+ runtimeControlView: null,
78
+ runtimeControlError: null,
79
+ activeSystemAction: null,
80
+ lastSystemActionError: null,
81
+ })
82
+ ).toBe('boom');
83
+ });
84
+
85
+ it('prefers the centralized action message while a system action is running', () => {
86
+ expect(
87
+ resolveChatRuntimeMessage({
88
+ lifecyclePhase: 'ready',
89
+ hasReachedReady: true,
90
+ lastReadyAt: Date.now(),
91
+ recoveryStartedAt: null,
92
+ bootstrapStatus: null,
93
+ lastError: null,
94
+ lastTransportError: null,
95
+ runtimeControlView: null,
96
+ runtimeControlError: null,
97
+ activeSystemAction: {
98
+ action: 'restart-service',
99
+ lifecycle: 'recovering',
100
+ serviceState: null,
101
+ message: 'NextClaw 正在恢复连接',
102
+ },
103
+ lastSystemActionError: null,
104
+ })
105
+ ).toBe('NextClaw 正在恢复连接');
106
+ });
107
+ });
108
+
109
+ describe('toSystemStatusView', () => {
110
+ it('keeps stalled chat blocked without surfacing a timeout banner', () => {
111
+ expect(
112
+ toSystemStatusView({
113
+ lifecyclePhase: 'stalled',
114
+ hasReachedReady: true,
115
+ lastReadyAt: Date.now(),
116
+ recoveryStartedAt: Date.now(),
117
+ bootstrapStatus: null,
118
+ lastError: 'Failed to fetch',
119
+ lastTransportError: 'Failed to fetch',
120
+ runtimeControlView: null,
121
+ runtimeControlError: null,
122
+ activeSystemAction: null,
123
+ lastSystemActionError: null,
124
+ })
125
+ ).toMatchObject({
126
+ isChatBlocked: true,
127
+ chatMessage: null,
128
+ connectionStatus: 'disconnected',
129
+ phase: 'stalled',
130
+ });
131
+ });
132
+ });
@@ -0,0 +1,202 @@
1
+ import { t } from '@/lib/i18n';
2
+ import type {
3
+ RuntimeControlAction,
4
+ RuntimeLifecycleState,
5
+ RuntimeServiceState,
6
+ } from '@/api/runtime-control.types';
7
+ import type {
8
+ RuntimeControlPanelView,
9
+ RuntimeStatusBadgeView,
10
+ SystemConnectionStatus,
11
+ SystemStatusPhase,
12
+ SystemStatusState,
13
+ SystemStatusView,
14
+ } from '@/features/system-status/types/system-status.types';
15
+
16
+ function resolveSystemStatusPhase(state: SystemStatusState): SystemStatusPhase {
17
+ return state.activeSystemAction ? 'service-transitioning' : state.lifecyclePhase;
18
+ }
19
+
20
+ export function resolveSystemConnectionStatus(
21
+ phase: SystemStatusPhase
22
+ ): SystemConnectionStatus {
23
+ if (phase === 'ready') {
24
+ return 'connected';
25
+ }
26
+ if (phase === 'startup-failed' || phase === 'stalled') {
27
+ return 'disconnected';
28
+ }
29
+ return 'connecting';
30
+ }
31
+
32
+ export function resolveChatRuntimeMessage(
33
+ state: SystemStatusState
34
+ ): string | null {
35
+ if (state.activeSystemAction?.message?.trim()) {
36
+ return state.activeSystemAction.message.trim();
37
+ }
38
+ if (state.lifecyclePhase === 'cold-starting') {
39
+ return t('chatRuntimeInitializing');
40
+ }
41
+ if (state.lifecyclePhase === 'startup-failed') {
42
+ return (
43
+ state.bootstrapStatus?.ncpAgent.error?.trim() ||
44
+ state.bootstrapStatus?.lastError?.trim() ||
45
+ state.lastError?.trim() ||
46
+ t('chatRuntimeInitializationFailed')
47
+ );
48
+ }
49
+ return null;
50
+ }
51
+
52
+ export function toSystemStatusView(
53
+ state: SystemStatusState
54
+ ): SystemStatusView {
55
+ const phase = resolveSystemStatusPhase(state);
56
+ return {
57
+ ...state,
58
+ phase,
59
+ connectionStatus: resolveSystemConnectionStatus(phase),
60
+ isChatBlocked: phase !== 'ready',
61
+ chatMessage: resolveChatRuntimeMessage(state),
62
+ };
63
+ }
64
+
65
+ function resolveActionLifecycleLabel(
66
+ action: RuntimeControlAction
67
+ ): RuntimeLifecycleState {
68
+ if (action === 'start-service') {
69
+ return 'starting-service';
70
+ }
71
+ if (action === 'stop-service') {
72
+ return 'stopping-service';
73
+ }
74
+ if (action === 'restart-service') {
75
+ return 'restarting-service';
76
+ }
77
+ return 'restarting-app';
78
+ }
79
+
80
+ function resolveActionServiceState(
81
+ action: RuntimeControlAction
82
+ ): RuntimeServiceState | null {
83
+ if (action === 'start-service') {
84
+ return 'starting';
85
+ }
86
+ if (action === 'stop-service') {
87
+ return 'stopping';
88
+ }
89
+ if (action === 'restart-service') {
90
+ return 'restarting';
91
+ }
92
+ return null;
93
+ }
94
+
95
+ export function buildActiveSystemActionState(params: {
96
+ action: RuntimeControlAction;
97
+ message: string | null;
98
+ }): SystemStatusState['activeSystemAction'] {
99
+ const { action, message } = params;
100
+ return {
101
+ action,
102
+ lifecycle: resolveActionLifecycleLabel(action),
103
+ serviceState: resolveActionServiceState(action),
104
+ message,
105
+ };
106
+ }
107
+
108
+ export function toRuntimeStatusBadgeView(
109
+ state: SystemStatusState
110
+ ): RuntimeStatusBadgeView {
111
+ if (state.runtimeControlError) {
112
+ return {
113
+ tone: 'inactive',
114
+ title: t('runtimeControlLoadFailed'),
115
+ description: state.runtimeControlError,
116
+ reasonLines: [],
117
+ actionLabel: null,
118
+ isBusy: false,
119
+ };
120
+ }
121
+
122
+ if (!state.runtimeControlView) {
123
+ return {
124
+ tone: 'inactive',
125
+ title: t('runtimeStatusLoadingTitle'),
126
+ description: t('runtimeStatusLoadingDescription'),
127
+ reasonLines: [],
128
+ actionLabel: null,
129
+ isBusy: Boolean(state.activeSystemAction),
130
+ };
131
+ }
132
+
133
+ if (state.activeSystemAction) {
134
+ return {
135
+ tone: 'attention',
136
+ title: t('runtimeControlTitle'),
137
+ description:
138
+ state.activeSystemAction.message ||
139
+ state.runtimeControlView.message ||
140
+ t('runtimeControlDescription'),
141
+ reasonLines: [],
142
+ actionLabel: null,
143
+ isBusy: true,
144
+ };
145
+ }
146
+
147
+ const view = state.runtimeControlView;
148
+ if (view.pendingRestart) {
149
+ return {
150
+ tone: 'attention',
151
+ title: t('runtimeStatusPendingRestartTitle'),
152
+ description: t('runtimeStatusPendingRestartDescription'),
153
+ reasonLines:
154
+ view.pendingRestart.changedPaths.length > 0
155
+ ? view.pendingRestart.changedPaths.map((path: string) =>
156
+ t('runtimeStatusPendingRestartReasonItem').replace('{path}', path)
157
+ )
158
+ : [view.pendingRestart.message],
159
+ actionLabel: view.canRestartService.available
160
+ ? t('runtimeStatusRestartAction')
161
+ : null,
162
+ isBusy: false,
163
+ };
164
+ }
165
+
166
+ return {
167
+ tone: view.lifecycle === 'healthy' ? 'healthy' : 'inactive',
168
+ title: t('runtimeStatusHealthyTitle'),
169
+ description: t('runtimeStatusHealthyDescription'),
170
+ reasonLines: [],
171
+ actionLabel: null,
172
+ isBusy: false,
173
+ };
174
+ }
175
+
176
+ export function toRuntimeControlPanelView(
177
+ state: SystemStatusState
178
+ ): RuntimeControlPanelView {
179
+ const action = state.activeSystemAction;
180
+ const controlView = state.runtimeControlView;
181
+ const visibleLifecycle =
182
+ action?.lifecycle ?? controlView?.lifecycle ?? 'healthy';
183
+ const visibleServiceState =
184
+ action?.serviceState ?? controlView?.serviceState ?? 'unknown';
185
+ const visibleMessage =
186
+ action?.message ||
187
+ state.lastSystemActionError ||
188
+ controlView?.message ||
189
+ t('runtimeControlDescription');
190
+
191
+ return {
192
+ controlView,
193
+ visibleLifecycle,
194
+ visibleServiceState,
195
+ visibleMessage,
196
+ busyAction: action?.action ?? null,
197
+ busy: Boolean(action),
198
+ pendingRestart: controlView?.pendingRestart ?? null,
199
+ errorMessage:
200
+ state.lastSystemActionError || state.runtimeControlError || null,
201
+ };
202
+ }
@@ -1,12 +1,9 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { applyNcpSessionRealtimeEvent } from '@/api/ncp-session-query-cache';
3
+ import { systemStatusManager } from '@/features/system-status';
3
4
  import { appClient } from '@/transport';
4
- import { useUiStore } from '@/stores/ui.store';
5
5
  import type { QueryClient } from '@tanstack/react-query';
6
6
 
7
- type ConnectionStatus = 'connected' | 'disconnected' | 'connecting';
8
- type SetConnectionStatus = (status: ConnectionStatus) => void;
9
-
10
7
  function shouldInvalidateConfigQuery(configPath: string) {
11
8
  const normalized = configPath.trim().toLowerCase();
12
9
  if (!normalized) {
@@ -39,22 +36,34 @@ function handleConfigUpdatedEvent(queryClient: QueryClient | undefined, path: st
39
36
  }
40
37
 
41
38
  function handleRealtimeEvent(
42
- queryClient: QueryClient | undefined,
43
- setConnectionStatus: SetConnectionStatus,
44
- shouldResyncSessionsRef: { current: boolean },
45
- event: Parameters<Parameters<typeof appClient.subscribe>[0]>[0]
39
+ params: {
40
+ queryClient: QueryClient | undefined;
41
+ shouldResyncSessions: boolean;
42
+ clearShouldResyncSessions: () => void;
43
+ markShouldResyncSessions: () => void;
44
+ event: Parameters<Parameters<typeof appClient.subscribe>[0]>[0];
45
+ }
46
46
  ): void {
47
+ const {
48
+ queryClient,
49
+ shouldResyncSessions,
50
+ clearShouldResyncSessions,
51
+ markShouldResyncSessions,
52
+ event,
53
+ } = params;
47
54
  if (event.type === 'connection.open') {
48
- setConnectionStatus('connected');
49
- if (shouldResyncSessionsRef.current) {
50
- shouldResyncSessionsRef.current = false;
55
+ systemStatusManager.handleConnectionRestored();
56
+ if (shouldResyncSessions) {
57
+ clearShouldResyncSessions();
51
58
  queryClient?.invalidateQueries({ queryKey: ['ncp-sessions'] });
52
59
  }
53
60
  return;
54
61
  }
55
62
  if (event.type === 'connection.close' || event.type === 'connection.error') {
56
- setConnectionStatus('disconnected');
57
- shouldResyncSessionsRef.current = true;
63
+ systemStatusManager.handleConnectionInterrupted(
64
+ event.type === 'connection.error' ? event.payload?.message : null
65
+ );
66
+ markShouldResyncSessions();
58
67
  return;
59
68
  }
60
69
  if (event.type === 'config.updated') {
@@ -76,14 +85,21 @@ function handleRealtimeEvent(
76
85
  }
77
86
 
78
87
  export function useRealtimeQueryBridge(queryClient?: QueryClient) {
79
- const { setConnectionStatus } = useUiStore();
80
88
  const shouldResyncSessionsRef = useRef(false);
81
89
 
82
90
  useEffect(() => {
83
- setConnectionStatus('connecting');
84
-
85
91
  return appClient.subscribe((event) =>
86
- handleRealtimeEvent(queryClient, setConnectionStatus, shouldResyncSessionsRef, event)
92
+ handleRealtimeEvent({
93
+ queryClient,
94
+ shouldResyncSessions: shouldResyncSessionsRef.current,
95
+ clearShouldResyncSessions: () => {
96
+ shouldResyncSessionsRef.current = false;
97
+ },
98
+ markShouldResyncSessions: () => {
99
+ shouldResyncSessionsRef.current = true;
100
+ },
101
+ event,
102
+ })
87
103
  );
88
- }, [queryClient, setConnectionStatus]);
104
+ }, [queryClient]);
89
105
  }
@@ -59,6 +59,14 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
59
59
  chatWorkspacePreviewTruncated: { zh: '内容已截断', en: 'Preview truncated' },
60
60
  chatSessionUnread: { zh: '会话有未读更新', en: 'Session has unread updates' },
61
61
  chatTyping: { zh: 'Agent 正在思考...', en: 'Agent is thinking...' },
62
+ chatRuntimeInitializing: {
63
+ zh: '聊天能力正在初始化。你可以先输入内容,完成后即可发送。',
64
+ en: 'Chat is still initializing. You can keep typing and send once startup finishes.'
65
+ },
66
+ chatRuntimeInitializationFailed: {
67
+ zh: '聊天能力启动失败,请稍后重试或检查服务日志。',
68
+ en: 'Chat startup failed. Please retry in a moment or inspect the service logs.'
69
+ },
62
70
  chatInputPlaceholder: { zh: '输入消息,输入 / 选择技能,Enter 发送,Shift + Enter 换行', en: 'Type a message, type / to select skills, Enter to send, Shift + Enter for newline' },
63
71
  chatInputHint: { zh: '支持多轮上下文,默认走当前会话。', en: 'Multi-turn context is preserved in the current session.' },
64
72
  chatSlashSectionCommands: { zh: '命令', en: 'Commands' },
@@ -0,0 +1,20 @@
1
+ export {
2
+ desktopPresenceManager,
3
+ DesktopPresenceManager,
4
+ } from './managers/desktop-presence.manager';
5
+ export {
6
+ desktopUpdateManager,
7
+ DesktopUpdateManager,
8
+ } from './managers/desktop-update.manager';
9
+ export { useDesktopPresenceStore } from './stores/desktop-presence.store';
10
+ export { useDesktopUpdateStore } from './stores/desktop-update.store';
11
+ export type {
12
+ DesktopPresencePreferences,
13
+ DesktopPresenceSnapshot,
14
+ DesktopReleaseChannel,
15
+ DesktopRuntimeControlResult,
16
+ DesktopUpdatePreferences,
17
+ DesktopUpdateSnapshot,
18
+ DesktopUpdateStatus,
19
+ NextClawDesktopBridge,
20
+ } from './types/desktop-update.types';
@@ -1,8 +1,8 @@
1
1
  import type {
2
2
  DesktopPresencePreferences,
3
3
  NextClawDesktopBridge
4
- } from '@/desktop/desktop-update.types';
5
- import { useDesktopPresenceStore } from '@/desktop/stores/desktop-presence.store';
4
+ } from '@/platforms/desktop/types/desktop-update.types';
5
+ import { useDesktopPresenceStore } from '@/platforms/desktop/stores/desktop-presence.store';
6
6
  import { t } from '@/lib/i18n';
7
7
  import { toast } from 'sonner';
8
8
 
@@ -3,8 +3,8 @@ import type {
3
3
  DesktopUpdatePreferences,
4
4
  DesktopUpdateSnapshot,
5
5
  NextClawDesktopBridge
6
- } from '@/desktop/desktop-update.types';
7
- import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
6
+ } from '@/platforms/desktop/types/desktop-update.types';
7
+ import { useDesktopUpdateStore } from '@/platforms/desktop/stores/desktop-update.store';
8
8
  import { t } from '@/lib/i18n';
9
9
  import { toast } from 'sonner';
10
10
 
@@ -1,4 +1,4 @@
1
- import type { DesktopPresenceSnapshot } from '@/desktop/desktop-update.types';
1
+ import type { DesktopPresenceSnapshot } from '@/platforms/desktop/types/desktop-update.types';
2
2
  import { create } from 'zustand';
3
3
 
4
4
  type DesktopPresenceBusyAction = 'loading' | 'saving-preferences' | null;
@@ -1,4 +1,4 @@
1
- import type { DesktopUpdateSnapshot } from '@/desktop/desktop-update.types';
1
+ import type { DesktopUpdateSnapshot } from '@/platforms/desktop/types/desktop-update.types';
2
2
  import { create } from 'zustand';
3
3
 
4
4
  type DesktopUpdateBusyAction =
@@ -1,12 +1,6 @@
1
1
  import { create } from 'zustand';
2
2
 
3
- type ConnectionStatus = 'connected' | 'disconnected' | 'connecting';
4
-
5
3
  interface UiState {
6
- // Connection status
7
- connectionStatus: ConnectionStatus;
8
- setConnectionStatus: (status: ConnectionStatus) => void;
9
-
10
4
  // Channel modal
11
5
  channelModal: { open: boolean; channel?: string };
12
6
  openChannelModal: (channel?: string) => void;
@@ -14,9 +8,6 @@ interface UiState {
14
8
  }
15
9
 
16
10
  export const useUiStore = create<UiState>((set) => ({
17
- connectionStatus: 'disconnected',
18
- setConnectionStatus: (status) => set({ connectionStatus: status }),
19
-
20
11
  channelModal: { open: false },
21
12
  openChannelModal: (channel) => set({ channelModal: { open: true, channel } }),
22
13
  closeChannelModal: () => set({ channelModal: { open: false } })
@@ -1,5 +1,5 @@
1
1
  import { API_BASE } from '@/api/api-base';
2
- import { LocalAppTransport } from './local.transport';
2
+ import { LocalAppTransport } from './local-transport.service';
3
3
  import { RemoteSessionMultiplexTransport } from './remote.transport';
4
4
  import type { AppTransport, RemoteRuntimeInfo, RequestInput, StreamInput, StreamSession } from './transport.types';
5
5
 
@@ -59,18 +59,18 @@ class AppClient {
59
59
 
60
60
  constructor(private readonly apiBase: string = API_BASE) {}
61
61
 
62
- private async getTransport(): Promise<AppTransport> {
62
+ private getTransport = async (): Promise<AppTransport> => {
63
63
  if (!this.transportPromise) {
64
64
  this.transportPromise = resolveRuntime(this.apiBase);
65
65
  }
66
66
  return await this.transportPromise;
67
- }
67
+ };
68
68
 
69
- async request<T>(input: RequestInput): Promise<T> {
69
+ request = async <T>(input: RequestInput): Promise<T> => {
70
70
  return await (await this.getTransport()).request<T>(input);
71
- }
71
+ };
72
72
 
73
- openStream<TFinal = unknown>(input: StreamInput): StreamSession<TFinal> {
73
+ openStream = <TFinal = unknown>(input: StreamInput): StreamSession<TFinal> => {
74
74
  let currentSession: StreamSession<TFinal> | null = null;
75
75
  let resolveFinished!: (value: TFinal) => void;
76
76
  let rejectFinished!: (error: Error) => void;
@@ -94,9 +94,9 @@ class AppClient {
94
94
  finished,
95
95
  cancel: () => currentSession?.cancel()
96
96
  };
97
- }
97
+ };
98
98
 
99
- subscribe(handler: (event: Parameters<Parameters<AppTransport['subscribe']>[0]>[0]) => void): () => void {
99
+ subscribe = (handler: (event: Parameters<Parameters<AppTransport['subscribe']>[0]>[0]) => void): () => void => {
100
100
  let unsubscribe = () => {};
101
101
  let active = true;
102
102
  void this.getTransport().then((transport) => {
@@ -117,7 +117,7 @@ class AppClient {
117
117
  active = false;
118
118
  unsubscribe();
119
119
  };
120
- }
120
+ };
121
121
  }
122
122
 
123
123
  export const appClient = new AppClient();
@@ -21,12 +21,16 @@ describe('appClient runtime detection', () => {
21
21
  );
22
22
  vi.stubGlobal('fetch', fetchMock);
23
23
 
24
- const { LocalAppTransport } = await import('@/transport/local.transport');
25
- const localRequest = vi
26
- .spyOn(LocalAppTransport.prototype, 'request')
27
- .mockResolvedValue({ ok: true } as never);
24
+ const localRequest = vi.fn().mockResolvedValue({ ok: true } as never);
25
+ vi.doMock('@/transport/local-transport.service', () => ({
26
+ LocalAppTransport: class {
27
+ request = localRequest;
28
+ openStream = vi.fn();
29
+ subscribe = vi.fn();
30
+ }
31
+ }));
28
32
 
29
- const { appClient } = await import('@/transport/app-client');
33
+ const { appClient } = await import('@/transport/app-client.service');
30
34
  const result = await appClient.request<{ ok: boolean }>({
31
35
  method: 'GET',
32
36
  path: '/api/config'
@@ -1,4 +1,4 @@
1
- export { appClient } from './app-client';
1
+ export { appClient } from './app-client.service';
2
2
  export type {
3
3
  AppEvent,
4
4
  AppTransport,
@@ -1,6 +1,7 @@
1
1
  import { API_BASE } from '@/api/api-base';
2
- import { requestRawApiResponse } from '@/api/raw-client';
2
+ import { requestRawApiResponse } from '@/api/raw-client.utils';
3
3
  import type { ApiResponse } from '@/api/types';
4
+ import { systemStatusManager } from '@/features/system-status';
4
5
  import type { AppEvent, AppTransport, RequestInput, StreamInput, StreamSession } from './transport.types';
5
6
  import { readSseStreamResult } from './sse-stream';
6
7
  import { resolveTransportWebSocketUrl } from './transport-websocket-url';
@@ -42,7 +43,7 @@ class LocalRealtimeGateway {
42
43
 
43
44
  constructor(private readonly wsUrl: string) {}
44
45
 
45
- subscribe(handler: EventHandler): () => void {
46
+ subscribe = (handler: EventHandler): () => void => {
46
47
  this.subscribers.add(handler);
47
48
  if (this.subscribers.size === 1) {
48
49
  this.connect();
@@ -56,15 +57,15 @@ class LocalRealtimeGateway {
56
57
  this.disconnect();
57
58
  }
58
59
  };
59
- }
60
+ };
60
61
 
61
- private emit(event: AppEvent): void {
62
+ private emit = (event: AppEvent): void => {
62
63
  for (const subscriber of this.subscribers) {
63
64
  subscriber(event);
64
65
  }
65
- }
66
+ };
66
67
 
67
- private connect(): void {
68
+ private connect = (): void => {
68
69
  if (this.socket && (this.socket.readyState === WebSocket.CONNECTING || this.socket.readyState === WebSocket.OPEN)) {
69
70
  return;
70
71
  }
@@ -96,9 +97,9 @@ class LocalRealtimeGateway {
96
97
  this.scheduleReconnect();
97
98
  }
98
99
  };
99
- }
100
+ };
100
101
 
101
- private scheduleReconnect(): void {
102
+ private scheduleReconnect = (): void => {
102
103
  if (this.reconnectTimer !== null) {
103
104
  return;
104
105
  }
@@ -106,9 +107,9 @@ class LocalRealtimeGateway {
106
107
  this.reconnectTimer = null;
107
108
  this.connect();
108
109
  }, 3_000);
109
- }
110
+ };
110
111
 
111
- private disconnect(): void {
112
+ private disconnect = (): void => {
112
113
  this.manualClose = true;
113
114
  if (this.reconnectTimer !== null) {
114
115
  window.clearTimeout(this.reconnectTimer);
@@ -116,7 +117,7 @@ class LocalRealtimeGateway {
116
117
  }
117
118
  this.socket?.close();
118
119
  this.socket = null;
119
- }
120
+ };
120
121
  }
121
122
 
122
123
  export class LocalAppTransport implements AppTransport {
@@ -154,7 +155,7 @@ export class LocalAppTransport implements AppTransport {
154
155
  return response.data;
155
156
  } catch (error) {
156
157
  if (controller?.signal.aborted) {
157
- const reason = controller.signal.reason;
158
+ const { reason } = controller.signal;
158
159
  throw new Error(typeof reason === 'string' && reason.trim() ? reason : `Request timed out: ${input.method} ${input.path}`);
159
160
  }
160
161
  if (error instanceof Error) {
@@ -196,6 +197,7 @@ export class LocalAppTransport implements AppTransport {
196
197
  signal: controller.signal
197
198
  });
198
199
  } catch (error) {
200
+ systemStatusManager.reportTransportFailure(formatUnknownTransportError(error));
199
201
  throw createErrorWithCause(
200
202
  `Stream request failed for ${input.method} ${input.path} | ${formatUnknownTransportError(error)}`,
201
203
  error