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