@nextclaw/ui 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +16 -2
  2. package/dist/assets/ChannelsList-CIMYaIji.js +1 -0
  3. package/dist/assets/{ChatPage-DM1ewbWf.js → ChatPage-B5UpeEIp.js} +2 -2
  4. package/dist/assets/{DocBrowser-BLv77lJ0.js → DocBrowser-BJ610SPa.js} +1 -1
  5. package/dist/assets/{LogoBadge-D7j1al-w.js → LogoBadge-BKq1GKWP.js} +1 -1
  6. package/dist/assets/MarketplacePage-Bs3sLsgx.js +49 -0
  7. package/dist/assets/{McpMarketplacePage-DpMjaD3m.js → McpMarketplacePage-BWTguHCs.js} +2 -2
  8. package/dist/assets/ModelConfig-B-oTP-Bc.js +1 -0
  9. package/dist/assets/ProvidersList-r7bD0-R0.js +1 -0
  10. package/dist/assets/RemoteAccessPage-D7On6waK.js +1 -0
  11. package/dist/assets/{RuntimeConfig-BbX4yFKy.js → RuntimeConfig-C11xVxH9.js} +1 -1
  12. package/dist/assets/{SearchConfig-BmmmeyJd.js → SearchConfig-BVZdCxiM.js} +1 -1
  13. package/dist/assets/{SecretsConfig-CWG8J01H.js → SecretsConfig-DuEDdC3X.js} +2 -2
  14. package/dist/assets/SessionsConfig-Y-Blf_-K.js +2 -0
  15. package/dist/assets/{chat-message-CGXiVhyN.js → chat-message-B6VCCEXF.js} +1 -1
  16. package/dist/assets/index-DfEAJJsA.css +1 -0
  17. package/dist/assets/index-DvA7S11O.js +8 -0
  18. package/dist/assets/{label-CCSffS1D.js → label-DzwitL78.js} +1 -1
  19. package/dist/assets/{page-layout-ud8wZ8gX.js → page-layout-DEq5N_8L.js} +1 -1
  20. package/dist/assets/popover-CY54V8F6.js +1 -0
  21. package/dist/assets/provider-models-BOeNnjk9.js +1 -0
  22. package/dist/assets/{security-config-DJJUCMov.js → security-config-CgbYP57d.js} +1 -1
  23. package/dist/assets/skeleton-zjQZMWu9.js +1 -0
  24. package/dist/assets/{status-dot-Fz9-eKsl.js → status-dot-CU_P0tvO.js} +1 -1
  25. package/dist/assets/{switch-B-_SrMSL.js → switch-PvjTvlcs.js} +1 -1
  26. package/dist/assets/{tabs-custom-6Tm1ZHfS.js → tabs-custom-Bke5J9ny.js} +1 -1
  27. package/dist/assets/useConfirmDialog-8tzzp_oW.js +1 -0
  28. package/dist/assets/vendor-CmQZsDAE.js +436 -0
  29. package/dist/index.html +3 -3
  30. package/package.json +4 -4
  31. package/src/App.tsx +36 -39
  32. package/src/account/components/account-panel.tsx +93 -0
  33. package/src/account/managers/account.manager.ts +179 -0
  34. package/src/account/stores/account.store.ts +68 -0
  35. package/src/api/types.ts +2 -0
  36. package/src/app-query-client.ts +10 -0
  37. package/src/components/config/ProviderForm.tsx +91 -641
  38. package/src/components/config/ProvidersList.tsx +10 -5
  39. package/src/components/config/provider-advanced-settings-section.tsx +92 -0
  40. package/src/components/config/provider-auth-section.tsx +113 -0
  41. package/src/components/config/provider-enabled-field.tsx +20 -0
  42. package/src/components/config/provider-form-support.ts +344 -0
  43. package/src/components/config/provider-models-section.tsx +198 -0
  44. package/src/components/config/provider-pill-selector.tsx +39 -0
  45. package/src/components/config/provider-status-badge.tsx +21 -0
  46. package/src/components/layout/Sidebar.tsx +26 -0
  47. package/src/components/remote/RemoteAccessPage.tsx +162 -442
  48. package/src/hooks/useRemoteAccess.ts +7 -6
  49. package/src/lib/i18n.remote.ts +108 -4
  50. package/src/lib/provider-models.ts +2 -2
  51. package/src/presenter/app-presenter-context.tsx +20 -0
  52. package/src/presenter/app.presenter.ts +12 -0
  53. package/src/remote/managers/remote-access.manager.ts +196 -0
  54. package/src/remote/remote-access.query.ts +78 -0
  55. package/src/remote/stores/remote-access.store.ts +44 -0
  56. package/dist/assets/ChannelsList-Byfj2R01.js +0 -1
  57. package/dist/assets/MarketplacePage-DuskLKYh.js +0 -49
  58. package/dist/assets/ModelConfig-ubaecweS.js +0 -1
  59. package/dist/assets/ProvidersList-w8MJH2LI.js +0 -1
  60. package/dist/assets/RemoteAccessPage-D79_5Kbn.js +0 -1
  61. package/dist/assets/SessionsConfig-D-vg_Lgv.js +0 -2
  62. package/dist/assets/index-COrhpAdh.css +0 -1
  63. package/dist/assets/index-CeRbsQ90.js +0 -8
  64. package/dist/assets/index-Ct7FQpxN.js +0 -1
  65. package/dist/assets/popover-Bfoe6YBX.js +0 -1
  66. package/dist/assets/provider-models-D3B_xWXx.js +0 -1
  67. package/dist/assets/skeleton-IOOTmHzP.js +0 -1
  68. package/dist/assets/useConfirmDialog-BeOW2bOI.js +0 -5
  69. package/dist/assets/vendor-CwsIoNvJ.js +0 -442
