@nextclaw/ui 0.12.9 → 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 (245) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.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-ChHQ7M5c.js → SessionsConfig-CvjxU40H.js} +2 -2
  14. package/dist/assets/{book-open-BdcxxoQu.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-DK5HPmIK.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/config-split-page-BAGSzUR3.js +1 -0
  20. package/dist/assets/{createLucideIcon-BSeTgkZW.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-6TrrnPCR.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BhDxnyvU.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-BgErLCNT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-Bl7dr_UG.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-eDHeDY0n.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-x89HbrZ4.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-C47QSEwg.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-3S6-H3Xw.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-BsLtHOH-.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-G48scll7.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-CBWjE2uj.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/dist/runtime-icons/claude.ico +0 -0
  59. package/dist/runtime-icons/codex-openai.svg +6 -0
  60. package/dist/runtime-icons/hermes-agent.png +0 -0
  61. package/module-structure.config.json +7 -0
  62. package/package.json +6 -6
  63. package/public/runtime-icons/claude.ico +0 -0
  64. package/public/runtime-icons/codex-openai.svg +6 -0
  65. package/public/runtime-icons/hermes-agent.png +0 -0
  66. package/src/api/chat-session-type.types.ts +7 -0
  67. package/src/api/config.ts +10 -0
  68. package/src/api/raw-client.test.ts +1 -1
  69. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  70. package/src/api/runtime-control.types.ts +8 -0
  71. package/src/api/types.ts +48 -0
  72. package/src/app/components/app-manager-provider.tsx +20 -0
  73. package/src/app/managers/app.manager.ts +12 -0
  74. package/src/app.tsx +223 -59
  75. package/src/components/agents/agent-dialogs.tsx +499 -0
  76. package/src/components/agents/agents-page.test.tsx +238 -0
  77. package/src/components/agents/agents-page.tsx +435 -0
  78. package/src/components/chat/chat-conversation-panel.test.tsx +30 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  81. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  82. package/src/components/chat/chat-page-shell.tsx +19 -13
  83. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  84. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  85. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  86. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  87. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  88. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  89. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  90. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  91. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +24 -72
  92. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  93. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  94. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  95. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  96. package/src/components/chat/ncp/ncp-chat-page.tsx +23 -2
  97. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  98. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  99. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  100. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  101. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  102. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  103. package/src/components/chat/stores/chat-input.store.ts +2 -1
  104. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  105. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  106. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  107. package/src/components/common/BrandHeader.tsx +3 -1
  108. package/src/components/common/session-context-icon.tsx +15 -2
  109. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  110. package/src/components/config/ChannelForm.test.tsx +89 -3
  111. package/src/components/config/ChannelForm.tsx +157 -188
  112. package/src/components/config/ChannelsList.test.tsx +163 -119
  113. package/src/components/config/ChannelsList.tsx +90 -101
  114. package/src/components/config/ProviderForm.tsx +108 -146
  115. package/src/components/config/ProvidersList.tsx +100 -123
  116. package/src/components/config/SearchConfig.tsx +423 -393
  117. package/src/components/config/channel-form-fields-section.tsx +70 -37
  118. package/src/components/config/config-split-page.tsx +109 -0
  119. package/src/components/config/desktop-update-config.test.tsx +10 -4
  120. package/src/components/config/desktop-update-config.tsx +5 -3
  121. package/src/components/config/provider-enabled-field.tsx +17 -10
  122. package/src/components/config/runtime-control-card.test.tsx +136 -158
  123. package/src/components/config/runtime-control-card.tsx +43 -68
  124. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  125. package/src/components/config/runtime-presence-card.tsx +97 -81
  126. package/src/components/layout/AppLayout.tsx +25 -37
  127. package/src/components/layout/Sidebar.tsx +4 -4
  128. package/src/components/layout/app-layout.test.tsx +46 -14
  129. package/src/components/layout/runtime-status-entry.test.tsx +101 -0
  130. package/src/components/layout/runtime-status-entry.tsx +95 -0
  131. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  132. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  133. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  134. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  135. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  136. package/src/components/marketplace/marketplace-page.tsx +596 -0
  137. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  138. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  139. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  140. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  141. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  142. package/src/components/ui/notice-card.tsx +129 -0
  143. package/src/components/ui/setting-row.tsx +51 -0
  144. package/src/components/ui/tag-chip.tsx +39 -0
  145. package/src/components/ui/textarea.tsx +19 -0
  146. package/src/features/account/components/account-panel.tsx +255 -0
  147. package/src/features/account/index.ts +6 -0
  148. package/src/{account → features/account}/managers/account.manager.ts +6 -5
  149. package/src/features/remote/components/remote-access-page.test.tsx +104 -0
  150. package/src/features/remote/components/remote-access-page.tsx +250 -0
  151. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  152. package/src/features/remote/index.ts +27 -0
  153. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  154. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  155. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  156. package/src/features/system-status/index.ts +12 -0
  157. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  158. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  159. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  160. package/src/features/system-status/stores/system-status.store.ts +32 -0
  161. package/src/features/system-status/types/system-status.types.ts +73 -0
  162. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  163. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  164. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  165. package/src/hooks/useConfig.ts +2 -1
  166. package/src/index.css +24 -0
  167. package/src/lib/app-resource-uri.test.ts +20 -0
  168. package/src/lib/app-resource-uri.ts +29 -0
  169. package/src/lib/i18n.chat.ts +8 -0
  170. package/src/lib/i18n.remote.ts +1 -1
  171. package/src/lib/i18n.runtime-control.ts +31 -0
  172. package/src/lib/i18n.ts +5 -8
  173. package/src/lib/session-context.utils.test.ts +71 -0
  174. package/src/lib/session-context.utils.ts +28 -3
  175. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  176. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  177. package/src/platforms/desktop/index.ts +20 -0
  178. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  179. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  180. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  181. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  182. package/src/stores/ui.store.ts +0 -9
  183. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  184. package/src/transport/app-client.test.ts +9 -5
  185. package/src/transport/index.ts +1 -1
  186. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  187. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  188. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  189. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  190. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  191. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  192. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  193. package/dist/assets/ProviderScopedModelInput-Da7khnBA.js +0 -1
  194. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  195. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  196. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  197. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  198. package/dist/assets/SecretsConfig-D281Rotl.js +0 -3
  199. package/dist/assets/app-query-client-VnFElj4E.js +0 -1
  200. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  201. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  202. package/dist/assets/client-_i4MU2bB.js +0 -7
  203. package/dist/assets/config-DtIQwrHF.js +0 -1
  204. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  205. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  206. package/dist/assets/dist-ccBFUi-o.js +0 -9
  207. package/dist/assets/index-CF9xve0E.js +0 -6
  208. package/dist/assets/index-FgA52VBt.css +0 -1
  209. package/dist/assets/infiniteQueryBehavior-ZDS92Qpp.js +0 -1
  210. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  211. package/dist/assets/page-layout-vZnghcFy.js +0 -1
  212. package/dist/assets/play-CFUwCA2E.js +0 -1
  213. package/dist/assets/plus-rYsv72JG.js +0 -1
  214. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  215. package/dist/assets/refresh-ccw-DT98i__E.js +0 -1
  216. package/dist/assets/rotate-cw-JtFzpNn6.js +0 -1
  217. package/dist/assets/search-3kFR_zh9.js +0 -1
  218. package/dist/assets/security-config-BWaiARNk.js +0 -1
  219. package/dist/assets/select-DJ2MUjBB.js +0 -41
  220. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  221. package/dist/assets/status-dot-vbanNPFU.js +0 -1
  222. package/dist/assets/use-infinite-scroll-loader-DkNhD-42.js +0 -1
  223. package/dist/assets/useConfirmDialog-BkvTN-vd.js +0 -1
  224. package/dist/assets/x-ByDbItbq.js +0 -1
  225. package/src/account/components/account-panel.tsx +0 -135
  226. package/src/components/agents/AgentDialogs.tsx +0 -400
  227. package/src/components/agents/AgentsPage.test.tsx +0 -217
  228. package/src/components/agents/AgentsPage.tsx +0 -352
  229. package/src/components/config/config-layout.ts +0 -10
  230. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  231. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  232. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  233. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  234. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  235. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  236. package/src/hooks/use-runtime-control.ts +0 -24
  237. package/src/presenter/app-presenter-context.tsx +0 -20
  238. package/src/presenter/app.presenter.ts +0 -12
  239. package/src/runtime-control/runtime-control.manager.ts +0 -118
  240. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  241. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  242. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  243. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  244. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  245. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -1,17 +1,44 @@
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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
4
  import { toast } from 'sonner';
7
5
  import { RuntimeControlCard } from '@/components/config/runtime-control-card';
8
6
  import { setLanguage } from '@/lib/i18n';
9
7
 
8
+ const baseControlView = {
9
+ environment: 'managed-local-service' as const,
10
+ lifecycle: 'healthy' as const,
11
+ serviceState: 'running' as const,
12
+ message: 'runtime healthy',
13
+ pendingRestart: null,
14
+ canStartService: {
15
+ available: false,
16
+ requiresConfirmation: false,
17
+ impact: 'brief-ui-disconnect' as const,
18
+ reasonIfUnavailable: '当前页面已经由运行中的本地服务托管。',
19
+ },
20
+ canRestartService: {
21
+ available: true,
22
+ requiresConfirmation: false,
23
+ impact: 'brief-ui-disconnect' as const,
24
+ },
25
+ canStopService: {
26
+ available: true,
27
+ requiresConfirmation: true,
28
+ impact: 'brief-ui-disconnect' as const,
29
+ },
30
+ canRestartApp: {
31
+ available: false,
32
+ requiresConfirmation: true,
33
+ impact: 'full-app-relaunch' as const,
34
+ reasonIfUnavailable: 'desktop only',
35
+ },
36
+ managementHint: 'This page is served by the running local service.',
37
+ };
38
+
10
39
  const mocks = vi.hoisted(() => ({
11
- useRuntimeControl: vi.fn(),
12
- useRuntimeServiceAction: vi.fn(),
13
- waitForRecovery: vi.fn(),
14
- restartApp: vi.fn(),
40
+ useRuntimeControlPanelView: vi.fn(),
41
+ runRuntimeControlAction: vi.fn(),
15
42
  }));
16
43
 
17
44
  vi.mock('sonner', () => ({
@@ -21,104 +48,28 @@ vi.mock('sonner', () => ({
21
48
  },
22
49
  }));
23
50
 
24
- vi.mock('@/hooks/use-runtime-control', () => ({
25
- useRuntimeControl: (...args: unknown[]) => mocks.useRuntimeControl(...args),
26
- useRuntimeServiceAction: (...args: unknown[]) => mocks.useRuntimeServiceAction(...args),
27
- }));
28
-
29
- vi.mock('@/runtime-control/runtime-control.manager', () => ({
30
- runtimeControlManager: {
31
- waitForRecovery: (...args: unknown[]) => mocks.waitForRecovery(...args),
32
- restartApp: (...args: unknown[]) => mocks.restartApp(...args),
51
+ vi.mock('@/features/system-status', () => ({
52
+ useRuntimeControlPanelView: (...args: unknown[]) =>
53
+ mocks.useRuntimeControlPanelView(...args),
54
+ systemStatusManager: {
55
+ runRuntimeControlAction: (...args: unknown[]) =>
56
+ mocks.runRuntimeControlAction(...args),
33
57
  },
34
58
  }));
35
59
 
36
- function createWrapper(queryClient: QueryClient) {
37
- return function Wrapper({ children }: { children: ReactNode }) {
38
- return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
39
- };
40
- }
41
-
42
60
  describe('RuntimeControlCard', () => {
43
61
  beforeEach(() => {
44
62
  setLanguage('zh');
45
63
  vi.clearAllMocks();
46
- mocks.useRuntimeControl.mockReturnValue({
47
- data: {
48
- environment: 'managed-local-service',
49
- lifecycle: 'healthy',
50
- serviceState: 'running',
51
- message: 'runtime healthy',
52
- canStartService: {
53
- available: false,
54
- requiresConfirmation: false,
55
- impact: 'brief-ui-disconnect',
56
- reasonIfUnavailable: '当前页面已经由运行中的本地服务托管。'
57
- },
58
- canRestartService: {
59
- available: true,
60
- requiresConfirmation: false,
61
- impact: 'brief-ui-disconnect',
62
- },
63
- canStopService: {
64
- available: true,
65
- requiresConfirmation: true,
66
- impact: 'brief-ui-disconnect',
67
- },
68
- canRestartApp: {
69
- available: false,
70
- requiresConfirmation: true,
71
- impact: 'full-app-relaunch',
72
- reasonIfUnavailable: 'desktop only',
73
- },
74
- managementHint: 'This page is served by the running local service.'
75
- },
76
- isError: false,
77
- error: null,
78
- });
79
- mocks.useRuntimeServiceAction.mockReturnValue({
80
- mutateAsync: vi.fn().mockResolvedValue({
81
- accepted: true,
82
- action: 'restart-service',
83
- lifecycle: 'restarting-service',
84
- message: 'Restart scheduled. This page may disconnect for a few seconds.',
85
- }),
86
- isPending: false,
87
- });
88
- mocks.waitForRecovery.mockResolvedValue({
89
- environment: 'managed-local-service',
90
- lifecycle: 'healthy',
91
- serviceState: 'running',
92
- message: 'runtime healthy',
93
- canStartService: {
94
- available: false,
95
- requiresConfirmation: false,
96
- impact: 'brief-ui-disconnect',
97
- reasonIfUnavailable: '当前页面已经由运行中的本地服务托管。'
98
- },
99
- canRestartService: {
100
- available: true,
101
- requiresConfirmation: false,
102
- impact: 'brief-ui-disconnect',
103
- },
104
- canStopService: {
105
- available: true,
106
- requiresConfirmation: true,
107
- impact: 'brief-ui-disconnect',
108
- },
109
- canRestartApp: {
110
- available: false,
111
- requiresConfirmation: true,
112
- impact: 'full-app-relaunch',
113
- reasonIfUnavailable: 'desktop only',
114
- },
115
- managementHint: 'This page is served by the running local service.'
116
- });
117
- mocks.restartApp.mockResolvedValue({
118
- accepted: true,
119
- action: 'restart-app',
120
- lifecycle: 'restarting-app',
121
- message: 'NextClaw app restart scheduled.',
64
+ mocks.useRuntimeControlPanelView.mockReturnValue({
65
+ controlView: baseControlView,
66
+ visibleLifecycle: 'healthy',
67
+ visibleServiceState: 'running',
68
+ visibleMessage: 'runtime healthy',
69
+ busyAction: null,
70
+ busy: false,
71
+ pendingRestart: null,
72
+ errorMessage: null,
122
73
  });
123
74
  });
124
75
 
@@ -127,14 +78,15 @@ describe('RuntimeControlCard', () => {
127
78
  });
128
79
 
129
80
  it('renders service management actions from the current capability view', () => {
130
- const queryClient = new QueryClient();
81
+ render(<RuntimeControlCard />);
131
82
 
132
- render(<RuntimeControlCard />, {
133
- wrapper: createWrapper(queryClient),
134
- });
83
+ const startButton = screen.getByRole('button', {
84
+ name: '启动服务',
85
+ }) as HTMLButtonElement;
86
+ const restartAppButton = screen.getByRole('button', {
87
+ name: '重启应用',
88
+ }) as HTMLButtonElement;
135
89
 
136
- const startButton = screen.getByRole('button', { name: '启动服务' }) as HTMLButtonElement;
137
- const restartAppButton = screen.getByRole('button', { name: '重启应用' }) as HTMLButtonElement;
138
90
  expect(screen.getByText('服务管理')).toBeTruthy();
139
91
  expect(screen.getByText('服务运行中')).toBeTruthy();
140
92
  expect(screen.getByRole('button', { name: '重启服务' })).toBeTruthy();
@@ -144,84 +96,61 @@ describe('RuntimeControlCard', () => {
144
96
  expect(screen.getByText('desktop only')).toBeTruthy();
145
97
  });
146
98
 
147
- it('runs the restart service flow and waits for recovery', async () => {
148
- const queryClient = new QueryClient();
99
+ it('runs the restart service flow through the system status manager', async () => {
149
100
  const user = userEvent.setup();
150
- const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
151
- const mutateAsync = vi.fn().mockResolvedValue({
101
+ mocks.runRuntimeControlAction.mockResolvedValue({
152
102
  accepted: true,
153
103
  action: 'restart-service',
154
104
  lifecycle: 'restarting-service',
155
105
  message: 'Restart scheduled. This page may disconnect for a few seconds.',
156
106
  });
157
- mocks.useRuntimeServiceAction.mockReturnValue({
158
- mutateAsync,
159
- isPending: false,
160
- });
161
107
 
162
- render(<RuntimeControlCard />, {
163
- wrapper: createWrapper(queryClient),
164
- });
108
+ render(<RuntimeControlCard />);
165
109
 
166
110
  await user.click(screen.getByRole('button', { name: '重启服务' }));
167
111
 
168
112
  await waitFor(() => {
169
- expect(mutateAsync).toHaveBeenCalledWith('restart-service');
170
- expect(mocks.waitForRecovery).toHaveBeenCalledTimes(1);
113
+ expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
114
+ 'restart-service'
115
+ );
171
116
  });
172
- expect(toast.success).toHaveBeenCalledWith('Restart scheduled. This page may disconnect for a few seconds.');
173
- expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['runtime-control'] });
117
+ expect(toast.success).toHaveBeenCalledWith(
118
+ 'Restart scheduled. This page may disconnect for a few seconds.'
119
+ );
174
120
  });
175
121
 
176
122
  it('runs the stop service flow after confirmation', async () => {
177
- const queryClient = new QueryClient();
178
123
  const user = userEvent.setup();
179
- const mutateAsync = vi.fn().mockResolvedValue({
124
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
125
+ mocks.runRuntimeControlAction.mockResolvedValue({
180
126
  accepted: true,
181
127
  action: 'stop-service',
182
128
  lifecycle: 'stopping-service',
183
129
  message: 'Stop scheduled. This page will disconnect shortly.',
184
130
  });
185
- mocks.useRuntimeServiceAction.mockReturnValue({
186
- mutateAsync,
187
- isPending: false,
188
- });
189
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
190
131
 
191
- render(<RuntimeControlCard />, {
192
- wrapper: createWrapper(queryClient),
193
- });
132
+ render(<RuntimeControlCard />);
194
133
 
195
134
  await user.click(screen.getByRole('button', { name: '停止服务' }));
196
135
 
197
136
  await waitFor(() => {
198
137
  expect(confirmSpy).toHaveBeenCalledTimes(1);
199
- expect(mutateAsync).toHaveBeenCalledWith('stop-service');
138
+ expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
139
+ 'stop-service'
140
+ );
200
141
  });
201
- expect(mocks.waitForRecovery).not.toHaveBeenCalled();
202
- expect(toast.success).toHaveBeenCalledWith('Stop scheduled. This page will disconnect shortly.');
142
+ expect(toast.success).toHaveBeenCalledWith(
143
+ 'Stop scheduled. This page will disconnect shortly.'
144
+ );
203
145
  });
204
146
 
205
147
  it('runs the desktop restart app flow after confirmation', async () => {
206
- const queryClient = new QueryClient();
207
148
  const user = userEvent.setup();
208
-
209
- mocks.useRuntimeControl.mockReturnValue({
210
- data: {
149
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
150
+ mocks.useRuntimeControlPanelView.mockReturnValue({
151
+ controlView: {
152
+ ...baseControlView,
211
153
  environment: 'desktop-embedded',
212
- lifecycle: 'healthy',
213
- serviceState: 'running',
214
- message: 'runtime healthy',
215
- canStartService: {
216
- available: false,
217
- requiresConfirmation: false,
218
- impact: 'none',
219
- },
220
- canRestartService: {
221
- available: true,
222
- requiresConfirmation: false,
223
- impact: 'brief-ui-disconnect',
224
- },
225
154
  canStopService: {
226
155
  available: false,
227
156
  requiresConfirmation: true,
@@ -232,24 +161,73 @@ describe('RuntimeControlCard', () => {
232
161
  requiresConfirmation: true,
233
162
  impact: 'full-app-relaunch',
234
163
  },
235
- managementHint: 'desktop launcher hint'
164
+ managementHint: 'desktop launcher hint',
236
165
  },
237
- isError: false,
238
- error: null,
166
+ visibleLifecycle: 'healthy',
167
+ visibleServiceState: 'running',
168
+ visibleMessage: 'runtime healthy',
169
+ busyAction: null,
170
+ busy: false,
171
+ pendingRestart: null,
172
+ errorMessage: null,
173
+ });
174
+ mocks.runRuntimeControlAction.mockResolvedValue({
175
+ accepted: true,
176
+ action: 'restart-app',
177
+ lifecycle: 'restarting-app',
178
+ message: 'NextClaw app restart scheduled.',
239
179
  });
240
180
 
241
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
242
-
243
- render(<RuntimeControlCard />, {
244
- wrapper: createWrapper(queryClient),
245
- });
181
+ render(<RuntimeControlCard />);
246
182
 
247
183
  await user.click(screen.getByRole('button', { name: '重启应用' }));
248
184
 
249
185
  await waitFor(() => {
250
186
  expect(confirmSpy).toHaveBeenCalledTimes(1);
251
- expect(mocks.restartApp).toHaveBeenCalledTimes(1);
187
+ expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
188
+ 'restart-app'
189
+ );
252
190
  });
253
- expect(toast.success).toHaveBeenCalledWith('NextClaw app restart scheduled.');
191
+ expect(toast.success).toHaveBeenCalledWith(
192
+ 'NextClaw app restart scheduled.'
193
+ );
194
+ });
195
+
196
+ it('shows a pending restart notice instead of auto-applying hidden restarts', () => {
197
+ mocks.useRuntimeControlPanelView.mockReturnValue({
198
+ controlView: {
199
+ ...baseControlView,
200
+ message: 'Saved changes are waiting for a manual restart.',
201
+ pendingRestart: {
202
+ changedPaths: ['plugins', 'ui'],
203
+ message: 'Saved changes are waiting for a manual restart.',
204
+ reasons: ['config reload requires restart: plugins, ui'],
205
+ requestedAt: '2026-04-17T10:00:00.000Z',
206
+ },
207
+ },
208
+ visibleLifecycle: 'healthy',
209
+ visibleServiceState: 'running',
210
+ visibleMessage: 'Saved changes are waiting for a manual restart.',
211
+ busyAction: null,
212
+ busy: false,
213
+ pendingRestart: {
214
+ changedPaths: ['plugins', 'ui'],
215
+ message: 'Saved changes are waiting for a manual restart.',
216
+ reasons: ['config reload requires restart: plugins, ui'],
217
+ requestedAt: '2026-04-17T10:00:00.000Z',
218
+ },
219
+ errorMessage: null,
220
+ });
221
+
222
+ render(<RuntimeControlCard />);
223
+
224
+ expect(screen.getByText('待重启')).toBeTruthy();
225
+ expect(
226
+ screen.getByText(
227
+ '这次改动已经保存,但系统不会自动重启。请在你方便的时候手动重启,重启完成后该提示会自动清空。'
228
+ )
229
+ ).toBeTruthy();
230
+ expect(screen.getByText('plugins')).toBeTruthy();
231
+ expect(screen.getByText('ui')).toBeTruthy();
254
232
  });
255
233
  });
@@ -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,27 +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
-
163
- const resetLocalState = () => {
164
- setLocalLifecycle(null);
165
- setLocalServiceState(null);
166
- setLocalMessage(null);
167
- setBusyAction(null);
168
- };
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);
169
146
 
170
147
  const handleServiceAction = async (action: Extract<RuntimeControlAction, 'start-service' | 'restart-service' | 'stop-service'>) => {
171
148
  const capability = action === 'start-service'
@@ -182,30 +159,11 @@ export function RuntimeControlCard() {
182
159
  return;
183
160
  }
184
161
 
185
- setBusyAction(action);
186
- setLocalLifecycle(action === 'start-service' ? 'starting-service' : action === 'stop-service' ? 'stopping-service' : 'restarting-service');
187
- setLocalServiceState(action === 'start-service' ? 'starting' : action === 'stop-service' ? 'stopping' : 'restarting');
188
- setLocalMessage(resolveActionHelp(action));
189
-
190
162
  try {
191
- const result = await serviceActionMutation.mutateAsync(action);
163
+ const result = await systemStatusManager.runRuntimeControlAction(action);
192
164
  toast.success(result.message);
193
- if (action === 'stop-service') {
194
- return;
195
- }
196
- setLocalLifecycle('recovering');
197
- setLocalMessage(t('runtimeControlRecoveringHelp'));
198
- const recoveredView = await runtimeControlManager.waitForRecovery();
199
- queryClient.setQueryData(['runtime-control'], recoveredView);
200
- await queryClient.invalidateQueries({ queryKey: ['runtime-control'] });
201
- resetLocalState();
202
- toast.success(t('runtimeControlRecovered'));
203
165
  } catch (error) {
204
166
  const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
205
- setLocalLifecycle('failed');
206
- setLocalServiceState(action === 'stop-service' ? 'running' : 'unknown');
207
- setLocalMessage(message);
208
- setBusyAction(null);
209
167
  toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
210
168
  }
211
169
  };
@@ -219,18 +177,11 @@ export function RuntimeControlCard() {
219
177
  return;
220
178
  }
221
179
 
222
- setBusyAction('restart-app');
223
- setLocalLifecycle('restarting-app');
224
- setLocalMessage(t('runtimeControlRestartingAppHelp'));
225
-
226
180
  try {
227
- const result = await runtimeControlManager.restartApp();
181
+ const result = await systemStatusManager.runRuntimeControlAction('restart-app');
228
182
  toast.success(result.message);
229
183
  } catch (error) {
230
184
  const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
231
- setLocalLifecycle('failed');
232
- setLocalMessage(message);
233
- setBusyAction(null);
234
185
  toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
235
186
  }
236
187
  };
@@ -254,13 +205,37 @@ export function RuntimeControlCard() {
254
205
  {controlView?.managementHint ? (
255
206
  <p className="text-xs text-gray-500">{controlView.managementHint}</p>
256
207
  ) : null}
257
- {runtimeControlQuery.isError && !busy ? (
208
+ {errorMessage && !busy ? (
258
209
  <p className="text-sm text-amber-700">
259
- {runtimeControlQuery.error instanceof Error ? runtimeControlQuery.error.message : t('runtimeControlLoadFailed')}
210
+ {errorMessage}
260
211
  </p>
261
212
  ) : null}
262
213
  </div>
263
214
 
215
+ {pendingRestart ? (
216
+ <div className="rounded-xl border border-amber-200 bg-amber-50 p-4 space-y-3">
217
+ <div className="text-sm font-medium text-amber-900">{t('runtimeControlPendingRestartTitle')}</div>
218
+ <p className="text-sm text-amber-800">{t('runtimeControlPendingRestartDescription')}</p>
219
+ {pendingRestart.changedPaths.length > 0 ? (
220
+ <div className="space-y-2">
221
+ <div className="text-xs font-medium uppercase tracking-[0.08em] text-amber-700">
222
+ {t('runtimeControlPendingRestartPaths')}
223
+ </div>
224
+ <div className="flex flex-wrap gap-2">
225
+ {pendingRestart.changedPaths.map((path: string) => (
226
+ <span
227
+ key={path}
228
+ className="rounded-full border border-amber-200 bg-white px-2.5 py-1 text-xs text-amber-800"
229
+ >
230
+ {path}
231
+ </span>
232
+ ))}
233
+ </div>
234
+ </div>
235
+ ) : null}
236
+ </div>
237
+ ) : null}
238
+
264
239
  <div className="flex flex-col gap-3 md:flex-row md:flex-wrap">
265
240
  {visibleActions.map((item) => {
266
241
  const isBusyAction = busyAction === item.action;
@@ -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 />);