@nextclaw/ui 0.9.2 → 0.9.4

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 (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/ChannelsList-DDfZIiJa.js +1 -0
  3. package/dist/assets/ChatPage-FpRraTxm.js +38 -0
  4. package/dist/assets/{DocBrowser-CVwUDJMO.js → DocBrowser-Kndx8OJj.js} +1 -1
  5. package/dist/assets/LogoBadge-hKHoLH9n.js +1 -0
  6. package/dist/assets/MarketplacePage-CZIJyfjK.js +49 -0
  7. package/dist/assets/McpMarketplacePage-BGrAMA37.js +40 -0
  8. package/dist/assets/{ModelConfig-CsX-_fyy.js → ModelConfig-BpKQeGfb.js} +1 -1
  9. package/dist/assets/ProvidersList-qfUL6mrW.js +1 -0
  10. package/dist/assets/RemoteAccessPage-BQuMsngI.js +1 -0
  11. package/dist/assets/{RuntimeConfig-CX2TGEG1.js → RuntimeConfig-CVlqNWKO.js} +1 -1
  12. package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-DXFV6Mvx.js} +1 -1
  13. package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-BGW9aUqv.js} +2 -2
  14. package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-BByfa1ke.js} +2 -2
  15. package/dist/assets/{chat-message-AWIcksDK.js → chat-message-ZwnDwDuQ.js} +1 -1
  16. package/dist/assets/index-BWvap_iq.js +8 -0
  17. package/dist/assets/index-COrhpAdh.css +1 -0
  18. package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
  19. package/dist/assets/{label-DD61y-4v.js → label-Bklr3fXc.js} +1 -1
  20. package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
  21. package/dist/assets/{page-layout-CfnoVycc.js → page-layout-sNhcbwtm.js} +1 -1
  22. package/dist/assets/{popover-DsugZ6rp.js → popover-C3rJrJJG.js} +1 -1
  23. package/dist/assets/{security-config-DIrf2Z0O.js → security-config-BueosYw1.js} +1 -1
  24. package/dist/assets/skeleton-CiG6msbm.js +1 -0
  25. package/dist/assets/status-dot-CsIV5YrS.js +1 -0
  26. package/dist/assets/{switch-NX5OmUXQ.js → switch-DSdHSIsC.js} +1 -1
  27. package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-BB-VjdL2.js} +1 -1
  28. package/dist/assets/{useConfirmDialog-BuQnVTeR.js → useConfirmDialog-BL5s8KDC.js} +2 -2
  29. package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
  30. package/dist/index.html +3 -3
  31. package/package.json +3 -3
  32. package/src/App.tsx +4 -0
  33. package/src/api/auth.types.ts +24 -0
  34. package/src/api/chat-session-type.types.ts +21 -0
  35. package/src/api/marketplace.ts +8 -2
  36. package/src/api/mcp-marketplace.ts +138 -0
  37. package/src/api/remote.ts +77 -0
  38. package/src/api/remote.types.ts +104 -0
  39. package/src/api/types.ts +28 -34
  40. package/src/components/chat/ChatSidebar.test.tsx +31 -2
  41. package/src/components/chat/ChatSidebar.tsx +26 -2
  42. package/src/components/chat/chat-page-data.ts +36 -38
  43. package/src/components/chat/chat-page-runtime.test.ts +96 -2
  44. package/src/components/chat/chat-page-runtime.ts +1 -135
  45. package/src/components/chat/chat-session-preference-governance.ts +303 -0
  46. package/src/components/chat/legacy/LegacyChatPage.tsx +4 -19
  47. package/src/components/chat/ncp/NcpChatPage.tsx +4 -19
  48. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
  49. package/src/components/chat/ncp/ncp-chat-page-data.ts +62 -21
  50. package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
  51. package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
  52. package/src/components/chat/stores/chat-input.store.ts +14 -1
  53. package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
  54. package/src/components/chat/useChatSessionTypeState.ts +55 -12
  55. package/src/components/layout/Sidebar.tsx +11 -1
  56. package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
  57. package/src/components/marketplace/MarketplacePage.tsx +52 -199
  58. package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
  59. package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
  60. package/src/components/marketplace/marketplace-localization.ts +77 -0
  61. package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
  62. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
  63. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
  64. package/src/components/remote/RemoteAccessPage.tsx +396 -0
  65. package/src/components/ui/input.tsx +1 -1
  66. package/src/components/ui/label.tsx +1 -1
  67. package/src/hooks/useMarketplace.ts +36 -7
  68. package/src/hooks/useMcpMarketplace.ts +99 -0
  69. package/src/hooks/useRemoteAccess.ts +120 -0
  70. package/src/hooks/useWebSocket.ts +25 -16
  71. package/src/lib/i18n.marketplace.ts +91 -0
  72. package/src/lib/i18n.remote.ts +142 -0
  73. package/src/lib/i18n.ts +10 -68
  74. package/dist/assets/ChannelsList-DKD6Llid.js +0 -1
  75. package/dist/assets/ChatPage-BK9X4Tin.js +0 -38
  76. package/dist/assets/LogoBadge-CYQ_b7jk.js +0 -1
  77. package/dist/assets/MarketplacePage-B_2z3ii_.js +0 -49
  78. package/dist/assets/ProvidersList-CZstsyv7.js +0 -1
  79. package/dist/assets/index-BEgClaDH.js +0 -8
  80. package/dist/assets/index-C8GsgIUn.css +0 -1
  81. package/dist/assets/skeleton-DJ-Wen2o.js +0 -1
