@nextclaw/ui 0.12.1 → 0.12.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 (95) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/assets/ChannelsList-DZWam3Ob.js +8 -0
  3. package/dist/assets/ChatPage-YBL7iJ1X.js +43 -0
  4. package/dist/assets/{DocBrowser-DjcghYGO.js → DocBrowser-C7-1sXqo.js} +1 -1
  5. package/dist/assets/DocBrowser-DQjtSsY3.js +1 -0
  6. package/dist/assets/{DocBrowserContext-CLlq7rZQ.js → DocBrowserContext-DN5tjUoS.js} +1 -1
  7. package/dist/assets/{LogoBadge-D_dOy5U3.js → LogoBadge-DDS1sU_U.js} +1 -1
  8. package/dist/assets/{MarketplacePage-BlIeNn3x.js → MarketplacePage-2tWWgwAb.js} +1 -1
  9. package/dist/assets/MarketplacePage-BorWJftJ.js +1 -0
  10. package/dist/assets/{McpMarketplacePage-mz2_IX1O.js → McpMarketplacePage-N-fB4HID.js} +1 -1
  11. package/dist/assets/ModelConfig-DvsBTUiE.js +1 -0
  12. package/dist/assets/ProviderScopedModelInput-D9woCARc.js +1 -0
  13. package/dist/assets/{ProvidersList-B0RCb_Vg.js → ProvidersList-D-qPGgC4.js} +1 -1
  14. package/dist/assets/{RemoteAccessPage-CcfQjLtx.js → RemoteAccessPage-COnjm8_x.js} +1 -1
  15. package/dist/assets/RuntimeConfig-BHpqcaHm.js +1 -0
  16. package/dist/assets/SearchConfig-DIT6M65Q.js +1 -0
  17. package/dist/assets/{SecretsConfig-DbiS3txa.js → SecretsConfig-Cefg1LFJ.js} +1 -1
  18. package/dist/assets/{SessionsConfig-CbIOcAp8.js → SessionsConfig-BZnmVTIu.js} +1 -1
  19. package/dist/assets/{book-open-BLxSL7Dk.js → book-open-DvWqOode.js} +1 -1
  20. package/dist/assets/chat-session-display-D4bYa0b8.js +1 -0
  21. package/dist/assets/{chunk-JZWAC4HX-Bp0t5xoO.js → chunk-JZWAC4HX-CxfKRD7X.js} +1 -1
  22. package/dist/assets/{config-C96FWufn.js → config-BeGwf2Ao.js} +1 -1
  23. package/dist/assets/{createLucideIcon-B_U7Nq4F.js → createLucideIcon-C7MmdIX3.js} +1 -1
  24. package/dist/assets/{dist-D9pHzW9z.js → dist-B6VMuIQN.js} +1 -1
  25. package/dist/assets/{dist-BFY-GyT4.js → dist-RWNFhxvR.js} +1 -1
  26. package/dist/assets/{external-link-BydIQTIH.js → external-link-U86Acd1t.js} +1 -1
  27. package/dist/assets/{hash-Djdf0x1C.js → hash-D-OVfV3Z.js} +1 -1
  28. package/dist/assets/i18n-hM3v-3YG.js +1 -0
  29. package/dist/assets/index-CpxuJa9o.css +1 -0
  30. package/dist/assets/{index-DqSv8Azv.js → index-DHmCjcxq.js} +3 -3
  31. package/dist/assets/{label-Bvv4Mrea.js → label-CHJ1ATds.js} +1 -1
  32. package/dist/assets/loader-circle-C8cpaL0w.js +1 -0
  33. package/dist/assets/{logos-CGJJRI5_.js → logos-U1_qDA3U.js} +1 -1
  34. package/dist/assets/{page-layout-6Nm4Cnvr.js → page-layout-Z1klaUFW.js} +1 -1
  35. package/dist/assets/plus-CrkO1kob.js +1 -0
  36. package/dist/assets/{popover-b9rSYI6X.js → popover-xWbqMnIN.js} +1 -1
  37. package/dist/assets/{react-CDZz_StC.js → react-3YE87-lE.js} +1 -1
  38. package/dist/assets/{refresh-ccw-BvSSnnCw.js → refresh-ccw-JQh1lwq-.js} +1 -1
  39. package/dist/assets/{save-CAf0_-b9.js → save-4VRlzkii.js} +1 -1
  40. package/dist/assets/search-EX-Papzl.js +1 -0
  41. package/dist/assets/{security-config-DF66-l25.js → security-config-DEgOD4VX.js} +1 -1
  42. package/dist/assets/{select-CEIMqc0H.js → select-DF-AUoie.js} +1 -1
  43. package/dist/assets/skeleton-B0mmt1vo.js +1 -0
  44. package/dist/assets/{status-dot-CmQI5Qq2.js → status-dot-Bq_8Ojvv.js} +1 -1
  45. package/dist/assets/{switch-B7SxDXyR.js → switch-D7JF_RZ-.js} +1 -1
  46. package/dist/assets/{tabs-custom-Dxt6EJJW.js → tabs-custom-CLksZ2bO.js} +1 -1
  47. package/dist/assets/{trash-2-BnQ1PDTw.js → trash-2-VV8jvziy.js} +1 -1
  48. package/dist/assets/{useConfirmDialog-B-vMOmhG.js → useConfirmDialog-CuQqiPx7.js} +1 -1
  49. package/dist/assets/{useMutation-Bi39Z9_J.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 +4 -4
  53. package/src/api/agents.ts +9 -1
  54. package/src/api/types.ts +25 -1
  55. package/src/components/agents/AgentDialogs.tsx +400 -0
  56. package/src/components/agents/AgentsPage.test.tsx +148 -1
  57. package/src/components/agents/AgentsPage.tsx +114 -115
  58. package/src/components/chat/ChatConversationPanel.test.tsx +69 -3
  59. package/src/components/chat/ChatConversationPanel.tsx +24 -3
  60. package/src/components/chat/managers/chat-session-list.manager.test.ts +34 -3
  61. package/src/components/chat/managers/chat-session-list.manager.ts +17 -4
  62. package/src/components/chat/ncp/NcpChatPage.tsx +1 -0
  63. package/src/components/chat/ncp/ncp-chat.presenter.ts +5 -1
  64. package/src/components/chat/ncp/ncp-session-adapter.ts +6 -1
  65. package/src/components/chat/stores/chat-session-list.store.ts +6 -1
  66. package/src/components/chat/useChatSessionTypeState.ts +10 -2
  67. package/src/components/common/ProviderScopedModelInput.tsx +149 -0
  68. package/src/components/config/ChannelForm.test.tsx +60 -0
  69. package/src/components/config/ChannelForm.tsx +52 -12
  70. package/src/components/config/ModelConfig.test.tsx +61 -0
  71. package/src/components/config/ModelConfig.tsx +15 -90
  72. package/src/components/config/RuntimeConfig.tsx +2 -2
  73. package/src/components/config/SearchConfig.test.tsx +150 -0
  74. package/src/components/config/SearchConfig.tsx +257 -71
  75. package/src/components/config/runtime-config-agent.utils.ts +5 -4
  76. package/src/hooks/agents/useAgents.ts +18 -1
  77. package/src/lib/i18n.agents.ts +19 -0
  78. package/src/lib/i18n.search.ts +37 -0
  79. package/src/lib/i18n.ts +6 -26
  80. package/dist/assets/ChannelsList-DekMP4a3.js +0 -8
  81. package/dist/assets/ChatPage-Dgw4vlDt.js +0 -43
  82. package/dist/assets/DocBrowser-CExjX5is.js +0 -1
  83. package/dist/assets/MarketplacePage-DGfzg1LG.js +0 -1
  84. package/dist/assets/ModelConfig-C_49_a9v.js +0 -1
  85. package/dist/assets/RuntimeConfig-DBWzwoY-.js +0 -1
  86. package/dist/assets/SearchConfig-jSdwlH4b.js +0 -1
  87. package/dist/assets/chat-session-display-8yW6-mtm.js +0 -1
  88. package/dist/assets/i18n-DAekxt_G.js +0 -1
  89. package/dist/assets/index-CHEgQIiO.css +0 -1
  90. package/dist/assets/loader-circle-CGXXikVG.js +0 -1
  91. package/dist/assets/plus-CrW9BJDy.js +0 -1
  92. package/dist/assets/provider-models-IJDA940D.js +0 -1
  93. package/dist/assets/search-DgoXxocn.js +0 -1
  94. package/dist/assets/skeleton-BiPUQkOD.js +0 -1
  95. package/dist/assets/x-PBSiWt3l.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,12 +1,26 @@
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
+ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
7
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
5
8
  import { setLanguage } from '@/lib/i18n';
6
9
 
7
10
  const mocks = vi.hoisted(() => ({
8
11
  createAgent: vi.fn(),
12
+ updateAgent: vi.fn(),
9
13
  deleteAgent: vi.fn(),
14
+ sessionTypesQuery: {
15
+ data: {
16
+ defaultType: 'native',
17
+ options: [
18
+ { value: 'native', label: 'Native', ready: true },
19
+ { value: 'codex', label: 'Codex', ready: true },
20
+ { value: 'claude', label: 'Claude Code', ready: false, reasonMessage: 'Configure Claude Code first.' }
21
+ ]
22
+ }
23
+ },
10
24
  agentsQuery: {
11
25
  data: {
12
26
  agents: [
@@ -15,6 +29,7 @@ const mocks = vi.hoisted(() => ({
15
29
  displayName: 'Main',
16
30
  description: '系统默认入口与总控协作者。',
17
31
  builtIn: true,
32
+ model: 'openai/gpt-5.1',
18
33
  workspace: '~/.nextclaw/workspace',
19
34
  avatarUrl: null
20
35
  },
@@ -23,12 +38,45 @@ const mocks = vi.hoisted(() => ({
23
38
  displayName: 'Researcher',
24
39
  description: '负责调研、信息筛选与结论提炼。',
25
40
  builtIn: false,
41
+ model: 'openai/gpt-5.2',
42
+ runtime: 'codex',
26
43
  workspace: '~/.nextclaw/workspace/agents/researcher',
27
44
  avatarUrl: null
28
45
  }
29
46
  ]
30
47
  },
31
48
  isLoading: false
49
+ },
50
+ configQuery: {
51
+ data: {
52
+ agents: {
53
+ defaults: {
54
+ model: 'openai/gpt-5.1',
55
+ workspace: '~/.nextclaw/workspace'
56
+ }
57
+ },
58
+ providers: {
59
+ openai: {
60
+ enabled: true,
61
+ apiKeySet: true,
62
+ models: ['gpt-5.1', 'gpt-5.2']
63
+ }
64
+ }
65
+ }
66
+ },
67
+ configMetaQuery: {
68
+ data: {
69
+ providers: [
70
+ {
71
+ name: 'openai',
72
+ displayName: 'OpenAI',
73
+ modelPrefix: 'openai',
74
+ defaultModels: ['openai/gpt-5.1', 'openai/gpt-5.2'],
75
+ keywords: [],
76
+ envKey: 'OPENAI_API_KEY'
77
+ }
78
+ ]
79
+ }
32
80
  }
33
81
  }));
34
82
 
@@ -38,20 +86,60 @@ vi.mock('@/hooks/agents/useAgents', () => ({
38
86
  mutateAsync: mocks.createAgent,
39
87
  isPending: false
40
88
  }),
89
+ useUpdateAgent: () => ({
90
+ mutateAsync: mocks.updateAgent,
91
+ isPending: false
92
+ }),
41
93
  useDeleteAgent: () => ({
42
94
  mutate: mocks.deleteAgent,
43
95
  isPending: false
44
96
  })
45
97
  }));
46
98
 
99
+ vi.mock('@/hooks/useConfig', () => ({
100
+ useConfig: () => mocks.configQuery,
101
+ useConfigMeta: () => mocks.configMetaQuery
102
+ }));
103
+
104
+ vi.mock('@/hooks/use-ncp-chat-session-types', () => ({
105
+ useNcpChatSessionTypes: () => mocks.sessionTypesQuery
106
+ }));
107
+
47
108
  describe('AgentsPage', () => {
48
109
  beforeEach(() => {
49
110
  setLanguage('zh');
50
111
  mocks.createAgent.mockReset();
112
+ mocks.updateAgent.mockReset();
51
113
  mocks.deleteAgent.mockReset();
114
+ if (!HTMLElement.prototype.hasPointerCapture) {
115
+ HTMLElement.prototype.hasPointerCapture = () => false;
116
+ }
117
+ if (!HTMLElement.prototype.setPointerCapture) {
118
+ HTMLElement.prototype.setPointerCapture = () => {};
119
+ }
120
+ if (!HTMLElement.prototype.releasePointerCapture) {
121
+ HTMLElement.prototype.releasePointerCapture = () => {};
122
+ }
123
+ useChatInputStore.setState({
124
+ snapshot: {
125
+ ...useChatInputStore.getState().snapshot,
126
+ pendingSessionType: 'native',
127
+ pendingProjectRoot: '/tmp/demo-project',
128
+ pendingProjectRootSessionKey: 'draft-session'
129
+ }
130
+ });
131
+ useChatSessionListStore.setState({
132
+ snapshot: {
133
+ ...useChatSessionListStore.getState().snapshot,
134
+ selectedAgentId: 'main',
135
+ selectedSessionKey: 'session-1'
136
+ }
137
+ });
52
138
  });
53
139
 
54
- it('renders the agents workspace in Chinese and keeps core actions visible', () => {
140
+ it('renders the agents workspace in Chinese and keeps core actions visible', async () => {
141
+ const user = userEvent.setup();
142
+
55
143
  render(
56
144
  <MemoryRouter>
57
145
  <AgentsPage />
@@ -63,8 +151,67 @@ describe('AgentsPage', () => {
63
151
  expect(screen.getByText('全部 Agent')).toBeTruthy();
64
152
  expect(screen.getAllByText('主目录').length).toBeGreaterThan(0);
65
153
  expect(screen.getAllByRole('button', { name: '开始对话' })).toHaveLength(2);
154
+ expect(screen.getAllByRole('button', { name: '编辑' })).toHaveLength(2);
66
155
  expect(screen.getByText('负责调研、信息筛选与结论提炼。')).toBeTruthy();
67
156
  expect(screen.queryByText('专属 Agent 身份,可沉淀自己的记忆、技能与角色风格。')).toBeNull();
68
157
  expect(screen.queryByText('Agent Gallery')).toBeNull();
158
+
159
+ await user.click(screen.getAllByRole('button', { name: '编辑' })[1]);
160
+
161
+ expect(screen.getByText('编辑 Agent 身份')).toBeTruthy();
162
+ expect(screen.getByText('主目录保持不变')).toBeTruthy();
163
+ expect(screen.getByDisplayValue('Researcher')).toBeTruthy();
164
+ expect(screen.getByDisplayValue('负责调研、信息筛选与结论提炼。').tagName).toBe('TEXTAREA');
165
+ expect(screen.getByDisplayValue('gpt-5.2')).toBeTruthy();
166
+ });
167
+
168
+ it('uses a runtime dropdown instead of manual text input when editing an agent', async () => {
169
+ const user = userEvent.setup();
170
+
171
+ render(
172
+ <MemoryRouter>
173
+ <AgentsPage />
174
+ </MemoryRouter>
175
+ );
176
+
177
+ await user.click(screen.getAllByRole('button', { name: '编辑' })[1]);
178
+
179
+ const runtimeTrigger = screen.getByRole('combobox', { name: 'Runtime' });
180
+ expect(screen.queryByPlaceholderText('Runtime(如 native 或 codex,可选)')).toBeNull();
181
+ expect(runtimeTrigger.textContent).toContain('Codex');
182
+ expect(screen.queryByText('跟随默认 Runtime')).toBeNull();
183
+
184
+ await user.click(runtimeTrigger);
185
+ await user.click(screen.getByRole('option', { name: 'Codex' }));
186
+ await user.click(screen.getByRole('button', { name: '保存编辑' }));
187
+
188
+ expect(mocks.updateAgent).toHaveBeenCalledWith({
189
+ agentId: 'researcher',
190
+ data: {
191
+ displayName: 'Researcher',
192
+ description: '负责调研、信息筛选与结论提炼。',
193
+ avatar: '',
194
+ model: 'openai/gpt-5.2',
195
+ runtime: 'codex'
196
+ }
197
+ });
198
+ });
199
+
200
+ it('starts a draft chat with the agent runtime as the pending session type', async () => {
201
+ const user = userEvent.setup();
202
+
203
+ render(
204
+ <MemoryRouter>
205
+ <AgentsPage />
206
+ </MemoryRouter>
207
+ );
208
+
209
+ await user.click(screen.getAllByRole('button', { name: '开始对话' })[1]);
210
+
211
+ expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe('researcher');
212
+ expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
213
+ expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
214
+ expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
215
+ expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
69
216
  });
70
217
  });