@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.
- package/CHANGELOG.md +51 -10
- package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
- package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
- package/dist/assets/{DocBrowser-DMfr0Oow.js → DocBrowser-rDOjI3ga.js} +1 -1
- package/dist/assets/{DocBrowserContext-BXydqby-.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
- package/dist/assets/{LogoBadge-hO7tY7hE.js → LogoBadge-DP8Ye7wJ.js} +1 -1
- package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
- package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
- package/dist/assets/ProvidersList-BCupBayq.js +1 -0
- package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
- package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
- package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
- package/dist/assets/SessionsConfig-CvjxU40H.js +2 -0
- package/dist/assets/{book-open-DzdUViDm.js → book-open-BE8M56IM.js} +1 -1
- package/dist/assets/chat-page-JKC6ln-y.js +58 -0
- package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-C5dEc8hV.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
- package/dist/assets/client-CszWMVKi.js +7 -0
- package/dist/assets/{config-split-page-BUout_Ak.js → config-split-page-BAGSzUR3.js} +1 -1
- package/dist/assets/{createLucideIcon-dy5ie7Ox.js → createLucideIcon-CCiTGX8L.js} +1 -1
- package/dist/assets/desktop-DfkLlkG2.js +1 -0
- package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
- package/dist/assets/dialog-BghZFPch.js +5 -0
- package/dist/assets/{dist-Cy7_j6hA.js → dist-Dd9cr-kz.js} +1 -1
- package/dist/assets/dist-ZwoAXs46.js +9 -0
- package/dist/assets/{download-BD0ETkB-.js → download-D7LOizcW.js} +1 -1
- package/dist/assets/es2015-CEAreese.js +41 -0
- package/dist/assets/{external-link-kZSAO8nT.js → external-link-qsnCMhw1.js} +1 -1
- package/dist/assets/{hash-BHJC2Ovu.js → hash-0zjWsNl-.js} +1 -1
- package/dist/assets/{i18n-CpTZLchQ.js → i18n-DvzXOGQX.js} +1 -1
- package/dist/assets/index-DvVTC9FF.css +1 -0
- package/dist/assets/index-lr6rQUSd.js +2 -0
- package/dist/assets/key-round-BLe9D8ND.js +1 -0
- package/dist/assets/loader-circle-wj7kARHv.js +1 -0
- package/dist/assets/{logos-B7gRObP8.js → logos-_v5b2SdG.js} +1 -1
- package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
- package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
- package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
- package/dist/assets/play-o6NmwGTi.js +1 -0
- package/dist/assets/plus-I9pBS4Fl.js +1 -0
- package/dist/assets/{refresh-cw-Bcv40SXy.js → refresh-cw-MNqgR3LZ.js} +1 -1
- package/dist/assets/remote-C9fXm4V5.js +1 -0
- package/dist/assets/{save-EqJPOF0G.js → save-D4bObrmH.js} +1 -1
- package/dist/assets/search-DxmL3IWE.js +1 -0
- package/dist/assets/security-config-BUm6FFfl.js +1 -0
- package/dist/assets/select-BILPf7zs.js +1 -0
- package/dist/assets/setting-row-BATDgg4r.js +1 -0
- package/dist/assets/skeleton-COKMAnJy.js +1 -0
- package/dist/assets/{switch-CM29eCAR.js → switch-CBOzecWS.js} +1 -1
- package/dist/assets/{tabs-custom-YcZUWn3o.js → tabs-custom-Bx3cNhD-.js} +1 -1
- package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
- package/dist/assets/{trash-2-mJT6oWa2.js → trash-2-CQUgYyRn.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
- package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
- package/dist/assets/{useMutation-CNcz2fgt.js → useMutation-__AYv-Pz.js} +1 -1
- package/dist/assets/x-BHUGQIUv.js +1 -0
- package/dist/index.html +22 -22
- package/module-structure.config.json +7 -0
- package/package.json +5 -5
- package/src/api/config.ts +10 -0
- package/src/api/raw-client.test.ts +1 -1
- package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
- package/src/api/types.ts +40 -0
- package/src/app/components/app-manager-provider.tsx +20 -0
- package/src/app/managers/app.manager.ts +12 -0
- package/src/app.tsx +8 -8
- package/src/components/chat/chat-conversation-panel.test.tsx +10 -0
- package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
- package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
- package/src/components/chat/chat-page-shell.tsx +1 -1
- package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
- package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
- package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +13 -37
- package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
- package/src/components/chat/ncp/ncp-chat-page.tsx +21 -2
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
- package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
- package/src/components/config/desktop-update-config.test.tsx +10 -4
- package/src/components/config/desktop-update-config.tsx +5 -3
- package/src/components/config/runtime-control-card.test.tsx +119 -197
- package/src/components/config/runtime-control-card.tsx +20 -70
- package/src/components/config/runtime-presence-card.test.tsx +10 -14
- package/src/components/config/runtime-presence-card.tsx +7 -5
- package/src/components/layout/Sidebar.tsx +4 -4
- package/src/components/layout/runtime-status-entry.test.tsx +45 -101
- package/src/components/layout/runtime-status-entry.tsx +15 -63
- package/src/components/layout/sidebar.layout.test.tsx +11 -5
- package/src/{account → features/account}/components/account-panel.tsx +13 -13
- package/src/features/account/index.ts +6 -0
- package/src/{account → features/account}/managers/account.manager.ts +3 -3
- package/src/{components/remote → features/remote/components}/remote-access-page.test.tsx +4 -5
- package/src/{components/remote → features/remote/components}/remote-access-page.tsx +15 -13
- package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
- package/src/features/remote/index.ts +27 -0
- package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
- package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
- package/src/features/system-status/hooks/use-system-status.ts +104 -0
- package/src/features/system-status/index.ts +12 -0
- package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
- package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
- package/src/features/system-status/managers/system-status.manager.ts +511 -0
- package/src/features/system-status/stores/system-status.store.ts +32 -0
- package/src/features/system-status/types/system-status.types.ts +73 -0
- package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
- package/src/features/system-status/utils/system-status.utils.ts +202 -0
- package/src/hooks/use-realtime-query-bridge.ts +34 -18
- package/src/lib/i18n.chat.ts +8 -0
- package/src/platforms/desktop/index.ts +20 -0
- package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
- package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
- package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
- package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
- package/src/stores/ui.store.ts +0 -9
- package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
- package/src/transport/app-client.test.ts +9 -5
- package/src/transport/index.ts +1 -1
- package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
- package/dist/assets/ChannelsList-M9FTK1Ak.js +0 -8
- package/dist/assets/DocBrowser-CH7-GxlL.js +0 -1
- package/dist/assets/ModelConfig-CNIgLf0e.js +0 -1
- package/dist/assets/ProviderScopedModelInput-B3HWP4oz.js +0 -1
- package/dist/assets/ProvidersList-CHjMnRhX.js +0 -1
- package/dist/assets/RuntimeConfig-psp8nMSG.js +0 -1
- package/dist/assets/SearchConfig-CSoKip1f.js +0 -1
- package/dist/assets/SecretsConfig-MEt6MjuD.js +0 -3
- package/dist/assets/SessionsConfig-DifCiXwR.js +0 -2
- package/dist/assets/app-query-client-9jNewezV.js +0 -1
- package/dist/assets/chat-page-CLp0UV0Y.js +0 -58
- package/dist/assets/chat-session-display-DsYHx0RZ.js +0 -1
- package/dist/assets/client-C-8fH7-c.js +0 -7
- package/dist/assets/config-CBScxsdV.js +0 -1
- package/dist/assets/desktop-update-config-2BS6BMkW.js +0 -1
- package/dist/assets/dist-BruyLa92.js +0 -9
- package/dist/assets/index-mW8W2FUu.css +0 -1
- package/dist/assets/index-zDZfXoI4.js +0 -6
- package/dist/assets/infiniteQueryBehavior-CyER9hv0.js +0 -1
- package/dist/assets/loader-circle-Bc2gCU33.js +0 -1
- package/dist/assets/marketplace-page-3qVMnF3d.js +0 -1
- package/dist/assets/marketplace-page-BhFIeQzI.js +0 -49
- package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +0 -40
- package/dist/assets/page-layout-0UcO9H9Z.js +0 -1
- package/dist/assets/play-CKDjSQFL.js +0 -1
- package/dist/assets/plus-CG0QrVY_.js +0 -1
- package/dist/assets/refresh-ccw-COVhNHtN.js +0 -1
- package/dist/assets/remote-access-page-CWHG-sug.js +0 -1
- package/dist/assets/rotate-cw-oHMKJMC8.js +0 -1
- package/dist/assets/search-BCAlB8nz.js +0 -1
- package/dist/assets/security-config-Slh0Mayz.js +0 -1
- package/dist/assets/select-CVz0t7MF.js +0 -41
- package/dist/assets/setting-row-CbVHAuQt.js +0 -1
- package/dist/assets/skeleton-D5rdKvzy.js +0 -1
- package/dist/assets/status-dot-DpPtVzQT.js +0 -1
- package/dist/assets/tag-chip-DMXdnLcj.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-DJ1L81Dz.js +0 -1
- package/dist/assets/useConfirmDialog-BsVuqu1x.js +0 -1
- package/dist/assets/x-Czwxm82I.js +0 -1
- package/src/hooks/use-runtime-control.ts +0 -24
- package/src/presenter/app-presenter-context.tsx +0 -20
- package/src/presenter/app.presenter.ts +0 -12
- package/src/runtime-control/runtime-control.manager.ts +0 -118
- /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
- /package/src/{account → features/account}/stores/account.store.ts +0 -0
- /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
- /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
- /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
- /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('@/
|
|
19
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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('@/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
render(<RuntimeControlCard />);
|
|
133
82
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
172
|
-
|
|
113
|
+
expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
|
|
114
|
+
'restart-service'
|
|
115
|
+
);
|
|
173
116
|
});
|
|
174
|
-
expect(toast.success).toHaveBeenCalledWith(
|
|
175
|
-
|
|
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
|
|
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(
|
|
138
|
+
expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
|
|
139
|
+
'stop-service'
|
|
140
|
+
);
|
|
202
141
|
});
|
|
203
|
-
expect(
|
|
204
|
-
|
|
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.
|
|
212
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
187
|
+
expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith(
|
|
188
|
+
'restart-app'
|
|
189
|
+
);
|
|
255
190
|
});
|
|
256
|
-
expect(toast.success).toHaveBeenCalledWith(
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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(
|
|
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
|
});
|