@nextclaw/ui 0.9.2 → 0.9.3

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 +6 -0
  2. package/dist/assets/ChannelsList-ZBPiF0y2.js +1 -0
  3. package/dist/assets/ChatPage-BOgoolWK.js +38 -0
  4. package/dist/assets/{DocBrowser-CVwUDJMO.js → DocBrowser-BUYNHg0Y.js} +1 -1
  5. package/dist/assets/LogoBadge-DXPq99LJ.js +1 -0
  6. package/dist/assets/MarketplacePage-Dx7nexYN.js +49 -0
  7. package/dist/assets/McpMarketplacePage-064wdotP.js +40 -0
  8. package/dist/assets/{ModelConfig-CsX-_fyy.js → ModelConfig-BDIfLesG.js} +1 -1
  9. package/dist/assets/ProvidersList-DrlIr46m.js +1 -0
  10. package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +1 -0
  11. package/dist/assets/{RuntimeConfig-CX2TGEG1.js → RuntimeConfig-BPxXEGzM.js} +1 -1
  12. package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-BIqnlpne.js} +1 -1
  13. package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-jKZEVF2q.js} +2 -2
  14. package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-C_FXgVe1.js} +2 -2
  15. package/dist/assets/{chat-message-AWIcksDK.js → chat-message-DmzpZJc_.js} +1 -1
  16. package/dist/assets/index-Byfw276e.js +8 -0
  17. package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
  18. package/dist/assets/index-bhNuQis7.css +1 -0
  19. package/dist/assets/{label-DD61y-4v.js → label-B1MloEtn.js} +1 -1
  20. package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
  21. package/dist/assets/{page-layout-CfnoVycc.js → page-layout-BGg1EhM5.js} +1 -1
  22. package/dist/assets/{popover-DsugZ6rp.js → popover-jJMv74Fp.js} +1 -1
  23. package/dist/assets/{security-config-DIrf2Z0O.js → security-config-Boh9NIMz.js} +1 -1
  24. package/dist/assets/skeleton-CmATs_b3.js +1 -0
  25. package/dist/assets/status-dot-DNyCdxPZ.js +1 -0
  26. package/dist/assets/{switch-NX5OmUXQ.js → switch-DE_MYk7x.js} +1 -1
  27. package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-B-zErYPr.js} +1 -1
  28. package/dist/assets/{useConfirmDialog-BuQnVTeR.js → useConfirmDialog-BqQ6QfhB.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 +4 -4
  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 +57 -0
  38. package/src/api/remote.types.ts +80 -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 +320 -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 +92 -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 +115 -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,320 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { RemoteRuntimeView, RemoteServiceView } from '@/api/types';
3
+ import {
4
+ useRemoteDoctor,
5
+ useRemoteLogin,
6
+ useRemoteLogout,
7
+ useRemoteServiceControl,
8
+ useRemoteSettings,
9
+ useRemoteStatus
10
+ } from '@/hooks/useRemoteAccess';
11
+ import { PageHeader, PageLayout } from '@/components/layout/page-layout';
12
+ import { Button } from '@/components/ui/button';
13
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
14
+ import { Input } from '@/components/ui/input';
15
+ import { Label } from '@/components/ui/label';
16
+ import { StatusDot } from '@/components/ui/status-dot';
17
+ import { Switch } from '@/components/ui/switch';
18
+ import { formatDateTime, t } from '@/lib/i18n';
19
+ import { Activity, KeyRound, Laptop, RefreshCcw, ServerCog, ShieldCheck, SquareTerminal } from 'lucide-react';
20
+
21
+ function getRuntimeStatus(runtime: RemoteRuntimeView | null): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
22
+ if (!runtime) {
23
+ return { status: 'inactive', label: t('remoteRuntimeMissing') };
24
+ }
25
+ if (runtime.state === 'connected') {
26
+ return { status: 'ready', label: t('remoteStateConnected') };
27
+ }
28
+ if (runtime.state === 'connecting') {
29
+ return { status: 'warning', label: t('remoteStateConnecting') };
30
+ }
31
+ if (runtime.state === 'error') {
32
+ return { status: 'warning', label: t('remoteStateError') };
33
+ }
34
+ if (runtime.state === 'disconnected') {
35
+ return { status: 'warning', label: t('remoteStateDisconnected') };
36
+ }
37
+ return { status: 'inactive', label: t('remoteStateDisabled') };
38
+ }
39
+
40
+ function getServiceStatus(service: RemoteServiceView): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
41
+ if (!service.running) {
42
+ return { status: 'inactive', label: t('remoteServiceStopped') };
43
+ }
44
+ return service.currentProcess
45
+ ? { status: 'ready', label: t('remoteServiceManagedRunning') }
46
+ : { status: 'active', label: t('remoteServiceRunning') };
47
+ }
48
+
49
+ function KeyValueRow(props: { label: string; value?: string | number | null; muted?: boolean }) {
50
+ const value = props.value === undefined || props.value === null || props.value === '' ? '-' : String(props.value);
51
+ return (
52
+ <div className="flex items-start justify-between gap-4 py-2 text-sm">
53
+ <span className="text-gray-500">{props.label}</span>
54
+ <span className={props.muted ? 'text-right text-gray-500' : 'text-right text-gray-800'}>{value}</span>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ export function RemoteAccessPage() {
60
+ const remoteStatus = useRemoteStatus();
61
+ const loginMutation = useRemoteLogin();
62
+ const logoutMutation = useRemoteLogout();
63
+ const settingsMutation = useRemoteSettings();
64
+ const doctorMutation = useRemoteDoctor();
65
+ const serviceMutation = useRemoteServiceControl();
66
+
67
+ const status = remoteStatus.data;
68
+ const runtimeStatus = useMemo(() => getRuntimeStatus(status?.runtime ?? null), [status?.runtime]);
69
+ const serviceStatus = useMemo(() => getServiceStatus(status?.service ?? { running: false, currentProcess: false }), [status?.service]);
70
+
71
+ const [email, setEmail] = useState('');
72
+ const [password, setPassword] = useState('');
73
+ const [loginApiBase, setLoginApiBase] = useState('');
74
+ const [register, setRegister] = useState(false);
75
+ const [enabled, setEnabled] = useState(false);
76
+ const [deviceName, setDeviceName] = useState('');
77
+ const [platformApiBase, setPlatformApiBase] = useState('');
78
+
79
+ useEffect(() => {
80
+ if (!status) {
81
+ return;
82
+ }
83
+ setEnabled(status.settings.enabled);
84
+ setDeviceName(status.settings.deviceName);
85
+ setPlatformApiBase(status.settings.platformApiBase);
86
+ if (!loginApiBase) {
87
+ setLoginApiBase(status.account.apiBase ?? status.settings.platformApiBase ?? '');
88
+ }
89
+ }, [loginApiBase, status]);
90
+
91
+ if (remoteStatus.isLoading && !status) {
92
+ return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
93
+ }
94
+
95
+ return (
96
+ <PageLayout className="space-y-6">
97
+ <PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
98
+
99
+ <div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
100
+ <Card>
101
+ <CardHeader>
102
+ <CardTitle className="flex items-center gap-2">
103
+ <Activity className="h-4 w-4 text-primary" />
104
+ {t('remoteOverviewTitle')}
105
+ </CardTitle>
106
+ <CardDescription>{t('remoteOverviewDescription')}</CardDescription>
107
+ </CardHeader>
108
+ <CardContent className="space-y-5">
109
+ <div className="flex flex-wrap gap-2">
110
+ <StatusDot status={status?.account.loggedIn ? 'ready' : 'inactive'} label={status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
111
+ <StatusDot status={serviceStatus.status} label={serviceStatus.label} />
112
+ <StatusDot status={runtimeStatus.status} label={runtimeStatus.label} />
113
+ </div>
114
+
115
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
116
+ <KeyValueRow label={t('remoteLocalOrigin')} value={status?.localOrigin} />
117
+ <KeyValueRow label={t('remotePublicPlatform')} value={status?.platformBase ?? status?.account.platformBase} />
118
+ <KeyValueRow label={t('remoteDeviceId')} value={status?.runtime?.deviceId} muted />
119
+ <KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
120
+ <KeyValueRow label={t('remoteLastError')} value={status?.runtime?.lastError} muted />
121
+ </div>
122
+ </CardContent>
123
+ </Card>
124
+
125
+ <Card>
126
+ <CardHeader>
127
+ <CardTitle className="flex items-center gap-2">
128
+ <Laptop className="h-4 w-4 text-primary" />
129
+ {t('remoteDeviceTitle')}
130
+ </CardTitle>
131
+ <CardDescription>{t('remoteDeviceDescription')}</CardDescription>
132
+ </CardHeader>
133
+ <CardContent className="space-y-4">
134
+ <div className="space-y-2">
135
+ <div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
136
+ <div>
137
+ <p className="text-sm font-medium text-gray-900">{t('remoteEnabled')}</p>
138
+ <p className="mt-1 text-xs text-gray-500">{t('remoteEnabledHelp')}</p>
139
+ </div>
140
+ <Switch checked={enabled} onCheckedChange={setEnabled} />
141
+ </div>
142
+ </div>
143
+
144
+ <div className="space-y-2">
145
+ <Label htmlFor="remote-device-name">{t('remoteDeviceName')}</Label>
146
+ <Input id="remote-device-name" value={deviceName} onChange={(event) => setDeviceName(event.target.value)} placeholder={t('remoteDeviceNamePlaceholder')} />
147
+ </div>
148
+
149
+ <div className="space-y-2">
150
+ <Label htmlFor="remote-platform-api-base">{t('remotePlatformApiBase')}</Label>
151
+ <Input
152
+ id="remote-platform-api-base"
153
+ value={platformApiBase}
154
+ onChange={(event) => setPlatformApiBase(event.target.value)}
155
+ placeholder="https://ai-gateway-api.nextclaw.io/v1"
156
+ />
157
+ <p className="text-xs text-gray-500">{t('remotePlatformApiBaseHelp')}</p>
158
+ </div>
159
+
160
+ <div className="flex flex-wrap gap-3">
161
+ <Button
162
+ onClick={() =>
163
+ settingsMutation.mutate({
164
+ enabled,
165
+ deviceName,
166
+ platformApiBase
167
+ })
168
+ }
169
+ disabled={settingsMutation.isPending}
170
+ >
171
+ {settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
172
+ </Button>
173
+ <Button
174
+ variant="outline"
175
+ onClick={() => serviceMutation.mutate('restart')}
176
+ disabled={serviceMutation.isPending}
177
+ >
178
+ <RefreshCcw className="mr-2 h-4 w-4" />
179
+ {t('remoteRestartService')}
180
+ </Button>
181
+ </div>
182
+ <p className="text-xs text-gray-500">{t('remoteSaveHint')}</p>
183
+ </CardContent>
184
+ </Card>
185
+ </div>
186
+
187
+ <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
188
+ <Card>
189
+ <CardHeader>
190
+ <CardTitle className="flex items-center gap-2">
191
+ <KeyRound className="h-4 w-4 text-primary" />
192
+ {t('remoteAccountTitle')}
193
+ </CardTitle>
194
+ <CardDescription>{t('remoteAccountDescription')}</CardDescription>
195
+ </CardHeader>
196
+ <CardContent className="space-y-4">
197
+ {status?.account.loggedIn ? (
198
+ <>
199
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
200
+ <KeyValueRow label={t('remoteAccountEmail')} value={status.account.email} />
201
+ <KeyValueRow label={t('remoteAccountRole')} value={status.account.role} />
202
+ <KeyValueRow label={t('remoteApiBase')} value={status.account.apiBase} />
203
+ </div>
204
+ <Button variant="outline" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
205
+ {logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
206
+ </Button>
207
+ </>
208
+ ) : (
209
+ <>
210
+ <div className="space-y-2">
211
+ <Label htmlFor="remote-email">{t('remoteEmail')}</Label>
212
+ <Input id="remote-email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="name@example.com" />
213
+ </div>
214
+ <div className="space-y-2">
215
+ <Label htmlFor="remote-password">{t('remotePassword')}</Label>
216
+ <Input id="remote-password" type="password" value={password} onChange={(event) => setPassword(event.target.value)} placeholder={t('remotePasswordPlaceholder')} />
217
+ </div>
218
+ <div className="space-y-2">
219
+ <Label htmlFor="remote-login-api-base">{t('remoteApiBase')}</Label>
220
+ <Input
221
+ id="remote-login-api-base"
222
+ value={loginApiBase}
223
+ onChange={(event) => setLoginApiBase(event.target.value)}
224
+ placeholder="https://ai-gateway-api.nextclaw.io/v1"
225
+ />
226
+ </div>
227
+ <div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
228
+ <div>
229
+ <p className="text-sm font-medium text-gray-900">{t('remoteRegisterIfNeeded')}</p>
230
+ <p className="mt-1 text-xs text-gray-500">{t('remoteRegisterIfNeededHelp')}</p>
231
+ </div>
232
+ <Switch checked={register} onCheckedChange={setRegister} />
233
+ </div>
234
+ <Button
235
+ onClick={() =>
236
+ loginMutation.mutate({
237
+ email,
238
+ password,
239
+ apiBase: loginApiBase,
240
+ register
241
+ })
242
+ }
243
+ disabled={loginMutation.isPending || !email.trim() || !password}
244
+ >
245
+ {loginMutation.isPending ? t('remoteLoggingIn') : register ? t('remoteCreateAccount') : t('remoteLogin')}
246
+ </Button>
247
+ </>
248
+ )}
249
+ </CardContent>
250
+ </Card>
251
+
252
+ <Card>
253
+ <CardHeader>
254
+ <CardTitle className="flex items-center gap-2">
255
+ <ServerCog className="h-4 w-4 text-primary" />
256
+ {t('remoteServiceTitle')}
257
+ </CardTitle>
258
+ <CardDescription>{t('remoteServiceDescription')}</CardDescription>
259
+ </CardHeader>
260
+ <CardContent className="space-y-4">
261
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
262
+ <KeyValueRow label={t('remoteServicePid')} value={status?.service.pid} />
263
+ <KeyValueRow label={t('remoteServiceUiUrl')} value={status?.service.uiUrl} />
264
+ <KeyValueRow label={t('remoteServiceCurrentProcess')} value={status?.service.currentProcess ? t('yes') : t('no')} />
265
+ </div>
266
+ <div className="flex flex-wrap gap-3">
267
+ <Button variant="primary" onClick={() => serviceMutation.mutate('start')} disabled={serviceMutation.isPending}>
268
+ {t('remoteStartService')}
269
+ </Button>
270
+ <Button variant="outline" onClick={() => serviceMutation.mutate('restart')} disabled={serviceMutation.isPending}>
271
+ {t('remoteRestartService')}
272
+ </Button>
273
+ <Button variant="outline" onClick={() => serviceMutation.mutate('stop')} disabled={serviceMutation.isPending}>
274
+ {t('remoteStopService')}
275
+ </Button>
276
+ </div>
277
+ <p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
278
+ </CardContent>
279
+ </Card>
280
+ </div>
281
+
282
+ <Card>
283
+ <CardHeader>
284
+ <CardTitle className="flex items-center gap-2">
285
+ <ShieldCheck className="h-4 w-4 text-primary" />
286
+ {t('remoteDoctorTitle')}
287
+ </CardTitle>
288
+ <CardDescription>{t('remoteDoctorDescription')}</CardDescription>
289
+ </CardHeader>
290
+ <CardContent className="space-y-4">
291
+ <div className="flex flex-wrap gap-3">
292
+ <Button variant="outline" onClick={() => doctorMutation.mutate()} disabled={doctorMutation.isPending}>
293
+ <SquareTerminal className="mr-2 h-4 w-4" />
294
+ {doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
295
+ </Button>
296
+ </div>
297
+
298
+ {doctorMutation.data ? (
299
+ <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
300
+ <KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(doctorMutation.data.generatedAt)} muted />
301
+ <div className="mt-3 space-y-2">
302
+ {doctorMutation.data.checks.map((check) => (
303
+ <div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
304
+ <div className="flex items-center justify-between gap-3">
305
+ <span className="text-sm font-medium text-gray-900">{check.name}</span>
306
+ <StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
307
+ </div>
308
+ <p className="mt-2 text-sm text-gray-600">{check.detail}</p>
309
+ </div>
310
+ ))}
311
+ </div>
312
+ </div>
313
+ ) : (
314
+ <p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>
315
+ )}
316
+ </CardContent>
317
+ </Card>
318
+ </PageLayout>
319
+ );
320
+ }
@@ -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 };
@@ -0,0 +1,92 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import {
3
+ controlRemoteService,
4
+ fetchRemoteStatus,
5
+ fetchRemoteDoctor,
6
+ loginRemote,
7
+ logoutRemote,
8
+ updateRemoteSettings
9
+ } from '@/api/remote';
10
+ import { t } from '@/lib/i18n';
11
+ import { toast } from 'sonner';
12
+
13
+ export function useRemoteStatus() {
14
+ return useQuery({
15
+ queryKey: ['remote-status'],
16
+ queryFn: fetchRemoteStatus,
17
+ staleTime: 5000,
18
+ refetchOnWindowFocus: true
19
+ });
20
+ }
21
+
22
+ export function useRemoteLogin() {
23
+ const queryClient = useQueryClient();
24
+
25
+ return useMutation({
26
+ mutationFn: loginRemote,
27
+ onSuccess: () => {
28
+ queryClient.invalidateQueries({ queryKey: ['remote-status'] });
29
+ toast.success(t('remoteLoginSuccess'));
30
+ },
31
+ onError: (error: Error) => {
32
+ toast.error(`${t('remoteLoginFailed')}: ${error.message}`);
33
+ }
34
+ });
35
+ }
36
+
37
+ export function useRemoteLogout() {
38
+ const queryClient = useQueryClient();
39
+
40
+ return useMutation({
41
+ mutationFn: logoutRemote,
42
+ onSuccess: () => {
43
+ queryClient.invalidateQueries({ queryKey: ['remote-status'] });
44
+ toast.success(t('remoteLogoutSuccess'));
45
+ },
46
+ onError: (error: Error) => {
47
+ toast.error(`${t('remoteLogoutFailed')}: ${error.message}`);
48
+ }
49
+ });
50
+ }
51
+
52
+ export function useRemoteSettings() {
53
+ const queryClient = useQueryClient();
54
+
55
+ return useMutation({
56
+ mutationFn: updateRemoteSettings,
57
+ onSuccess: () => {
58
+ queryClient.invalidateQueries({ queryKey: ['remote-status'] });
59
+ toast.success(t('remoteSettingsSaved'));
60
+ },
61
+ onError: (error: Error) => {
62
+ toast.error(`${t('remoteSettingsSaveFailed')}: ${error.message}`);
63
+ }
64
+ });
65
+ }
66
+
67
+ export function useRemoteDoctor() {
68
+ return useMutation({
69
+ mutationFn: fetchRemoteDoctor,
70
+ onSuccess: () => {
71
+ toast.success(t('remoteDoctorCompleted'));
72
+ },
73
+ onError: (error: Error) => {
74
+ toast.error(`${t('remoteDoctorFailed')}: ${error.message}`);
75
+ }
76
+ });
77
+ }
78
+
79
+ export function useRemoteServiceControl() {
80
+ const queryClient = useQueryClient();
81
+
82
+ return useMutation({
83
+ mutationFn: controlRemoteService,
84
+ onSuccess: (result) => {
85
+ queryClient.invalidateQueries({ queryKey: ['remote-status'] });
86
+ toast.success(result.message);
87
+ },
88
+ onError: (error: Error) => {
89
+ toast.error(`${t('remoteServiceActionFailed')}: ${error.message}`);
90
+ }
91
+ });
92
+ }
@@ -4,6 +4,30 @@ import { API_BASE } from '@/api/client';
4
4
  import { useUiStore } from '@/stores/ui.store';
5
5
  import type { QueryClient } from '@tanstack/react-query';
6
6
 
7
+ function shouldInvalidateConfigQuery(configPath: string) {
8
+ const normalized = configPath.trim().toLowerCase();
9
+ if (!normalized) {
10
+ return true;
11
+ }
12
+ if (normalized.startsWith('plugins') || normalized.startsWith('skills')) {
13
+ return false;
14
+ }
15
+ return true;
16
+ }
17
+
18
+ function invalidateMarketplaceQueries(queryClient: QueryClient | undefined, configPath: string): void {
19
+ if (configPath.startsWith('plugins')) {
20
+ queryClient?.invalidateQueries({ queryKey: ['ncp-session-types'] });
21
+ queryClient?.invalidateQueries({ queryKey: ['marketplace-installed', 'plugin'] });
22
+ queryClient?.invalidateQueries({ queryKey: ['marketplace-items'] });
23
+ }
24
+ if (configPath.startsWith('mcp')) {
25
+ queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-installed'] });
26
+ queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-items'] });
27
+ queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-doctor'] });
28
+ }
29
+ }
30
+
7
31
  export function useWebSocket(queryClient?: QueryClient) {
8
32
  const [ws, setWs] = useState<ConfigWebSocket | null>(null);
9
33
  const { setConnectionStatus } = useUiStore();
@@ -83,17 +107,6 @@ export function useWebSocket(queryClient?: QueryClient) {
83
107
  queryClient.invalidateQueries({ queryKey: ['ncp-session-messages'] });
84
108
  };
85
109
 
86
- const shouldInvalidateConfigQuery = (configPath: string) => {
87
- const normalized = configPath.trim().toLowerCase();
88
- if (!normalized) {
89
- return true;
90
- }
91
- if (normalized.startsWith('plugins') || normalized.startsWith('skills')) {
92
- return false;
93
- }
94
- return true;
95
- };
96
-
97
110
  setConnectionStatus('connecting');
98
111
 
99
112
  client.on('connection.open', () => {
@@ -121,11 +134,7 @@ export function useWebSocket(queryClient?: QueryClient) {
121
134
  if (configPath.startsWith('session')) {
122
135
  invalidateSessionQueries();
123
136
  }
124
- if (configPath.startsWith('plugins')) {
125
- queryClient?.invalidateQueries({ queryKey: ['ncp-session-types'] });
126
- queryClient?.invalidateQueries({ queryKey: ['marketplace-installed', 'plugin'] });
127
- queryClient?.invalidateQueries({ queryKey: ['marketplace-items'] });
128
- }
137
+ invalidateMarketplaceQueries(queryClient, configPath);
129
138
  });
130
139
 
131
140
  client.on('run.updated', (event) => {