@nextclaw/ui 0.12.6 → 0.12.8
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 +90 -0
- package/dist/assets/{ChannelsList-D8p4OlM6.js → ChannelsList-KIQIxluX.js} +1 -1
- package/dist/assets/{DocBrowser-Cse_F8Nn.js → DocBrowser-BMxf9CIK.js} +1 -1
- package/dist/assets/DocBrowser-CyDgAtO9.js +1 -0
- package/dist/assets/{DocBrowserContext-Bai1WU2H.js → DocBrowserContext-Ce28gRXt.js} +1 -1
- package/dist/assets/{LogoBadge-BdxMPc9v.js → LogoBadge-o92MOA2L.js} +1 -1
- package/dist/assets/{MarketplacePage-BbpAkllU.js → MarketplacePage-BySqkYDh.js} +1 -1
- package/dist/assets/MarketplacePage-C0olZaek.js +1 -0
- package/dist/assets/{McpMarketplacePage-CxPFOgxv.js → McpMarketplacePage-DqKaiXO9.js} +1 -1
- package/dist/assets/{ModelConfig-3GLqQ5GY.js → ModelConfig-IrmzoslW.js} +1 -1
- package/dist/assets/{ProviderScopedModelInput-BYNouw-i.js → ProviderScopedModelInput-CmTIzgI7.js} +1 -1
- package/dist/assets/{ProvidersList-BR1gJ4Dm.js → ProvidersList-8_Kalfwl.js} +1 -1
- package/dist/assets/{RemoteAccessPage-DyYVWsyK.js → RemoteAccessPage-CyQlSjPf.js} +1 -1
- package/dist/assets/RuntimeConfig-Bk0uYBhf.js +1 -0
- package/dist/assets/{SearchConfig-DTeJvp8m.js → SearchConfig-DNBR-UbE.js} +1 -1
- package/dist/assets/{SecretsConfig-CCYO6NcV.js → SecretsConfig-Ba1RPJaG.js} +1 -1
- package/dist/assets/{SessionsConfig-Du39vDgt.js → SessionsConfig-Doqp5ghH.js} +1 -1
- package/dist/assets/{app-query-client-Dr5d-K8d.js → app-query-client-DniXoIN5.js} +1 -1
- package/dist/assets/{book-open-Da4OEPqB.js → book-open-DocgeQtR.js} +1 -1
- package/dist/assets/chat-page-Bph8M5zo.js +58 -0
- package/dist/assets/chat-session-display-CoN3Wmn-.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-CoFVxHXV.js → chunk-JZWAC4HX-BvKvh1R8.js} +1 -1
- package/dist/assets/{client-CSk58DcF.js → client-CVqPF5ie.js} +1 -1
- package/dist/assets/{config-D8KzikVB.js → config-Bop2oB18.js} +1 -1
- package/dist/assets/{createLucideIcon-83gaZMtv.js → createLucideIcon-DVv8taGY.js} +1 -1
- package/dist/assets/desktop-update-config-1KBrqLBC.js +1 -0
- package/dist/assets/{dist-toEYs-MZ.js → dist-Da5Gm_pO.js} +1 -1
- package/dist/assets/{dist-aTmhMDVh.js → dist-DmAlInRu.js} +1 -1
- package/dist/assets/{external-link-QQ0TC6X4.js → external-link-DFjw3x1B.js} +1 -1
- package/dist/assets/{hash-DaFBEkmi.js → hash-DJtaCejM.js} +1 -1
- package/dist/assets/i18n-CwHZ-9vt.js +1 -0
- package/dist/assets/{index-CE4N7ItL.css → index-DafCdM4F.css} +1 -1
- package/dist/assets/{index-riX7Sg0_.js → index-DdksE6U3.js} +3 -3
- package/dist/assets/{infiniteQueryBehavior-BmHX_ayZ.js → infiniteQueryBehavior-DHSEQ3OH.js} +1 -1
- package/dist/assets/loader-circle-PsSP0H9n.js +1 -0
- package/dist/assets/{logos-Dzlz30M3.js → logos-DEFUIR12.js} +1 -1
- package/dist/assets/{page-layout-D2eRufRQ.js → page-layout-Da3i3r6G.js} +1 -1
- package/dist/assets/play-DBQbBxTA.js +1 -0
- package/dist/assets/plus-DUOVbsyQ.js +1 -0
- package/dist/assets/{popover-BSXxm5bj.js → popover-C_mWOFzI.js} +1 -1
- package/dist/assets/{refresh-ccw-B3zMtN-_.js → refresh-ccw-D6HkNtfz.js} +1 -1
- package/dist/assets/{refresh-cw-DlZkIHnJ.js → refresh-cw-DRcvRrnc.js} +1 -1
- package/dist/assets/rotate-cw-BmDKfXtH.js +1 -0
- package/dist/assets/{save-Us9fg4Sj.js → save-DHGmi2e9.js} +1 -1
- package/dist/assets/search-MChQRYR1.js +1 -0
- package/dist/assets/{security-config-BGWYwxNr.js → security-config-CbXfPZzr.js} +1 -1
- package/dist/assets/{select-DLYqySQK.js → select-Caud8QvU.js} +1 -1
- package/dist/assets/skeleton-B-4vRq_Z.js +1 -0
- package/dist/assets/{status-dot-DGayudyB.js → status-dot-DurKKSwA.js} +1 -1
- package/dist/assets/{switch-Dz2ScsKx.js → switch-0rmPBRKI.js} +1 -1
- package/dist/assets/{tabs-custom-CdKyjiGk.js → tabs-custom-5JLVL6v8.js} +1 -1
- package/dist/assets/{trash-2-Db-mZOZs.js → trash-2-C6caKPoz.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-DBJX5hj0.js → use-infinite-scroll-loader-dwnaa_qi.js} +1 -1
- package/dist/assets/{useConfirmDialog-DL0a-oGC.js → useConfirmDialog-mMeWD_yo.js} +1 -1
- package/dist/assets/{useMutation-BdZm-9PL.js → useMutation-BmxxvCNf.js} +1 -1
- package/dist/assets/x-DuMhMATD.js +1 -0
- package/dist/index.html +20 -20
- package/package.json +6 -6
- package/src/api/runtime-control.ts +34 -0
- package/src/api/runtime-control.types.ts +58 -0
- package/src/api/types.ts +13 -0
- package/src/{App.test.tsx → app.test.tsx} +1 -1
- package/src/{App.tsx → app.tsx} +1 -1
- package/src/components/chat/ChatConversationPanel.test.tsx +78 -16
- package/src/components/chat/ChatSidebar.test.tsx +36 -7
- package/src/components/chat/ChatSidebar.tsx +19 -26
- package/src/components/chat/chat-child-session-panel.tsx +16 -8
- package/src/components/chat/chat-page-runtime.test.ts +1 -1
- package/src/components/chat/{ChatPage.tsx → chat-page.tsx} +1 -1
- package/src/components/chat/managers/chat-session-list.manager.test.ts +82 -31
- package/src/components/chat/managers/chat-session-list.manager.ts +79 -14
- package/src/components/chat/managers/chat-ui.manager.ts +2 -0
- package/src/components/chat/ncp/README.md +1 -1
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +7 -1
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +1 -1
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +5 -1
- package/src/components/chat/ncp/ncp-session-adapter.ts +12 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +4 -0
- package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +99 -0
- package/src/components/chat/stores/chat-session-list.store.ts +25 -54
- package/src/components/common/ProviderScopedModelInput.tsx +12 -2
- package/src/components/config/ModelConfig.test.tsx +108 -2
- package/src/components/config/RuntimeConfig.tsx +14 -6
- package/src/components/config/desktop-update-config.test.tsx +85 -0
- package/src/components/config/desktop-update-config.tsx +44 -3
- package/src/components/config/runtime-control-card.test.tsx +255 -0
- package/src/components/config/runtime-control-card.tsx +301 -0
- package/src/components/config/runtime-presence-card.test.tsx +154 -0
- package/src/components/config/runtime-presence-card.tsx +163 -0
- package/src/desktop/desktop-update.types.ts +25 -0
- package/src/desktop/managers/desktop-presence.manager.ts +91 -0
- package/src/desktop/managers/desktop-update.manager.ts +37 -1
- package/src/desktop/stores/desktop-presence.store.ts +18 -0
- package/src/desktop/stores/desktop-update.store.ts +7 -1
- package/src/hooks/use-runtime-control.ts +24 -0
- package/src/lib/desktop-update-labels.utils.ts +28 -2
- package/src/lib/i18n.runtime-control.ts +120 -0
- package/src/lib/i18n.ts +2 -4
- package/src/main.tsx +1 -1
- package/src/runtime-control/runtime-control.manager.ts +118 -0
- package/dist/assets/ChatPage-A45t1Rmf.js +0 -58
- package/dist/assets/DocBrowser-B2MpsnU9.js +0 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +0 -1
- package/dist/assets/RuntimeConfig-ChdfK4Y_.js +0 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +0 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +0 -1
- package/dist/assets/i18n-C3jb83S6.js +0 -1
- package/dist/assets/loader-circle-BjMg63eu.js +0 -1
- package/dist/assets/plus-CIXME2pD.js +0 -1
- package/dist/assets/search-B_Qr0f6C.js +0 -1
- package/dist/assets/skeleton-CYQJazv6.js +0 -1
- package/dist/assets/x-B8Tho_xC.js +0 -1
- /package/dist/assets/{config-hints-GSUMvmSo.js → config-hints-BZoDjXye.js} +0 -0
- /package/dist/assets/{config-layout-CgBMG7OL.js → config-layout-DmlGaay2.js} +0 -0
- /package/src/components/chat/ncp/{NcpChatPage.tsx → ncp-chat-page.tsx} +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { RuntimePresenceCard } from '@/components/config/runtime-presence-card';
|
|
5
|
+
import { useDesktopPresenceStore } from '@/desktop/stores/desktop-presence.store';
|
|
6
|
+
import { setLanguage } from '@/lib/i18n';
|
|
7
|
+
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
useRuntimeControl: vi.fn(),
|
|
10
|
+
toastSuccess: vi.fn(),
|
|
11
|
+
toastError: vi.fn()
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('@/hooks/use-runtime-control', () => ({
|
|
15
|
+
useRuntimeControl: (...args: unknown[]) => mocks.useRuntimeControl(...args)
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('sonner', () => ({
|
|
19
|
+
toast: {
|
|
20
|
+
success: (...args: unknown[]) => mocks.toastSuccess(...args),
|
|
21
|
+
error: (...args: unknown[]) => mocks.toastError(...args)
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe('RuntimePresenceCard', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
setLanguage('zh');
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
useDesktopPresenceStore.setState({
|
|
30
|
+
supported: false,
|
|
31
|
+
initialized: false,
|
|
32
|
+
busyAction: null,
|
|
33
|
+
snapshot: null
|
|
34
|
+
});
|
|
35
|
+
mocks.useRuntimeControl.mockReturnValue({
|
|
36
|
+
data: {
|
|
37
|
+
environment: 'managed-local-service',
|
|
38
|
+
lifecycle: 'healthy',
|
|
39
|
+
serviceState: 'running',
|
|
40
|
+
canStartService: {
|
|
41
|
+
available: false,
|
|
42
|
+
requiresConfirmation: false,
|
|
43
|
+
impact: 'brief-ui-disconnect',
|
|
44
|
+
reasonIfUnavailable: 'running local service'
|
|
45
|
+
},
|
|
46
|
+
message: 'runtime healthy',
|
|
47
|
+
canRestartService: {
|
|
48
|
+
available: true,
|
|
49
|
+
requiresConfirmation: false,
|
|
50
|
+
impact: 'brief-ui-disconnect'
|
|
51
|
+
},
|
|
52
|
+
canStopService: {
|
|
53
|
+
available: true,
|
|
54
|
+
requiresConfirmation: true,
|
|
55
|
+
impact: 'brief-ui-disconnect'
|
|
56
|
+
},
|
|
57
|
+
canRestartApp: {
|
|
58
|
+
available: false,
|
|
59
|
+
requiresConfirmation: true,
|
|
60
|
+
impact: 'full-app-relaunch',
|
|
61
|
+
reasonIfUnavailable: 'desktop only'
|
|
62
|
+
},
|
|
63
|
+
managementHint: 'managed service hint'
|
|
64
|
+
},
|
|
65
|
+
isError: false,
|
|
66
|
+
error: null
|
|
67
|
+
});
|
|
68
|
+
window.nextclawDesktop = undefined;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('explains that closing the browser does not stop the managed local service', () => {
|
|
72
|
+
render(<RuntimePresenceCard />);
|
|
73
|
+
|
|
74
|
+
expect(screen.getByText('浏览器只是本地服务控制面')).toBeTruthy();
|
|
75
|
+
expect(screen.getByText('关闭浏览器标签页不会停止本地 NextClaw 服务。服务生命周期由本地受管服务负责,而不是由页面生命周期决定。')).toBeTruthy();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('loads desktop presence settings and updates close-to-background preference', async () => {
|
|
79
|
+
const user = userEvent.setup();
|
|
80
|
+
const getPresenceState = vi.fn().mockResolvedValue({
|
|
81
|
+
closeToBackground: true,
|
|
82
|
+
launchAtLogin: false,
|
|
83
|
+
supportsLaunchAtLogin: true,
|
|
84
|
+
launchAtLoginReason: null
|
|
85
|
+
});
|
|
86
|
+
const updatePresencePreferences = vi.fn().mockResolvedValue({
|
|
87
|
+
closeToBackground: false,
|
|
88
|
+
launchAtLogin: false,
|
|
89
|
+
supportsLaunchAtLogin: true,
|
|
90
|
+
launchAtLoginReason: null
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
window.nextclawDesktop = {
|
|
94
|
+
platform: 'darwin',
|
|
95
|
+
version: '32.2.1',
|
|
96
|
+
getUpdateState: vi.fn(),
|
|
97
|
+
checkForUpdates: vi.fn(),
|
|
98
|
+
downloadUpdate: vi.fn(),
|
|
99
|
+
applyDownloadedUpdate: vi.fn(),
|
|
100
|
+
updatePreferences: vi.fn(),
|
|
101
|
+
updateChannel: vi.fn(),
|
|
102
|
+
restartService: vi.fn(),
|
|
103
|
+
restartApp: vi.fn(),
|
|
104
|
+
getPresenceState,
|
|
105
|
+
updatePresencePreferences,
|
|
106
|
+
onUpdateStateChanged: vi.fn(() => () => {})
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
mocks.useRuntimeControl.mockReturnValue({
|
|
110
|
+
data: {
|
|
111
|
+
environment: 'desktop-embedded',
|
|
112
|
+
lifecycle: 'healthy',
|
|
113
|
+
serviceState: 'running',
|
|
114
|
+
canStartService: {
|
|
115
|
+
available: false,
|
|
116
|
+
requiresConfirmation: false,
|
|
117
|
+
impact: 'none'
|
|
118
|
+
},
|
|
119
|
+
message: 'runtime healthy',
|
|
120
|
+
canRestartService: {
|
|
121
|
+
available: true,
|
|
122
|
+
requiresConfirmation: false,
|
|
123
|
+
impact: 'brief-ui-disconnect'
|
|
124
|
+
},
|
|
125
|
+
canStopService: {
|
|
126
|
+
available: false,
|
|
127
|
+
requiresConfirmation: true,
|
|
128
|
+
impact: 'brief-ui-disconnect'
|
|
129
|
+
},
|
|
130
|
+
canRestartApp: {
|
|
131
|
+
available: true,
|
|
132
|
+
requiresConfirmation: true,
|
|
133
|
+
impact: 'full-app-relaunch'
|
|
134
|
+
},
|
|
135
|
+
managementHint: 'desktop hint'
|
|
136
|
+
},
|
|
137
|
+
isError: false,
|
|
138
|
+
error: null
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
render(<RuntimePresenceCard />);
|
|
142
|
+
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(getPresenceState).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(screen.getByText('关闭窗口时隐藏到后台')).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await user.click(screen.getByRole('switch', { name: '关闭窗口时继续在后台运行' }));
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(updatePresencePreferences).toHaveBeenCalledWith({ closeToBackground: false });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
3
|
+
import { Label } from '@/components/ui/label';
|
|
4
|
+
import { Switch } from '@/components/ui/switch';
|
|
5
|
+
import { desktopPresenceManager } from '@/desktop/managers/desktop-presence.manager';
|
|
6
|
+
import { useDesktopPresenceStore } from '@/desktop/stores/desktop-presence.store';
|
|
7
|
+
import { useRuntimeControl } from '@/hooks/use-runtime-control';
|
|
8
|
+
import { t } from '@/lib/i18n';
|
|
9
|
+
|
|
10
|
+
function PresenceHint(props: { title: string; description: string }) {
|
|
11
|
+
const { description, title } = props;
|
|
12
|
+
return (
|
|
13
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
|
|
14
|
+
<p className="text-sm font-medium text-gray-900">{title}</p>
|
|
15
|
+
<p className="mt-2 text-sm leading-6 text-gray-600">{description}</p>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function RuntimePresenceCard() {
|
|
21
|
+
const runtimeControlQuery = useRuntimeControl();
|
|
22
|
+
const environment = runtimeControlQuery.data?.environment;
|
|
23
|
+
const supported = useDesktopPresenceStore((state) => state.supported);
|
|
24
|
+
const initialized = useDesktopPresenceStore((state) => state.initialized);
|
|
25
|
+
const busyAction = useDesktopPresenceStore((state) => state.busyAction);
|
|
26
|
+
const snapshot = useDesktopPresenceStore((state) => state.snapshot);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (environment === 'desktop-embedded') {
|
|
30
|
+
void desktopPresenceManager.start();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
desktopPresenceManager.markUnsupported();
|
|
34
|
+
}, [environment]);
|
|
35
|
+
|
|
36
|
+
if (environment === 'desktop-embedded') {
|
|
37
|
+
return (
|
|
38
|
+
<Card>
|
|
39
|
+
<CardHeader>
|
|
40
|
+
<CardTitle>{t('runtimePresenceTitle')}</CardTitle>
|
|
41
|
+
<CardDescription>{t('runtimePresenceDescription')}</CardDescription>
|
|
42
|
+
</CardHeader>
|
|
43
|
+
<CardContent className="space-y-4">
|
|
44
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
|
|
45
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">
|
|
46
|
+
{t('runtimePresenceBehaviorLabel')}
|
|
47
|
+
</p>
|
|
48
|
+
<p className="mt-2 text-sm font-medium text-gray-900">
|
|
49
|
+
{snapshot?.closeToBackground ? t('runtimePresenceBehaviorBackground') : t('runtimePresenceBehaviorQuit')}
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{!initialized || (supported && !snapshot) ? (
|
|
54
|
+
<p className="text-sm text-gray-500">{t('runtimePresenceLoading')}</p>
|
|
55
|
+
) : null}
|
|
56
|
+
|
|
57
|
+
{snapshot ? (
|
|
58
|
+
<div className="space-y-4">
|
|
59
|
+
<div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
|
|
60
|
+
<div className="space-y-2">
|
|
61
|
+
<Label htmlFor="runtime-presence-close-background">{t('runtimePresenceCloseToBackground')}</Label>
|
|
62
|
+
<p className="text-sm text-gray-500">{t('runtimePresenceCloseToBackgroundHelp')}</p>
|
|
63
|
+
</div>
|
|
64
|
+
<Switch
|
|
65
|
+
id="runtime-presence-close-background"
|
|
66
|
+
aria-label={t('runtimePresenceCloseToBackground')}
|
|
67
|
+
checked={snapshot.closeToBackground}
|
|
68
|
+
disabled={busyAction === 'saving-preferences'}
|
|
69
|
+
onCheckedChange={(checked) => {
|
|
70
|
+
void desktopPresenceManager.updatePreferences({ closeToBackground: checked });
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
<Label htmlFor="runtime-presence-launch-login">{t('runtimePresenceLaunchAtLogin')}</Label>
|
|
78
|
+
<p className="text-sm text-gray-500">
|
|
79
|
+
{snapshot.supportsLaunchAtLogin
|
|
80
|
+
? t('runtimePresenceLaunchAtLoginHelp')
|
|
81
|
+
: snapshot.launchAtLoginReason ?? t('runtimePresenceLaunchAtLoginUnavailable')}
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
<Switch
|
|
85
|
+
id="runtime-presence-launch-login"
|
|
86
|
+
aria-label={t('runtimePresenceLaunchAtLogin')}
|
|
87
|
+
checked={snapshot.launchAtLogin}
|
|
88
|
+
disabled={!snapshot.supportsLaunchAtLogin || busyAction === 'saving-preferences'}
|
|
89
|
+
onCheckedChange={(checked) => {
|
|
90
|
+
void desktopPresenceManager.updatePreferences({ launchAtLogin: checked });
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
) : null}
|
|
96
|
+
</CardContent>
|
|
97
|
+
</Card>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (environment === 'managed-local-service') {
|
|
102
|
+
return (
|
|
103
|
+
<Card>
|
|
104
|
+
<CardHeader>
|
|
105
|
+
<CardTitle>{t('runtimePresenceTitle')}</CardTitle>
|
|
106
|
+
<CardDescription>{t('runtimePresenceDescription')}</CardDescription>
|
|
107
|
+
</CardHeader>
|
|
108
|
+
<CardContent>
|
|
109
|
+
<PresenceHint
|
|
110
|
+
title={t('runtimePresenceManagedLocalTitle')}
|
|
111
|
+
description={t('runtimePresenceManagedLocalDescription')}
|
|
112
|
+
/>
|
|
113
|
+
</CardContent>
|
|
114
|
+
</Card>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (environment === 'self-hosted-web') {
|
|
119
|
+
return (
|
|
120
|
+
<Card>
|
|
121
|
+
<CardHeader>
|
|
122
|
+
<CardTitle>{t('runtimePresenceTitle')}</CardTitle>
|
|
123
|
+
<CardDescription>{t('runtimePresenceDescription')}</CardDescription>
|
|
124
|
+
</CardHeader>
|
|
125
|
+
<CardContent>
|
|
126
|
+
<PresenceHint
|
|
127
|
+
title={t('runtimePresenceSelfHostedTitle')}
|
|
128
|
+
description={t('runtimePresenceSelfHostedDescription')}
|
|
129
|
+
/>
|
|
130
|
+
</CardContent>
|
|
131
|
+
</Card>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (environment === 'shared-web') {
|
|
136
|
+
return (
|
|
137
|
+
<Card>
|
|
138
|
+
<CardHeader>
|
|
139
|
+
<CardTitle>{t('runtimePresenceTitle')}</CardTitle>
|
|
140
|
+
<CardDescription>{t('runtimePresenceDescription')}</CardDescription>
|
|
141
|
+
</CardHeader>
|
|
142
|
+
<CardContent>
|
|
143
|
+
<PresenceHint
|
|
144
|
+
title={t('runtimePresenceSharedTitle')}
|
|
145
|
+
description={t('runtimePresenceSharedDescription')}
|
|
146
|
+
/>
|
|
147
|
+
</CardContent>
|
|
148
|
+
</Card>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Card>
|
|
154
|
+
<CardHeader>
|
|
155
|
+
<CardTitle>{t('runtimePresenceTitle')}</CardTitle>
|
|
156
|
+
<CardDescription>{t('runtimePresenceDescription')}</CardDescription>
|
|
157
|
+
</CardHeader>
|
|
158
|
+
<CardContent>
|
|
159
|
+
<p className="text-sm text-gray-500">{t('runtimePresenceLoading')}</p>
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -7,6 +7,8 @@ export type DesktopUpdateStatus =
|
|
|
7
7
|
| 'up-to-date'
|
|
8
8
|
| 'failed';
|
|
9
9
|
|
|
10
|
+
export type DesktopReleaseChannel = 'stable' | 'beta';
|
|
11
|
+
|
|
10
12
|
export type DesktopUpdatePreferences = {
|
|
11
13
|
automaticChecks: boolean;
|
|
12
14
|
autoDownload: boolean;
|
|
@@ -14,6 +16,7 @@ export type DesktopUpdatePreferences = {
|
|
|
14
16
|
|
|
15
17
|
export type DesktopUpdateSnapshot = {
|
|
16
18
|
status: DesktopUpdateStatus;
|
|
19
|
+
channel: DesktopReleaseChannel;
|
|
17
20
|
launcherVersion: string;
|
|
18
21
|
currentVersion: string | null;
|
|
19
22
|
availableVersion: string | null;
|
|
@@ -24,6 +27,23 @@ export type DesktopUpdateSnapshot = {
|
|
|
24
27
|
preferences: DesktopUpdatePreferences;
|
|
25
28
|
};
|
|
26
29
|
|
|
30
|
+
export type DesktopRuntimeControlResult = {
|
|
31
|
+
accepted: boolean;
|
|
32
|
+
action: 'restart-service' | 'restart-app';
|
|
33
|
+
lifecycle: 'restarting-service' | 'restarting-app';
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type DesktopPresencePreferences = {
|
|
38
|
+
closeToBackground: boolean;
|
|
39
|
+
launchAtLogin: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type DesktopPresenceSnapshot = DesktopPresencePreferences & {
|
|
43
|
+
supportsLaunchAtLogin: boolean;
|
|
44
|
+
launchAtLoginReason: string | null;
|
|
45
|
+
};
|
|
46
|
+
|
|
27
47
|
export type NextClawDesktopBridge = {
|
|
28
48
|
platform: string;
|
|
29
49
|
version: string;
|
|
@@ -32,5 +52,10 @@ export type NextClawDesktopBridge = {
|
|
|
32
52
|
downloadUpdate: () => Promise<DesktopUpdateSnapshot>;
|
|
33
53
|
applyDownloadedUpdate: () => Promise<DesktopUpdateSnapshot>;
|
|
34
54
|
updatePreferences: (preferences: Partial<DesktopUpdatePreferences>) => Promise<DesktopUpdateSnapshot>;
|
|
55
|
+
updateChannel: (channel: DesktopReleaseChannel) => Promise<DesktopUpdateSnapshot>;
|
|
56
|
+
restartService: () => Promise<DesktopRuntimeControlResult>;
|
|
57
|
+
restartApp: () => Promise<DesktopRuntimeControlResult>;
|
|
58
|
+
getPresenceState: () => Promise<DesktopPresenceSnapshot>;
|
|
59
|
+
updatePresencePreferences: (preferences: Partial<DesktopPresencePreferences>) => Promise<DesktopPresenceSnapshot>;
|
|
35
60
|
onUpdateStateChanged: (listener: (snapshot: DesktopUpdateSnapshot) => void) => () => void;
|
|
36
61
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DesktopPresencePreferences,
|
|
3
|
+
NextClawDesktopBridge
|
|
4
|
+
} from '@/desktop/desktop-update.types';
|
|
5
|
+
import { useDesktopPresenceStore } from '@/desktop/stores/desktop-presence.store';
|
|
6
|
+
import { t } from '@/lib/i18n';
|
|
7
|
+
import { toast } from 'sonner';
|
|
8
|
+
|
|
9
|
+
export class DesktopPresenceManager {
|
|
10
|
+
start = async () => {
|
|
11
|
+
const desktopApi = this.getDesktopApi();
|
|
12
|
+
if (!desktopApi) {
|
|
13
|
+
this.markUnsupported();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
useDesktopPresenceStore.setState({
|
|
18
|
+
supported: true,
|
|
19
|
+
initialized: false,
|
|
20
|
+
busyAction: 'loading'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const snapshot = await desktopApi.getPresenceState();
|
|
25
|
+
useDesktopPresenceStore.setState({
|
|
26
|
+
supported: true,
|
|
27
|
+
initialized: true,
|
|
28
|
+
busyAction: null,
|
|
29
|
+
snapshot
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
useDesktopPresenceStore.setState({
|
|
33
|
+
supported: true,
|
|
34
|
+
initialized: true,
|
|
35
|
+
busyAction: null
|
|
36
|
+
});
|
|
37
|
+
toast.error(`${t('runtimePresenceLoadFailed')}: ${this.getErrorMessage(error)}`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
markUnsupported = () => {
|
|
42
|
+
useDesktopPresenceStore.setState({
|
|
43
|
+
supported: false,
|
|
44
|
+
initialized: true,
|
|
45
|
+
busyAction: null,
|
|
46
|
+
snapshot: null
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
updatePreferences = async (preferences: Partial<DesktopPresencePreferences>) => {
|
|
51
|
+
const desktopApi = this.getDesktopApi();
|
|
52
|
+
if (!desktopApi) {
|
|
53
|
+
throw new Error(t('runtimePresenceLaunchAtLoginUnavailable'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
useDesktopPresenceStore.setState({
|
|
57
|
+
busyAction: 'saving-preferences'
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const snapshot = await desktopApi.updatePresencePreferences(preferences);
|
|
62
|
+
useDesktopPresenceStore.setState({
|
|
63
|
+
supported: true,
|
|
64
|
+
initialized: true,
|
|
65
|
+
snapshot
|
|
66
|
+
});
|
|
67
|
+
toast.success(t('runtimePresenceSaved'));
|
|
68
|
+
return snapshot;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
toast.error(`${t('runtimePresenceSaveFailed')}: ${this.getErrorMessage(error)}`);
|
|
71
|
+
throw error;
|
|
72
|
+
} finally {
|
|
73
|
+
useDesktopPresenceStore.setState({
|
|
74
|
+
busyAction: null
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
private getDesktopApi = (): NextClawDesktopBridge | null => {
|
|
80
|
+
if (typeof window === 'undefined') {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return window.nextclawDesktop ?? null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
private getErrorMessage = (error: unknown): string => {
|
|
87
|
+
return error instanceof Error ? error.message : t('error');
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const desktopPresenceManager = new DesktopPresenceManager();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
DesktopReleaseChannel,
|
|
2
3
|
DesktopUpdatePreferences,
|
|
3
4
|
DesktopUpdateSnapshot,
|
|
4
5
|
NextClawDesktopBridge
|
|
@@ -7,7 +8,7 @@ import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
|
|
|
7
8
|
import { t } from '@/lib/i18n';
|
|
8
9
|
import { toast } from 'sonner';
|
|
9
10
|
|
|
10
|
-
type DesktopUpdateBusyAction = 'checking' | 'downloading' | 'applying' | 'saving-preferences';
|
|
11
|
+
type DesktopUpdateBusyAction = 'checking' | 'downloading' | 'applying' | 'saving-preferences' | 'switching-channel';
|
|
11
12
|
|
|
12
13
|
export class DesktopUpdateManager {
|
|
13
14
|
private unsubscribe: (() => void) | null = null;
|
|
@@ -125,6 +126,37 @@ export class DesktopUpdateManager {
|
|
|
125
126
|
}
|
|
126
127
|
};
|
|
127
128
|
|
|
129
|
+
updateChannel = async (channel: DesktopReleaseChannel) => {
|
|
130
|
+
const currentChannel = useDesktopUpdateStore.getState().snapshot?.channel;
|
|
131
|
+
if (currentChannel === channel) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let snapshot: DesktopUpdateSnapshot;
|
|
136
|
+
try {
|
|
137
|
+
snapshot = await this.runSnapshotCommand(
|
|
138
|
+
'switching-channel',
|
|
139
|
+
t('desktopUpdatesChannelChangeFailed'),
|
|
140
|
+
async (desktopApi) => await desktopApi.updateChannel(channel)
|
|
141
|
+
);
|
|
142
|
+
} catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (snapshot.status === 'update-available' && snapshot.availableVersion) {
|
|
147
|
+
toast.success(
|
|
148
|
+
t('desktopUpdatesChannelChangedWithUpdate')
|
|
149
|
+
.replace('{channel}', this.getChannelLabel(channel))
|
|
150
|
+
.replace('{version}', snapshot.availableVersion)
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
toast.success(
|
|
156
|
+
t('desktopUpdatesChannelChanged').replace('{channel}', this.getChannelLabel(channel))
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
128
160
|
private runSnapshotCommand = async (
|
|
129
161
|
busyAction: DesktopUpdateBusyAction,
|
|
130
162
|
fallbackMessage: string,
|
|
@@ -158,6 +190,10 @@ export class DesktopUpdateManager {
|
|
|
158
190
|
private getErrorMessage = (error: unknown): string => {
|
|
159
191
|
return error instanceof Error ? error.message : t('error');
|
|
160
192
|
};
|
|
193
|
+
|
|
194
|
+
private getChannelLabel = (channel: DesktopReleaseChannel): string => {
|
|
195
|
+
return channel === 'beta' ? t('desktopUpdatesChannelBeta') : t('desktopUpdatesChannelStable');
|
|
196
|
+
};
|
|
161
197
|
}
|
|
162
198
|
|
|
163
199
|
export const desktopUpdateManager = new DesktopUpdateManager();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DesktopPresenceSnapshot } from '@/desktop/desktop-update.types';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
|
|
4
|
+
type DesktopPresenceBusyAction = 'loading' | 'saving-preferences' | null;
|
|
5
|
+
|
|
6
|
+
type DesktopPresenceStoreState = {
|
|
7
|
+
supported: boolean;
|
|
8
|
+
initialized: boolean;
|
|
9
|
+
busyAction: DesktopPresenceBusyAction;
|
|
10
|
+
snapshot: DesktopPresenceSnapshot | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const useDesktopPresenceStore = create<DesktopPresenceStoreState>(() => ({
|
|
14
|
+
supported: false,
|
|
15
|
+
initialized: false,
|
|
16
|
+
busyAction: null,
|
|
17
|
+
snapshot: null
|
|
18
|
+
}));
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { DesktopUpdateSnapshot } from '@/desktop/desktop-update.types';
|
|
2
2
|
import { create } from 'zustand';
|
|
3
3
|
|
|
4
|
-
type DesktopUpdateBusyAction =
|
|
4
|
+
type DesktopUpdateBusyAction =
|
|
5
|
+
| 'checking'
|
|
6
|
+
| 'downloading'
|
|
7
|
+
| 'applying'
|
|
8
|
+
| 'saving-preferences'
|
|
9
|
+
| 'switching-channel'
|
|
10
|
+
| null;
|
|
5
11
|
|
|
6
12
|
type DesktopUpdateStoreState = {
|
|
7
13
|
supported: boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import type { RuntimeControlView } from '@/api/runtime-control.types';
|
|
3
|
+
import { runtimeControlManager } from '@/runtime-control/runtime-control.manager';
|
|
4
|
+
|
|
5
|
+
export function useRuntimeControl() {
|
|
6
|
+
return useQuery({
|
|
7
|
+
queryKey: ['runtime-control'],
|
|
8
|
+
queryFn: async (): Promise<RuntimeControlView> => await runtimeControlManager.getControl(),
|
|
9
|
+
staleTime: 5_000,
|
|
10
|
+
refetchOnWindowFocus: true
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useRuntimeServiceAction() {
|
|
15
|
+
const queryClient = useQueryClient();
|
|
16
|
+
|
|
17
|
+
return useMutation({
|
|
18
|
+
mutationFn: async (action: 'start-service' | 'restart-service' | 'stop-service') =>
|
|
19
|
+
await runtimeControlManager.controlService(action),
|
|
20
|
+
onSuccess: async () => {
|
|
21
|
+
await queryClient.invalidateQueries({ queryKey: ['runtime-control'] });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -28,9 +28,10 @@ export const DESKTOP_UPDATE_LABELS: Record<string, { zh: string; en: string }> =
|
|
|
28
28
|
desktopUpdatesStatusUpToDate: { zh: '已是最新', en: 'Up to Date' },
|
|
29
29
|
desktopUpdatesStatusFailed: { zh: '更新失败', en: 'Failed' },
|
|
30
30
|
desktopUpdatesLauncherVersion: { zh: '桌面壳版本', en: 'Launcher Version' },
|
|
31
|
-
desktopUpdatesCurrentBundleVersion: { zh: '
|
|
31
|
+
desktopUpdatesCurrentBundleVersion: { zh: '当前内核版本', en: 'Current Kernel Version' },
|
|
32
32
|
desktopUpdatesAvailableVersion: { zh: '可用版本', en: 'Available Version' },
|
|
33
33
|
desktopUpdatesLastCheckedAt: { zh: '上次检查时间', en: 'Last Checked' },
|
|
34
|
+
desktopUpdatesCurrentChannel: { zh: '当前更新通道', en: 'Current Release Channel' },
|
|
34
35
|
desktopUpdatesDownloadedBannerTitle: { zh: '更新已就绪', en: 'Update Ready' },
|
|
35
36
|
desktopUpdatesDownloadedBannerDescription: {
|
|
36
37
|
zh: '版本 {version} 已下载完成,等你确认后即可重启应用并完成更新。',
|
|
@@ -41,6 +42,22 @@ export const DESKTOP_UPDATE_LABELS: Record<string, { zh: string; en: string }> =
|
|
|
41
42
|
zh: '默认自动检查更新,但是否后台下载由你决定。',
|
|
42
43
|
en: 'Automatic checks stay on by default, while background download remains under your control.'
|
|
43
44
|
},
|
|
45
|
+
desktopUpdatesReleaseChannel: { zh: '更新通道', en: 'Release Channel' },
|
|
46
|
+
desktopUpdatesReleaseChannelHelp: {
|
|
47
|
+
zh: 'Stable 面向日常主力使用;Beta 用于提前体验新版本,但可能更不稳定。',
|
|
48
|
+
en: 'Stable is for everyday use, while Beta lets you try newer builds earlier with more risk.'
|
|
49
|
+
},
|
|
50
|
+
desktopUpdatesReleaseChannelDowngradeHint: {
|
|
51
|
+
zh: '切回 Stable 后不会立刻强制降级;只有当 Stable 追平或超过当前版本时,才会继续提供 Stable 更新。',
|
|
52
|
+
en: 'Switching back to Stable does not force an immediate downgrade. Stable updates resume once that channel catches up with or exceeds your current version.'
|
|
53
|
+
},
|
|
54
|
+
desktopUpdatesChannelStable: { zh: 'Stable', en: 'Stable' },
|
|
55
|
+
desktopUpdatesChannelBeta: { zh: 'Beta', en: 'Beta' },
|
|
56
|
+
desktopUpdatesBetaBadgeTitle: { zh: '当前正在跟随 Beta 通道', en: 'Following the Beta Channel' },
|
|
57
|
+
desktopUpdatesBetaBadgeDescription: {
|
|
58
|
+
zh: '你会更早收到新版本,但也可能遇到更多变动和回归。',
|
|
59
|
+
en: 'You will receive new versions earlier, but you may also encounter more change and regression risk.'
|
|
60
|
+
},
|
|
44
61
|
desktopUpdatesAutomaticChecks: { zh: '自动检查更新', en: 'Automatic Update Checks' },
|
|
45
62
|
desktopUpdatesAutomaticChecksHelp: {
|
|
46
63
|
zh: '启动应用后自动检查是否有新版本。',
|
|
@@ -65,8 +82,17 @@ export const DESKTOP_UPDATE_LABELS: Record<string, { zh: string; en: string }> =
|
|
|
65
82
|
desktopUpdatesDownloadFailed: { zh: '下载更新失败', en: 'Failed to download update' },
|
|
66
83
|
desktopUpdatesApplyFailed: { zh: '应用更新失败', en: 'Failed to apply update' },
|
|
67
84
|
desktopUpdatesPreferencesFailed: { zh: '保存更新偏好失败', en: 'Failed to save update preferences' },
|
|
85
|
+
desktopUpdatesChannelChangeFailed: { zh: '切换更新通道失败', en: 'Failed to change the release channel' },
|
|
68
86
|
desktopUpdatesAlreadyLatest: { zh: '当前已经是最新版本。', en: 'You already have the latest version.' },
|
|
69
87
|
desktopUpdatesReadyToApply: { zh: '更新已下载完成,可以在方便的时候重启应用。', en: 'The update is ready. Restart the app whenever you want to apply it.' },
|
|
70
88
|
desktopUpdatesAvailable: { zh: '发现新版本 {version}。', en: 'Version {version} is available.' },
|
|
71
|
-
desktopUpdatesUnknownVersion: { zh: '新版本', en: 'a new version' }
|
|
89
|
+
desktopUpdatesUnknownVersion: { zh: '新版本', en: 'a new version' },
|
|
90
|
+
desktopUpdatesChannelChanged: {
|
|
91
|
+
zh: '已切换到 {channel} 更新通道。',
|
|
92
|
+
en: 'Switched to the {channel} release channel.'
|
|
93
|
+
},
|
|
94
|
+
desktopUpdatesChannelChangedWithUpdate: {
|
|
95
|
+
zh: '已切换到 {channel} 通道,发现版本 {version}。',
|
|
96
|
+
en: 'Switched to the {channel} channel and found version {version}.'
|
|
97
|
+
}
|
|
72
98
|
};
|