@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
@@ -1,5 +1,3 @@
1
- import { useState } from 'react';
2
- import { useQueryClient } from '@tanstack/react-query';
3
1
  import type {
4
2
  RuntimeActionCapability,
5
3
  RuntimeControlAction,
@@ -9,9 +7,11 @@ import type {
9
7
  } from '@/api/runtime-control.types';
10
8
  import { Button } from '@/components/ui/button';
11
9
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
12
- import { useRuntimeControl, useRuntimeServiceAction } from '@/hooks/use-runtime-control';
13
10
  import { t } from '@/lib/i18n';
14
- import { runtimeControlManager } from '@/runtime-control/runtime-control.manager';
11
+ import {
12
+ useRuntimeControlPanelView,
13
+ systemStatusManager,
14
+ } from '@/features/system-status';
15
15
  import { Loader2, RotateCw, Square, Play } from 'lucide-react';
16
16
  import { toast } from 'sonner';
17
17
 
@@ -117,19 +117,6 @@ function resolveVisibleActions(controlView: RuntimeControlView | undefined): Vis
117
117
  return actions.filter((item) => item.capability.available || Boolean(item.capability.reasonIfUnavailable));
118
118
  }
119
119
 
120
- function resolveActionHelp(action: RuntimeControlAction): string {
121
- if (action === 'start-service') {
122
- return t('runtimeControlStartingServiceHelp');
123
- }
124
- if (action === 'restart-service') {
125
- return t('runtimeControlRestartingServiceHelp');
126
- }
127
- if (action === 'stop-service') {
128
- return t('runtimeControlStoppingServiceHelp');
129
- }
130
- return t('runtimeControlRestartingAppHelp');
131
- }
132
-
133
120
  function RuntimeActionIcon(props: { icon: VisibleRuntimeAction['icon']; busy: boolean }) {
134
121
  const { busy, icon } = props;
135
122
  if (busy) {
@@ -145,28 +132,17 @@ function RuntimeActionIcon(props: { icon: VisibleRuntimeAction['icon']; busy: bo
145
132
  }
146
133
 
147
134
  export function RuntimeControlCard() {
148
- const queryClient = useQueryClient();
149
- const runtimeControlQuery = useRuntimeControl();
150
- const serviceActionMutation = useRuntimeServiceAction();
151
- const [localLifecycle, setLocalLifecycle] = useState<RuntimeLifecycleState | null>(null);
152
- const [localServiceState, setLocalServiceState] = useState<RuntimeServiceState | null>(null);
153
- const [localMessage, setLocalMessage] = useState<string | null>(null);
154
- const [busyAction, setBusyAction] = useState<RuntimeControlAction | null>(null);
155
-
156
- const controlView = runtimeControlQuery.data;
157
- const displayedLifecycle = localLifecycle ?? controlView?.lifecycle ?? 'healthy';
158
- const displayedServiceState = localServiceState ?? controlView?.serviceState ?? 'unknown';
159
- const displayedMessage = localMessage ?? controlView?.message ?? t('runtimeControlDescription');
160
- const busy = serviceActionMutation.isPending || busyAction !== null || displayedLifecycle === 'recovering';
161
- const visibleActions = resolveVisibleActions(controlView);
162
- const pendingRestart = controlView?.pendingRestart ?? null;
163
-
164
- const resetLocalState = () => {
165
- setLocalLifecycle(null);
166
- setLocalServiceState(null);
167
- setLocalMessage(null);
168
- setBusyAction(null);
169
- };
135
+ const {
136
+ busy,
137
+ busyAction,
138
+ controlView,
139
+ errorMessage,
140
+ pendingRestart,
141
+ visibleLifecycle: displayedLifecycle,
142
+ visibleMessage: displayedMessage,
143
+ visibleServiceState: displayedServiceState,
144
+ } = useRuntimeControlPanelView();
145
+ const visibleActions = resolveVisibleActions(controlView ?? undefined);
170
146
 
171
147
  const handleServiceAction = async (action: Extract<RuntimeControlAction, 'start-service' | 'restart-service' | 'stop-service'>) => {
172
148
  const capability = action === 'start-service'
@@ -183,30 +159,11 @@ export function RuntimeControlCard() {
183
159
  return;
184
160
  }
185
161
 
186
- setBusyAction(action);
187
- setLocalLifecycle(action === 'start-service' ? 'starting-service' : action === 'stop-service' ? 'stopping-service' : 'restarting-service');
188
- setLocalServiceState(action === 'start-service' ? 'starting' : action === 'stop-service' ? 'stopping' : 'restarting');
189
- setLocalMessage(resolveActionHelp(action));
190
-
191
162
  try {
192
- const result = await serviceActionMutation.mutateAsync(action);
163
+ const result = await systemStatusManager.runRuntimeControlAction(action);
193
164
  toast.success(result.message);
194
- if (action === 'stop-service') {
195
- return;
196
- }
197
- setLocalLifecycle('recovering');
198
- setLocalMessage(t('runtimeControlRecoveringHelp'));
199
- const recoveredView = await runtimeControlManager.waitForRecovery();
200
- queryClient.setQueryData(['runtime-control'], recoveredView);
201
- await queryClient.invalidateQueries({ queryKey: ['runtime-control'] });
202
- resetLocalState();
203
- toast.success(t('runtimeControlRecovered'));
204
165
  } catch (error) {
205
166
  const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
206
- setLocalLifecycle('failed');
207
- setLocalServiceState(action === 'stop-service' ? 'running' : 'unknown');
208
- setLocalMessage(message);
209
- setBusyAction(null);
210
167
  toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
211
168
  }
212
169
  };
@@ -220,18 +177,11 @@ export function RuntimeControlCard() {
220
177
  return;
221
178
  }
222
179
 
223
- setBusyAction('restart-app');
224
- setLocalLifecycle('restarting-app');
225
- setLocalMessage(t('runtimeControlRestartingAppHelp'));
226
-
227
180
  try {
228
- const result = await runtimeControlManager.restartApp();
181
+ const result = await systemStatusManager.runRuntimeControlAction('restart-app');
229
182
  toast.success(result.message);
230
183
  } catch (error) {
231
184
  const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
232
- setLocalLifecycle('failed');
233
- setLocalMessage(message);
234
- setBusyAction(null);
235
185
  toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
236
186
  }
237
187
  };
@@ -255,9 +205,9 @@ export function RuntimeControlCard() {
255
205
  {controlView?.managementHint ? (
256
206
  <p className="text-xs text-gray-500">{controlView.managementHint}</p>
257
207
  ) : null}
258
- {runtimeControlQuery.isError && !busy ? (
208
+ {errorMessage && !busy ? (
259
209
  <p className="text-sm text-amber-700">
260
- {runtimeControlQuery.error instanceof Error ? runtimeControlQuery.error.message : t('runtimeControlLoadFailed')}
210
+ {errorMessage}
261
211
  </p>
262
212
  ) : null}
263
213
  </div>
@@ -272,7 +222,7 @@ export function RuntimeControlCard() {
272
222
  {t('runtimeControlPendingRestartPaths')}
273
223
  </div>
274
224
  <div className="flex flex-wrap gap-2">
275
- {pendingRestart.changedPaths.map((path) => (
225
+ {pendingRestart.changedPaths.map((path: string) => (
276
226
  <span
277
227
  key={path}
278
228
  className="rounded-full border border-amber-200 bg-white px-2.5 py-1 text-xs text-amber-800"
@@ -2,17 +2,17 @@ import { render, screen, waitFor } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { RuntimePresenceCard } from '@/components/config/runtime-presence-card';
5
- import { useDesktopPresenceStore } from '@/desktop/stores/desktop-presence.store';
6
5
  import { setLanguage } from '@/lib/i18n';
6
+ import { useDesktopPresenceStore } from '@/platforms/desktop';
7
7
 
8
8
  const mocks = vi.hoisted(() => ({
9
- useRuntimeControl: vi.fn(),
9
+ useSystemStatus: vi.fn(),
10
10
  toastSuccess: vi.fn(),
11
11
  toastError: vi.fn()
12
12
  }));
13
13
 
14
- vi.mock('@/hooks/use-runtime-control', () => ({
15
- useRuntimeControl: (...args: unknown[]) => mocks.useRuntimeControl(...args)
14
+ vi.mock('@/features/system-status', () => ({
15
+ useSystemStatus: (...args: unknown[]) => mocks.useSystemStatus(...args)
16
16
  }));
17
17
 
18
18
  vi.mock('sonner', () => ({
@@ -32,8 +32,8 @@ describe('RuntimePresenceCard', () => {
32
32
  busyAction: null,
33
33
  snapshot: null
34
34
  });
35
- mocks.useRuntimeControl.mockReturnValue({
36
- data: {
35
+ mocks.useSystemStatus.mockReturnValue({
36
+ runtimeControlView: {
37
37
  environment: 'managed-local-service',
38
38
  lifecycle: 'healthy',
39
39
  serviceState: 'running',
@@ -61,9 +61,7 @@ describe('RuntimePresenceCard', () => {
61
61
  reasonIfUnavailable: 'desktop only'
62
62
  },
63
63
  managementHint: 'managed service hint'
64
- },
65
- isError: false,
66
- error: null
64
+ }
67
65
  });
68
66
  window.nextclawDesktop = undefined;
69
67
  });
@@ -106,8 +104,8 @@ describe('RuntimePresenceCard', () => {
106
104
  onUpdateStateChanged: vi.fn(() => () => {})
107
105
  };
108
106
 
109
- mocks.useRuntimeControl.mockReturnValue({
110
- data: {
107
+ mocks.useSystemStatus.mockReturnValue({
108
+ runtimeControlView: {
111
109
  environment: 'desktop-embedded',
112
110
  lifecycle: 'healthy',
113
111
  serviceState: 'running',
@@ -133,9 +131,7 @@ describe('RuntimePresenceCard', () => {
133
131
  impact: 'full-app-relaunch'
134
132
  },
135
133
  managementHint: 'desktop hint'
136
- },
137
- isError: false,
138
- error: null
134
+ }
139
135
  });
140
136
 
141
137
  render(<RuntimePresenceCard />);
@@ -9,9 +9,11 @@ import {
9
9
  import { NoticeCard } from "@/components/ui/notice-card";
10
10
  import { SettingRow } from "@/components/ui/setting-row";
11
11
  import { Switch } from "@/components/ui/switch";
12
- import { desktopPresenceManager } from "@/desktop/managers/desktop-presence.manager";
13
- import { useDesktopPresenceStore } from "@/desktop/stores/desktop-presence.store";
14
- import { useRuntimeControl } from "@/hooks/use-runtime-control";
12
+ import {
13
+ desktopPresenceManager,
14
+ useDesktopPresenceStore,
15
+ } from "@/platforms/desktop";
16
+ import { useSystemStatus } from "@/features/system-status";
15
17
  import { t } from "@/lib/i18n";
16
18
 
17
19
  function PresenceHint(props: { title: string; description: string }) {
@@ -20,8 +22,8 @@ function PresenceHint(props: { title: string; description: string }) {
20
22
  }
21
23
 
22
24
  export function RuntimePresenceCard() {
23
- const runtimeControlQuery = useRuntimeControl();
24
- const environment = runtimeControlQuery.data?.environment;
25
+ const systemStatus = useSystemStatus();
26
+ const environment = systemStatus.runtimeControlView?.environment;
25
27
  const supported = useDesktopPresenceStore((state) => state.supported);
26
28
  const initialized = useDesktopPresenceStore((state) => state.initialized);
27
29
  const busyAction = useDesktopPresenceStore((state) => state.busyAction);
@@ -9,8 +9,8 @@ import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/comp
9
9
  import { useI18n } from '@/components/providers/I18nProvider';
10
10
  import { useTheme } from '@/components/providers/ThemeProvider';
11
11
  import { SelectItem } from '@/components/ui/select';
12
- import { useRemoteStatus } from '@/hooks/useRemoteAccess';
13
- import { useAppPresenter } from '@/presenter/app-presenter-context';
12
+ import { useAppManager } from '@/app/components/app-manager-provider';
13
+ import { useRemoteStatus } from '@/features/remote';
14
14
 
15
15
  type SidebarMode = 'main' | 'settings';
16
16
 
@@ -19,7 +19,7 @@ type SidebarProps = {
19
19
  };
20
20
 
21
21
  export function Sidebar({ mode }: SidebarProps) {
22
- const presenter = useAppPresenter();
22
+ const manager = useAppManager();
23
23
  const docBrowser = useDocBrowser();
24
24
  const remoteStatus = useRemoteStatus();
25
25
  const { language, setLanguage } = useI18n();
@@ -182,7 +182,7 @@ export function Sidebar({ mode }: SidebarProps) {
182
182
  <div className={cn('shrink-0 border-t border-[#dde0ea] bg-secondary', isSettingsMode ? 'mt-2 pt-3' : 'mt-3 pt-3')}>
183
183
  {isSettingsMode ? (
184
184
  <SidebarActionItem
185
- onClick={() => presenter.accountManager.openAccountPanel()}
185
+ onClick={() => manager.accountManager.openAccountPanel()}
186
186
  icon={KeyRound}
187
187
  label={t('remoteAccountEntryManage')}
188
188
  density="compact"
@@ -1,40 +1,31 @@
1
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
1
  import { render, screen, waitFor } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
4
- import type { ReactNode } from 'react';
5
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
6
4
  import { toast } from 'sonner';
7
5
  import { RuntimeStatusEntry } from '@/components/layout/runtime-status-entry';
8
6
  import { setLanguage } from '@/lib/i18n';
9
7
 
10
8
  const mocks = vi.hoisted(() => ({
11
- useRuntimeControl: vi.fn(),
12
- controlService: vi.fn()
9
+ useRuntimeStatusBadgeView: vi.fn(),
10
+ runRuntimeControlAction: vi.fn(),
13
11
  }));
14
12
 
15
13
  vi.mock('sonner', () => ({
16
14
  toast: {
17
15
  success: vi.fn(),
18
- error: vi.fn()
19
- }
16
+ error: vi.fn(),
17
+ },
20
18
  }));
21
19
 
22
- vi.mock('@/hooks/use-runtime-control', () => ({
23
- useRuntimeControl: (...args: unknown[]) => mocks.useRuntimeControl(...args)
20
+ vi.mock('@/features/system-status', () => ({
21
+ useRuntimeStatusBadgeView: (...args: unknown[]) =>
22
+ mocks.useRuntimeStatusBadgeView(...args),
23
+ systemStatusManager: {
24
+ runRuntimeControlAction: (...args: unknown[]) =>
25
+ mocks.runRuntimeControlAction(...args),
26
+ },
24
27
  }));
25
28
 
26
- vi.mock('@/runtime-control/runtime-control.manager', () => ({
27
- runtimeControlManager: {
28
- controlService: (...args: unknown[]) => mocks.controlService(...args)
29
- }
30
- }));
31
-
32
- function createWrapper(queryClient: QueryClient) {
33
- return function Wrapper({ children }: { children: ReactNode }) {
34
- return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
35
- };
36
- }
37
-
38
29
  describe('RuntimeStatusEntry', () => {
39
30
  beforeEach(() => {
40
31
  setLanguage('zh');
@@ -42,112 +33,65 @@ describe('RuntimeStatusEntry', () => {
42
33
  });
43
34
 
44
35
  it('shows a compact pending-restart entry with reasons and a restart action', async () => {
45
- const queryClient = new QueryClient();
46
36
  const user = userEvent.setup();
47
- const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
48
-
49
- mocks.useRuntimeControl.mockReturnValue({
50
- data: {
51
- environment: 'managed-local-service',
52
- lifecycle: 'healthy',
53
- serviceState: 'running',
54
- message: 'runtime healthy',
55
- pendingRestart: {
56
- changedPaths: ['plugins', 'ui'],
57
- message: 'Saved changes are waiting for a manual restart.',
58
- reasons: ['config reload requires restart: plugins, ui'],
59
- requestedAt: '2026-04-17T12:00:00.000Z'
60
- },
61
- canStartService: {
62
- available: false,
63
- requiresConfirmation: false,
64
- impact: 'brief-ui-disconnect'
65
- },
66
- canRestartService: {
67
- available: true,
68
- requiresConfirmation: false,
69
- impact: 'brief-ui-disconnect'
70
- },
71
- canStopService: {
72
- available: true,
73
- requiresConfirmation: true,
74
- impact: 'brief-ui-disconnect'
75
- },
76
- canRestartApp: {
77
- available: false,
78
- requiresConfirmation: true,
79
- impact: 'full-app-relaunch'
80
- }
81
- },
82
- isError: false,
83
- error: null
37
+
38
+ mocks.useRuntimeStatusBadgeView.mockReturnValue({
39
+ tone: 'attention',
40
+ title: '待重启',
41
+ description:
42
+ '这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。',
43
+ reasonLines: [
44
+ 'plugins 改动将在重启后生效。',
45
+ 'ui 改动将在重启后生效。',
46
+ ],
47
+ actionLabel: '立即重启',
48
+ isBusy: false,
84
49
  });
85
- mocks.controlService.mockResolvedValue({
50
+ mocks.runRuntimeControlAction.mockResolvedValue({
86
51
  accepted: true,
87
52
  action: 'restart-service',
88
53
  lifecycle: 'restarting-service',
89
- message: 'Restart scheduled. This page may disconnect for a few seconds.'
54
+ message: 'Restart scheduled. This page may disconnect for a few seconds.',
90
55
  });
91
56
 
92
- render(<RuntimeStatusEntry />, {
93
- wrapper: createWrapper(queryClient)
94
- });
57
+ render(<RuntimeStatusEntry />);
95
58
 
96
59
  await user.click(screen.getByTestId('runtime-status-entry'));
97
60
 
98
61
  expect(screen.getByText('待重启')).toBeTruthy();
99
- expect(screen.getByText('这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。')).toBeTruthy();
62
+ expect(
63
+ screen.getByText(
64
+ '这些改动已经保存,但不会自动重启。你可以在这里查看原因,并在方便的时候手动重启。'
65
+ )
66
+ ).toBeTruthy();
100
67
  expect(screen.getByText('plugins 改动将在重启后生效。')).toBeTruthy();
101
68
  expect(screen.getByText('ui 改动将在重启后生效。')).toBeTruthy();
102
69
 
103
70
  await user.click(screen.getByRole('button', { name: '立即重启' }));
104
71
 
105
72
  await waitFor(() => {
106
- expect(mocks.controlService).toHaveBeenCalledWith('restart-service');
73
+ expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
74
+ 'restart-service'
75
+ );
107
76
  });
108
- expect(toast.success).toHaveBeenCalledWith('Restart scheduled. This page may disconnect for a few seconds.');
109
- expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['runtime-control'] });
77
+ expect(toast.success).toHaveBeenCalledWith(
78
+ 'Restart scheduled. This page may disconnect for a few seconds.'
79
+ );
110
80
  });
111
81
 
112
82
  it('shows a healthy status without restart controls when no action is needed', async () => {
113
- const queryClient = new QueryClient();
114
83
  const user = userEvent.setup();
115
84
 
116
- mocks.useRuntimeControl.mockReturnValue({
117
- data: {
118
- environment: 'managed-local-service',
119
- lifecycle: 'healthy',
120
- serviceState: 'running',
121
- message: 'runtime healthy',
122
- pendingRestart: null,
123
- canStartService: {
124
- available: false,
125
- requiresConfirmation: false,
126
- impact: 'brief-ui-disconnect'
127
- },
128
- canRestartService: {
129
- available: true,
130
- requiresConfirmation: false,
131
- impact: 'brief-ui-disconnect'
132
- },
133
- canStopService: {
134
- available: true,
135
- requiresConfirmation: true,
136
- impact: 'brief-ui-disconnect'
137
- },
138
- canRestartApp: {
139
- available: false,
140
- requiresConfirmation: true,
141
- impact: 'full-app-relaunch'
142
- }
143
- },
144
- isError: false,
145
- error: null
85
+ mocks.useRuntimeStatusBadgeView.mockReturnValue({
86
+ tone: 'healthy',
87
+ title: '系统正常',
88
+ description: '所有系统状态都正常。',
89
+ reasonLines: [],
90
+ actionLabel: null,
91
+ isBusy: false,
146
92
  });
147
93
 
148
- render(<RuntimeStatusEntry />, {
149
- wrapper: createWrapper(queryClient)
150
- });
94
+ render(<RuntimeStatusEntry />);
151
95
 
152
96
  await user.click(screen.getByTestId('runtime-status-entry'));
153
97
 
@@ -1,8 +1,8 @@
1
- import { useState } from 'react';
2
- import { useQueryClient } from '@tanstack/react-query';
3
- import { useRuntimeControl } from '@/hooks/use-runtime-control';
4
- import { runtimeControlManager } from '@/runtime-control/runtime-control.manager';
5
1
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
2
+ import {
3
+ systemStatusManager,
4
+ useRuntimeStatusBadgeView,
5
+ } from '@/features/system-status';
6
6
  import { t } from '@/lib/i18n';
7
7
  import { cn } from '@/lib/utils';
8
8
  import { toast } from 'sonner';
@@ -21,72 +21,24 @@ type RuntimeStatusSummary = {
21
21
  reasonLines: string[];
22
22
  title: string;
23
23
  tone: RuntimeStatusTone;
24
+ isBusy: boolean;
24
25
  };
25
26
 
26
- function buildRuntimeStatusSummary(
27
- view: ReturnType<typeof useRuntimeControl>['data']
28
- ): RuntimeStatusSummary {
29
- if (!view) {
30
- return {
31
- tone: 'inactive',
32
- title: t('runtimeStatusLoadingTitle'),
33
- description: t('runtimeStatusLoadingDescription'),
34
- reasonLines: [],
35
- actionLabel: null
36
- };
37
- }
38
-
39
- if (view.pendingRestart) {
40
- return {
41
- tone: 'attention',
42
- title: t('runtimeStatusPendingRestartTitle'),
43
- description: t('runtimeStatusPendingRestartDescription'),
44
- reasonLines:
45
- view.pendingRestart.changedPaths.length > 0
46
- ? view.pendingRestart.changedPaths.map((path) =>
47
- t('runtimeStatusPendingRestartReasonItem').replace('{path}', path)
48
- )
49
- : [view.pendingRestart.message],
50
- actionLabel: view.canRestartService.available ? t('runtimeStatusRestartAction') : null
51
- };
52
- }
53
-
54
- return {
55
- tone: view.lifecycle === 'healthy' ? 'healthy' : 'inactive',
56
- title: t('runtimeStatusHealthyTitle'),
57
- description: t('runtimeStatusHealthyDescription'),
58
- reasonLines: [],
59
- actionLabel: null
60
- };
61
- }
62
-
63
27
  export function RuntimeStatusEntry() {
64
- const queryClient = useQueryClient();
65
- const runtimeControlQuery = useRuntimeControl();
66
- const [isRestarting, setIsRestarting] = useState(false);
67
- const runtimeView = runtimeControlQuery.data;
68
- const summary = buildRuntimeStatusSummary(runtimeView);
69
- const title = runtimeControlQuery.isError ? t('runtimeControlLoadFailed') : summary.title;
70
- const description =
71
- runtimeControlQuery.isError && runtimeControlQuery.error instanceof Error
72
- ? runtimeControlQuery.error.message
73
- : summary.description;
74
- const canRestart = Boolean(runtimeView?.pendingRestart && runtimeView.canRestartService.available);
28
+ const summary = useRuntimeStatusBadgeView() as RuntimeStatusSummary;
29
+ const canRestart = summary.actionLabel === t('runtimeStatusRestartAction');
75
30
 
76
31
  const handleRestart = async () => {
77
32
  if (!canRestart) {
78
33
  return;
79
34
  }
80
- setIsRestarting(true);
81
35
  try {
82
- const result = await runtimeControlManager.controlService('restart-service');
36
+ const result =
37
+ await systemStatusManager.runRuntimeControlAction('restart-service');
83
38
  toast.success(result.message);
84
- await queryClient.invalidateQueries({ queryKey: ['runtime-control'] });
85
39
  } catch (error) {
86
40
  const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
87
41
  toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
88
- } finally {
89
- setIsRestarting(false);
90
42
  }
91
43
  };
92
44
 
@@ -96,8 +48,8 @@ export function RuntimeStatusEntry() {
96
48
  <button
97
49
  type="button"
98
50
  className="inline-flex items-center justify-center rounded-full p-0.5 transition-transform hover:scale-105"
99
- aria-label={title}
100
- title={title}
51
+ aria-label={summary.title}
52
+ title={summary.title}
101
53
  data-testid="runtime-status-entry"
102
54
  >
103
55
  <span className={cn('h-2.5 w-2.5 rounded-full', runtimeStatusToneStyles[summary.tone])} />
@@ -109,8 +61,8 @@ export function RuntimeStatusEntry() {
109
61
  className="w-[290px] space-y-3 rounded-2xl border border-gray-200 bg-white p-4"
110
62
  >
111
63
  <div className="space-y-1">
112
- <div className="text-sm font-semibold text-gray-900">{title}</div>
113
- <p className="text-xs leading-5 text-gray-600">{description}</p>
64
+ <div className="text-sm font-semibold text-gray-900">{summary.title}</div>
65
+ <p className="text-xs leading-5 text-gray-600">{summary.description}</p>
114
66
  </div>
115
67
  {summary.reasonLines.length > 0 ? (
116
68
  <div className="space-y-2">
@@ -130,10 +82,10 @@ export function RuntimeStatusEntry() {
130
82
  <button
131
83
  type="button"
132
84
  onClick={() => void handleRestart()}
133
- disabled={isRestarting}
85
+ disabled={summary.isBusy}
134
86
  className="text-sm font-semibold text-sky-600 transition-colors hover:text-sky-700 disabled:text-gray-400"
135
87
  >
136
- {isRestarting ? t('runtimeStatusRestartingAction') : summary.actionLabel}
88
+ {summary.isBusy ? t('runtimeStatusRestartingAction') : summary.actionLabel}
137
89
  </button>
138
90
  </div>
139
91
  ) : null}
@@ -22,17 +22,23 @@ vi.mock('@/components/doc-browser', () => ({
22
22
  })
23
23
  }));
24
24
 
25
- vi.mock('@/presenter/app-presenter-context', () => ({
26
- useAppPresenter: () => ({
25
+ vi.mock('@/app/components/app-manager-provider', () => ({
26
+ useAppManager: () => ({
27
27
  accountManager: {
28
28
  openAccountPanel: mocks.openAccountPanel
29
29
  }
30
30
  })
31
31
  }));
32
32
 
33
- vi.mock('@/hooks/useRemoteAccess', () => ({
34
- useRemoteStatus: () => mocks.remoteStatus
35
- }));
33
+ vi.mock('@/features/remote', async () => {
34
+ const actual = await vi.importActual<typeof import('@/features/remote')>(
35
+ '@/features/remote'
36
+ );
37
+ return {
38
+ ...actual,
39
+ useRemoteStatus: () => mocks.remoteStatus
40
+ };
41
+ });
36
42
 
37
43
  vi.mock('@/components/providers/I18nProvider', () => ({
38
44
  useI18n: () => ({