@nextclaw/ui 0.12.0 → 0.12.2

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 (96) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/dist/assets/ChannelsList-uKmkpD25.js +8 -0
  3. package/dist/assets/ChatPage-CslhBPfT.js +43 -0
  4. package/dist/assets/{DocBrowser-DxdSujSc.js → DocBrowser-C7-1sXqo.js} +1 -1
  5. package/dist/assets/DocBrowser-DQjtSsY3.js +1 -0
  6. package/dist/assets/{DocBrowserContext-CQ-8jMha.js → DocBrowserContext-DN5tjUoS.js} +1 -1
  7. package/dist/assets/{LogoBadge-D-KQIN4U.js → LogoBadge-DDS1sU_U.js} +1 -1
  8. package/dist/assets/MarketplacePage-BZQW70ti.js +1 -0
  9. package/dist/assets/{MarketplacePage-CRNvxtvx.js → MarketplacePage-DE0QjYVv.js} +1 -1
  10. package/dist/assets/{McpMarketplacePage-Cu7GmCcc.js → McpMarketplacePage-CeLvv1xy.js} +1 -1
  11. package/dist/assets/ModelConfig-D1JtGtQv.js +1 -0
  12. package/dist/assets/ProviderScopedModelInput-SAJH6nkC.js +1 -0
  13. package/dist/assets/{ProvidersList-BWbUb7-2.js → ProvidersList-1rKi3aQT.js} +1 -1
  14. package/dist/assets/{RemoteAccessPage-NsawrZb0.js → RemoteAccessPage-bIAKxDky.js} +1 -1
  15. package/dist/assets/RuntimeConfig-BTk9319O.js +1 -0
  16. package/dist/assets/SearchConfig-EjeszXbv.js +1 -0
  17. package/dist/assets/{SecretsConfig-CgDZOd3w.js → SecretsConfig-cnAXvREZ.js} +1 -1
  18. package/dist/assets/{SessionsConfig-Dd-KM7F7.js → SessionsConfig-BIXiDaK2.js} +1 -1
  19. package/dist/assets/{book-open-FnK2xCQd.js → book-open-DvWqOode.js} +1 -1
  20. package/dist/assets/chat-session-display-D4bYa0b8.js +1 -0
  21. package/dist/assets/{chunk-JZWAC4HX-B5l0hr_u.js → chunk-JZWAC4HX-CxfKRD7X.js} +1 -1
  22. package/dist/assets/{config-JKmXfZ3q.js → config-BeGwf2Ao.js} +1 -1
  23. package/dist/assets/{createLucideIcon-o1WWhwhd.js → createLucideIcon-C7MmdIX3.js} +1 -1
  24. package/dist/assets/{dist-C_moWYv7.js → dist-B6VMuIQN.js} +1 -1
  25. package/dist/assets/{dist-DazA6Wd_.js → dist-RWNFhxvR.js} +1 -1
  26. package/dist/assets/{external-link-BKje3SiD.js → external-link-U86Acd1t.js} +1 -1
  27. package/dist/assets/{hash-DfW4DT8O.js → hash-D-OVfV3Z.js} +1 -1
  28. package/dist/assets/i18n-hM3v-3YG.js +1 -0
  29. package/dist/assets/{index-BZaB1TqM.js → index-8XNPYwJu.js} +3 -3
  30. package/dist/assets/index-CpxuJa9o.css +1 -0
  31. package/dist/assets/{label-BzDWmdOe.js → label-CHJ1ATds.js} +1 -1
  32. package/dist/assets/loader-circle-C8cpaL0w.js +1 -0
  33. package/dist/assets/{logos-CTLlde_T.js → logos-U1_qDA3U.js} +1 -1
  34. package/dist/assets/{page-layout-BagR3t59.js → page-layout-Z1klaUFW.js} +1 -1
  35. package/dist/assets/plus-CrkO1kob.js +1 -0
  36. package/dist/assets/{popover-5DWhNfd4.js → popover-xWbqMnIN.js} +1 -1
  37. package/dist/assets/{react-C3yu5yge.js → react-3YE87-lE.js} +1 -1
  38. package/dist/assets/{refresh-ccw-BAJf-h7w.js → refresh-ccw-JQh1lwq-.js} +1 -1
  39. package/dist/assets/{save-aa6z4GJL.js → save-4VRlzkii.js} +1 -1
  40. package/dist/assets/search-EX-Papzl.js +1 -0
  41. package/dist/assets/{security-config-DRDxrApx.js → security-config-CGazBahs.js} +1 -1
  42. package/dist/assets/{select-BHJPiJWt.js → select-DF-AUoie.js} +1 -1
  43. package/dist/assets/skeleton-B0mmt1vo.js +1 -0
  44. package/dist/assets/{status-dot-DUwsTIdv.js → status-dot-Bq_8Ojvv.js} +1 -1
  45. package/dist/assets/{switch-B6nCfcOB.js → switch-D7JF_RZ-.js} +1 -1
  46. package/dist/assets/{tabs-custom-B57SMElx.js → tabs-custom-CLksZ2bO.js} +1 -1
  47. package/dist/assets/{trash-2-CrjYH5ok.js → trash-2-VV8jvziy.js} +1 -1
  48. package/dist/assets/{useConfirmDialog-DsxnXB1B.js → useConfirmDialog-D6HxybcM.js} +1 -1
  49. package/dist/assets/{useMutation-oTTWXgLG.js → useMutation-DBTWPbTg.js} +1 -1
  50. package/dist/assets/x-B4sxJkGY.js +1 -0
  51. package/dist/index.html +18 -18
  52. package/package.json +6 -6
  53. package/src/api/agents.ts +9 -1
  54. package/src/api/types.ts +25 -4
  55. package/src/components/agents/AgentDialogs.tsx +400 -0
  56. package/src/components/agents/AgentsPage.test.tsx +112 -1
  57. package/src/components/agents/AgentsPage.tsx +104 -112
  58. package/src/components/chat/ChatConversationPanel.test.tsx +31 -0
  59. package/src/components/chat/ChatConversationPanel.tsx +7 -6
  60. package/src/components/chat/ChatSidebar.test.tsx +41 -1
  61. package/src/components/chat/ChatWelcome.test.tsx +7 -2
  62. package/src/components/chat/ChatWelcome.tsx +38 -35
  63. package/src/components/chat/chat-page-runtime.test.ts +30 -0
  64. package/src/components/chat/chat-sidebar-session-item.tsx +6 -2
  65. package/src/components/chat/ncp/NcpChatPage.tsx +23 -1
  66. package/src/components/chat/ncp/ncp-session-adapter.ts +6 -1
  67. package/src/components/chat/useChatSessionTypeState.ts +1 -1
  68. package/src/components/common/ProviderScopedModelInput.tsx +149 -0
  69. package/src/components/config/ChannelForm.test.tsx +60 -0
  70. package/src/components/config/ChannelForm.tsx +52 -12
  71. package/src/components/config/ModelConfig.test.tsx +61 -0
  72. package/src/components/config/ModelConfig.tsx +15 -90
  73. package/src/components/config/RuntimeConfig.tsx +3 -24
  74. package/src/components/config/SearchConfig.test.tsx +150 -0
  75. package/src/components/config/SearchConfig.tsx +257 -71
  76. package/src/components/config/runtime-config-agent.utils.ts +5 -4
  77. package/src/hooks/agents/useAgents.ts +18 -1
  78. package/src/lib/i18n.agents.ts +21 -2
  79. package/src/lib/i18n.search.ts +37 -0
  80. package/src/lib/i18n.ts +6 -28
  81. package/dist/assets/ChannelsList-NKNKsf1J.js +0 -8
  82. package/dist/assets/ChatPage-p23OnnEI.js +0 -43
  83. package/dist/assets/DocBrowser-C8b2uPgL.js +0 -1
  84. package/dist/assets/MarketplacePage-GGkEXowp.js +0 -1
  85. package/dist/assets/ModelConfig-CEpx9fro.js +0 -1
  86. package/dist/assets/RuntimeConfig-BJHBsVTd.js +0 -1
  87. package/dist/assets/SearchConfig-BsaX_WYy.js +0 -1
  88. package/dist/assets/chat-session-display-BD_AN71I.js +0 -1
  89. package/dist/assets/i18n-BK1w-oBy.js +0 -1
  90. package/dist/assets/index-DaR9igPC.css +0 -1
  91. package/dist/assets/loader-circle-DdZPxBUz.js +0 -1
  92. package/dist/assets/plus-DP2PSCPO.js +0 -1
  93. package/dist/assets/provider-models-DJ29qHuA.js +0 -1
  94. package/dist/assets/search-pD6ZwQYF.js +0 -1
  95. package/dist/assets/skeleton-D6kCk9Y6.js +0 -1
  96. package/dist/assets/x-CTIQHUuD.js +0 -1
