@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
@@ -4,6 +4,7 @@ import { NcpChatInputManager } from '@/components/chat/ncp/ncp-chat-input.manage
4
4
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
5
5
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
6
  import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
7
+ import { useSystemStatusStore } from '@/features/system-status';
7
8
 
8
9
  describe('NcpChatInputManager', () => {
9
10
  beforeEach(() => {
@@ -15,8 +16,23 @@ describe('NcpChatInputManager', () => {
15
16
  attachments: [],
16
17
  selectedSkills: [],
17
18
  selectedSessionType: 'native',
18
- selectedModel: '',
19
+ selectedModel: 'gpt-5',
19
20
  selectedThinkingLevel: null,
21
+ isProviderStateResolved: true,
22
+ modelOptions: [
23
+ {
24
+ value: 'gpt-5',
25
+ modelLabel: 'GPT-5',
26
+ providerLabel: 'OpenAI',
27
+ thinkingCapability: null,
28
+ },
29
+ ],
30
+ },
31
+ });
32
+ useSystemStatusStore.setState({
33
+ state: {
34
+ ...useSystemStatusStore.getState().state,
35
+ lifecyclePhase: 'ready',
20
36
  },
21
37
  });
22
38
  useChatSessionListStore.setState({
@@ -96,4 +112,38 @@ describe('NcpChatInputManager', () => {
96
112
  );
97
113
  expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('draft-root-session');
98
114
  });
115
+
116
+ it('does not send while the runtime is still blocked during startup', async () => {
117
+ useChatInputStore.setState({
118
+ snapshot: {
119
+ ...useChatInputStore.getState().snapshot,
120
+ isProviderStateResolved: false,
121
+ modelOptions: [],
122
+ },
123
+ });
124
+ useSystemStatusStore.setState({
125
+ state: {
126
+ ...useSystemStatusStore.getState().state,
127
+ lifecyclePhase: 'cold-starting',
128
+ },
129
+ });
130
+ const streamActionsManager = {
131
+ sendMessage: vi.fn().mockResolvedValue(undefined),
132
+ stopCurrentRun: vi.fn().mockResolvedValue(undefined),
133
+ } as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
134
+ const sessionListManager = {
135
+ ensureDraftSession: vi.fn(() => 'draft-session'),
136
+ promoteRootDraftSessionRoute: vi.fn(),
137
+ } as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
138
+ const manager = new NcpChatInputManager(
139
+ {} as ConstructorParameters<typeof NcpChatInputManager>[0],
140
+ streamActionsManager,
141
+ sessionListManager,
142
+ );
143
+
144
+ await manager.send();
145
+
146
+ expect(streamActionsManager.sendMessage).not.toHaveBeenCalled();
147
+ expect(sessionListManager.promoteRootDraftSessionRoute).not.toHaveBeenCalled();
148
+ });
99
149
  });
@@ -2,8 +2,8 @@ import { render, screen } 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 { DesktopUpdateConfig } from '@/components/config/desktop-update-config';
5
- import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
6
5
  import { setLanguage } from '@/lib/i18n';
6
+ import { useDesktopUpdateStore } from '@/platforms/desktop';
7
7
 
8
8
  const mocks = vi.hoisted(() => ({
9
9
  start: vi.fn(),
@@ -15,9 +15,15 @@ const mocks = vi.hoisted(() => ({
15
15
  updateChannel: vi.fn()
16
16
  }));
17
17
 
18
- vi.mock('@/desktop/managers/desktop-update.manager', () => ({
19
- desktopUpdateManager: mocks
20
- }));
18
+ vi.mock('@/platforms/desktop', async () => {
19
+ const actual = await vi.importActual<typeof import('@/platforms/desktop')>(
20
+ '@/platforms/desktop'
21
+ );
22
+ return {
23
+ ...actual,
24
+ desktopUpdateManager: mocks,
25
+ };
26
+ });
21
27
 
22
28
  describe('DesktopUpdateConfig', () => {
23
29
  beforeEach(() => {
@@ -5,11 +5,13 @@ import { Label } from '@/components/ui/label';
5
5
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
6
6
  import { Switch } from '@/components/ui/switch';
7
7
  import { PageHeader, PageLayout } from '@/components/layout/page-layout';
8
- import { desktopUpdateManager } from '@/desktop/managers/desktop-update.manager';
9
- import type { DesktopReleaseChannel } from '@/desktop/desktop-update.types';
10
- import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
11
8
  import { formatDateTime, t } from '@/lib/i18n';
12
9
  import { cn } from '@/lib/utils';
10
+ import {
11
+ desktopUpdateManager,
12
+ type DesktopReleaseChannel,
13
+ useDesktopUpdateStore,
14
+ } from '@/platforms/desktop';
13
15
  import { Download, ExternalLink, RefreshCw, RotateCw } from 'lucide-react';
14
16
 
15
17
  function formatVersion(value: string | null): string {
@@ -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,106 +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
- pendingRestart: null,
53
- canStartService: {
54
- available: false,
55
- requiresConfirmation: false,
56
- impact: 'brief-ui-disconnect',
57
- reasonIfUnavailable: '当前页面已经由运行中的本地服务托管。'
58
- },
59
- canRestartService: {
60
- available: true,
61
- requiresConfirmation: false,
62
- impact: 'brief-ui-disconnect',
63
- },
64
- canStopService: {
65
- available: true,
66
- requiresConfirmation: true,
67
- impact: 'brief-ui-disconnect',
68
- },
69
- canRestartApp: {
70
- available: false,
71
- requiresConfirmation: true,
72
- impact: 'full-app-relaunch',
73
- reasonIfUnavailable: 'desktop only',
74
- },
75
- managementHint: 'This page is served by the running local service.'
76
- },
77
- isError: false,
78
- error: null,
79
- });
80
- mocks.useRuntimeServiceAction.mockReturnValue({
81
- mutateAsync: vi.fn().mockResolvedValue({
82
- accepted: true,
83
- action: 'restart-service',
84
- lifecycle: 'restarting-service',
85
- message: 'Restart scheduled. This page may disconnect for a few seconds.',
86
- }),
87
- isPending: false,
88
- });
89
- mocks.waitForRecovery.mockResolvedValue({
90
- environment: 'managed-local-service',
91
- lifecycle: 'healthy',
92
- serviceState: 'running',
93
- message: 'runtime healthy',
64
+ mocks.useRuntimeControlPanelView.mockReturnValue({
65
+ controlView: baseControlView,
66
+ visibleLifecycle: 'healthy',
67
+ visibleServiceState: 'running',
68
+ visibleMessage: 'runtime healthy',
69
+ busyAction: null,
70
+ busy: false,
94
71
  pendingRestart: null,
95
- canStartService: {
96
- available: false,
97
- requiresConfirmation: false,
98
- impact: 'brief-ui-disconnect',
99
- reasonIfUnavailable: '当前页面已经由运行中的本地服务托管。'
100
- },
101
- canRestartService: {
102
- available: true,
103
- requiresConfirmation: false,
104
- impact: 'brief-ui-disconnect',
105
- },
106
- canStopService: {
107
- available: true,
108
- requiresConfirmation: true,
109
- impact: 'brief-ui-disconnect',
110
- },
111
- canRestartApp: {
112
- available: false,
113
- requiresConfirmation: true,
114
- impact: 'full-app-relaunch',
115
- reasonIfUnavailable: 'desktop only',
116
- },
117
- managementHint: 'This page is served by the running local service.'
118
- });
119
- mocks.restartApp.mockResolvedValue({
120
- accepted: true,
121
- action: 'restart-app',
122
- lifecycle: 'restarting-app',
123
- message: 'NextClaw app restart scheduled.',
72
+ errorMessage: null,
124
73
  });
125
74
  });
126
75
 
@@ -129,14 +78,15 @@ describe('RuntimeControlCard', () => {
129
78
  });
130
79
 
131
80
  it('renders service management actions from the current capability view', () => {
132
- const queryClient = new QueryClient();
81
+ render(<RuntimeControlCard />);
133
82
 
134
- render(<RuntimeControlCard />, {
135
- wrapper: createWrapper(queryClient),
136
- });
83
+ const startButton = screen.getByRole('button', {
84
+ name: '启动服务',
85
+ }) as HTMLButtonElement;
86
+ const restartAppButton = screen.getByRole('button', {
87
+ name: '重启应用',
88
+ }) as HTMLButtonElement;
137
89
 
138
- const startButton = screen.getByRole('button', { name: '启动服务' }) as HTMLButtonElement;
139
- const restartAppButton = screen.getByRole('button', { name: '重启应用' }) as HTMLButtonElement;
140
90
  expect(screen.getByText('服务管理')).toBeTruthy();
141
91
  expect(screen.getByText('服务运行中')).toBeTruthy();
142
92
  expect(screen.getByRole('button', { name: '重启服务' })).toBeTruthy();
@@ -146,85 +96,61 @@ describe('RuntimeControlCard', () => {
146
96
  expect(screen.getByText('desktop only')).toBeTruthy();
147
97
  });
148
98
 
149
- it('runs the restart service flow and waits for recovery', async () => {
150
- const queryClient = new QueryClient();
99
+ it('runs the restart service flow through the system status manager', async () => {
151
100
  const user = userEvent.setup();
152
- const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
153
- const mutateAsync = vi.fn().mockResolvedValue({
101
+ mocks.runRuntimeControlAction.mockResolvedValue({
154
102
  accepted: true,
155
103
  action: 'restart-service',
156
104
  lifecycle: 'restarting-service',
157
105
  message: 'Restart scheduled. This page may disconnect for a few seconds.',
158
106
  });
159
- mocks.useRuntimeServiceAction.mockReturnValue({
160
- mutateAsync,
161
- isPending: false,
162
- });
163
107
 
164
- render(<RuntimeControlCard />, {
165
- wrapper: createWrapper(queryClient),
166
- });
108
+ render(<RuntimeControlCard />);
167
109
 
168
110
  await user.click(screen.getByRole('button', { name: '重启服务' }));
169
111
 
170
112
  await waitFor(() => {
171
- expect(mutateAsync).toHaveBeenCalledWith('restart-service');
172
- expect(mocks.waitForRecovery).toHaveBeenCalledTimes(1);
113
+ expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
114
+ 'restart-service'
115
+ );
173
116
  });
174
- expect(toast.success).toHaveBeenCalledWith('Restart scheduled. This page may disconnect for a few seconds.');
175
- expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['runtime-control'] });
117
+ expect(toast.success).toHaveBeenCalledWith(
118
+ 'Restart scheduled. This page may disconnect for a few seconds.'
119
+ );
176
120
  });
177
121
 
178
122
  it('runs the stop service flow after confirmation', async () => {
179
- const queryClient = new QueryClient();
180
123
  const user = userEvent.setup();
181
- const mutateAsync = vi.fn().mockResolvedValue({
124
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
125
+ mocks.runRuntimeControlAction.mockResolvedValue({
182
126
  accepted: true,
183
127
  action: 'stop-service',
184
128
  lifecycle: 'stopping-service',
185
129
  message: 'Stop scheduled. This page will disconnect shortly.',
186
130
  });
187
- mocks.useRuntimeServiceAction.mockReturnValue({
188
- mutateAsync,
189
- isPending: false,
190
- });
191
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
192
131
 
193
- render(<RuntimeControlCard />, {
194
- wrapper: createWrapper(queryClient),
195
- });
132
+ render(<RuntimeControlCard />);
196
133
 
197
134
  await user.click(screen.getByRole('button', { name: '停止服务' }));
198
135
 
199
136
  await waitFor(() => {
200
137
  expect(confirmSpy).toHaveBeenCalledTimes(1);
201
- expect(mutateAsync).toHaveBeenCalledWith('stop-service');
138
+ expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
139
+ 'stop-service'
140
+ );
202
141
  });
203
- expect(mocks.waitForRecovery).not.toHaveBeenCalled();
204
- 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
+ );
205
145
  });
206
146
 
207
147
  it('runs the desktop restart app flow after confirmation', async () => {
208
- const queryClient = new QueryClient();
209
148
  const user = userEvent.setup();
210
-
211
- mocks.useRuntimeControl.mockReturnValue({
212
- data: {
149
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
150
+ mocks.useRuntimeControlPanelView.mockReturnValue({
151
+ controlView: {
152
+ ...baseControlView,
213
153
  environment: 'desktop-embedded',
214
- lifecycle: 'healthy',
215
- serviceState: 'running',
216
- message: 'runtime healthy',
217
- pendingRestart: null,
218
- canStartService: {
219
- available: false,
220
- requiresConfirmation: false,
221
- impact: 'none',
222
- },
223
- canRestartService: {
224
- available: true,
225
- requiresConfirmation: false,
226
- impact: 'brief-ui-disconnect',
227
- },
228
154
  canStopService: {
229
155
  available: false,
230
156
  requiresConfirmation: true,
@@ -235,76 +161,72 @@ describe('RuntimeControlCard', () => {
235
161
  requiresConfirmation: true,
236
162
  impact: 'full-app-relaunch',
237
163
  },
238
- managementHint: 'desktop launcher hint'
164
+ managementHint: 'desktop launcher hint',
239
165
  },
240
- isError: false,
241
- error: null,
166
+ visibleLifecycle: 'healthy',
167
+ visibleServiceState: 'running',
168
+ visibleMessage: 'runtime healthy',
169
+ busyAction: null,
170
+ busy: false,
171
+ pendingRestart: null,
172
+ errorMessage: null,
242
173
  });
243
-
244
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
245
-
246
- render(<RuntimeControlCard />, {
247
- wrapper: createWrapper(queryClient),
174
+ mocks.runRuntimeControlAction.mockResolvedValue({
175
+ accepted: true,
176
+ action: 'restart-app',
177
+ lifecycle: 'restarting-app',
178
+ message: 'NextClaw app restart scheduled.',
248
179
  });
249
180
 
181
+ render(<RuntimeControlCard />);
182
+
250
183
  await user.click(screen.getByRole('button', { name: '重启应用' }));
251
184
 
252
185
  await waitFor(() => {
253
186
  expect(confirmSpy).toHaveBeenCalledTimes(1);
254
- expect(mocks.restartApp).toHaveBeenCalledTimes(1);
187
+ expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
188
+ 'restart-app'
189
+ );
255
190
  });
256
- expect(toast.success).toHaveBeenCalledWith('NextClaw app restart scheduled.');
191
+ expect(toast.success).toHaveBeenCalledWith(
192
+ 'NextClaw app restart scheduled.'
193
+ );
257
194
  });
258
195
 
259
196
  it('shows a pending restart notice instead of auto-applying hidden restarts', () => {
260
- const queryClient = new QueryClient();
261
-
262
- mocks.useRuntimeControl.mockReturnValue({
263
- data: {
264
- environment: 'managed-local-service',
265
- lifecycle: 'healthy',
266
- serviceState: 'running',
197
+ mocks.useRuntimeControlPanelView.mockReturnValue({
198
+ controlView: {
199
+ ...baseControlView,
267
200
  message: 'Saved changes are waiting for a manual restart.',
268
201
  pendingRestart: {
269
202
  changedPaths: ['plugins', 'ui'],
270
203
  message: 'Saved changes are waiting for a manual restart.',
271
204
  reasons: ['config reload requires restart: plugins, ui'],
272
- requestedAt: '2026-04-17T10:00:00.000Z'
205
+ requestedAt: '2026-04-17T10:00:00.000Z',
273
206
  },
274
- canStartService: {
275
- available: false,
276
- requiresConfirmation: false,
277
- impact: 'brief-ui-disconnect',
278
- reasonIfUnavailable: '当前页面已经由运行中的本地服务托管。'
279
- },
280
- canRestartService: {
281
- available: true,
282
- requiresConfirmation: false,
283
- impact: 'brief-ui-disconnect',
284
- },
285
- canStopService: {
286
- available: true,
287
- requiresConfirmation: true,
288
- impact: 'brief-ui-disconnect',
289
- },
290
- canRestartApp: {
291
- available: false,
292
- requiresConfirmation: true,
293
- impact: 'full-app-relaunch',
294
- reasonIfUnavailable: 'desktop only',
295
- },
296
- managementHint: 'This page is served by the running local service.'
297
207
  },
298
- isError: false,
299
- error: null,
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,
300
220
  });
301
221
 
302
- render(<RuntimeControlCard />, {
303
- wrapper: createWrapper(queryClient),
304
- });
222
+ render(<RuntimeControlCard />);
305
223
 
306
224
  expect(screen.getByText('待重启')).toBeTruthy();
307
- expect(screen.getByText('这次改动已经保存,但系统不会自动重启。请在你方便的时候手动重启,重启完成后该提示会自动清空。')).toBeTruthy();
225
+ expect(
226
+ screen.getByText(
227
+ '这次改动已经保存,但系统不会自动重启。请在你方便的时候手动重启,重启完成后该提示会自动清空。'
228
+ )
229
+ ).toBeTruthy();
308
230
  expect(screen.getByText('plugins')).toBeTruthy();
309
231
  expect(screen.getByText('ui')).toBeTruthy();
310
232
  });