@@ -1,449 +1,112 @@
1
- import { useEffect, useMemo, useState } from 'react';
2
- import type { RemoteRuntimeView, RemoteServiceView } from '@/api/types';
3
- import {
4
- useRemoteDoctor,
5
- useRemoteBrowserAuthPoll,
6
- useRemoteBrowserAuthStart,
7
- useRemoteLogout,
8
- useRemoteServiceControl,
9
- useRemoteSettings,
10
- useRemoteStatus
11
- } from '@/hooks/useRemoteAccess';
1
+ import type { RemoteAccessView } from '@/api/remote.types';
12
2
  import { PageHeader, PageLayout } from '@/components/layout/page-layout';
13
3
  import { Button } from '@/components/ui/button';
14
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
15
- import { Input } from '@/components/ui/input';
16
- import { Label } from '@/components/ui/label';
17
5
  import { StatusDot } from '@/components/ui/status-dot';
18
- import { Switch } from '@/components/ui/switch';
6
+ import { useRemoteStatus } from '@/hooks/useRemoteAccess';
19
7
  import { formatDateTime, t } from '@/lib/i18n';
20
- import { Activity, KeyRound, Laptop, RefreshCcw, ServerCog, ShieldCheck, SquareTerminal } from 'lucide-react';
21
- import type { RemoteAccessView, RemoteDoctorView } from '@/api/remote.types';
22
-
23
- function getRuntimeStatus(runtime: RemoteRuntimeView | null): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
24
- if (!runtime) {
25
- return { status: 'inactive', label: t('remoteRuntimeMissing') };
26
- }
27
- if (runtime.state === 'connected') {
28
- return { status: 'ready', label: t('remoteStateConnected') };
29
- }
30
- if (runtime.state === 'connecting') {
31
- return { status: 'warning', label: t('remoteStateConnecting') };
32
- }
33
- if (runtime.state === 'error') {
34
- return { status: 'warning', label: t('remoteStateError') };
35
- }
36
- if (runtime.state === 'disconnected') {
37
- return { status: 'warning', label: t('remoteStateDisconnected') };
38
- }
39
- return { status: 'inactive', label: t('remoteStateDisabled') };
40
- }
41
-
42
- function getServiceStatus(service: RemoteServiceView): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
43
- if (!service.running) {
44
- return { status: 'inactive', label: t('remoteServiceStopped') };
45
- }
46
- return service.currentProcess
47
- ? { status: 'ready', label: t('remoteServiceManagedRunning') }
48
- : { status: 'active', label: t('remoteServiceRunning') };
49
- }
8
+ import { useAppPresenter } from '@/presenter/app-presenter-context';
9
+ import { resolveRemoteWebBase } from '@/remote/remote-access.query';
10
+ import { useRemoteAccessStore } from '@/remote/stores/remote-access.store';
11
+ import { Laptop, RefreshCcw, SquareArrowOutUpRight } from 'lucide-react';
12
+ import { useEffect, useMemo } from 'react';
13
+
14
+ type RemoteHeroView = {
15
+ badgeStatus: 'active' | 'inactive' | 'ready' | 'setup' | 'warning';
16
+ badgeLabel: string;
17
+ title: string;
18
+ description: string;
19
+ };
50
20
 