@@ -0,0 +1,400 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { AgentProfileView } from '@/api/types';
3
+ import { normalizeSessionType, resolveSessionTypeLabel, type ChatSessionTypeOption } from '@/components/chat/useChatSessionTypeState';
4
+ import { ProviderScopedModelInput } from '@/components/common/ProviderScopedModelInput';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle
14
+ } from '@/components/ui/dialog';
15
+ import { Input } from '@/components/ui/input';
16
+ import { t } from '@/lib/i18n';
17
+ import type { ProviderModelCatalogItem } from '@/lib/provider-models';
18
+ import { Pencil, Plus } from 'lucide-react';
19
+
20
+ export type AgentCreateFormState = {
21
+ id: string;
22
+ displayName: string;
23
+ description: string;
24
+ avatar: string;
25
+ home: string;
26
+ model: string;
27
+ runtime: string;
28
+ };
29
+
30
+ export type AgentEditFormState = {
31
+ displayName: string;
32
+ description: string;
33
+ avatar: string;
34
+ model: string;
35
+ runtime: string;
36
+ };
37
+
38
+ export const EMPTY_AGENT_CREATE_FORM: AgentCreateFormState = {
39
+ id: '',
40
+ displayName: '',
41
+ description: '',
42
+ avatar: '',
43
+ model: '',
44
+ home: '',
45
+ runtime: ''
46
+ };
47
+
48
+ export const EMPTY_AGENT_EDIT_FORM: AgentEditFormState = {
49
+ displayName: '',
50
+ description: '',
51
+ avatar: '',
52
+ model: '',
53
+ runtime: ''
54
+ };
55
+
56
+ export function toAgentEditFormState(agent: AgentProfileView): AgentEditFormState {
57
+ return {
58
+ displayName: agent.displayName ?? '',
59
+ description: agent.description ?? '',
60
+ avatar: agent.avatar ?? '',
61
+ model: agent.model ?? '',
62
+ runtime: agent.runtime ?? agent.engine ?? ''
63
+ };
64
+ }
65
+
66
+ function buildRuntimeSelectOptions(params: {
67
+ runtimeOptions: ChatSessionTypeOption[];
68
+ currentRuntime: string;
69
+ }): ChatSessionTypeOption[] {
70
+ const { runtimeOptions, currentRuntime: rawCurrentRuntime } = params;
71
+ const currentRuntime = rawCurrentRuntime.trim();
72
+ if (!currentRuntime) {
73
+ return runtimeOptions;
74
+ }
75
+ const normalizedCurrentRuntime = normalizeSessionType(currentRuntime);
76
+ if (runtimeOptions.some((option) => option.value === normalizedCurrentRuntime)) {
77
+ return runtimeOptions;
78
+ }
79
+ return [
80
+ ...runtimeOptions,
81
+ {
82
+ value: normalizedCurrentRuntime,
83
+ label: resolveSessionTypeLabel(normalizedCurrentRuntime),
84
+ ready: false,
85
+ reason: 'unavailable',
86
+ reasonMessage: null,
87
+ supportedModels: undefined,
88
+ recommendedModel: null,
89
+ cta: null
90
+ }
91
+ ].sort((left, right) => {
92
+ if (left.value === 'native') {
93
+ return -1;
94
+ }
95
+ if (right.value === 'native') {
96
+ return 1;
97
+ }
98
+ return left.value.localeCompare(right.value);
99
+ });
100
+ }
101
+
102
+ type AgentRuntimeSelectFieldProps = {
103
+ value: string;
104
+ disabled?: boolean;
105
+ runtimeOptions: ChatSessionTypeOption[];
106
+ defaultRuntime: string;
107
+ onChange: (value: string) => void;
108
+ };
109
+
110
+ function AgentRuntimeSelectField({
111
+ value,
112
+ disabled = false,
113
+ runtimeOptions,
114
+ defaultRuntime,
115
+ onChange
116
+ }: AgentRuntimeSelectFieldProps) {
117
+ const normalizedValue = value.trim() ? normalizeSessionType(value) : '';
118
+ const selectOptions = buildRuntimeSelectOptions({
119
+ runtimeOptions,
120
+ currentRuntime: value
121
+ });
122
+ const selectedRuntimeOption = selectOptions.find((option) => option.value === normalizedValue) ?? null;
123
+ const helperText =
124
+ selectedRuntimeOption?.reasonMessage?.trim() ||
125
+ (selectedRuntimeOption?.ready === false ? t('agentsRuntimeUnavailableHelp') : '');
126
+
127
+ return (
128
+ <div className="space-y-2">
129
+ <Select
130
+ value={normalizedValue || defaultRuntime}
131
+ onValueChange={(nextValue) => onChange(nextValue === defaultRuntime ? '' : nextValue)}
132
+ disabled={disabled}
133
+ >
134
+ <SelectTrigger aria-label={t('agentsCardRuntimeLabel')} className="rounded-xl">
135
+ <SelectValue placeholder={t('agentsRuntimeSelectPlaceholder')} />
136
+ </SelectTrigger>
137
+ <SelectContent className="rounded-xl">
138
+ {selectOptions.map((option) => (
139
+ <SelectItem
140
+ key={option.value}
141
+ value={option.value}
142
+ disabled={option.ready === false && option.value !== normalizedValue}
143
+ className="rounded-lg"
144
+ >
145
+ {option.label}
146
+ </SelectItem>
147
+ ))}
148
+ </SelectContent>
149
+ </Select>
150
+ {helperText ? <p className="text-xs text-gray-500">{helperText}</p> : null}
151
+ </div>
152
+ );
153
+ }
154
+
155
+ type AgentCreateDialogProps = {
156
+ open: boolean;
157
+ pending: boolean;
158
+ providerCatalog: ProviderModelCatalogItem[];
159
+ runtimeOptions: ChatSessionTypeOption[];
160
+ defaultRuntime: string;
161
+ onOpenChange: (open: boolean) => void;
162
+ onSubmit: (form: AgentCreateFormState) => Promise<void> | void;
163
+ };
164
+
165
+ export function AgentCreateDialog({
166
+ open,
167
+ pending,
168
+ providerCatalog,
169
+ runtimeOptions,
170
+ defaultRuntime,
171
+ onOpenChange,
172
+ onSubmit
173
+ }: AgentCreateDialogProps) {
174
+ const [form, setForm] = useState<AgentCreateFormState>(EMPTY_AGENT_CREATE_FORM);
175
+
176
+ useEffect(() => {
177
+ if (open || pending) {
178
+ return;
179
+ }
180
+ setForm(EMPTY_AGENT_CREATE_FORM);
181
+ }, [open, pending]);
182
+
183
+ return (
184
+ <Dialog open={open} onOpenChange={onOpenChange}>
185
+ <DialogContent className="flex max-h-[calc(100vh-2rem)] flex-col overflow-hidden border-none bg-[linear-gradient(180deg,#fff9f1_0%,#ffffff_24%)] p-0 sm:max-h-[760px] sm:max-w-xl">
186
+ <div className="shrink-0 border-b border-[#f0e2c8] px-6 py-6">
187
+ <DialogHeader className="text-left">
188
+ <DialogTitle>{t('agentsCreateDialogTitle')}</DialogTitle>
189
+ <DialogDescription>{t('agentsCreateDialogDescription')}</DialogDescription>
190
+ </DialogHeader>
191
+ </div>
192
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-6">
193
+ <div className="space-y-4">
194
+ <div className="grid gap-4 md:grid-cols-2">
195
+ <Input
196
+ value={form.id}
197
+ onChange={(event) =>
198
+ setForm((prev) => ({ ...prev, id: event.target.value }))
199
+ }
200
+ placeholder={t('agentsFormIdPlaceholder')}
201
+ />
202
+ <Input
203
+ value={form.displayName}
204
+ onChange={(event) =>
205
+ setForm((prev) => ({
206
+ ...prev,
207
+ displayName: event.target.value
208
+ }))
209
+ }
210
+ placeholder={t('agentsFormNamePlaceholder')}
211
+ />
212
+ <textarea
213
+ value={form.description}
214
+ onChange={(event) =>
215
+ setForm((prev) => ({
216
+ ...prev,
217
+ description: event.target.value
218
+ }))
219
+ }
220
+ placeholder={t('agentsFormDescriptionPlaceholder')}
221
+ rows={4}
222
+ className="min-h-28 rounded-xl border border-gray-200/80 bg-white px-3.5 py-2.5 text-sm text-gray-900 placeholder:text-gray-300 focus:border-primary/40 focus:outline-none focus:ring-1 focus:ring-primary/40 md:col-span-2"
223
+ />
224
+ <Input
225
+ value={form.avatar}
226
+ onChange={(event) =>
227
+ setForm((prev) => ({ ...prev, avatar: event.target.value }))
228
+ }
229
+ placeholder={t('agentsFormAvatarPlaceholder')}
230
+ />
231
+ <Input
232
+ value={form.home}
233
+ onChange={(event) =>
234
+ setForm((prev) => ({ ...prev, home: event.target.value }))
235
+ }
236
+ placeholder={t('agentsFormHomePlaceholder')}
237
+ />
238
+ <ProviderScopedModelInput
239
+ value={form.model}
240
+ onChange={(value) =>
241
+ setForm((prev) => ({ ...prev, model: value }))
242
+ }
243
+ providerCatalog={providerCatalog}
244
+ disabled={pending}
245
+ className="md:col-span-2"
246
+ />
247
+ <AgentRuntimeSelectField
248
+ value={form.runtime}
249
+ onChange={(value) =>
250
+ setForm((prev) => ({ ...prev, runtime: value }))
251
+ }
252
+ runtimeOptions={runtimeOptions}
253
+ defaultRuntime={defaultRuntime}
254
+ disabled={pending}
255
+ />
256
+ </div>
257
+ <div className="rounded-2xl border border-[#efe3ca] bg-[#fff9ef] px-4 py-3 text-xs leading-6 text-[#7a6246]">
258
+ {t('agentsCreateDialogHint')}
259
+ </div>
260
+ </div>
261
+ </div>
262
+ <DialogFooter className="shrink-0 border-t border-[#f1e7d4] px-6 py-5">
263
+ <Button type="button" variant="ghost" onClick={() => onOpenChange(false)} disabled={pending}>
264
+ {t('cancel')}
265
+ </Button>
266
+ <Button
267
+ type="button"
268
+ className="rounded-2xl bg-[#1f5c4d] px-5 text-white hover:bg-[#184d40]"
269
+ onClick={() => void onSubmit(form)}
270
+ disabled={pending || form.id.trim().length === 0}
271
+ >
272
+ <Plus className="mr-2 h-4 w-4" />
273
+ {t('agentsCreateAction')}
274
+ </Button>
275
+ </DialogFooter>
276
+ </DialogContent>
277
+ </Dialog>
278
+ );
279
+ }
280
+
281
+ type AgentEditDialogProps = {
282
+ agent: AgentProfileView | null;
283
+ pending: boolean;
284
+ providerCatalog: ProviderModelCatalogItem[];
285
+ runtimeOptions: ChatSessionTypeOption[];
286
+ defaultRuntime: string;
287
+ onOpenChange: (open: boolean) => void;
288
+ onSubmit: (agentId: string, form: AgentEditFormState) => Promise<void> | void;
289
+ };
290
+
291
+ export function AgentEditDialog({
292
+ agent,
293
+ pending,
294
+ providerCatalog,
295
+ runtimeOptions,
296
+ defaultRuntime,
297
+ onOpenChange,
298
+ onSubmit
299
+ }: AgentEditDialogProps) {
300
+ const [form, setForm] = useState<AgentEditFormState>(EMPTY_AGENT_EDIT_FORM);
301
+
302
+ useEffect(() => {
303
+ if (!agent) {
304
+ setForm(EMPTY_AGENT_EDIT_FORM);
305
+ return;
306
+ }
307
+ setForm(toAgentEditFormState(agent));
308
+ }, [agent]);
309
+
310
+ return (
311
+ <Dialog open={agent !== null} onOpenChange={onOpenChange}>
312
+ <DialogContent className="flex max-h-[calc(100vh-2rem)] flex-col overflow-hidden border-none bg-[linear-gradient(180deg,#fff9f1_0%,#ffffff_24%)] p-0 sm:max-h-[760px] sm:max-w-xl">
313
+ <div className="shrink-0 border-b border-[#f0e2c8] px-6 py-6">
314
+ <DialogHeader className="text-left">
315
+ <DialogTitle>{t('agentsEditDialogTitle')}</DialogTitle>
316
+ <DialogDescription>{t('agentsEditDialogDescription')}</DialogDescription>
317
+ </DialogHeader>
318
+ </div>
319
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-6">
320
+ <div className="space-y-4">
321
+ <div className="rounded-2xl border border-[#efe3ca] bg-[#fff9ef] px-4 py-3 text-xs leading-6 text-[#7a6246]">
322
+ <div className="font-semibold uppercase tracking-[0.16em] text-[#9b6118]">
323
+ {t('agentsEditHomeReadonly')}
324
+ </div>
325
+ <div className="mt-1 break-all text-sm text-[#3f3323]">
326
+ {agent?.workspace ?? '-'}
327
+ </div>
328
+ <div className="mt-1 text-[11px] text-[#8d7456]">
329
+ {t('agentsEditHomeReadonlyHint')}
330
+ </div>
331
+ </div>
332
+ <div className="grid gap-4 md:grid-cols-2">
333
+ <Input
334
+ value={form.displayName}
335
+ onChange={(event) =>
336
+ setForm((prev) => ({
337
+ ...prev,
338
+ displayName: event.target.value
339
+ }))
340
+ }
341
+ placeholder={t('agentsFormNamePlaceholder')}
342
+ />
343
+ <textarea
344
+ value={form.description}
345
+ onChange={(event) =>
346
+ setForm((prev) => ({
347
+ ...prev,
348
+ description: event.target.value
349
+ }))
350
+ }
351
+ placeholder={t('agentsFormDescriptionPlaceholder')}
352
+ rows={4}
353
+ className="min-h-28 rounded-xl border border-gray-200/80 bg-white px-3.5 py-2.5 text-sm text-gray-900 placeholder:text-gray-300 focus:border-primary/40 focus:outline-none focus:ring-1 focus:ring-primary/40 md:col-span-2"
354
+ />
355
+ <Input
356
+ value={form.avatar}
357
+ onChange={(event) =>
358
+ setForm((prev) => ({ ...prev, avatar: event.target.value }))
359
+ }
360
+ placeholder={t('agentsFormAvatarPlaceholder')}
361
+ />
362
+ <ProviderScopedModelInput
363
+ value={form.model}
364
+ onChange={(value) =>
365
+ setForm((prev) => ({ ...prev, model: value }))
366
+ }
367
+ providerCatalog={providerCatalog}
368
+ disabled={pending}
369
+ className="md:col-span-2"
370
+ />
371
+ <AgentRuntimeSelectField
372
+ value={form.runtime}
373
+ onChange={(value) =>
374
+ setForm((prev) => ({ ...prev, runtime: value }))
375
+ }
376
+ runtimeOptions={runtimeOptions}
377
+ defaultRuntime={defaultRuntime}
378
+ disabled={pending}
379
+ />
380
+ </div>
381
+ </div>
382
+ </div>
383
+ <DialogFooter className="shrink-0 border-t border-[#f1e7d4] px-6 py-5">
384
+ <Button type="button" variant="ghost" onClick={() => onOpenChange(false)} disabled={pending}>
385
+ {t('cancel')}
386
+ </Button>
387
+ <Button
388
+ type="button"
389
+ className="rounded-2xl bg-[#1f5c4d] px-5 text-white hover:bg-[#184d40]"
390
+ onClick={() => agent && onSubmit(agent.id, form)}
391
+ disabled={pending || agent === null}
392
+ >
393
+ <Pencil className="mr-2 h-4 w-4" />
394
+ {t('agentsEditSaveAction')}
395
+ </Button>
396
+ </DialogFooter>
397
+ </DialogContent>
398
+ </Dialog>
399
+ );
400
+ }
@@ -1,4 +1,5 @@
1
1
  import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
