@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/assets/{api-V8m62-13.js → api-D0U10BcF.js} +3 -3
  3. package/dist/assets/{app-presenter-provider-Bzj9h_ZS.js → app-presenter-provider-Dffdm_Ek.js} +48 -48
  4. package/dist/assets/{appearance-settings-page-DwBwu1AU.js → appearance-settings-page-sfusZc2d.js} +1 -1
  5. package/dist/assets/{book-open-DtVSr5O9.js → book-open-w3160PZf.js} +1 -1
  6. package/dist/assets/{channels-list-page-CYIqMliJ.js → channels-list-page-Dkfe87Zq.js} +1 -1
  7. package/dist/assets/{chat-page-BC0icTtI.js → chat-page-CeapbQnG.js} +1 -1
  8. package/dist/assets/{config-split-page-B88prd8s.js → config-split-page-CnQI8cKT.js} +1 -1
  9. package/dist/assets/{confirm-dialog-BB09IC2u.js → confirm-dialog-RZUWA_j9.js} +1 -1
  10. package/dist/assets/desktop-update-config-CbSYQMYz.js +1 -0
  11. package/dist/assets/{dist-Bytser-a.js → dist-DEJlYi0E.js} +1 -1
  12. package/dist/assets/{doc-browser-B1FXvZm6.js → doc-browser-DwXHr-mm.js} +1 -1
  13. package/dist/assets/{doc-browser-context-D5VLNoj3.js → doc-browser-context-CpvHiuQG.js} +1 -1
  14. package/dist/assets/doc-browser-jVTq8iY_2.js +1 -0
  15. package/dist/assets/{download-CdO6NoHj.js → download-BP7ZxrwI.js} +1 -1
  16. package/dist/assets/{external-link-C5NFoh5I.js → external-link-DvHCEe3h.js} +1 -1
  17. package/dist/assets/{eye-B51oNRRc.js → eye-C--6dxv_.js} +1 -1
  18. package/dist/assets/{folder-DxH7Nef-.js → folder-BfeO1zCY.js} +1 -1
  19. package/dist/assets/{form-actions-C9pUwtYV.js → form-actions-5OsDRt0g.js} +1 -1
  20. package/dist/assets/index-BlKbckX4.css +1 -0
  21. package/dist/assets/{index-UnY4HtVa.js → index-DoZR11Nh.js} +2 -2
  22. package/dist/assets/loader-circle-BWdgwVnn.js +1 -0
  23. package/dist/assets/{logo-badge-D2vORX3a.js → logo-badge-TdPa95AC.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-XnRWiC8c.js +1 -0
  25. package/dist/assets/{mcp-marketplace-page-CGVu7mfL.js → mcp-marketplace-page-cm3ifaKu.js} +1 -1
  26. package/dist/assets/{model-config-page-DYpchGVE.js → model-config-page-CXnqSbSI.js} +1 -1
  27. package/dist/assets/plus-BeApS21t.js +1 -0
  28. package/dist/assets/{popover-CONujDHf.js → popover-EulxhA0q.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DA3Ua9v8.js → provider-scoped-model-input-ll_Btc9h.js} +1 -1
  30. package/dist/assets/{providers-config-page-DXsoyMrh.js → providers-config-page-Bcuhqkdl.js} +1 -1
  31. package/dist/assets/{react-Io6Pfq8D.js → react-Bb0EYsLS.js} +1 -1
  32. package/dist/assets/{refresh-cw-C6j2tjul.js → refresh-cw-CexL11m1.js} +1 -1
  33. package/dist/assets/remote-CqKpuOP9.js +1 -0
  34. package/dist/assets/rotate-cw-DHXoiqFB.js +1 -0
  35. package/dist/assets/{runtime-config-page-eYrrKBrA.js → runtime-config-page-D-Aqxizs.js} +1 -1
  36. package/dist/assets/{save-CBBZVvZ-.js → save-DtccrZld.js} +1 -1
  37. package/dist/assets/{search-DFF842z2.js → search-BArc_WOX.js} +1 -1
  38. package/dist/assets/{search-config-page-DUyuwD7d.js → search-config-page-XTKf5rc0.js} +1 -1
  39. package/dist/assets/{secrets-config-page-DwzKQJ76.js → secrets-config-page-Cx-qptcj.js} +1 -1
  40. package/dist/assets/security-config-DWGFM2u9.js +1 -0
  41. package/dist/assets/{select-BiOpo2kV.js → select-C9FB0jlt.js} +1 -1
  42. package/dist/assets/{setting-row-C3WQXLtS.js → setting-row-DFvioO2a.js} +1 -1
  43. package/dist/assets/{settings-2-DyOvA_6K.js → settings-2-RMWqf-ey.js} +1 -1
  44. package/dist/assets/{skeleton-CQIAEbA4.js → skeleton-DQ5WhuDG.js} +1 -1
  45. package/dist/assets/{tabs-BTSENlI2.js → tabs-D_mjyOOf.js} +1 -1
  46. package/dist/assets/{tag-chip-BG5JIGwY.js → tag-chip-C011rd9q.js} +1 -1
  47. package/dist/assets/{trash-2-CTIfwpt1.js → trash-2-bDhfsO1O.js} +1 -1
  48. package/dist/assets/{use-config-CY4uF2Ou.js → use-config-BNrlfEck.js} +1 -1
  49. package/dist/assets/{use-confirm-dialog-Bj4u78F9.js → use-confirm-dialog-CTNA1PC0.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-ChJUcSQ6.js → use-viewport-layout-P0q6Nt_N.js} +1 -1
  51. package/dist/assets/x-qvh05iyu.js +1 -0
  52. package/dist/index.html +29 -29
  53. package/package.json +9 -9
  54. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +3 -3
  55. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +2 -2
  56. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +25 -8
  57. package/src/features/system-status/components/desktop-update-config.tsx +10 -47
  58. package/src/features/system-status/managers/runtime-update.manager.ts +2 -21
  59. package/src/features/system-status/stores/runtime-update.store.ts +0 -1
  60. package/src/platforms/desktop/index.ts +0 -1
  61. package/src/platforms/desktop/managers/desktop-update.manager.ts +1 -14
  62. package/src/platforms/desktop/stores/desktop-update.store.ts +0 -1
  63. package/src/platforms/desktop/types/desktop-update.types.ts +0 -5
  64. package/src/shared/components/common/__tests__/brand-header.test.tsx +43 -46
  65. package/src/shared/components/common/brand-header.tsx +55 -51
  66. package/src/shared/lib/api/runtime-update.utils.ts +1 -5
  67. package/src/shared/lib/i18n/locales/en-US/chat.json +1 -1
  68. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +7 -9
  69. package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -1
  70. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +7 -9
  71. package/dist/assets/desktop-update-config-B-q0OhxO.js +0 -1
  72. package/dist/assets/doc-browser-Bodhf0fj2.js +0 -1
  73. package/dist/assets/index-DtWf7kk9.css +0 -1
  74. package/dist/assets/loader-circle-BRSfaztA.js +0 -1
  75. package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +0 -1
  76. package/dist/assets/plus-BgMBV9HJ.js +0 -1
  77. package/dist/assets/remote-C1y3ARHu.js +0 -1
  78. package/dist/assets/rotate-cw-Bv2B6fAV.js +0 -1
  79. package/dist/assets/security-config-D8me3pXl.js +0 -1
  80. 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({ status }: { status: string }) {
40
- return <span className={cn('inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1', getStatusTone(status))}>{getStatusLabel(status)}</span>;
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(status: string): string {
120
- return t(STATUS_LABEL_KEYS[status] ?? 'desktopUpdatesStatusIdle');
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 status={snapshot.status} /></div>
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('desktopUpdatesPreferencesTitle')}</CardTitle>
288
- <CardDescription>{t('desktopUpdatesPreferencesDescription')}</CardDescription>
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 { UpdatePreferences, UpdateSnapshot } from '@nextclaw/shared';
2
- import { applyRuntimeUpdate, checkRuntimeUpdate, downloadRuntimeUpdate, fetchRuntimeUpdate, updateRuntimeUpdateChannel, updateRuntimeUpdatePreferences } from '@/shared/lib/api';
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) {
@@ -5,7 +5,6 @@ export type RuntimeUpdateBusyAction =
5
5
  | 'checking'
6
6
  | 'downloading'
7
7
  | 'applying'
8
- | 'saving-preferences'
9
8
  | 'switching-channel'
10
9
  | null;
11
10
 
@@ -20,7 +20,6 @@ export type {
20
20
  DesktopPresenceSnapshot,
21
21
  DesktopReleaseChannel,
22
22
  DesktopRuntimeControlResult,
23
- DesktopUpdatePreferences,
24
23
  DesktopUpdateSnapshot,
25
24
  DesktopUpdateStatus,
26
25
  NextClawDesktopBridge,
@@ -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' | 'saving-preferences' | 'switching-channel';
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) {
@@ -5,7 +5,6 @@ type DesktopUpdateBusyAction =
5
5
  | 'checking'
6
6
  | 'downloading'
7
7
  | 'applying'
8
- | 'saving-preferences'
9
8
  | 'switching-channel'
10
9
  | null;
11
10
 
@@ -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: null,
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
- expect(screen.getByText('下载 50%')).toBeTruthy();
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
- await user.click(screen.getByRole('button', { name: '下载' }));
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
- await user.hover(updateReleaseNotes);
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).toHaveBeenCalledWith('https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12');
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: null,
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
- await user.click(screen.getByRole('button', { name: '更新' }));
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('uses the failed update wording only for failed snapshots', async () => {
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: 'runtime bundle sha256 mismatch',
326
- preferences: {
327
- automaticChecks: true,
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
- const issueIcon = screen.getByLabelText('更新失败');
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('runtime bundle sha256 mismatch');
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
  });