@nextclaw/ui 0.15.7 → 0.15.9
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 +33 -0
- package/dist/assets/{api-V8m62-13.js → api-D0U10BcF.js} +3 -3
- package/dist/assets/{app-presenter-provider-Bzj9h_ZS.js → app-presenter-provider-Dffdm_Ek.js} +48 -48
- package/dist/assets/{appearance-settings-page-DwBwu1AU.js → appearance-settings-page-sfusZc2d.js} +1 -1
- package/dist/assets/{book-open-DtVSr5O9.js → book-open-w3160PZf.js} +1 -1
- package/dist/assets/{channels-list-page-CYIqMliJ.js → channels-list-page-Dkfe87Zq.js} +1 -1
- package/dist/assets/{chat-page-BC0icTtI.js → chat-page-CeapbQnG.js} +1 -1
- package/dist/assets/{config-split-page-B88prd8s.js → config-split-page-CnQI8cKT.js} +1 -1
- package/dist/assets/{confirm-dialog-BB09IC2u.js → confirm-dialog-RZUWA_j9.js} +1 -1
- package/dist/assets/desktop-update-config-CbSYQMYz.js +1 -0
- package/dist/assets/{dist-Bytser-a.js → dist-DEJlYi0E.js} +1 -1
- package/dist/assets/{doc-browser-B1FXvZm6.js → doc-browser-DwXHr-mm.js} +1 -1
- package/dist/assets/{doc-browser-context-D5VLNoj3.js → doc-browser-context-CpvHiuQG.js} +1 -1
- package/dist/assets/doc-browser-jVTq8iY_2.js +1 -0
- package/dist/assets/{download-CdO6NoHj.js → download-BP7ZxrwI.js} +1 -1
- package/dist/assets/{external-link-C5NFoh5I.js → external-link-DvHCEe3h.js} +1 -1
- package/dist/assets/{eye-B51oNRRc.js → eye-C--6dxv_.js} +1 -1
- package/dist/assets/{folder-DxH7Nef-.js → folder-BfeO1zCY.js} +1 -1
- package/dist/assets/{form-actions-C9pUwtYV.js → form-actions-5OsDRt0g.js} +1 -1
- package/dist/assets/index-BlKbckX4.css +1 -0
- package/dist/assets/{index-UnY4HtVa.js → index-DoZR11Nh.js} +2 -2
- package/dist/assets/loader-circle-BWdgwVnn.js +1 -0
- package/dist/assets/{logo-badge-D2vORX3a.js → logo-badge-TdPa95AC.js} +1 -1
- package/dist/assets/mcp-marketplace-page-XnRWiC8c.js +1 -0
- package/dist/assets/{mcp-marketplace-page-CGVu7mfL.js → mcp-marketplace-page-cm3ifaKu.js} +1 -1
- package/dist/assets/{model-config-page-DYpchGVE.js → model-config-page-CXnqSbSI.js} +1 -1
- package/dist/assets/plus-BeApS21t.js +1 -0
- package/dist/assets/{popover-CONujDHf.js → popover-EulxhA0q.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DA3Ua9v8.js → provider-scoped-model-input-ll_Btc9h.js} +1 -1
- package/dist/assets/{providers-config-page-DXsoyMrh.js → providers-config-page-Bcuhqkdl.js} +1 -1
- package/dist/assets/{react-Io6Pfq8D.js → react-Bb0EYsLS.js} +1 -1
- package/dist/assets/{refresh-cw-C6j2tjul.js → refresh-cw-CexL11m1.js} +1 -1
- package/dist/assets/remote-CqKpuOP9.js +1 -0
- package/dist/assets/rotate-cw-DHXoiqFB.js +1 -0
- package/dist/assets/{runtime-config-page-eYrrKBrA.js → runtime-config-page-D-Aqxizs.js} +1 -1
- package/dist/assets/{save-CBBZVvZ-.js → save-DtccrZld.js} +1 -1
- package/dist/assets/{search-DFF842z2.js → search-BArc_WOX.js} +1 -1
- package/dist/assets/{search-config-page-DUyuwD7d.js → search-config-page-XTKf5rc0.js} +1 -1
- package/dist/assets/{secrets-config-page-DwzKQJ76.js → secrets-config-page-Cx-qptcj.js} +1 -1
- package/dist/assets/security-config-DWGFM2u9.js +1 -0
- package/dist/assets/{select-BiOpo2kV.js → select-C9FB0jlt.js} +1 -1
- package/dist/assets/{setting-row-C3WQXLtS.js → setting-row-DFvioO2a.js} +1 -1
- package/dist/assets/{settings-2-DyOvA_6K.js → settings-2-RMWqf-ey.js} +1 -1
- package/dist/assets/{skeleton-CQIAEbA4.js → skeleton-DQ5WhuDG.js} +1 -1
- package/dist/assets/{tabs-BTSENlI2.js → tabs-D_mjyOOf.js} +1 -1
- package/dist/assets/{tag-chip-BG5JIGwY.js → tag-chip-C011rd9q.js} +1 -1
- package/dist/assets/{trash-2-CTIfwpt1.js → trash-2-bDhfsO1O.js} +1 -1
- package/dist/assets/{use-config-CY4uF2Ou.js → use-config-BNrlfEck.js} +1 -1
- package/dist/assets/{use-confirm-dialog-Bj4u78F9.js → use-confirm-dialog-CTNA1PC0.js} +1 -1
- package/dist/assets/{use-viewport-layout-ChJUcSQ6.js → use-viewport-layout-P0q6Nt_N.js} +1 -1
- package/dist/assets/x-qvh05iyu.js +1 -0
- package/dist/index.html +29 -29
- package/package.json +9 -9
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +3 -3
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +2 -2
- package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +25 -8
- package/src/features/system-status/components/desktop-update-config.tsx +10 -47
- package/src/features/system-status/managers/runtime-update.manager.ts +2 -21
- package/src/features/system-status/stores/runtime-update.store.ts +0 -1
- package/src/platforms/desktop/index.ts +0 -1
- package/src/platforms/desktop/managers/desktop-update.manager.ts +1 -14
- package/src/platforms/desktop/stores/desktop-update.store.ts +0 -1
- package/src/platforms/desktop/types/desktop-update.types.ts +0 -5
- package/src/shared/components/common/__tests__/brand-header.test.tsx +43 -46
- package/src/shared/components/common/brand-header.tsx +55 -51
- package/src/shared/lib/api/runtime-update.utils.ts +1 -5
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -1
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +7 -9
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -1
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +7 -9
- package/dist/assets/desktop-update-config-B-q0OhxO.js +0 -1
- package/dist/assets/doc-browser-Bodhf0fj2.js +0 -1
- package/dist/assets/index-DtWf7kk9.css +0 -1
- package/dist/assets/loader-circle-BRSfaztA.js +0 -1
- package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +0 -1
- package/dist/assets/plus-BgMBV9HJ.js +0 -1
- package/dist/assets/remote-C1y3ARHu.js +0 -1
- package/dist/assets/rotate-cw-Bv2B6fAV.js +0 -1
- package/dist/assets/security-config-D8me3pXl.js +0 -1
- package/dist/assets/x-C6cM8wkT.js +0 -1
|
@@ -13,7 +13,6 @@ const mocks = vi.hoisted(() => ({
|
|
|
13
13
|
checkForUpdates: vi.fn(),
|
|
14
14
|
downloadUpdate: vi.fn(),
|
|
15
15
|
applyDownloadedUpdate: vi.fn(),
|
|
16
|
-
updatePreferences: vi.fn(),
|
|
17
16
|
updateChannel: vi.fn()
|
|
18
17
|
}));
|
|
19
18
|
|
|
@@ -51,7 +50,6 @@ describe('DesktopUpdateConfig', () => {
|
|
|
51
50
|
mocks.checkForUpdates.mockReset();
|
|
52
51
|
mocks.downloadUpdate.mockReset();
|
|
53
52
|
mocks.applyDownloadedUpdate.mockReset();
|
|
54
|
-
mocks.updatePreferences.mockReset();
|
|
55
53
|
mocks.updateChannel.mockReset();
|
|
56
54
|
|
|
57
55
|
if (!HTMLElement.prototype.hasPointerCapture) {
|
|
@@ -80,16 +78,11 @@ describe('DesktopUpdateConfig', () => {
|
|
|
80
78
|
releaseNotesUrl: null,
|
|
81
79
|
lastCheckedAt: '2026-04-13T12:00:00.000Z',
|
|
82
80
|
progress: null,
|
|
83
|
-
canAutoDownload: false,
|
|
84
81
|
canApplyInApp: false,
|
|
85
82
|
requiresRestart: false,
|
|
86
83
|
blockReason: null,
|
|
87
84
|
recoveryCommand: null,
|
|
88
|
-
errorMessage: null
|
|
89
|
-
preferences: {
|
|
90
|
-
automaticChecks: true,
|
|
91
|
-
autoDownload: false
|
|
92
|
-
}
|
|
85
|
+
errorMessage: null
|
|
93
86
|
}
|
|
94
87
|
});
|
|
95
88
|
});
|
|
@@ -108,6 +101,9 @@ describe('DesktopUpdateConfig', () => {
|
|
|
108
101
|
expect(screen.getByText('当前更新通道')).toBeTruthy();
|
|
109
102
|
expect(screen.getAllByText('Beta').length).toBeGreaterThan(0);
|
|
110
103
|
expect(screen.getByText('当前正在跟随 Beta 通道')).toBeTruthy();
|
|
104
|
+
expect(screen.getByText('系统每两小时自动检查更新,下载和应用都由你手动确认。')).toBeTruthy();
|
|
105
|
+
expect(screen.queryByText('自动检查更新')).toBeNull();
|
|
106
|
+
expect(screen.queryByText('发现更新后自动后台下载')).toBeNull();
|
|
111
107
|
expect(screen.getByText('切回 Stable 后不会立刻强制降级;只有当 Stable 追平或超过当前版本时,才会继续提供 Stable 更新。')).toBeTruthy();
|
|
112
108
|
});
|
|
113
109
|
|
|
@@ -145,6 +141,27 @@ describe('DesktopUpdateConfig', () => {
|
|
|
145
141
|
expect(screen.getByText('50 B / 100 B')).toBeTruthy();
|
|
146
142
|
});
|
|
147
143
|
|
|
144
|
+
it('labels a failed check separately from an update failure', () => {
|
|
145
|
+
useRuntimeUpdateStore.setState((state) => ({
|
|
146
|
+
...state,
|
|
147
|
+
snapshot: state.snapshot
|
|
148
|
+
? {
|
|
149
|
+
...state.snapshot,
|
|
150
|
+
status: 'failed',
|
|
151
|
+
currentVersion: '0.24.0',
|
|
152
|
+
failureStage: 'check',
|
|
153
|
+
errorMessage: 'fetch failed: getaddrinfo ENOTFOUND updates.nextclaw.io'
|
|
154
|
+
}
|
|
155
|
+
: null
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
renderDesktopUpdateConfig();
|
|
159
|
+
|
|
160
|
+
expect(screen.getByText('检查更新失败')).toBeTruthy();
|
|
161
|
+
expect(screen.getByText('0.24.0')).toBeTruthy();
|
|
162
|
+
expect(screen.getByText('fetch failed: getaddrinfo ENOTFOUND updates.nextclaw.io')).toBeTruthy();
|
|
163
|
+
});
|
|
164
|
+
|
|
148
165
|
it('loads structured release notes from the docs JSON endpoint', async () => {
|
|
149
166
|
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
150
167
|
schemaVersion: 1,
|
|
@@ -5,7 +5,6 @@ import { Button } from '@/shared/components/ui/button';
|
|
|
5
5
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
|
6
6
|
import { Label } from '@/shared/components/ui/label';
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
|
8
|
-
import { Switch } from '@/shared/components/ui/switch';
|
|
9
8
|
import { PageHeader, PageLayout } from '@/app/components/layout/page-layout';
|
|
10
9
|
import { formatDateTime, getLanguage, t } from '@/shared/lib/i18n';
|
|
11
10
|
import { cn } from '@/shared/lib/utils';
|
|
@@ -36,8 +35,8 @@ const RELEASE_NOTES_KIND_LABEL_KEYS: Record<string, string> = {
|
|
|
36
35
|
compatibility: 'desktopUpdatesReleaseNotesCompatibility'
|
|
37
36
|
};
|
|
38
37
|
|
|
39
|
-
function StatusBadge({
|
|
40
|
-
return <span className={cn('inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1', getStatusTone(status))}>{getStatusLabel(
|
|
38
|
+
function StatusBadge({ snapshot }: { snapshot: UpdateSnapshot }) {
|
|
39
|
+
return <span className={cn('inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1', getStatusTone(snapshot.status))}>{getStatusLabel(snapshot)}</span>;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
function OverviewStat({ label, value }: { label: string; value: string }) {
|
|
@@ -66,30 +65,6 @@ function DownloadProgress({ snapshot }: { snapshot: UpdateSnapshot }) {
|
|
|
66
65
|
);
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
function PreferenceToggle({
|
|
70
|
-
label,
|
|
71
|
-
help,
|
|
72
|
-
checked,
|
|
73
|
-
disabled,
|
|
74
|
-
onCheckedChange,
|
|
75
|
-
}: {
|
|
76
|
-
label: string;
|
|
77
|
-
help: string;
|
|
78
|
-
checked: boolean;
|
|
79
|
-
disabled: boolean;
|
|
80
|
-
onCheckedChange: (checked: boolean) => void;
|
|
81
|
-
}) {
|
|
82
|
-
return (
|
|
83
|
-
<div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
|
|
84
|
-
<div className="space-y-1">
|
|
85
|
-
<Label>{label}</Label>
|
|
86
|
-
<p className="text-sm text-gray-500">{help}</p>
|
|
87
|
-
</div>
|
|
88
|
-
<Switch checked={checked} disabled={disabled} onCheckedChange={onCheckedChange} />
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
68
|
function formatVersion(value: string | null): string {
|
|
94
69
|
return value?.trim() || '-';
|
|
95
70
|
}
|
|
@@ -116,8 +91,11 @@ function formatBytes(value: number): string {
|
|
|
116
91
|
function getChannelLabel(channel: UpdateSnapshot['channel']): string {
|
|
117
92
|
return channel === 'beta' ? t('desktopUpdatesChannelBeta') : t('desktopUpdatesChannelStable');
|
|
118
93
|
}
|
|
119
|
-
function getStatusLabel(
|
|
120
|
-
|
|
94
|
+
function getStatusLabel(snapshot: UpdateSnapshot): string {
|
|
95
|
+
if (snapshot.status === 'failed' && snapshot.failureStage) {
|
|
96
|
+
return t(`desktopUpdatesFailureStage.${snapshot.failureStage}`);
|
|
97
|
+
}
|
|
98
|
+
return t(STATUS_LABEL_KEYS[snapshot.status] ?? 'desktopUpdatesStatusIdle');
|
|
121
99
|
}
|
|
122
100
|
function getStatusTone(status: string): string {
|
|
123
101
|
if (status === 'downloaded') {
|
|
@@ -232,7 +210,6 @@ export function DesktopUpdateConfig() {
|
|
|
232
210
|
const isChecking = busyAction === 'checking';
|
|
233
211
|
const isDownloading = busyAction === 'downloading';
|
|
234
212
|
const isApplying = busyAction === 'applying';
|
|
235
|
-
const isSavingPreferences = busyAction === 'saving-preferences';
|
|
236
213
|
const isSwitchingChannel = busyAction === 'switching-channel';
|
|
237
214
|
const canDownload = snapshot.status === 'update-available' && !isDownloading && !isApplying;
|
|
238
215
|
const canApply = snapshot.status === 'downloaded' && !isApplying;
|
|
@@ -256,7 +233,7 @@ export function DesktopUpdateConfig() {
|
|
|
256
233
|
<CardDescription>{t('desktopUpdatesOverviewDescription')}</CardDescription>
|
|
257
234
|
</CardHeader>
|
|
258
235
|
<CardContent className="space-y-5">
|
|
259
|
-
<div className="flex flex-wrap items-center gap-3"><span className="text-sm font-medium text-gray-700">{t('desktopUpdatesStatusLabel')}</span><StatusBadge
|
|
236
|
+
<div className="flex flex-wrap items-center gap-3"><span className="text-sm font-medium text-gray-700">{t('desktopUpdatesStatusLabel')}</span><StatusBadge snapshot={snapshot} /></div>
|
|
260
237
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">{overviewStats.map(([label, value]) => <OverviewStat key={label} label={label} value={value} />)}</div>
|
|
261
238
|
{snapshot.channel === 'beta' ? (
|
|
262
239
|
<div className="rounded-2xl border border-amber-200 bg-amber-50/70 p-4">
|
|
@@ -284,8 +261,8 @@ export function DesktopUpdateConfig() {
|
|
|
284
261
|
<ReleaseNotesPreview snapshot={snapshot} />
|
|
285
262
|
<Card>
|
|
286
263
|
<CardHeader>
|
|
287
|
-
<CardTitle>{t('
|
|
288
|
-
<CardDescription>{t('
|
|
264
|
+
<CardTitle>{t('desktopUpdatesChannelSettingsTitle')}</CardTitle>
|
|
265
|
+
<CardDescription>{t('desktopUpdatesChannelSettingsDescription')}</CardDescription>
|
|
289
266
|
</CardHeader>
|
|
290
267
|
<CardContent className="space-y-5">
|
|
291
268
|
<div className="rounded-xl border border-gray-200 p-4">
|
|
@@ -306,20 +283,6 @@ export function DesktopUpdateConfig() {
|
|
|
306
283
|
<p className="text-sm text-gray-500">{t('desktopUpdatesReleaseChannelDowngradeHint')}</p>
|
|
307
284
|
</div>
|
|
308
285
|
</div>
|
|
309
|
-
<PreferenceToggle
|
|
310
|
-
label={t('desktopUpdatesAutomaticChecks')}
|
|
311
|
-
help={t('desktopUpdatesAutomaticChecksHelp')}
|
|
312
|
-
checked={snapshot.preferences.automaticChecks}
|
|
313
|
-
disabled={isSavingPreferences || isSwitchingChannel}
|
|
314
|
-
onCheckedChange={(checked) => void runtimeUpdateManager.updatePreferences({ automaticChecks: checked })}
|
|
315
|
-
/>
|
|
316
|
-
<PreferenceToggle
|
|
317
|
-
label={t('desktopUpdatesAutoDownload')}
|
|
318
|
-
help={t('desktopUpdatesAutoDownloadHelp')}
|
|
319
|
-
checked={snapshot.preferences.autoDownload}
|
|
320
|
-
disabled={isSavingPreferences || isSwitchingChannel}
|
|
321
|
-
onCheckedChange={(checked) => void runtimeUpdateManager.updatePreferences({ autoDownload: checked })}
|
|
322
|
-
/>
|
|
323
286
|
</CardContent>
|
|
324
287
|
</Card>
|
|
325
288
|
<Card>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { applyRuntimeUpdate, checkRuntimeUpdate, downloadRuntimeUpdate, fetchRuntimeUpdate, updateRuntimeUpdateChannel
|
|
1
|
+
import type { UpdateSnapshot } from '@nextclaw/shared';
|
|
2
|
+
import { applyRuntimeUpdate, checkRuntimeUpdate, downloadRuntimeUpdate, fetchRuntimeUpdate, updateRuntimeUpdateChannel } from '@/shared/lib/api';
|
|
3
3
|
import type { NextClawDesktopBridge } from '@/platforms/desktop';
|
|
4
4
|
import { t } from '@/shared/lib/i18n';
|
|
5
5
|
import { toast } from 'sonner';
|
|
@@ -10,7 +10,6 @@ interface RuntimeUpdateSourceBase {
|
|
|
10
10
|
checkForUpdates: () => Promise<UpdateSnapshot>;
|
|
11
11
|
downloadUpdate: () => Promise<UpdateSnapshot>;
|
|
12
12
|
applyDownloadedUpdate: () => Promise<UpdateSnapshot>;
|
|
13
|
-
updatePreferences: (preferences: Partial<UpdatePreferences>) => Promise<UpdateSnapshot>;
|
|
14
13
|
updateChannel: (channel: UpdateSnapshot['channel']) => Promise<UpdateSnapshot>;
|
|
15
14
|
}
|
|
16
15
|
|
|
@@ -50,10 +49,6 @@ class DesktopBridgeRuntimeUpdateSource implements DesktopBridgeRuntimeUpdateSour
|
|
|
50
49
|
return await this.desktopApi.applyDownloadedUpdate();
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
updatePreferences = async (preferences: Partial<UpdatePreferences>): Promise<UpdateSnapshot> => {
|
|
54
|
-
return await this.desktopApi.updatePreferences(preferences);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
52
|
updateChannel = async (channel: UpdateSnapshot['channel']): Promise<UpdateSnapshot> => {
|
|
58
53
|
return await this.desktopApi.updateChannel(channel);
|
|
59
54
|
};
|
|
@@ -78,10 +73,6 @@ class HostRuntimeUpdateSource implements HostRuntimeUpdateSourceContract {
|
|
|
78
73
|
return await applyRuntimeUpdate();
|
|
79
74
|
};
|
|
80
75
|
|
|
81
|
-
updatePreferences = async (preferences: Partial<UpdatePreferences>): Promise<UpdateSnapshot> => {
|
|
82
|
-
return await updateRuntimeUpdatePreferences(preferences);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
76
|
updateChannel = async (channel: UpdateSnapshot['channel']): Promise<UpdateSnapshot> => {
|
|
86
77
|
return await updateRuntimeUpdateChannel(channel);
|
|
87
78
|
};
|
|
@@ -217,16 +208,6 @@ export class RuntimeUpdateManager {
|
|
|
217
208
|
}
|
|
218
209
|
};
|
|
219
210
|
|
|
220
|
-
updatePreferences = async (preferences: Partial<UpdatePreferences>) => {
|
|
221
|
-
try {
|
|
222
|
-
await this.runSnapshotCommand('saving-preferences', t('runtimeUpdatesPreferencesFailed'), async (source) => {
|
|
223
|
-
return await source.updatePreferences(preferences);
|
|
224
|
-
});
|
|
225
|
-
} catch {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
|
|
230
211
|
updateChannel = async (channel: UpdateSnapshot['channel']) => {
|
|
231
212
|
const currentChannel = useRuntimeUpdateStore.getState().snapshot?.channel;
|
|
232
213
|
if (currentChannel === channel) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DesktopReleaseChannel,
|
|
3
|
-
DesktopUpdatePreferences,
|
|
4
3
|
DesktopUpdateSnapshot,
|
|
5
4
|
NextClawDesktopBridge
|
|
6
5
|
} from '@/platforms/desktop/types/desktop-update.types';
|
|
@@ -8,7 +7,7 @@ import { useDesktopUpdateStore } from '@/platforms/desktop/stores/desktop-update
|
|
|
8
7
|
import { t } from '@/shared/lib/i18n';
|
|
9
8
|
import { toast } from 'sonner';
|
|
10
9
|
|
|
11
|
-
type DesktopUpdateBusyAction = 'checking' | 'downloading' | 'applying' | '
|
|
10
|
+
type DesktopUpdateBusyAction = 'checking' | 'downloading' | 'applying' | 'switching-channel';
|
|
12
11
|
|
|
13
12
|
export class DesktopUpdateManager {
|
|
14
13
|
private unsubscribe: (() => void) | null = null;
|
|
@@ -120,18 +119,6 @@ export class DesktopUpdateManager {
|
|
|
120
119
|
}
|
|
121
120
|
};
|
|
122
121
|
|
|
123
|
-
updatePreferences = async (preferences: Partial<DesktopUpdatePreferences>) => {
|
|
124
|
-
try {
|
|
125
|
-
await this.runSnapshotCommand(
|
|
126
|
-
'saving-preferences',
|
|
127
|
-
t('desktopUpdatesPreferencesFailed'),
|
|
128
|
-
async (desktopApi) => await desktopApi.updatePreferences(preferences)
|
|
129
|
-
);
|
|
130
|
-
} catch {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
122
|
updateChannel = async (channel: DesktopReleaseChannel) => {
|
|
136
123
|
const currentChannel = useDesktopUpdateStore.getState().snapshot?.channel;
|
|
137
124
|
if (currentChannel === channel) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
InstallationKind,
|
|
3
3
|
UpdateBlockReason,
|
|
4
|
-
UpdatePreferences,
|
|
5
4
|
UpdateProgress,
|
|
6
5
|
UpdateSnapshot,
|
|
7
6
|
UpdateStatus,
|
|
@@ -20,13 +19,10 @@ export type DesktopUpdateBlockReason = UpdateBlockReason;
|
|
|
20
19
|
|
|
21
20
|
export type DesktopUpdateProgress = UpdateProgress;
|
|
22
21
|
|
|
23
|
-
export type DesktopUpdatePreferences = UpdatePreferences;
|
|
24
|
-
|
|
25
22
|
export type DesktopUpdateSnapshot = UpdateSnapshot & {
|
|
26
23
|
status: DesktopUpdateStatus;
|
|
27
24
|
channel: DesktopReleaseChannel;
|
|
28
25
|
launcherVersion: string;
|
|
29
|
-
preferences: DesktopUpdatePreferences;
|
|
30
26
|
};
|
|
31
27
|
|
|
32
28
|
export type DesktopRuntimeControlResult = {
|
|
@@ -68,7 +64,6 @@ export type NextClawDesktopBridge = {
|
|
|
68
64
|
checkForUpdates: () => Promise<DesktopUpdateSnapshot>;
|
|
69
65
|
downloadUpdate: () => Promise<DesktopUpdateSnapshot>;
|
|
70
66
|
applyDownloadedUpdate: () => Promise<DesktopUpdateSnapshot>;
|
|
71
|
-
updatePreferences: (preferences: Partial<DesktopUpdatePreferences>) => Promise<DesktopUpdateSnapshot>;
|
|
72
67
|
updateChannel: (channel: DesktopReleaseChannel) => Promise<DesktopUpdateSnapshot>;
|
|
73
68
|
restartService: () => Promise<DesktopRuntimeControlResult>;
|
|
74
69
|
restartApp: () => Promise<DesktopRuntimeControlResult>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
4
|
import { MemoryRouter } from 'react-router-dom';
|
|
@@ -103,23 +103,18 @@ describe('BrandHeader', () => {
|
|
|
103
103
|
availableVersion: '0.18.12',
|
|
104
104
|
downloadedVersion: null,
|
|
105
105
|
minimumHostVersion: null,
|
|
106
|
-
releaseNotesUrl:
|
|
106
|
+
releaseNotesUrl: 'https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12',
|
|
107
107
|
lastCheckedAt: null,
|
|
108
108
|
progress: {
|
|
109
109
|
downloadedBytes: 50,
|
|
110
110
|
totalBytes: 100,
|
|
111
111
|
percent: 50
|
|
112
112
|
},
|
|
113
|
-
canAutoDownload: true,
|
|
114
113
|
canApplyInApp: false,
|
|
115
114
|
requiresRestart: false,
|
|
116
115
|
blockReason: null,
|
|
117
116
|
recoveryCommand: null,
|
|
118
|
-
errorMessage: null
|
|
119
|
-
preferences: {
|
|
120
|
-
automaticChecks: true,
|
|
121
|
-
autoDownload: true
|
|
122
|
-
}
|
|
117
|
+
errorMessage: null
|
|
123
118
|
}
|
|
124
119
|
});
|
|
125
120
|
|
|
@@ -130,8 +125,17 @@ describe('BrandHeader', () => {
|
|
|
130
125
|
expect(screen.getAllByText('v0.18.11')).toHaveLength(1);
|
|
131
126
|
await user.hover(version);
|
|
132
127
|
expect((await screen.findByRole('tooltip')).textContent).toBe('v0.18.11');
|
|
133
|
-
|
|
128
|
+
const downloadProgress = screen.getByText('下载 50%');
|
|
129
|
+
expect(downloadProgress).toBeTruthy();
|
|
134
130
|
expect(screen.queryByRole('button', { name: '更新' })).toBeNull();
|
|
131
|
+
const progressReleaseNotes = screen.getByRole('button', { name: '查看 v0.18.12 更新说明' });
|
|
132
|
+
expect(progressReleaseNotes.parentElement?.className).toContain('invisible');
|
|
133
|
+
expect(progressReleaseNotes.parentElement?.className).toContain('group-hover/update-release-notes:visible');
|
|
134
|
+
|
|
135
|
+
await user.hover(downloadProgress);
|
|
136
|
+
await user.click(progressReleaseNotes);
|
|
137
|
+
|
|
138
|
+
expect(mocks.openExternalUrl).toHaveBeenCalledWith('https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12');
|
|
135
139
|
});
|
|
136
140
|
|
|
137
141
|
it('opens release notes from the current version tooltip when that version has docs notes', async () => {
|
|
@@ -180,16 +184,11 @@ describe('BrandHeader', () => {
|
|
|
180
184
|
releaseNotesUrl: 'https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12',
|
|
181
185
|
lastCheckedAt: null,
|
|
182
186
|
progress: null,
|
|
183
|
-
canAutoDownload: true,
|
|
184
187
|
canApplyInApp: false,
|
|
185
188
|
requiresRestart: false,
|
|
186
189
|
blockReason: null,
|
|
187
190
|
recoveryCommand: null,
|
|
188
|
-
errorMessage: null
|
|
189
|
-
preferences: {
|
|
190
|
-
automaticChecks: true,
|
|
191
|
-
autoDownload: true
|
|
192
|
-
}
|
|
191
|
+
errorMessage: null
|
|
193
192
|
}
|
|
194
193
|
});
|
|
195
194
|
|
|
@@ -201,15 +200,16 @@ describe('BrandHeader', () => {
|
|
|
201
200
|
await user.click(currentVersion);
|
|
202
201
|
expect(mocks.openExternalUrl).toHaveBeenCalledWith('https://docs.nextclaw.io/zh/notes/2026-07-14-nextclaw-v0-18-11');
|
|
203
202
|
|
|
204
|
-
|
|
205
|
-
expect(mocks.downloadUpdate).toHaveBeenCalledTimes(1);
|
|
206
|
-
|
|
203
|
+
const downloadUpdate = screen.getByRole('button', { name: '下载' });
|
|
207
204
|
const updateReleaseNotes = screen.getByRole('button', { name: '查看 v0.18.12 更新说明' });
|
|
208
|
-
|
|
209
|
-
expect((await screen.findByRole('tooltip')).textContent).toBe('查看 v0.18.12 更新说明');
|
|
205
|
+
expect(updateReleaseNotes.parentElement?.className).toContain('group-hover/update-release-notes:visible');
|
|
210
206
|
|
|
207
|
+
await user.hover(downloadUpdate);
|
|
211
208
|
await user.click(updateReleaseNotes);
|
|
212
|
-
expect(mocks.openExternalUrl).
|
|
209
|
+
expect(mocks.openExternalUrl).toHaveBeenLastCalledWith('https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12');
|
|
210
|
+
|
|
211
|
+
await user.click(downloadUpdate);
|
|
212
|
+
expect(mocks.downloadUpdate).toHaveBeenCalledTimes(1);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
215
|
it('applies the downloaded update from the version-adjacent update button', async () => {
|
|
@@ -227,25 +227,28 @@ describe('BrandHeader', () => {
|
|
|
227
227
|
availableVersion: null,
|
|
228
228
|
downloadedVersion: '0.18.12',
|
|
229
229
|
minimumHostVersion: null,
|
|
230
|
-
releaseNotesUrl:
|
|
230
|
+
releaseNotesUrl: 'https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12',
|
|
231
231
|
lastCheckedAt: null,
|
|
232
232
|
progress: null,
|
|
233
|
-
canAutoDownload: true,
|
|
234
233
|
canApplyInApp: true,
|
|
235
234
|
requiresRestart: false,
|
|
236
235
|
blockReason: null,
|
|
237
236
|
recoveryCommand: null,
|
|
238
|
-
errorMessage: null
|
|
239
|
-
preferences: {
|
|
240
|
-
automaticChecks: true,
|
|
241
|
-
autoDownload: true
|
|
242
|
-
}
|
|
237
|
+
errorMessage: null
|
|
243
238
|
}
|
|
244
239
|
});
|
|
245
240
|
|
|
246
241
|
renderBrandHeader();
|
|
247
242
|
|
|
248
|
-
|
|
243
|
+
const applyUpdate = screen.getByRole('button', { name: '更新' });
|
|
244
|
+
const updateReleaseNotes = screen.getByRole('button', { name: '查看 v0.18.12 更新说明' });
|
|
245
|
+
expect(updateReleaseNotes.parentElement?.className).toContain('group-focus-within/update-release-notes:visible');
|
|
246
|
+
|
|
247
|
+
fireEvent.focus(applyUpdate);
|
|
248
|
+
await user.click(updateReleaseNotes);
|
|
249
|
+
expect(mocks.openExternalUrl).toHaveBeenCalledWith('https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12');
|
|
250
|
+
|
|
251
|
+
await user.click(applyUpdate);
|
|
249
252
|
|
|
250
253
|
expect(mocks.applyDownloadedUpdate).toHaveBeenCalledTimes(1);
|
|
251
254
|
expect(mocks.downloadUpdate).not.toHaveBeenCalled();
|
|
@@ -269,16 +272,11 @@ describe('BrandHeader', () => {
|
|
|
269
272
|
releaseNotesUrl: null,
|
|
270
273
|
lastCheckedAt: null,
|
|
271
274
|
progress: null,
|
|
272
|
-
canAutoDownload: true,
|
|
273
275
|
canApplyInApp: false,
|
|
274
276
|
requiresRestart: false,
|
|
275
277
|
blockReason: 'signature-verification-unavailable',
|
|
276
278
|
recoveryCommand: 'Set NEXTCLAW_UPDATE_BUNDLE_PUBLIC_KEY',
|
|
277
|
-
errorMessage: 'Runtime bundle updates require a configured update public key.'
|
|
278
|
-
preferences: {
|
|
279
|
-
automaticChecks: true,
|
|
280
|
-
autoDownload: true
|
|
281
|
-
}
|
|
279
|
+
errorMessage: 'Runtime bundle updates require a configured update public key.'
|
|
282
280
|
}
|
|
283
281
|
});
|
|
284
282
|
|
|
@@ -299,7 +297,7 @@ describe('BrandHeader', () => {
|
|
|
299
297
|
expect(tooltip.textContent).toContain('Set NEXTCLAW_UPDATE_BUNDLE_PUBLIC_KEY');
|
|
300
298
|
});
|
|
301
299
|
|
|
302
|
-
it('
|
|
300
|
+
it('distinguishes a failed post-restart check from a failed update', async () => {
|
|
303
301
|
const user = userEvent.setup();
|
|
304
302
|
useRuntimeUpdateStore.setState({
|
|
305
303
|
supported: true,
|
|
@@ -317,29 +315,28 @@ describe('BrandHeader', () => {
|
|
|
317
315
|
releaseNotesUrl: null,
|
|
318
316
|
lastCheckedAt: null,
|
|
319
317
|
progress: null,
|
|
320
|
-
canAutoDownload: true,
|
|
321
318
|
canApplyInApp: false,
|
|
322
319
|
requiresRestart: false,
|
|
323
320
|
blockReason: null,
|
|
324
321
|
recoveryCommand: null,
|
|
325
|
-
errorMessage: '
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
autoDownload: true
|
|
329
|
-
}
|
|
322
|
+
errorMessage: 'fetch failed: getaddrinfo ENOTFOUND updates.nextclaw.io',
|
|
323
|
+
failureStage: 'check',
|
|
324
|
+
diagnosticCommand: 'nextclaw logs path'
|
|
330
325
|
}
|
|
331
326
|
});
|
|
332
327
|
|
|
333
|
-
renderBrandHeader();
|
|
328
|
+
renderBrandHeader({ productVersion: '0.24.0' });
|
|
334
329
|
|
|
335
|
-
|
|
330
|
+
expect(screen.getByText('v0.24.0')).not.toBeNull();
|
|
331
|
+
const issueIcon = screen.getByLabelText('检查更新失败');
|
|
336
332
|
|
|
337
333
|
expect(issueIcon.textContent).toBe('!');
|
|
338
334
|
await user.hover(issueIcon);
|
|
339
335
|
|
|
340
336
|
const tooltip = await screen.findByRole('tooltip');
|
|
341
|
-
expect(tooltip.textContent).toContain('
|
|
342
|
-
expect(tooltip.textContent).toContain('
|
|
337
|
+
expect(tooltip.textContent).toContain('检查更新失败');
|
|
338
|
+
expect(tooltip.textContent).toContain('fetch failed: getaddrinfo ENOTFOUND updates.nextclaw.io');
|
|
339
|
+
expect(tooltip.textContent).toContain('完整日志:nextclaw logs path');
|
|
343
340
|
expect(screen.queryByText('更新被阻塞')).toBeNull();
|
|
344
341
|
});
|
|
345
342
|
});
|