2
3
  import { MemoryRouter } from 'react-router-dom';
3
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
5
  import { AgentsPage } from '@/components/agents/AgentsPage';
@@ -6,7 +7,18 @@ import { setLanguage } from '@/lib/i18n';
6
7
 
7
8
  const mocks = vi.hoisted(() => ({
8
9
  createAgent: vi.fn(),
10
+ updateAgent: vi.fn(),
9
11
  deleteAgent: vi.fn(),
12
+ sessionTypesQuery: {
13
+ data: {
14
+ defaultType: 'native',
15
+ options: [
16
+ { value: 'native', label: 'Native', ready: true },
17
+ { value: 'codex', label: 'Codex', ready: true },
18
+ { value: 'claude', label: 'Claude Code', ready: false, reasonMessage: 'Configure Claude Code first.' }
19
+ ]
20
+ }
21
+ },
10
22
  agentsQuery: {
11
23
  data: {
12
24
  agents: [
@@ -15,6 +27,7 @@ const mocks = vi.hoisted(() => ({
15
27
  displayName: 'Main',
16
28
  description: '系统默认入口与总控协作者。',
17
29
  builtIn: true,
30
+ model: 'openai/gpt-5.1',
18
31
  workspace: '~/.nextclaw/workspace',
19
32
  avatarUrl: null
20
33
  },
@@ -23,12 +36,44 @@ const mocks = vi.hoisted(() => ({
23
36
  displayName: 'Researcher',
24
37
  description: '负责调研、信息筛选与结论提炼。',
25
38
  builtIn: false,
39
+ model: 'openai/gpt-5.2',
26
40
  workspace: '~/.nextclaw/workspace/agents/researcher',
27
41
  avatarUrl: null
28
42
  }
29
43
  ]
30
44
  },
31
45
  isLoading: false
46
+ },
47
+ configQuery: {
48
+ data: {
49
+ agents: {
50
+ defaults: {
51
+ model: 'openai/gpt-5.1',
52
+ workspace: '~/.nextclaw/workspace'
53
+ }
54
+ },
55
+ providers: {
56
+ openai: {
57
+ enabled: true,
58
+ apiKeySet: true,
59
+ models: ['gpt-5.1', 'gpt-5.2']
60
+ }
61
+ }
62
+ }
63
+ },
64
+ configMetaQuery: {
65
+ data: {
66
+ providers: [
67
+ {
68
+ name: 'openai',
69
+ displayName: 'OpenAI',
70
+ modelPrefix: 'openai',
71
+ defaultModels: ['openai/gpt-5.1', 'openai/gpt-5.2'],
72
+ keywords: [],
73
+ envKey: 'OPENAI_API_KEY'
74
+ }
75
+ ]
76
+ }
32
77
  }
33
78
  }));
34
79
 
@@ -38,20 +83,45 @@ vi.mock('@/hooks/agents/useAgents', () => ({
38
83
  mutateAsync: mocks.createAgent,
39
84
  isPending: false
40
85
  }),
86
+ useUpdateAgent: () => ({
87
+ mutateAsync: mocks.updateAgent,
88
+ isPending: false
89
+ }),
41
90
  useDeleteAgent: () => ({
42
91
  mutate: mocks.deleteAgent,
43
92
  isPending: false
44
93
  })
45
94
  }));
46
95
 
96
+ vi.mock('@/hooks/useConfig', () => ({
97
+ useConfig: () => mocks.configQuery,
98
+ useConfigMeta: () => mocks.configMetaQuery
99
+ }));
100
+
101
+ vi.mock('@/hooks/use-ncp-chat-session-types', () => ({
102
+ useNcpChatSessionTypes: () => mocks.sessionTypesQuery
103
+ }));
104
+
47
105
  describe('AgentsPage', () => {
48
106
  beforeEach(() => {
49
107
  setLanguage('zh');
50
108
  mocks.createAgent.mockReset();
109
+ mocks.updateAgent.mockReset();
51
110
  mocks.deleteAgent.mockReset();
111
+ if (!HTMLElement.prototype.hasPointerCapture) {
112
+ HTMLElement.prototype.hasPointerCapture = () => false;
113
+ }
114
+ if (!HTMLElement.prototype.setPointerCapture) {
115
+ HTMLElement.prototype.setPointerCapture = () => {};
116
+ }
117
+ if (!HTMLElement.prototype.releasePointerCapture) {
118
+ HTMLElement.prototype.releasePointerCapture = () => {};
119
+ }
52
120
  });
53
121
 
54
- it('renders the agents workspace in Chinese and keeps core actions visible', () => {
122
+ it('renders the agents workspace in Chinese and keeps core actions visible', async () => {
123
+ const user = userEvent.setup();
124
+
55
125
  render(
56
126
  <MemoryRouter>
57
127
  <AgentsPage />
@@ -63,8 +133,49 @@ describe('AgentsPage', () => {
63
133
  expect(screen.getByText('全部 Agent')).toBeTruthy();
64
134
  expect(screen.getAllByText('主目录').length).toBeGreaterThan(0);
65
135
  expect(screen.getAllByRole('button', { name: '开始对话' })).toHaveLength(2);
136
+ expect(screen.getAllByRole('button', { name: '编辑' })).toHaveLength(2);
66
137
  expect(screen.getByText('负责调研、信息筛选与结论提炼。')).toBeTruthy();
67
138
  expect(screen.queryByText('专属 Agent 身份,可沉淀自己的记忆、技能与角色风格。')).toBeNull();
68
139
  expect(screen.queryByText('Agent Gallery')).toBeNull();
140
+
141
+ await user.click(screen.getAllByRole('button', { name: '编辑' })[1]);
142
+
143
+ expect(screen.getByText('编辑 Agent 身份')).toBeTruthy();
144
+ expect(screen.getByText('主目录保持不变')).toBeTruthy();
145
+ expect(screen.getByDisplayValue('Researcher')).toBeTruthy();
146
+ expect(screen.getByDisplayValue('负责调研、信息筛选与结论提炼。').tagName).toBe('TEXTAREA');
147
+ expect(screen.getByDisplayValue('gpt-5.2')).toBeTruthy();
148
+ });
149
+
150
+ it('uses a runtime dropdown instead of manual text input when editing an agent', async () => {
151
+ const user = userEvent.setup();
152
+
153
+ render(
154
+ <MemoryRouter>
155
+ <AgentsPage />
156
+ </MemoryRouter>
157
+ );
158
+
159
+ await user.click(screen.getAllByRole('button', { name: '编辑' })[1]);
160
+
161
+ const runtimeTrigger = screen.getByRole('combobox', { name: 'Runtime' });
162
+ expect(screen.queryByPlaceholderText('Runtime(如 native 或 codex,可选)')).toBeNull();
163
+ expect(runtimeTrigger.textContent).toContain('Native');
164
+ expect(screen.queryByText('跟随默认 Runtime')).toBeNull();
165
+
166
+ await user.click(runtimeTrigger);
167
+ await user.click(screen.getByRole('option', { name: 'Codex' }));
168
+ await user.click(screen.getByRole('button', { name: '保存编辑' }));
169
+
170
+ expect(mocks.updateAgent).toHaveBeenCalledWith({
171
+ agentId: 'researcher',
172
+ data: {
173
+ displayName: 'Researcher',
174
+ description: '负责调研、信息筛选与结论提炼。',
175
+ avatar: '',
176
+ model: 'openai/gpt-5.2',
177
+ runtime: 'codex'
178
+ }
179
+ });
69
180
  });
70
181
  });