@nextclaw/ui 0.12.5 → 0.12.6
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 +25 -0
- package/dist/assets/ChannelsList-D8p4OlM6.js +8 -0
- package/dist/assets/ChatPage-A45t1Rmf.js +58 -0
- package/dist/assets/DocBrowser-B2MpsnU9.js +1 -0
- package/dist/assets/{DocBrowser-QUZ3nfmH.js → DocBrowser-Cse_F8Nn.js} +1 -1
- package/dist/assets/{DocBrowserContext-CpiIfhJO.js → DocBrowserContext-Bai1WU2H.js} +1 -1
- package/dist/assets/{LogoBadge-BUK13xK5.js → LogoBadge-BdxMPc9v.js} +1 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
- package/dist/assets/MarketplacePage-BbpAkllU.js +49 -0
- package/dist/assets/{McpMarketplacePage-BG4T_Pcx.js → McpMarketplacePage-CxPFOgxv.js} +2 -2
- package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
- package/dist/assets/{ProviderScopedModelInput-DGn6sFEN.js → ProviderScopedModelInput-BYNouw-i.js} +1 -1
- package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
- package/dist/assets/{RemoteAccessPage-ff15qO-c.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
- package/dist/assets/{RuntimeConfig-TgPandXF.js → RuntimeConfig-ChdfK4Y_.js} +1 -1
- package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
- package/dist/assets/{SecretsConfig-Bew4EF2A.js → SecretsConfig-CCYO6NcV.js} +2 -2
- package/dist/assets/SessionsConfig-Du39vDgt.js +2 -0
- package/dist/assets/app-query-client-Dr5d-K8d.js +1 -0
- package/dist/assets/{book-open-CJG8Yz3U.js → book-open-Da4OEPqB.js} +1 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-D5b3Iyas.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
- package/dist/assets/client-CSk58DcF.js +7 -0
- package/dist/assets/config-D8KzikVB.js +1 -0
- package/dist/assets/{createLucideIcon-_FMJqZw2.js → createLucideIcon-83gaZMtv.js} +1 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
- package/dist/assets/dist-aTmhMDVh.js +9 -0
- package/dist/assets/{dist-B1fpOuON.js → dist-toEYs-MZ.js} +1 -1
- package/dist/assets/{external-link-b7gAJWYY.js → external-link-QQ0TC6X4.js} +1 -1
- package/dist/assets/{hash-Bhy4TwfZ.js → hash-DaFBEkmi.js} +1 -1
- package/dist/assets/i18n-C3jb83S6.js +1 -0
- package/dist/assets/index-CE4N7ItL.css +1 -0
- package/dist/assets/index-riX7Sg0_.js +6 -0
- package/dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
- package/dist/assets/loader-circle-BjMg63eu.js +1 -0
- package/dist/assets/{logos-GMeYU9vc.js → logos-Dzlz30M3.js} +1 -1
- package/dist/assets/{page-layout-C8UbWuMt.js → page-layout-D2eRufRQ.js} +1 -1
- package/dist/assets/plus-CIXME2pD.js +1 -0
- package/dist/assets/{popover-8HSx9wQj.js → popover-BSXxm5bj.js} +1 -1
- package/dist/assets/{refresh-ccw-CA4_C7Zg.js → refresh-ccw-B3zMtN-_.js} +1 -1
- package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
- package/dist/assets/{save-BtvMy4lk.js → save-Us9fg4Sj.js} +1 -1
- package/dist/assets/search-B_Qr0f6C.js +1 -0
- package/dist/assets/security-config-BGWYwxNr.js +1 -0
- package/dist/assets/{select-xp_Ac8ip.js → select-DLYqySQK.js} +1 -1
- package/dist/assets/skeleton-CYQJazv6.js +1 -0
- package/dist/assets/{status-dot-Cn4Pp7DZ.js → status-dot-DGayudyB.js} +1 -1
- package/dist/assets/{switch-BTi6UOij.js → switch-Dz2ScsKx.js} +1 -1
- package/dist/assets/{tabs-custom-BiiN8DME.js → tabs-custom-CdKyjiGk.js} +1 -1
- package/dist/assets/{trash-2-BpsF0N-r.js → trash-2-Db-mZOZs.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
- package/dist/assets/{useConfirmDialog-BJIwUZjH.js → useConfirmDialog-DL0a-oGC.js} +1 -1
- package/dist/assets/useMutation-BdZm-9PL.js +1 -0
- package/dist/assets/x-B8Tho_xC.js +1 -0
- package/dist/index.html +20 -19
- package/package.json +5 -5
- package/src/App.tsx +2 -0
- package/src/api/raw-client.test.ts +37 -0
- package/src/api/raw-client.ts +51 -8
- package/src/components/chat/ChatConversationPanel.test.tsx +161 -1
- package/src/components/chat/ChatSidebar.test.tsx +109 -4
- package/src/components/chat/ChatSidebar.tsx +62 -9
- package/src/components/chat/chat-child-session-panel.tsx +56 -18
- package/src/components/chat/chat-sidebar-session-item.tsx +189 -121
- package/src/components/chat/containers/chat-message-list.container.test.tsx +21 -3
- package/src/components/chat/containers/chat-message-list.container.tsx +14 -0
- package/src/components/chat/managers/chat-session-list.manager.test.ts +34 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +13 -0
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +1 -1
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +45 -5
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +18 -5
- package/src/components/chat/stores/chat-session-list.store.ts +96 -5
- package/src/components/config/ProviderForm.tsx +9 -15
- package/src/components/config/desktop-update-config.tsx +230 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/layout/sidebar.layout.test.tsx +1 -0
- package/src/desktop/desktop-update.types.ts +36 -0
- package/src/desktop/managers/desktop-update.manager.ts +163 -0
- package/src/desktop/stores/desktop-update.store.ts +18 -0
- package/src/lib/desktop-update-labels.utils.ts +72 -0
- package/src/lib/i18n.chat.ts +13 -0
- package/src/lib/i18n.ts +3 -9
- package/src/lib/ui-document-title.ts +1 -0
- package/src/transport/local.transport.ts +57 -18
- package/src/vite-env.d.ts +10 -0
- package/dist/assets/ChannelsList-C6-lh55g.js +0 -8
- package/dist/assets/ChatPage-DOW0gPc2.js +0 -45
- package/dist/assets/DocBrowser-CGyeswYP.js +0 -1
- package/dist/assets/MarketplacePage-BDVwhIYE.js +0 -1
- package/dist/assets/MarketplacePage-LnKKL3xK.js +0 -49
- package/dist/assets/ModelConfig-LtWuogIw.js +0 -1
- package/dist/assets/ProvidersList-ma-_MlLo.js +0 -1
- package/dist/assets/SearchConfig-C9iBt7pl.js +0 -1
- package/dist/assets/SessionsConfig-2r2yAGZg.js +0 -2
- package/dist/assets/chat-session-display-DkAC5OMC.js +0 -1
- package/dist/assets/config-zvnxSXSP.js +0 -1
- package/dist/assets/dist-BCXX7FD-.js +0 -15
- package/dist/assets/i18n-DJg9BPYk.js +0 -1
- package/dist/assets/index-BoJbxdvZ.css +0 -1
- package/dist/assets/index-CtlT4E9Y.js +0 -6
- package/dist/assets/infiniteQueryBehavior-CTcVlD9s.js +0 -1
- package/dist/assets/loader-circle-B60I0hEk.js +0 -1
- package/dist/assets/plus-CR7RfK3H.js +0 -1
- package/dist/assets/react-BB4jko2M.js +0 -1
- package/dist/assets/search-C60UA27E.js +0 -1
- package/dist/assets/security-config-BkFDYZ6j.js +0 -1
- package/dist/assets/skeleton-uxz_5h3A.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-C8jBv11-.js +0 -1
- package/dist/assets/useMutation-BjBOKHj_.js +0 -1
- package/dist/assets/x-BfTu-g7D.js +0 -1
- package/src/components/chat/ChatSessionsSidebar.tsx +0 -100
- /package/dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
- /package/dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { create } from 'zustand';
|
|
1
|
+
import { create, type StateCreator } from 'zustand';
|
|
2
2
|
import { createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
|
|
3
|
+
import type { SessionRunStatus } from '@/lib/session-run-status';
|
|
3
4
|
|
|
4
5
|
export type ChatSessionListMode = 'time-first' | 'project-first';
|
|
5
6
|
|
|
@@ -11,11 +12,47 @@ export type ChatSessionListSnapshot = {
|
|
|
11
12
|
listMode: ChatSessionListMode;
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
export function hasUnreadSessionUpdate(
|
|
16
|
+
updatedAt: string | null | undefined,
|
|
17
|
+
readUpdatedAt: string | undefined,
|
|
18
|
+
): boolean {
|
|
19
|
+
const normalizedUpdatedAt = updatedAt?.trim();
|
|
20
|
+
if (!normalizedUpdatedAt) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const normalizedReadUpdatedAt = readUpdatedAt?.trim();
|
|
24
|
+
if (!normalizedReadUpdatedAt) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return normalizedUpdatedAt.localeCompare(normalizedReadUpdatedAt) > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shouldShowUnreadSessionIndicator(params: {
|
|
31
|
+
active: boolean;
|
|
32
|
+
updatedAt: string | null | undefined;
|
|
33
|
+
readUpdatedAt: string | undefined;
|
|
34
|
+
runStatus?: SessionRunStatus;
|
|
35
|
+
}): boolean {
|
|
36
|
+
const { active, readUpdatedAt, runStatus, updatedAt } = params;
|
|
37
|
+
if (active || runStatus === 'running') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return hasUnreadSessionUpdate(updatedAt, readUpdatedAt);
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
type ChatSessionListStore = {
|
|
15
44
|
snapshot: ChatSessionListSnapshot;
|
|
45
|
+
readUpdatedAtBySessionKey: Record<string, string>;
|
|
46
|
+
hasHydratedReadWatermarks: boolean;
|
|
16
47
|
setSnapshot: (patch: Partial<ChatSessionListSnapshot>) => void;
|
|
48
|
+
markSessionRead: (sessionKey: string, updatedAt: string | null | undefined) => void;
|
|
49
|
+
hydrateReadWatermarks: (
|
|
50
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
51
|
+
) => void;
|
|
17
52
|
};
|
|
18
53
|
|
|
54
|
+
type ChatSessionListStoreSet = Parameters<StateCreator<ChatSessionListStore>>[0];
|
|
55
|
+
|
|
19
56
|
const initialSnapshot: ChatSessionListSnapshot = {
|
|
20
57
|
selectedSessionKey: null,
|
|
21
58
|
draftSessionKey: createNcpSessionId(),
|
|
@@ -24,13 +61,67 @@ const initialSnapshot: ChatSessionListSnapshot = {
|
|
|
24
61
|
listMode: 'time-first'
|
|
25
62
|
};
|
|
26
63
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
setSnapshot: (patch) =>
|
|
64
|
+
function createSetSnapshotAction(set: ChatSessionListStoreSet) {
|
|
65
|
+
return (patch: Partial<ChatSessionListSnapshot>) =>
|
|
30
66
|
set((state) => ({
|
|
31
67
|
snapshot: {
|
|
32
68
|
...state.snapshot,
|
|
33
69
|
...patch
|
|
34
70
|
}
|
|
35
|
-
}))
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createMarkSessionReadAction(set: ChatSessionListStoreSet) {
|
|
75
|
+
return (sessionKey: string, updatedAt: string | null | undefined) =>
|
|
76
|
+
set((state) => {
|
|
77
|
+
const normalizedSessionKey = sessionKey.trim();
|
|
78
|
+
const normalizedUpdatedAt = updatedAt?.trim();
|
|
79
|
+
if (!normalizedSessionKey || !normalizedUpdatedAt) {
|
|
80
|
+
return state;
|
|
81
|
+
}
|
|
82
|
+
if (state.readUpdatedAtBySessionKey[normalizedSessionKey] === normalizedUpdatedAt) {
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
readUpdatedAtBySessionKey: {
|
|
88
|
+
...state.readUpdatedAtBySessionKey,
|
|
89
|
+
[normalizedSessionKey]: normalizedUpdatedAt
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createHydrateReadWatermarksAction(set: ChatSessionListStoreSet) {
|
|
96
|
+
return (
|
|
97
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
98
|
+
) =>
|
|
99
|
+
set((state) => {
|
|
100
|
+
if (state.hasHydratedReadWatermarks) {
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
const nextReadUpdatedAtBySessionKey = { ...state.readUpdatedAtBySessionKey };
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const normalizedSessionKey = entry.sessionKey.trim();
|
|
106
|
+
const normalizedUpdatedAt = entry.updatedAt?.trim();
|
|
107
|
+
if (!normalizedSessionKey || !normalizedUpdatedAt || nextReadUpdatedAtBySessionKey[normalizedSessionKey]) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
nextReadUpdatedAtBySessionKey[normalizedSessionKey] = normalizedUpdatedAt;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
...state,
|
|
114
|
+
hasHydratedReadWatermarks: true,
|
|
115
|
+
readUpdatedAtBySessionKey: nextReadUpdatedAtBySessionKey
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const useChatSessionListStore = create<ChatSessionListStore>((set) => ({
|
|
121
|
+
snapshot: initialSnapshot,
|
|
122
|
+
readUpdatedAtBySessionKey: {},
|
|
123
|
+
hasHydratedReadWatermarks: false,
|
|
124
|
+
setSnapshot: createSetSnapshotAction(set),
|
|
125
|
+
markSessionRead: createMarkSessionReadAction(set),
|
|
126
|
+
hydrateReadWatermarks: createHydrateReadWatermarksAction(set)
|
|
36
127
|
}));
|
|
@@ -143,6 +143,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
143
143
|
() => providerAuth?.methods ?? [],
|
|
144
144
|
[providerAuth?.methods]
|
|
145
145
|
);
|
|
146
|
+
const supportsWireApi = Boolean(providerSpec?.supportsWireApi) || isCustomProvider;
|
|
146
147
|
const providerAuthMethodOptions = useMemo(
|
|
147
148
|
() =>
|
|
148
149
|
providerAuthMethods.map((method) => ({
|
|
@@ -174,25 +175,18 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
174
175
|
() => providerAuthMethods.find((method) => method.id === resolvedAuthMethodId),
|
|
175
176
|
[providerAuthMethods, resolvedAuthMethodId]
|
|
176
177
|
);
|
|
177
|
-
const selectedAuthMethodHint =
|
|
178
|
-
selectedAuthMethod?.hint?.[language] || selectedAuthMethod?.hint?.en || '';
|
|
179
178
|
const shouldUseAuthMethodPills = shouldUsePillSelector({
|
|
180
179
|
required: providerAuth?.kind === 'device_code',
|
|
181
180
|
hasDefault: Boolean(providerAuth?.defaultMethodId?.trim()),
|
|
182
181
|
optionCount: providerAuthMethods.length
|
|
183
182
|
});
|
|
184
|
-
const providerAuthNote =
|
|
185
|
-
providerAuth?.note?.[language] ||
|
|
186
|
-
providerAuth?.note?.en ||
|
|
187
|
-
providerAuth?.displayName ||
|
|
188
|
-
'';
|
|
189
183
|
const wireApiOptions = providerSpec?.wireApiOptions || ['auto', 'chat', 'responses'];
|
|
190
184
|
const wireApiSelectOptions: PillSelectOption[] = wireApiOptions.map((option) => ({
|
|
191
185
|
value: option,
|
|
192
186
|
label: option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')
|
|
193
187
|
}));
|
|
194
188
|
const shouldUseWireApiPills = shouldUsePillSelector({
|
|
195
|
-
required:
|
|
189
|
+
required: supportsWireApi,
|
|
196
190
|
hasDefault: typeof providerSpec?.defaultWireApi === 'string' && providerSpec.defaultWireApi.length > 0,
|
|
197
191
|
optionCount: wireApiSelectOptions.length
|
|
198
192
|
});
|
|
@@ -302,7 +296,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
302
296
|
const apiKeyChanged = apiKey.trim().length > 0;
|
|
303
297
|
const apiBaseChanged = apiBase.trim() !== currentApiBase.trim();
|
|
304
298
|
const headersChanged = !headersEqual(extraHeaders, currentHeaders);
|
|
305
|
-
const wireApiChanged =
|
|
299
|
+
const wireApiChanged = supportsWireApi ? wireApi !== currentWireApi : false;
|
|
306
300
|
const modelsChanged = !modelListsEqual(models, currentEditableModels);
|
|
307
301
|
const modelThinkingChanged = !modelThinkingEqual(modelThinking, currentModelThinking);
|
|
308
302
|
const displayNameChanged = isCustomProvider
|
|
@@ -331,7 +325,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
331
325
|
currentApiBase,
|
|
332
326
|
extraHeaders,
|
|
333
327
|
currentHeaders,
|
|
334
|
-
|
|
328
|
+
supportsWireApi,
|
|
335
329
|
wireApi,
|
|
336
330
|
currentWireApi,
|
|
337
331
|
models,
|
|
@@ -422,7 +416,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
422
416
|
payload.extraHeaders = normalizedHeaders;
|
|
423
417
|
}
|
|
424
418
|
|
|
425
|
-
if (
|
|
419
|
+
if (supportsWireApi && wireApi !== currentWireApi) {
|
|
426
420
|
payload.wireApi = wireApi;
|
|
427
421
|
}
|
|
428
422
|
if (!modelListsEqual(models, currentEditableModels)) {
|
|
@@ -450,7 +444,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
450
444
|
if (apiKey.trim().length > 0) {
|
|
451
445
|
payload.apiKey = apiKey.trim();
|
|
452
446
|
}
|
|
453
|
-
if (
|
|
447
|
+
if (supportsWireApi) {
|
|
454
448
|
payload.wireApi = wireApi;
|
|
455
449
|
}
|
|
456
450
|
|
|
@@ -608,10 +602,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
608
602
|
|
|
609
603
|
<ProviderAuthSection
|
|
610
604
|
providerAuth={providerAuth}
|
|
611
|
-
providerAuthNote={
|
|
605
|
+
providerAuthNote={providerAuth?.note?.[language] || providerAuth?.note?.en || providerAuth?.displayName || ''}
|
|
612
606
|
providerAuthMethodOptions={providerAuthMethodOptions}
|
|
613
607
|
providerAuthMethodsCount={providerAuthMethods.length}
|
|
614
|
-
selectedAuthMethodHint={
|
|
608
|
+
selectedAuthMethodHint={selectedAuthMethod?.hint?.[language] || selectedAuthMethod?.hint?.en || ''}
|
|
615
609
|
shouldUseAuthMethodPills={shouldUseAuthMethodPills}
|
|
616
610
|
resolvedAuthMethodId={resolvedAuthMethodId}
|
|
617
611
|
onAuthMethodChange={setAuthMethodId}
|
|
@@ -663,7 +657,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
663
657
|
<ProviderAdvancedSettingsSection
|
|
664
658
|
showAdvanced={showAdvanced}
|
|
665
659
|
onShowAdvancedChange={setShowAdvanced}
|
|
666
|
-
supportsWireApi={
|
|
660
|
+
supportsWireApi={supportsWireApi}
|
|
667
661
|
wireApiLabel={wireApiHint?.label ?? t('wireApi')}
|
|
668
662
|
wireApi={wireApi}
|
|
669
663
|
onWireApiChange={setWireApi}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
4
|
+
import { Label } from '@/components/ui/label';
|
|
5
|
+
import { Switch } from '@/components/ui/switch';
|
|
6
|
+
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
7
|
+
import { desktopUpdateManager } from '@/desktop/managers/desktop-update.manager';
|
|
8
|
+
import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
|
|
9
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
import { Download, ExternalLink, RefreshCw, RotateCw } from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
function formatVersion(value: string | null): string {
|
|
14
|
+
return value?.trim() || '-';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatLastCheckedAt(value: string | null): string {
|
|
18
|
+
return value ? formatDateTime(value) : '-';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getStatusLabel(status: string): string {
|
|
22
|
+
if (status === 'checking') {
|
|
23
|
+
return t('desktopUpdatesStatusChecking');
|
|
24
|
+
}
|
|
25
|
+
if (status === 'update-available') {
|
|
26
|
+
return t('desktopUpdatesStatusAvailable');
|
|
27
|
+
}
|
|
28
|
+
if (status === 'downloading') {
|
|
29
|
+
return t('desktopUpdatesStatusDownloading');
|
|
30
|
+
}
|
|
31
|
+
if (status === 'downloaded') {
|
|
32
|
+
return t('desktopUpdatesStatusDownloaded');
|
|
33
|
+
}
|
|
34
|
+
if (status === 'up-to-date') {
|
|
35
|
+
return t('desktopUpdatesStatusUpToDate');
|
|
36
|
+
}
|
|
37
|
+
if (status === 'failed') {
|
|
38
|
+
return t('desktopUpdatesStatusFailed');
|
|
39
|
+
}
|
|
40
|
+
return t('desktopUpdatesStatusIdle');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getStatusTone(status: string): string {
|
|
44
|
+
if (status === 'downloaded') {
|
|
45
|
+
return 'bg-emerald-50 text-emerald-700 ring-emerald-100';
|
|
46
|
+
}
|
|
47
|
+
if (status === 'update-available' || status === 'downloading' || status === 'checking') {
|
|
48
|
+
return 'bg-amber-50 text-amber-700 ring-amber-100';
|
|
49
|
+
}
|
|
50
|
+
if (status === 'failed') {
|
|
51
|
+
return 'bg-red-50 text-red-700 ring-red-100';
|
|
52
|
+
}
|
|
53
|
+
return 'bg-gray-100 text-gray-700 ring-gray-200';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DesktopUpdateConfig() {
|
|
57
|
+
const { supported, initialized, busyAction, snapshot } = useDesktopUpdateStore();
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
void desktopUpdateManager.start();
|
|
61
|
+
return () => {
|
|
62
|
+
desktopUpdateManager.stop();
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
if (!initialized) {
|
|
67
|
+
return <div className="p-8 text-gray-400">{t('loading')}</div>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!supported || !snapshot) {
|
|
71
|
+
return (
|
|
72
|
+
<PageLayout className="space-y-6">
|
|
73
|
+
<PageHeader
|
|
74
|
+
title={t('desktopUpdatesPageTitle')}
|
|
75
|
+
description={t('desktopUpdatesPageDescription')}
|
|
76
|
+
/>
|
|
77
|
+
<Card>
|
|
78
|
+
<CardHeader>
|
|
79
|
+
<CardTitle>{t('desktopUpdatesDesktopOnlyTitle')}</CardTitle>
|
|
80
|
+
<CardDescription>{t('desktopUpdatesDesktopOnlyDescription')}</CardDescription>
|
|
81
|
+
</CardHeader>
|
|
82
|
+
<CardContent>
|
|
83
|
+
<p className="text-sm text-gray-500">{t('desktopUpdatesDesktopOnlyFutureHint')}</p>
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
</PageLayout>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isChecking = busyAction === 'checking';
|
|
91
|
+
const isDownloading = busyAction === 'downloading';
|
|
92
|
+
const isApplying = busyAction === 'applying';
|
|
93
|
+
const isSavingPreferences = busyAction === 'saving-preferences';
|
|
94
|
+
const canDownload = snapshot.status === 'update-available' && !isDownloading && !isApplying;
|
|
95
|
+
const canApply = snapshot.status === 'downloaded' && !isApplying;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<PageLayout className="space-y-6">
|
|
99
|
+
<PageHeader
|
|
100
|
+
title={t('desktopUpdatesPageTitle')}
|
|
101
|
+
description={t('desktopUpdatesPageDescription')}
|
|
102
|
+
actions={(
|
|
103
|
+
<Button
|
|
104
|
+
variant="outline"
|
|
105
|
+
onClick={() => void desktopUpdateManager.checkForUpdates()}
|
|
106
|
+
disabled={isChecking || isDownloading || isApplying}
|
|
107
|
+
>
|
|
108
|
+
<RefreshCw className={cn('mr-2 h-4 w-4', isChecking && 'animate-spin')} />
|
|
109
|
+
{t('desktopUpdatesCheckNow')}
|
|
110
|
+
</Button>
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<Card>
|
|
115
|
+
<CardHeader>
|
|
116
|
+
<CardTitle>{t('desktopUpdatesOverviewTitle')}</CardTitle>
|
|
117
|
+
<CardDescription>{t('desktopUpdatesOverviewDescription')}</CardDescription>
|
|
118
|
+
</CardHeader>
|
|
119
|
+
<CardContent className="space-y-5">
|
|
120
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
121
|
+
<span className="text-sm font-medium text-gray-700">{t('desktopUpdatesStatusLabel')}</span>
|
|
122
|
+
<span className={cn('inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1', getStatusTone(snapshot.status))}>
|
|
123
|
+
{getStatusLabel(snapshot.status)}
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
128
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
129
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{t('desktopUpdatesLauncherVersion')}</p>
|
|
130
|
+
<p className="mt-2 text-base font-semibold text-gray-900">{formatVersion(snapshot.launcherVersion)}</p>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
133
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{t('desktopUpdatesCurrentBundleVersion')}</p>
|
|
134
|
+
<p className="mt-2 text-base font-semibold text-gray-900">{formatVersion(snapshot.currentVersion)}</p>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
137
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{t('desktopUpdatesAvailableVersion')}</p>
|
|
138
|
+
<p className="mt-2 text-base font-semibold text-gray-900">{formatVersion(snapshot.availableVersion)}</p>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
141
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-gray-500">{t('desktopUpdatesLastCheckedAt')}</p>
|
|
142
|
+
<p className="mt-2 text-base font-semibold text-gray-900">{formatLastCheckedAt(snapshot.lastCheckedAt)}</p>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{snapshot.downloadedVersion ? (
|
|
147
|
+
<div className="rounded-2xl border border-emerald-200 bg-emerald-50/70 p-4">
|
|
148
|
+
<p className="text-sm font-semibold text-emerald-800">{t('desktopUpdatesDownloadedBannerTitle')}</p>
|
|
149
|
+
<p className="mt-1 text-sm text-emerald-700">
|
|
150
|
+
{t('desktopUpdatesDownloadedBannerDescription').replace('{version}', snapshot.downloadedVersion)}
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
) : null}
|
|
154
|
+
|
|
155
|
+
{snapshot.errorMessage ? (
|
|
156
|
+
<div className="rounded-2xl border border-red-200 bg-red-50/70 p-4 text-sm text-red-700">
|
|
157
|
+
{snapshot.errorMessage}
|
|
158
|
+
</div>
|
|
159
|
+
) : null}
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
|
|
163
|
+
<Card>
|
|
164
|
+
<CardHeader>
|
|
165
|
+
<CardTitle>{t('desktopUpdatesPreferencesTitle')}</CardTitle>
|
|
166
|
+
<CardDescription>{t('desktopUpdatesPreferencesDescription')}</CardDescription>
|
|
167
|
+
</CardHeader>
|
|
168
|
+
<CardContent className="space-y-5">
|
|
169
|
+
<div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
|
|
170
|
+
<div className="space-y-1">
|
|
171
|
+
<Label>{t('desktopUpdatesAutomaticChecks')}</Label>
|
|
172
|
+
<p className="text-sm text-gray-500">{t('desktopUpdatesAutomaticChecksHelp')}</p>
|
|
173
|
+
</div>
|
|
174
|
+
<Switch
|
|
175
|
+
checked={snapshot.preferences.automaticChecks}
|
|
176
|
+
disabled={isSavingPreferences}
|
|
177
|
+
onCheckedChange={(checked) => void desktopUpdateManager.updatePreferences({ automaticChecks: checked })}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="flex items-start justify-between gap-4 rounded-xl border border-gray-200 p-4">
|
|
182
|
+
<div className="space-y-1">
|
|
183
|
+
<Label>{t('desktopUpdatesAutoDownload')}</Label>
|
|
184
|
+
<p className="text-sm text-gray-500">{t('desktopUpdatesAutoDownloadHelp')}</p>
|
|
185
|
+
</div>
|
|
186
|
+
<Switch
|
|
187
|
+
checked={snapshot.preferences.autoDownload}
|
|
188
|
+
disabled={isSavingPreferences}
|
|
189
|
+
onCheckedChange={(checked) => void desktopUpdateManager.updatePreferences({ autoDownload: checked })}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
</CardContent>
|
|
193
|
+
</Card>
|
|
194
|
+
|
|
195
|
+
<Card>
|
|
196
|
+
<CardHeader>
|
|
197
|
+
<CardTitle>{t('desktopUpdatesActionsTitle')}</CardTitle>
|
|
198
|
+
<CardDescription>{t('desktopUpdatesActionsDescription')}</CardDescription>
|
|
199
|
+
</CardHeader>
|
|
200
|
+
<CardContent className="flex flex-wrap items-center gap-3">
|
|
201
|
+
<Button
|
|
202
|
+
variant="outline"
|
|
203
|
+
onClick={() => void desktopUpdateManager.checkForUpdates()}
|
|
204
|
+
disabled={isChecking || isDownloading || isApplying}
|
|
205
|
+
>
|
|
206
|
+
<RefreshCw className={cn('mr-2 h-4 w-4', isChecking && 'animate-spin')} />
|
|
207
|
+
{t('desktopUpdatesCheckNow')}
|
|
208
|
+
</Button>
|
|
209
|
+
|
|
210
|
+
<Button onClick={() => void desktopUpdateManager.downloadUpdate()} disabled={!canDownload}>
|
|
211
|
+
<Download className={cn('mr-2 h-4 w-4', isDownloading && 'animate-bounce')} />
|
|
212
|
+
{t('desktopUpdatesDownloadNow')}
|
|
213
|
+
</Button>
|
|
214
|
+
|
|
215
|
+
<Button variant="secondary" onClick={() => void desktopUpdateManager.applyDownloadedUpdate()} disabled={!canApply}>
|
|
216
|
+
<RotateCw className={cn('mr-2 h-4 w-4', isApplying && 'animate-spin')} />
|
|
217
|
+
{t('desktopUpdatesApplyNow')}
|
|
218
|
+
</Button>
|
|
219
|
+
|
|
220
|
+
{snapshot.releaseNotesUrl ? (
|
|
221
|
+
<Button variant="ghost" onClick={() => window.open(snapshot.releaseNotesUrl ?? '', '_blank', 'noopener,noreferrer')}>
|
|
222
|
+
<ExternalLink className="mr-2 h-4 w-4" />
|
|
223
|
+
{t('desktopUpdatesReleaseNotes')}
|
|
224
|
+
</Button>
|
|
225
|
+
) : null}
|
|
226
|
+
</CardContent>
|
|
227
|
+
</Card>
|
|
228
|
+
</PageLayout>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
3
3
|
import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
4
|
-
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search, Shield, Wrench, Wifi, Bot } from 'lucide-react';
|
|
4
|
+
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search, Shield, Wrench, Wifi, Bot, Download } from 'lucide-react';
|
|
5
5
|
import { NavLink } from 'react-router-dom';
|
|
6
6
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
7
7
|
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
@@ -105,6 +105,11 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
105
105
|
label: t('runtime'),
|
|
106
106
|
icon: GitBranch,
|
|
107
107
|
},
|
|
108
|
+
{
|
|
109
|
+
target: '/updates',
|
|
110
|
+
label: t('updates'),
|
|
111
|
+
icon: Download,
|
|
112
|
+
},
|
|
108
113
|
{
|
|
109
114
|
target: '/remote',
|
|
110
115
|
label: t('remote'),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type DesktopUpdateStatus =
|
|
2
|
+
| 'idle'
|
|
3
|
+
| 'checking'
|
|
4
|
+
| 'update-available'
|
|
5
|
+
| 'downloading'
|
|
6
|
+
| 'downloaded'
|
|
7
|
+
| 'up-to-date'
|
|
8
|
+
| 'failed';
|
|
9
|
+
|
|
10
|
+
export type DesktopUpdatePreferences = {
|
|
11
|
+
automaticChecks: boolean;
|
|
12
|
+
autoDownload: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type DesktopUpdateSnapshot = {
|
|
16
|
+
status: DesktopUpdateStatus;
|
|
17
|
+
launcherVersion: string;
|
|
18
|
+
currentVersion: string | null;
|
|
19
|
+
availableVersion: string | null;
|
|
20
|
+
downloadedVersion: string | null;
|
|
21
|
+
releaseNotesUrl: string | null;
|
|
22
|
+
lastCheckedAt: string | null;
|
|
23
|
+
errorMessage: string | null;
|
|
24
|
+
preferences: DesktopUpdatePreferences;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type NextClawDesktopBridge = {
|
|
28
|
+
platform: string;
|
|
29
|
+
version: string;
|
|
30
|
+
getUpdateState: () => Promise<DesktopUpdateSnapshot>;
|
|
31
|
+
checkForUpdates: () => Promise<DesktopUpdateSnapshot>;
|
|
32
|
+
downloadUpdate: () => Promise<DesktopUpdateSnapshot>;
|
|
33
|
+
applyDownloadedUpdate: () => Promise<DesktopUpdateSnapshot>;
|
|
34
|
+
updatePreferences: (preferences: Partial<DesktopUpdatePreferences>) => Promise<DesktopUpdateSnapshot>;
|
|
35
|
+
onUpdateStateChanged: (listener: (snapshot: DesktopUpdateSnapshot) => void) => () => void;
|
|
36
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DesktopUpdatePreferences,
|
|
3
|
+
DesktopUpdateSnapshot,
|
|
4
|
+
NextClawDesktopBridge
|
|
5
|
+
} from '@/desktop/desktop-update.types';
|
|
6
|
+
import { useDesktopUpdateStore } from '@/desktop/stores/desktop-update.store';
|
|
7
|
+
import { t } from '@/lib/i18n';
|
|
8
|
+
import { toast } from 'sonner';
|
|
9
|
+
|
|
10
|
+
type DesktopUpdateBusyAction = 'checking' | 'downloading' | 'applying' | 'saving-preferences';
|
|
11
|
+
|
|
12
|
+
export class DesktopUpdateManager {
|
|
13
|
+
private unsubscribe: (() => void) | null = null;
|
|
14
|
+
|
|
15
|
+
start = async () => {
|
|
16
|
+
const desktopApi = this.getDesktopApi();
|
|
17
|
+
if (!desktopApi) {
|
|
18
|
+
useDesktopUpdateStore.setState({
|
|
19
|
+
supported: false,
|
|
20
|
+
initialized: true,
|
|
21
|
+
snapshot: null
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!this.unsubscribe) {
|
|
27
|
+
this.unsubscribe = desktopApi.onUpdateStateChanged((snapshot) => {
|
|
28
|
+
useDesktopUpdateStore.setState({
|
|
29
|
+
supported: true,
|
|
30
|
+
initialized: true,
|
|
31
|
+
snapshot
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
useDesktopUpdateStore.setState({
|
|
37
|
+
supported: true,
|
|
38
|
+
initialized: false
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const snapshot = await desktopApi.getUpdateState();
|
|
43
|
+
useDesktopUpdateStore.setState({
|
|
44
|
+
supported: true,
|
|
45
|
+
initialized: true,
|
|
46
|
+
snapshot
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
useDesktopUpdateStore.setState({
|
|
50
|
+
supported: true,
|
|
51
|
+
initialized: true
|
|
52
|
+
});
|
|
53
|
+
toast.error(`${t('desktopUpdatesLoadFailed')}: ${this.getErrorMessage(error)}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
stop = () => {
|
|
58
|
+
this.unsubscribe?.();
|
|
59
|
+
this.unsubscribe = null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
checkForUpdates = async () => {
|
|
63
|
+
let snapshot: DesktopUpdateSnapshot;
|
|
64
|
+
try {
|
|
65
|
+
snapshot = await this.runSnapshotCommand('checking', t('desktopUpdatesCheckFailed'), async (desktopApi) => {
|
|
66
|
+
return await desktopApi.checkForUpdates();
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (snapshot.status === 'up-to-date') {
|
|
73
|
+
toast.success(t('desktopUpdatesAlreadyLatest'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (snapshot.status === 'update-available') {
|
|
77
|
+
toast.success(
|
|
78
|
+
t('desktopUpdatesAvailable').replace('{version}', snapshot.availableVersion ?? t('desktopUpdatesUnknownVersion'))
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (snapshot.status === 'downloaded') {
|
|
83
|
+
toast.success(t('desktopUpdatesReadyToApply'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (snapshot.status === 'failed' && snapshot.errorMessage) {
|
|
87
|
+
toast.error(snapshot.errorMessage);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
downloadUpdate = async () => {
|
|
92
|
+
let snapshot: DesktopUpdateSnapshot;
|
|
93
|
+
try {
|
|
94
|
+
snapshot = await this.runSnapshotCommand('downloading', t('desktopUpdatesDownloadFailed'), async (desktopApi) => {
|
|
95
|
+
return await desktopApi.downloadUpdate();
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (snapshot.status === 'downloaded') {
|
|
102
|
+
toast.success(t('desktopUpdatesReadyToApply'));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
applyDownloadedUpdate = async () => {
|
|
107
|
+
try {
|
|
108
|
+
await this.runSnapshotCommand('applying', t('desktopUpdatesApplyFailed'), async (desktopApi) => {
|
|
109
|
+
return await desktopApi.applyDownloadedUpdate();
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
updatePreferences = async (preferences: Partial<DesktopUpdatePreferences>) => {
|
|
117
|
+
try {
|
|
118
|
+
await this.runSnapshotCommand(
|
|
119
|
+
'saving-preferences',
|
|
120
|
+
t('desktopUpdatesPreferencesFailed'),
|
|
121
|
+
async (desktopApi) => await desktopApi.updatePreferences(preferences)
|
|
122
|
+
);
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
private runSnapshotCommand = async (
|
|
129
|
+
busyAction: DesktopUpdateBusyAction,
|
|
130
|
+
fallbackMessage: string,
|
|
131
|
+
job: (desktopApi: NextClawDesktopBridge) => Promise<DesktopUpdateSnapshot>
|
|
132
|
+
): Promise<DesktopUpdateSnapshot> => {
|
|
133
|
+
const desktopApi = this.getDesktopApi();
|
|
134
|
+
if (!desktopApi) {
|
|
135
|
+
throw new Error(t('desktopUpdatesDesktopOnlyDescription'));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
useDesktopUpdateStore.setState({ busyAction });
|
|
139
|
+
try {
|
|
140
|
+
const snapshot = await job(desktopApi);
|
|
141
|
+
useDesktopUpdateStore.setState({ snapshot });
|
|
142
|
+
return snapshot;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
toast.error(`${fallbackMessage}: ${this.getErrorMessage(error)}`);
|
|
145
|
+
throw error;
|
|
146
|
+
} finally {
|
|
147
|
+
useDesktopUpdateStore.setState({ busyAction: null });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
private getDesktopApi = (): NextClawDesktopBridge | null => {
|
|
152
|
+
if (typeof window === 'undefined') {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return window.nextclawDesktop ?? null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
private getErrorMessage = (error: unknown): string => {
|
|
159
|
+
return error instanceof Error ? error.message : t('error');
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const desktopUpdateManager = new DesktopUpdateManager();
|