@@ -0,0 +1,396 @@
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';
12
+ import { PageHeader, PageLayout } from '@/components/layout/page-layout';
13
+ import { Button } from '@/components/ui/button';
14
+ 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
+ import { StatusDot } from '@/components/ui/status-dot';
18
+ import { Switch } from '@/components/ui/switch';
19
+ import { formatDateTime, t } from '@/lib/i18n';
20
+ import { Activity, KeyRound, Laptop, RefreshCcw, ServerCog, ShieldCheck, SquareTerminal } from 'lucide-react';
21
+
22
+ function getRuntimeStatus(runtime: RemoteRuntimeView | null): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
23
+ if (!runtime) {
24
+ return { status: 'inactive', label: t('remoteRuntimeMissing') };
25
+ }
26
+ if (runtime.state === 'connected') {
27
+ return { status: 'ready', label: t('remoteStateConnected') };
28
+ }
29
+ if (runtime.state === 'connecting') {
30
+ return { status: 'warning', label: t('remoteStateConnecting') };
31
+ }
32
+ if (runtime.state === 'error') {
33
+ return { status: 'warning', label: t('remoteStateError') };
34
+ }
35
+ if (runtime.state === 'disconnected') {
36
+ return { status: 'warning', label: t('remoteStateDisconnected') };
37
+ }
38
+ return { status: 'inactive', label: t('remoteStateDisabled') };
39
+ }
40
+
41
+ function getServiceStatus(service: RemoteServiceView): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
42
+ if (!service.running) {
43
+ return { status: 'inactive', label: t('remoteServiceStopped') };
44
+ }
45
+ return service.currentProcess
46
+ ? { status: 'ready', label: t('remoteServiceManagedRunning') }
47
+ : { status: 'active', label: t('remoteServiceRunning') };
48
+ }
49
+
50
+ function KeyValueRow(props: { label: string; value?: string | number | null; muted?: boolean }) {
51
+ const value = props.value === undefined || props.value === null || props.value === '' ? '-' : String(props.value);
52
+ return (
53
+ <div className="flex items-start justify-between gap-4 py-2 text-sm">
54
+ <span className="text-gray-500">{props.label}</span>
55
+ <span className={props.muted ? 'text-right text-gray-500' : 'text-right text-gray-800'}>{value}</span>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export function RemoteAccessPage() {
61
+ const remoteStatus = useRemoteStatus();
62
+ const browserAuthStartMutation = useRemoteBrowserAuthStart();
63
+ const browserAuthPollMutation = useRemoteBrowserAuthPoll();
64
+ const logoutMutation = useRemoteLogout();
65
+ const settingsMutation = useRemoteSettings();
66
+ const doctorMutation = useRemoteDoctor();
67
+ const serviceMutation = useRemoteServiceControl();
68
+
69
+ const status = remoteStatus.data;
70
+ const runtimeStatus = useMemo(() => getRuntimeStatus(status?.runtime ?? null), [status?.runtime]);
71
+ const serviceStatus = useMemo(() => getServiceStatus(status?.service ?? { running: false, currentProcess: false }), [status?.service]);
72
+
73
+ const [enabled, setEnabled] = useState(false);
74
+ const [deviceName, setDeviceName] = useState('');
75
+ const [platformApiBase, setPlatformApiBase] = useState('');
76
+ const [authSessionId, setAuthSessionId] = useState<string | null>(null);
77
+ const [authVerificationUri, setAuthVerificationUri] = useState<string | null>(null);
78
+ const [authStatusMessage, setAuthStatusMessage] = useState('');
79
+ const [authExpiresAt, setAuthExpiresAt] = useState<string | null>(null);
80
+ const [authPollIntervalMs, setAuthPollIntervalMs] = useState(1500);
81
+
82
+ useEffect(() => {
83
+ if (!status) {
84
+ return;
85
+ }
86
+ setEnabled(status.settings.enabled);
87
+ setDeviceName(status.settings.deviceName);
88
+ setPlatformApiBase(status.settings.platformApiBase);
89
+ }, [status]);
90
+
91
+ useEffect(() => {
92
+ if (!status?.account.loggedIn) {
93
+ return;
94
+ }
95
+ setAuthSessionId(null);
96
+ setAuthVerificationUri(null);
97
+ setAuthExpiresAt(null);
98
+ setAuthStatusMessage('');
99
+ setAuthPollIntervalMs(1500);
100
+ }, [status?.account.loggedIn]);
101
+
102
+ useEffect(() => {
103
+ if (!authSessionId || status?.account.loggedIn) {
104
+ return;
105
+ }
106
+
107
+ let cancelled = false;
108
+ const timerId = window.setTimeout(async () => {
109
+ try {
110
+ const result = await browserAuthPollMutation.mutateAsync({
111
+ sessionId: authSessionId,
112
+ apiBase: platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined
113
+ });
114
+ if (cancelled) {
115
+ return;
116
+ }
117
+ if (result.status === 'pending') {
118
+ setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
119
+ setAuthPollIntervalMs(result.nextPollMs ?? 1500);
120
+ return;
121
+ }
122
+ if (result.status === 'authorized') {
123
+ setAuthStatusMessage(t('remoteBrowserAuthCompleted'));
124
+ setAuthSessionId(null);
125
+ setAuthVerificationUri(null);
126
+ return;
127
+ }
128
+ setAuthStatusMessage(result.message || t('remoteBrowserAuthExpired'));
129
+ setAuthSessionId(null);
130
+ setAuthVerificationUri(null);
131
+ } catch {
132
+ if (cancelled) {
133
+ return;
134
+ }
135
+ setAuthSessionId(null);
136
+ setAuthVerificationUri(null);
137
+ }
138
+ }, authPollIntervalMs);
139
+
140
+ return () => {
141
+ cancelled = true;
142
+ window.clearTimeout(timerId);
143
+ };
144
+ }, [
145
+ authPollIntervalMs,
146
+ authSessionId,
147
+ browserAuthPollMutation,
148
+ platformApiBase,
149
+ status?.account.apiBase,
150
+ status?.account.loggedIn,
151
+ status?.settings.platformApiBase
152
+ ]);
153
+
154
+ const handleStartBrowserAuth = async () => {
155
+ const apiBase = platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined;
156
+ const result = await browserAuthStartMutation.mutateAsync({ apiBase });
157
+ setAuthSessionId(result.sessionId);
158
+ setAuthVerificationUri(result.verificationUri);
159
+ setAuthExpiresAt(result.expiresAt);
160
+ setAuthPollIntervalMs(result.intervalMs);
161
+ setAuthStatusMessage(t('remoteBrowserAuthWaiting'));
162
+ const opened = window.open(result.verificationUri, '_blank', 'noopener,noreferrer');
163
+ if (!opened) {
164
+ setAuthStatusMessage(t('remoteBrowserAuthPopupBlocked'));
165
+ }
166
+ };
167
+
168
+ const handleResumeBrowserAuth = () => {
169
+ if (!authVerificationUri) {
170
+ return;
171
+ }
172
+ window.open(authVerificationUri, '_blank', 'noopener,noreferrer');
173
+ };
174
+
175
+ if (remoteStatus.isLoading && !status) {
176
+ return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
177
+ }
178
+
179
+ return (
180
+ <PageLayout className="space-y-6">
181
+ <PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
182
+
183
+ <div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
184
+ <Card>
185
+ <CardHeader>
186
+ <CardTitle className="flex items-center gap-2">
187
+ <Activity className="h-4 w-4 text-primary" />
188
+ {t('remoteOverviewTitle')}
189
+ </CardTitle>
190
+ <CardDescription>{t('remoteOverviewDescription')}</CardDescription>
191
+ </CardHeader>
192
+ <CardContent className="space-y-5">
193
+ <div className="flex flex-wrap gap-2">
194
+ <StatusDot status={status?.account.loggedIn ? 'ready' : 'inactive'} label={status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
195
+ <StatusDot status={serviceStatus.status} label={serviceStatus.label} />
196
+ <StatusDot status={runtimeStatus.status} label={runtimeStatus.label} />
197
+ </div>
198
+
199
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
200
+ <KeyValueRow label={t('remoteLocalOrigin')} value={status?.localOrigin} />
201
+ <KeyValueRow label={t('remotePublicPlatform')} value={status?.platformBase ?? status?.account.platformBase} />
202
+ <KeyValueRow label={t('remoteDeviceId')} value={status?.runtime?.deviceId} muted />
203
+ <KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
204
+ <KeyValueRow label={t('remoteLastError')} value={status?.runtime?.lastError} muted />
205
+ </div>
206
+ </CardContent>
207
+ </Card>
208
+
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={enabled} onCheckedChange={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={deviceName} onChange={(event) => 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={platformApiBase}
238
+ onChange={(event) => 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
+ settingsMutation.mutate({
248
+ enabled,
249
+ deviceName,
250
+ platformApiBase
251
+ })
252
+ }
253
+ disabled={settingsMutation.isPending}
254
+ >
255
+ {settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
256
+ </Button>
257
+ <Button
258
+ variant="outline"
259
+ onClick={() => serviceMutation.mutate('restart')}
260
+ disabled={serviceMutation.isPending}
261
+ >
262
+ <RefreshCcw className="mr-2 h-4 w-4" />
263
+ {t('remoteRestartService')}
264
+ </Button>
265
+ </div>
266
+ <p className="text-xs text-gray-500">{t('remoteSaveHint')}</p>
267
+ </CardContent>
268
+ </Card>
269
+ </div>
270
+
271
+ <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
272
+ <Card>
273
+ <CardHeader>
274
+ <CardTitle className="flex items-center gap-2">
275
+ <KeyRound className="h-4 w-4 text-primary" />
276
+ {t('remoteAccountTitle')}
277
+ </CardTitle>
278
+ <CardDescription>{t('remoteAccountDescription')}</CardDescription>
279
+ </CardHeader>
280
+ <CardContent className="space-y-4">
281
+ {status?.account.loggedIn ? (
282
+ <>
283
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
284
+ <KeyValueRow label={t('remoteAccountEmail')} value={status.account.email} />
285
+ <KeyValueRow label={t('remoteAccountRole')} value={status.account.role} />
286
+ <KeyValueRow label={t('remoteApiBase')} value={status.account.apiBase} />
287
+ </div>
288
+ <Button variant="outline" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
289
+ {logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
290
+ </Button>
291
+ </>
292
+ ) : (
293
+ <>
294
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
295
+ <p className="text-sm font-medium text-gray-900">{t('remoteBrowserAuthTitle')}</p>
296
+ <p className="mt-1 text-sm text-gray-600">{t('remoteBrowserAuthDescription')}</p>
297
+ <div className="mt-3 border-t border-white/80 pt-3">
298
+ <KeyValueRow label={t('remoteApiBase')} value={platformApiBase || status?.settings.platformApiBase || status?.account.apiBase} muted />
299
+ <KeyValueRow label={t('remoteBrowserAuthSession')} value={authSessionId} muted />
300
+ <KeyValueRow
301
+ label={t('remoteBrowserAuthExpiresAt')}
302
+ value={authExpiresAt ? formatDateTime(authExpiresAt) : '-'}
303
+ muted
304
+ />
305
+ </div>
306
+ </div>
307
+ {authStatusMessage ? <p className="text-sm text-gray-600">{authStatusMessage}</p> : null}
308
+ <div className="flex flex-wrap gap-3">
309
+ <Button onClick={handleStartBrowserAuth} disabled={browserAuthStartMutation.isPending || !!authSessionId}>
310
+ {browserAuthStartMutation.isPending
311
+ ? t('remoteBrowserAuthStarting')
312
+ : authSessionId
313
+ ? t('remoteBrowserAuthAuthorizing')
314
+ : t('remoteBrowserAuthAction')}
315
+ </Button>
316
+ {authVerificationUri ? (
317
+ <Button variant="outline" onClick={handleResumeBrowserAuth}>
318
+ {t('remoteBrowserAuthResume')}
319
+ </Button>
320
+ ) : null}
321
+ </div>
322
+ <p className="text-xs text-gray-500">{t('remoteBrowserAuthHint')}</p>
323
+ </>
324
+ )}
325
+ </CardContent>
326
+ </Card>
327
+
328
+ <Card>
329
+ <CardHeader>
330
+ <CardTitle className="flex items-center gap-2">
331
+ <ServerCog className="h-4 w-4 text-primary" />
332
+ {t('remoteServiceTitle')}
333
+ </CardTitle>
334
+ <CardDescription>{t('remoteServiceDescription')}</CardDescription>
335
+ </CardHeader>
336
+ <CardContent className="space-y-4">
337
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
338
+ <KeyValueRow label={t('remoteServicePid')} value={status?.service.pid} />
339
+ <KeyValueRow label={t('remoteServiceUiUrl')} value={status?.service.uiUrl} />
340
+ <KeyValueRow label={t('remoteServiceCurrentProcess')} value={status?.service.currentProcess ? t('yes') : t('no')} />
341
+ </div>
342
+ <div className="flex flex-wrap gap-3">
343
+ <Button variant="primary" onClick={() => serviceMutation.mutate('start')} disabled={serviceMutation.isPending}>
344
+ {t('remoteStartService')}
345
+ </Button>
346
+ <Button variant="outline" onClick={() => serviceMutation.mutate('restart')} disabled={serviceMutation.isPending}>
347
+ {t('remoteRestartService')}
348
+ </Button>
349
+ <Button variant="outline" onClick={() => serviceMutation.mutate('stop')} disabled={serviceMutation.isPending}>
350
+ {t('remoteStopService')}
351
+ </Button>
352
+ </div>
353
+ <p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
354
+ </CardContent>
355
+ </Card>
356
+ </div>
357
+
358
+ <Card>
359
+ <CardHeader>
360
+ <CardTitle className="flex items-center gap-2">
361
+ <ShieldCheck className="h-4 w-4 text-primary" />
362
+ {t('remoteDoctorTitle')}
363
+ </CardTitle>
364
+ <CardDescription>{t('remoteDoctorDescription')}</CardDescription>
365
+ </CardHeader>
366
+ <CardContent className="space-y-4">
367
+ <div className="flex flex-wrap gap-3">
368
+ <Button variant="outline" onClick={() => doctorMutation.mutate()} disabled={doctorMutation.isPending}>
369
+ <SquareTerminal className="mr-2 h-4 w-4" />
370
+ {doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
371
+ </Button>
372
+ </div>
373
+
374
+ {doctorMutation.data ? (
375
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
376
+ <KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(doctorMutation.data.generatedAt)} muted />
377
+ <div className="mt-3 space-y-2">
378
+ {doctorMutation.data.checks.map((check) => (
379
+ <div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
380
+ <div className="flex items-center justify-between gap-3">
381
+ <span className="text-sm font-medium text-gray-900">{check.name}</span>
382
+ <StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
383
+ </div>
384
+ <p className="mt-2 text-sm text-gray-600">{check.detail}</p>
385
+ </div>
386
+ ))}
387
+ </div>
388
+ </div>
389
+ ) : (
390
+ <p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>
391
+ )}
392
+ </CardContent>
393
+ </Card>
394
+ </PageLayout>
395
+ );
396
+ }
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { cn } from '@/lib/utils';
3
3
 
4
- export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { }
4
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
5
5
 
6
6
  const Input = React.forwardRef<HTMLInputElement, InputProps>(
7
7
  ({ className, type, ...props }, ref) => {
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { cn } from '@/lib/utils';
3
3
 
4
- export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
4
+ export type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
5
5
 
6
6
  const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
7
7
  ({ className, ...props }, ref) => (
@@ -1,6 +1,10 @@
1
1
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
2
  import { toast } from 'sonner';
3
3
  import { t } from '@/lib/i18n';
4
+ import {
5
+ applyInstallResultToInstalledView,
6
+ applyManageResultToInstalledView
7
+ } from '@/components/marketplace/marketplace-installed-cache';
4
8
  import {
5
9
  fetchMarketplaceItem,
6
10
  fetchMarketplaceInstalled,
@@ -10,7 +14,12 @@ import {
10
14
  manageMarketplaceItem,
11
15
  type MarketplaceListParams
12
16
  } from '@/api/marketplace';
13
- import type { MarketplaceInstallRequest, MarketplaceItemType, MarketplaceManageRequest } from '@/api/types';
17
+ import type {
18
+ MarketplaceInstallRequest,
19
+ MarketplaceInstalledView,
20
+ MarketplaceItemType,
21
+ MarketplaceManageRequest
22
+ } from '@/api/types';
14
23
 
15
24
  export function useMarketplaceItems(params: MarketplaceListParams) {
16
25
  return useQuery({
@@ -59,9 +68,19 @@ export function useInstallMarketplaceItem() {
59
68
 
60
69
  return useMutation({
61
70
  mutationFn: (request: MarketplaceInstallRequest) => installMarketplaceItem(request),
62
- onSuccess: (result) => {
63
- queryClient.invalidateQueries({ queryKey: ['marketplace-installed', result.type] });
64
- queryClient.invalidateQueries({ queryKey: ['marketplace-items'] });
71
+ onSuccess: (result, variables) => {
72
+ queryClient.setQueryData<MarketplaceInstalledView | undefined>(
73
+ ['marketplace-installed', result.type],
74
+ (view) => applyInstallResultToInstalledView({
75
+ view,
76
+ request: variables,
77
+ result
78
+ })
79
+ );
80
+ queryClient.invalidateQueries({
81
+ queryKey: ['marketplace-installed', result.type],
82
+ refetchType: 'inactive'
83
+ });
65
84
  if (result.type === 'plugin') {
66
85
  queryClient.invalidateQueries({ queryKey: ['ncp-session-types'] });
67
86
  }
@@ -81,9 +100,19 @@ export function useManageMarketplaceItem() {
81
100
 
82
101
  return useMutation({
83
102
  mutationFn: (request: MarketplaceManageRequest) => manageMarketplaceItem(request),
84
- onSuccess: (result) => {
85
- queryClient.invalidateQueries({ queryKey: ['marketplace-installed', result.type] });
86
- queryClient.invalidateQueries({ queryKey: ['marketplace-items'] });
103
+ onSuccess: (result, variables) => {
104
+ queryClient.setQueryData<MarketplaceInstalledView | undefined>(
105
+ ['marketplace-installed', result.type],
106
+ (view) => applyManageResultToInstalledView({
107
+ view,
108
+ request: variables,
109
+ result
110
+ })
111
+ );
112
+ queryClient.invalidateQueries({
113
+ queryKey: ['marketplace-installed', result.type],
114
+ refetchType: 'inactive'
115
+ });
87
116
  if (result.type === 'plugin') {
88
117
  queryClient.invalidateQueries({ queryKey: ['ncp-session-types'] });
89
118
  }
@@ -0,0 +1,99 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { toast } from 'sonner';
3
+ import {
4
+ doctorMcpMarketplaceItem,
5
+ fetchMcpMarketplaceContent,
6
+ fetchMcpMarketplaceInstalled,
7
+ fetchMcpMarketplaceItem,
8
+ fetchMcpMarketplaceItems,
9
+ fetchMcpMarketplaceRecommendations,
10
+ installMcpMarketplaceItem,
11
+ manageMcpMarketplaceItem,
12
+ type McpMarketplaceListParams
13
+ } from '@/api/mcp-marketplace';
14
+ import { t } from '@/lib/i18n';
15
+
16
+ export function useMcpMarketplaceItems(params: McpMarketplaceListParams) {
17
+ return useQuery({
18
+ queryKey: ['marketplace-mcp-items', params],
19
+ queryFn: () => fetchMcpMarketplaceItems(params),
20
+ staleTime: 15_000
21
+ });
22
+ }
23
+
24
+ export function useMcpMarketplaceInstalled() {
25
+ return useQuery({
26
+ queryKey: ['marketplace-mcp-installed'],
27
+ queryFn: () => fetchMcpMarketplaceInstalled(),
28
+ staleTime: 10_000
29
+ });
30
+ }
31
+
32
+ export function useMcpMarketplaceItem(slug: string | null) {
33
+ return useQuery({
34
+ queryKey: ['marketplace-mcp-item', slug],
35
+ queryFn: () => fetchMcpMarketplaceItem(slug as string),
36
+ enabled: Boolean(slug),
37
+ staleTime: 30_000
38
+ });
39
+ }
40
+
41
+ export function useMcpMarketplaceContent(slug: string | null) {
42
+ return useQuery({
43
+ queryKey: ['marketplace-mcp-content', slug],
44
+ queryFn: () => fetchMcpMarketplaceContent(slug as string),
45
+ enabled: Boolean(slug),
46
+ staleTime: 30_000
47
+ });
48
+ }
49
+
50
+ export function useMcpMarketplaceRecommendations(params: { scene?: string; limit?: number }) {
51
+ return useQuery({
52
+ queryKey: ['marketplace-mcp-recommendations', params],
53
+ queryFn: () => fetchMcpMarketplaceRecommendations(params),
54
+ staleTime: 30_000
55
+ });
56
+ }
57
+
58
+ export function useInstallMcpMarketplaceItem() {
59
+ const queryClient = useQueryClient();
60
+ return useMutation({
61
+ mutationFn: installMcpMarketplaceItem,
62
+ onSuccess: (result) => {
63
+ queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-installed'] });
64
+ queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-items'] });
65
+ queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-doctor'] });
66
+ toast.success(result.message || t('marketplaceInstallSuccessMcp'));
67
+ },
68
+ onError: (error: Error) => {
69
+ toast.error(error.message || t('marketplaceInstallFailed'));
70
+ }
71
+ });
72
+ }
73
+
74
+ export function useManageMcpMarketplaceItem() {
75
+ const queryClient = useQueryClient();
76
+ return useMutation({
77
+ mutationFn: manageMcpMarketplaceItem,
78
+ onSuccess: (result: { message?: string }) => {
79
+ queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-installed'] });
80
+ queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-items'] });
81
+ queryClient.invalidateQueries({ queryKey: ['marketplace-mcp-doctor'] });
82
+ toast.success(result.message || t('marketplaceMcpManageSuccess'));
83
+ },
84
+ onError: (error: Error) => {
85
+ toast.error(error.message || t('marketplaceOperationFailed'));
86
+ }
87
+ });
88
+ }
89
+
90
+ export function useDoctorMcpMarketplaceItem(name: string | null) {
91
+ return useQuery({
92
+ queryKey: ['marketplace-mcp-doctor', name],
93
+ queryFn: () => doctorMcpMarketplaceItem(name as string),
94
+ enabled: Boolean(name),
95
+ staleTime: 15_000
96
+ });
97
+ }
98
+
99
+ export { fetchMcpMarketplaceContent };