51
21
  function KeyValueRow(props: { label: string; value?: string | number | null; muted?: boolean }) {
52
22
  const value = props.value === undefined || props.value === null || props.value === '' ? '-' : String(props.value);
53
23
  return (
54
24
  <div className="flex items-start justify-between gap-4 py-2 text-sm">
55
25
  <span className="text-gray-500">{props.label}</span>
56
- <span className={props.muted ? 'text-right text-gray-500' : 'text-right text-gray-800'}>{value}</span>
26
+ <span className={props.muted ? 'text-right text-gray-500' : 'text-right text-gray-900'}>{value}</span>
57
27
  </div>
58
28
  );
59
29
  }
60
30
 
61
- function resolvePlatformApiBase(platformApiBase: string, status: RemoteAccessView | undefined) {
62
- return platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined;
63
- }
64
-
65
- function useRemoteBrowserAuthFlow(props: {
66
- status: RemoteAccessView | undefined;
67
- platformApiBase: string;
68
- startBrowserAuth: ReturnType<typeof useRemoteBrowserAuthStart>;
69
- pollBrowserAuth: ReturnType<typeof useRemoteBrowserAuthPoll>;
70
- }) {
71
- const { status, platformApiBase, startBrowserAuth, pollBrowserAuth } = props;
72
- const [authSessionId, setAuthSessionId] = useState<string | null>(null);
73
- const [authVerificationUri, setAuthVerificationUri] = useState<string | null>(null);
74
- const [authStatusMessage, setAuthStatusMessage] = useState('');
75
- const [authExpiresAt, setAuthExpiresAt] = useState<string | null>(null);
76
- const [authPollIntervalMs, setAuthPollIntervalMs] = useState(1500);
77
-
78
- useEffect(() => {
79
- if (!status?.account.loggedIn) {
80
- return;
81
- }
82
- setAuthSessionId(null);
83
- setAuthVerificationUri(null);
84
- setAuthExpiresAt(null);
85
- setAuthStatusMessage('');
86
- setAuthPollIntervalMs(1500);
87
- }, [status?.account.loggedIn]);
88
-
89
- useEffect(() => {
90
- if (!authSessionId || status?.account.loggedIn) {
91
- return;
92
- }
31
+ function buildHeroView(status: RemoteAccessView | undefined): RemoteHeroView {
32
+ if (!status?.account.loggedIn) {
33
+ return {
34
+ badgeStatus: 'setup',
35
+ badgeLabel: t('statusSetup'),
36
+ title: t('remoteStatusNeedsSignIn'),
37
+ description: t('remoteStatusNeedsSignInDescription')
38
+ };
39
+ }
93
40
 
94
- let cancelled = false;
95
- const timerId = window.setTimeout(async () => {
96
- try {
97
- const result = await pollBrowserAuth.mutateAsync({
98
- sessionId: authSessionId,
99
- apiBase: resolvePlatformApiBase(platformApiBase, status)
100
- });
101
- if (cancelled) {
102
- return;
103
- }
104
- if (result.status === 'pending') {
105
- setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
106
- setAuthPollIntervalMs(result.nextPollMs ?? 1500);
107
- return;
108
- }
109
- if (result.status === 'authorized') {
110
- setAuthStatusMessage(t('remoteBrowserAuthCompleted'));
111
- setAuthSessionId(null);
112
- setAuthVerificationUri(null);
113
- return;
114
- }
115
- setAuthStatusMessage(result.message || t('remoteBrowserAuthExpired'));
116
- setAuthSessionId(null);
117
- setAuthVerificationUri(null);
118
- } catch {
119
- if (cancelled) {
120
- return;
121
- }
122
- setAuthSessionId(null);
123
- setAuthVerificationUri(null);
124
- }
125
- }, authPollIntervalMs);
41
+ if (!status.settings.enabled) {
42
+ return {
43
+ badgeStatus: 'inactive',
44
+ badgeLabel: t('statusInactive'),
45
+ title: t('remoteStatusNeedsEnable'),
46
+ description: t('remoteStatusNeedsEnableDescription')
47
+ };
48
+ }
126
49
 
127
- return () => {
128
- cancelled = true;
129
- window.clearTimeout(timerId);
50
+ if (!status.service.running) {
51
+ return {
52
+ badgeStatus: 'warning',
53
+ badgeLabel: t('remoteServiceStopped'),
54
+ title: t('remoteStatusNeedsServiceTitle'),
55
+ description: t('remoteStatusNeedsServiceDescription')
130
56
  };
131
- }, [authPollIntervalMs, authSessionId, platformApiBase, pollBrowserAuth, status]);
57
+ }
132
58
 
133
- const start = async () => {
134
- const result = await startBrowserAuth.mutateAsync({
135
- apiBase: resolvePlatformApiBase(platformApiBase, status)
136
- });
137
- setAuthSessionId(result.sessionId);
138
- setAuthVerificationUri(result.verificationUri);
139
- setAuthExpiresAt(result.expiresAt);
140
- setAuthPollIntervalMs(result.intervalMs);
141
- setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
142
- const opened = window.open(result.verificationUri, '_blank', 'noopener,noreferrer');
143
- if (!opened) {
144
- setAuthStatusMessage(t('remoteBrowserAuthPopupBlocked'));
145
- }
146
- };
59
+ if (status.runtime?.state === 'connected') {
60
+ return {
61
+ badgeStatus: 'ready',
62
+ badgeLabel: t('statusReady'),
63
+ title: t('remoteStatusReadyTitle'),
64
+ description: t('remoteStatusReadyDescription')
65
+ };
66
+ }
147
67
 
148
- const resume = () => {
149
- if (!authVerificationUri) {
150
- return;
151
- }
152
- window.open(authVerificationUri, '_blank', 'noopener,noreferrer');
153
- };
68
+ if (status.runtime?.state === 'connecting') {
69
+ return {
70
+ badgeStatus: 'active',
71
+ badgeLabel: t('connecting'),
72
+ title: t('remoteStatusConnectingTitle'),
73
+ description: t('remoteStatusConnectingDescription')
74
+ };
75
+ }
154
76
 
155
77
  return {
156
- authExpiresAt,
157
- authSessionId,
158
- authStatusMessage,
159
- authVerificationUri,
160
- resume,
161
- start
78
+ badgeStatus: 'warning',
79
+ badgeLabel: t('remoteStateDisconnected'),
80
+ title: t('remoteStatusIssueTitle'),
81
+ description: t('remoteStatusIssueDescription')
162
82
  };
163
83
  }
164
84
 
165
- function RemoteOverviewCard(props: {
166
- status: RemoteAccessView | undefined;
167
- runtimeStatus: { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string };
168
- serviceStatus: { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string };
169
- }) {
170
- return (
171
- <Card>
172
- <CardHeader>
173
- <CardTitle className="flex items-center gap-2">
174
- <Activity className="h-4 w-4 text-primary" />
175
- {t('remoteOverviewTitle')}
176
- </CardTitle>
177
- <CardDescription>{t('remoteOverviewDescription')}</CardDescription>
178
- </CardHeader>
179
- <CardContent className="space-y-5">
180
- <div className="flex flex-wrap gap-2">
181
- <StatusDot status={props.status?.account.loggedIn ? 'ready' : 'inactive'} label={props.status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
182
- <StatusDot status={props.serviceStatus.status} label={props.serviceStatus.label} />
183
- <StatusDot status={props.runtimeStatus.status} label={props.runtimeStatus.label} />
184
- </div>
185
-
186
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
187
- <KeyValueRow label={t('remoteLocalOrigin')} value={props.status?.localOrigin} />
188
- <KeyValueRow label={t('remotePublicPlatform')} value={props.status?.platformBase ?? props.status?.account.platformBase} />
189
- <KeyValueRow label={t('remoteDeviceId')} value={props.status?.runtime?.deviceId} muted />
190
- <KeyValueRow label={t('remoteLastConnectedAt')} value={props.status?.runtime?.lastConnectedAt ? formatDateTime(props.status.runtime.lastConnectedAt) : '-'} muted />
191
- <KeyValueRow label={t('remoteLastError')} value={props.status?.runtime?.lastError} muted />
192
- </div>
193
- </CardContent>
194
- </Card>
195
- );
196
- }
197
-
198
- function RemoteDeviceCard(props: {
199
- enabled: boolean;
200
- setEnabled: (value: boolean) => void;
201
- deviceName: string;
202
- setDeviceName: (value: string) => void;
203
- platformApiBase: string;
204
- setPlatformApiBase: (value: string) => void;
205
- settingsMutation: ReturnType<typeof useRemoteSettings>;
206
- serviceMutation: ReturnType<typeof useRemoteServiceControl>;
207
- }) {
208
- return (
209
- <Card>
210
- <CardHeader>
211
- <CardTitle className="flex items-center gap-2">
212
- <Laptop className="h-4 w-4 text-primary" />
213
- {t('remoteDeviceTitle')}
214
- </CardTitle>
215
- <CardDescription>{t('remoteDeviceDescription')}</CardDescription>
216
- </CardHeader>
217
- <CardContent className="space-y-4">
218
- <div className="space-y-2">
219
- <div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
220
- <div>
221
- <p className="text-sm font-medium text-gray-900">{t('remoteEnabled')}</p>
222
- <p className="mt-1 text-xs text-gray-500">{t('remoteEnabledHelp')}</p>
223
- </div>
224
- <Switch checked={props.enabled} onCheckedChange={props.setEnabled} />
225
- </div>
226
- </div>
227
-
228
- <div className="space-y-2">
229
- <Label htmlFor="remote-device-name">{t('remoteDeviceName')}</Label>
230
- <Input id="remote-device-name" value={props.deviceName} onChange={(event) => props.setDeviceName(event.target.value)} placeholder={t('remoteDeviceNamePlaceholder')} />
231
- </div>
232
-
233
- <div className="space-y-2">
234
- <Label htmlFor="remote-platform-api-base">{t('remotePlatformApiBase')}</Label>
235
- <Input
236
- id="remote-platform-api-base"
237
- value={props.platformApiBase}
238
- onChange={(event) => props.setPlatformApiBase(event.target.value)}
239
- placeholder="https://ai-gateway-api.nextclaw.io/v1"
240
- />
241
- <p className="text-xs text-gray-500">{t('remotePlatformApiBaseHelp')}</p>
242
- </div>
243
-
244
- <div className="flex flex-wrap gap-3">
245
- <Button
246
- onClick={() =>
247
- props.settingsMutation.mutate({
248
- enabled: props.enabled,
249
- deviceName: props.deviceName,
250
- platformApiBase: props.platformApiBase
251
- })
252
- }
253
- disabled={props.settingsMutation.isPending}
254
- >
255
- {props.settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
256
- </Button>
257
- <Button variant="outline" onClick={() => props.serviceMutation.mutate('restart')} disabled={props.serviceMutation.isPending}>
258
- <RefreshCcw className="mr-2 h-4 w-4" />
259
- {t('remoteRestartService')}
260
- </Button>
261
- </div>
262
- <p className="text-xs text-gray-500">{t('remoteSaveHint')}</p>
263
- </CardContent>
264
- </Card>
265
- );
266
- }
267
-
268
- function RemoteAccountCard(props: {
269
- status: RemoteAccessView | undefined;
270
- platformApiBase: string;
271
- browserAuthStartMutation: ReturnType<typeof useRemoteBrowserAuthStart>;
272
- logoutMutation: ReturnType<typeof useRemoteLogout>;
273
- authSessionId: string | null;
274
- authExpiresAt: string | null;
275
- authStatusMessage: string;
276
- authVerificationUri: string | null;
277
- onStartBrowserAuth: () => Promise<void>;
278
- onResumeBrowserAuth: () => void;
279
- }) {
280
- return (
281
- <Card>
282
- <CardHeader>
283
- <CardTitle className="flex items-center gap-2">
284
- <KeyRound className="h-4 w-4 text-primary" />
285
- {t('remoteAccountTitle')}
286
- </CardTitle>
287
- <CardDescription>{t('remoteAccountDescription')}</CardDescription>
288
- </CardHeader>
289
- <CardContent className="space-y-4">
290
- {props.status?.account.loggedIn ? (
291
- <>
292
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
293
- <KeyValueRow label={t('remoteAccountEmail')} value={props.status.account.email} />
294
- <KeyValueRow label={t('remoteAccountRole')} value={props.status.account.role} />
295
- <KeyValueRow label={t('remoteApiBase')} value={props.status.account.apiBase} />
296
- </div>
297
- <Button variant="outline" onClick={() => props.logoutMutation.mutate()} disabled={props.logoutMutation.isPending}>
298
- {props.logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
299
- </Button>
300
- </>
301
- ) : (
302
- <>
303
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
304
- <p className="text-sm font-medium text-gray-900">{t('remoteBrowserAuthTitle')}</p>
305
- <p className="mt-1 text-sm text-gray-600">{t('remoteBrowserAuthDescription')}</p>
306
- <div className="mt-3 border-t border-white/80 pt-3">
307
- <KeyValueRow label={t('remoteApiBase')} value={props.platformApiBase || props.status?.settings.platformApiBase || props.status?.account.apiBase} muted />
308
- <KeyValueRow label={t('remoteBrowserAuthSession')} value={props.authSessionId} muted />
309
- <KeyValueRow label={t('remoteBrowserAuthExpiresAt')} value={props.authExpiresAt ? formatDateTime(props.authExpiresAt) : '-'} muted />
310
- </div>
311
- </div>
312
- {props.authStatusMessage ? <p className="text-sm text-gray-600">{props.authStatusMessage}</p> : null}
313
- <div className="flex flex-wrap gap-3">
314
- <Button onClick={props.onStartBrowserAuth} disabled={props.browserAuthStartMutation.isPending || !!props.authSessionId}>
315
- {props.browserAuthStartMutation.isPending
316
- ? t('remoteBrowserAuthStarting')
317
- : props.authSessionId
318
- ? t('remoteBrowserAuthAuthorizing')
319
- : t('remoteBrowserAuthAction')}
320
- </Button>
321
- {props.authVerificationUri ? (
322
- <Button variant="outline" onClick={props.onResumeBrowserAuth}>
323
- {t('remoteBrowserAuthResume')}
324
- </Button>
325
- ) : null}
326
- </div>
327
- <p className="text-xs text-gray-500">{t('remoteBrowserAuthHint')}</p>
328
- </>
329
- )}
330
- </CardContent>
331
- </Card>
332
- );
333
- }
334
-
335
- function RemoteServiceCard(props: {
336
- status: RemoteAccessView | undefined;
337
- serviceMutation: ReturnType<typeof useRemoteServiceControl>;
338
- }) {
339
- return (
340
- <Card>
341
- <CardHeader>
342
- <CardTitle className="flex items-center gap-2">
343
- <ServerCog className="h-4 w-4 text-primary" />
344
- {t('remoteServiceTitle')}
345
- </CardTitle>
346
- <CardDescription>{t('remoteServiceDescription')}</CardDescription>
347
- </CardHeader>
348
- <CardContent className="space-y-4">
349
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
350
- <KeyValueRow label={t('remoteServicePid')} value={props.status?.service.pid} />
351
- <KeyValueRow label={t('remoteServiceUiUrl')} value={props.status?.service.uiUrl} />
352
- <KeyValueRow label={t('remoteServiceCurrentProcess')} value={props.status?.service.currentProcess ? t('yes') : t('no')} />
353
- </div>
354
- <div className="flex flex-wrap gap-3">
355
- <Button variant="primary" onClick={() => props.serviceMutation.mutate('start')} disabled={props.serviceMutation.isPending}>
356
- {t('remoteStartService')}
357
- </Button>
358
- <Button variant="outline" onClick={() => props.serviceMutation.mutate('restart')} disabled={props.serviceMutation.isPending}>
359
- {t('remoteRestartService')}
360
- </Button>
361
- <Button variant="outline" onClick={() => props.serviceMutation.mutate('stop')} disabled={props.serviceMutation.isPending}>
362
- {t('remoteStopService')}
363
- </Button>
364
- </div>
365
- <p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
366
- </CardContent>
367
- </Card>
368
- );
369
- }
370
-
371
- function RemoteDoctorCard(props: {
372
- doctorMutation: ReturnType<typeof useRemoteDoctor>;
373
- }) {
374
- return (
375
- <Card>
376
- <CardHeader>
377
- <CardTitle className="flex items-center gap-2">
378
- <ShieldCheck className="h-4 w-4 text-primary" />
379
- {t('remoteDoctorTitle')}
380
- </CardTitle>
381
- <CardDescription>{t('remoteDoctorDescription')}</CardDescription>
382
- </CardHeader>
383
- <CardContent className="space-y-4">
384
- <div className="flex flex-wrap gap-3">
385
- <Button variant="outline" onClick={() => props.doctorMutation.mutate()} disabled={props.doctorMutation.isPending}>
386
- <SquareTerminal className="mr-2 h-4 w-4" />
387
- {props.doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
388
- </Button>
389
- </div>
390
-
391
- {props.doctorMutation.data ? <DoctorResultPanel doctor={props.doctorMutation.data} /> : <p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>}
392
- </CardContent>
393
- </Card>
394
- );
395
- }
396
-
397
- function DoctorResultPanel(props: { doctor: RemoteDoctorView }) {
398
- return (
399
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
400
- <KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(props.doctor.generatedAt)} muted />
401
- <div className="mt-3 space-y-2">
402
- {props.doctor.checks.map((check) => (
403
- <div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
404
- <div className="flex items-center justify-between gap-3">
405
- <span className="text-sm font-medium text-gray-900">{check.name}</span>
406
- <StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
407
- </div>
408
- <p className="mt-2 text-sm text-gray-600">{check.detail}</p>
409
- </div>
410
- ))}
411
- </div>
412
- </div>
413
- );
85
+ function buildIssueHint(status: RemoteAccessView | undefined) {
86
+ if (!status?.settings.enabled) {
87
+ return null;
88
+ }
89
+ if (!status.service.running) {
90
+ return t('remoteStatusIssueDetailServiceStopped');
91
+ }
92
+ return status.runtime?.lastError?.trim() || t('remoteStatusIssueDetailGeneric');
414
93
  }
415
94
 
416
95
  export function RemoteAccessPage() {
96
+ const presenter = useAppPresenter();
417
97
  const remoteStatus = useRemoteStatus();
418
- const browserAuthStartMutation = useRemoteBrowserAuthStart();
419
- const browserAuthPollMutation = useRemoteBrowserAuthPoll();
420
- const logoutMutation = useRemoteLogout();
421
- const settingsMutation = useRemoteSettings();
422
- const doctorMutation = useRemoteDoctor();
423
- const serviceMutation = useRemoteServiceControl();
424
-
425
98
  const status = remoteStatus.data;
426
- const runtimeStatus = useMemo(() => getRuntimeStatus(status?.runtime ?? null), [status?.runtime]);
427
- const serviceStatus = useMemo(() => getServiceStatus(status?.service ?? { running: false, currentProcess: false }), [status?.service]);
428
-
429
- const [enabled, setEnabled] = useState(false);
430
- const [deviceName, setDeviceName] = useState('');
431
- const [platformApiBase, setPlatformApiBase] = useState('');
432
- const browserAuth = useRemoteBrowserAuthFlow({
433
- status,
434
- platformApiBase,
435
- startBrowserAuth: browserAuthStartMutation,
436
- pollBrowserAuth: browserAuthPollMutation
437
- });
99
+ const actionLabel = useRemoteAccessStore((state) => state.actionLabel);
100
+ const heroView = useMemo(() => buildHeroView(status), [status]);
101
+ const issueHint = useMemo(() => buildIssueHint(status), [status]);
102
+ const busy = Boolean(actionLabel);
103
+ const deviceName = status?.runtime?.deviceName?.trim() || status?.settings.deviceName?.trim() || t('remoteDeviceNameAuto');
104
+ const canOpenDeviceList = Boolean(status?.account.loggedIn && resolveRemoteWebBase(status));
105
+ const shouldShowIssueHint = Boolean(status?.settings.enabled && status?.account.loggedIn && heroView.badgeStatus === 'warning');
438
106
 
439
107
  useEffect(() => {
440
- if (!status) {
441
- return;
442
- }
443
- setEnabled(status.settings.enabled);
444
- setDeviceName(status.settings.deviceName);
445
- setPlatformApiBase(status.settings.platformApiBase);
446
- }, [status]);
108
+ presenter.remoteAccessManager.syncStatus(status);
109
+ }, [presenter, status]);
447
110
 
448
111
  if (remoteStatus.isLoading && !status) {
449
112
  return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
@@ -453,37 +116,94 @@ export function RemoteAccessPage() {
453
116
  <PageLayout className="space-y-6">
454
117
  <PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
455
118
 
456
- <div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
457
- <RemoteOverviewCard status={status} runtimeStatus={runtimeStatus} serviceStatus={serviceStatus} />
458
- <RemoteDeviceCard
459
- enabled={enabled}
460
- setEnabled={setEnabled}
461
- deviceName={deviceName}
462
- setDeviceName={setDeviceName}
463
- platformApiBase={platformApiBase}
464
- setPlatformApiBase={setPlatformApiBase}
465
- settingsMutation={settingsMutation}
466
- serviceMutation={serviceMutation}
467
- />
468
- </div>
119
+ <div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
120
+ <Card>
121
+ <CardHeader className="space-y-4">
122
+ <div className="flex flex-wrap items-center gap-3">
123
+ <CardTitle>{heroView.title}</CardTitle>
124
+ <StatusDot status={heroView.badgeStatus} label={heroView.badgeLabel} />
125
+ </div>
126
+ <CardDescription>{heroView.description}</CardDescription>
127
+ </CardHeader>
128
+ <CardContent className="space-y-5">
129
+ <div className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
130
+ <KeyValueRow label={t('remoteSignedInAccount')} value={status?.account.email} />
131
+ <KeyValueRow label={t('remoteDeviceName')} value={deviceName} />
132
+ <KeyValueRow label={t('remoteConnectionStatus')} value={heroView.badgeLabel} />
133
+ <KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
134
+ </div>
135
+
136
+ <div className="flex flex-wrap gap-3">
137
+ {!status?.account.loggedIn ? (
138
+ <Button onClick={() => void presenter.remoteAccessManager.enableRemoteAccess(status)} disabled={busy}>
139
+ {actionLabel || t('remoteSignInAndEnable')}
140
+ </Button>
141
+ ) : !status.settings.enabled ? (
142
+ <Button onClick={() => void presenter.remoteAccessManager.enableRemoteAccess(status)} disabled={busy}>
143
+ {actionLabel || t('remoteEnableNow')}
144
+ </Button>
145
+ ) : (
146
+ <Button onClick={() => void presenter.remoteAccessManager.repairRemoteAccess(status)} disabled={busy}>
147
+ <RefreshCcw className="mr-2 h-4 w-4" />
148
+ {actionLabel || t('remoteReconnectNow')}
149
+ </Button>
150
+ )}
151
+
152
+ <Button
153
+ variant="outline"
154
+ onClick={() => void presenter.accountManager.openNextClawWeb()}
155
+ disabled={busy || !canOpenDeviceList}
156
+ >
157
+ <SquareArrowOutUpRight className="mr-2 h-4 w-4" />
158
+ {t('remoteOpenDeviceList')}
159
+ </Button>
160
+
161
+ {status?.settings.enabled ? (
162
+ <Button variant="outline" onClick={() => void presenter.remoteAccessManager.disableRemoteAccess(status)} disabled={busy}>
163
+ {t('remoteDisable')}
164
+ </Button>
165
+ ) : null}
166
+ </div>
469
167
 
470
- <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
471
- <RemoteAccountCard
472
- status={status}
473
- platformApiBase={platformApiBase}
474
- browserAuthStartMutation={browserAuthStartMutation}
475
- logoutMutation={logoutMutation}
476
- authSessionId={browserAuth.authSessionId}
477
- authExpiresAt={browserAuth.authExpiresAt}
478
- authStatusMessage={browserAuth.authStatusMessage}
479
- authVerificationUri={browserAuth.authVerificationUri}
480
- onStartBrowserAuth={browserAuth.start}
481
- onResumeBrowserAuth={browserAuth.resume}
482
- />
483
- <RemoteServiceCard status={status} serviceMutation={serviceMutation} />
168
+ {shouldShowIssueHint ? (
169
+ <div className="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3">
170
+ <p className="text-sm font-medium text-amber-900">{t('remoteStatusIssueDetailTitle')}</p>
171
+ <p className="mt-1 text-sm leading-6 text-amber-800">{issueHint}</p>
172
+ </div>
173
+ ) : null}
174
+
175
+ <p className="text-xs text-gray-500">{t('remoteOpenWebHint')}</p>
176
+ </CardContent>
177
+ </Card>
178
+
179
+ <Card>
180
+ <CardHeader>
181
+ <CardTitle className="flex items-center gap-2">
182
+ <Laptop className="h-4 w-4 text-primary" />
183
+ {t('remoteDeviceSectionTitle')}
184
+ </CardTitle>
185
+ <CardDescription>{t('remoteDeviceSectionDescription')}</CardDescription>
186
+ </CardHeader>
187
+ <CardContent className="space-y-5">
188
+ <div className="flex flex-wrap gap-2">
189
+ <StatusDot status={status?.account.loggedIn ? 'ready' : 'inactive'} label={status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
190
+ <StatusDot status={status?.settings.enabled ? 'active' : 'inactive'} label={status?.settings.enabled ? t('remoteEnabled') : t('remoteStateDisabled')} />
191
+ <StatusDot status={status?.service.running ? 'active' : 'inactive'} label={status?.service.running ? t('remoteServiceRunning') : t('remoteServiceStopped')} />
192
+ </div>
193
+
194
+ <div className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
195
+ <KeyValueRow label={t('remoteDeviceName')} value={deviceName} />
196
+ <KeyValueRow label={t('remoteConnectionStatus')} value={heroView.badgeLabel} />
197
+ <KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
198
+ </div>
199
+
200
+ <div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600">
201
+ {status?.account.loggedIn ? t('remoteOpenWebHint') : t('remoteStatusNeedsSignInDescription')}
202
+ </div>
203
+ </CardContent>
204
+ </Card>
484
205
  </div>
485
206
 
486
- <RemoteDoctorCard doctorMutation={doctorMutation} />
487
207
  </PageLayout>
488
208
  );
489
209
  }