返回 DeepSeek-Reasonix
SubagentsPanel.tsx
根目录 / desktop / frontend / src / components / SubagentsPanel.tsx
1 import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
2 import { Check, ChevronDown } from "lucide-react";
3
4 import { app } from "../lib/bridge";
5 import { asArray } from "../lib/array";
6 import { useT } from "../lib/i18n";
7 import { PROJECT_COLOR_OPTIONS, projectColorValue, type ProjectColorKey } from "../lib/projectColors";
8 import type { MCPToolView, SettingsView, SkillView, SubagentProfileInput } from "../lib/types";
9
10 import { InlineConfirmButton } from "./InlineConfirmButton";
11 import { AnchoredPopover } from "./AnchoredPopover";
12 import { CopyButton } from "./CopyButton";
13 import { allRefs, EFFORT_PRESETS, ModelPicker, toRef } from "./SettingsPanel";
14 import { Tooltip } from "./Tooltip";
15
16 const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
17
18 function subagentScopeLabel(scope: string, t: ReturnType<typeof useT>): string {
19 switch (scope) {
20 case "builtin":
21 return t("caps.skillScopeBuiltin");
22 case "project":
23 return t("caps.skillScopeProject");
24 case "custom":
25 return t("caps.skillScopeCustom");
26 case "global":
27 return t("caps.skillScopeGlobal");
28 default:
29 return scope;
30 }
31 }
32
33 function toolsSummaryLabel(allowedTools: string[] | undefined, t: ReturnType<typeof useT>): string {
34 if (!allowedTools || allowedTools.length === 0) return t("subagents.allTools");
35 return t("subagents.toolCount", { n: allowedTools.length });
36 }
37
38 function builtinDescription(name: string, fallback: string, t: ReturnType<typeof useT>): string {
39 switch (name) {
40 case "explore":
41 return t("subagents.builtinExploreDescription");
42 case "research":
43 return t("subagents.builtinResearchDescription");
44 case "review":
45 return t("subagents.builtinReviewDescription");
46 case "security-review":
47 return t("subagents.builtinSecurityReviewDescription");
48 default:
49 return fallback;
50 }
51 }
52
53 function shortModelRef(ref: string): string {
54 const parts = ref.split("/");
55 return parts[parts.length - 1] || ref;
56 }
57
58 // SubagentsSettingsPage is a self-contained subagent-profile management page
59 // embedded inside the settings centre. A "subagent profile" is a skill file
60 // with runAs=subagent + invocation=manual (see internal/skill): it stays
61 // invocable by name but is excluded from the pinned Skills index, so the
62 // model never discovers or auto-invokes a profile the user configured for
63 // their own deliberate use. This reads the same app.SkillsSettings() list
64 // the Skills tab uses and filters client-side — the underlying file is the
65 // same, just shown through a different lens — rather than adding a
66 // redundant, parallel backend list endpoint.
67 export function SubagentsSettingsPage({ s, onUseInChat }: { s: SettingsView; onUseInChat: (command: string) => void }) {
68 const t = useT();
69 const [skills, setSkills] = useState<SkillView[] | null>(null);
70 const [tools, setTools] = useState<MCPToolView[]>([]);
71 const [busy, setBusy] = useState(false);
72 const [err, setErr] = useState<string | null>(null);
73 const [query, setQuery] = useState("");
74 const [adding, setAdding] = useState(false);
75 const [editingSkill, setEditingSkill] = useState<SkillView | null>(null);
76 const formOpen = adding || editingSkill !== null;
77
78 const reload = useCallback(async () => {
79 const [settingsView, availableTools] = await Promise.all([
80 app.SkillsSettings().catch(() => ({ skills: [], skillRoots: [] })),
81 app.AvailableSubagentTools().catch(() => []),
82 ]);
83 setSkills(asArray<SkillView>(settingsView?.skills).filter((sk) => sk.runAs === "subagent"));
84 setTools(asArray<MCPToolView>(availableTools));
85 }, []);
86 useEffect(() => { void reload(); }, [reload]);
87
88 const mutate = async (fn: () => Promise<unknown>) => {
89 setBusy(true);
90 setErr(null);
91 try {
92 await fn();
93 await reload();
94 return true;
95 } catch (e) {
96 setErr(String((e as Error)?.message ?? e));
97 return false;
98 } finally {
99 setBusy(false);
100 }
101 };
102
103 const filtered = useMemo(() => {
104 if (!skills) return [];
105 const q = query.trim().toLowerCase();
106 if (!q) return skills;
107 return skills.filter((sk) => `${sk.name} ${sk.description}`.toLowerCase().includes(q));
108 }, [skills, query]);
109
110 const builtins = useMemo(() => filtered.filter((sk) => sk.scope === "builtin"), [filtered]);
111 // This page creates only project/global profiles. Manual subagents loaded
112 // from configured custom paths remain visible, but their external ownership
113 // is preserved: they are read-only here and managed from the Skills page.
114 const custom = useMemo(
115 () => filtered.filter((sk) => (sk.scope === "project" || sk.scope === "global") && sk.invocationMode === "manual"),
116 [filtered],
117 );
118 const external = useMemo(
119 () => filtered.filter((sk) => sk.scope === "custom" && sk.invocationMode === "manual"),
120 [filtered],
121 );
122
123 if (!skills) return <div className="empty">{t("caps.loading")}</div>;
124
125 return (
126 <section className="mem-section">
127 {err && <div className="banner banner--error">{err}</div>}
128 {!formOpen && (
129 <div className="cap-search subagents-toolbar">
130 <input
131 className="mem-input"
132 type="search"
133 placeholder={t("subagents.search")}
134 value={query}
135 onChange={(e) => setQuery(e.target.value)}
136 />
137 <button
138 className="btn btn--small"
139 type="button"
140 disabled={busy}
141 onClick={() => { setEditingSkill(null); setAdding(true); }}
142 >
143 {t("subagents.new")}
144 </button>
145 <button className="btn btn--small" type="button" disabled={busy} onClick={() => void mutate(() => app.RefreshSkills())}>
146 {t("caps.refreshSkills")}
147 </button>
148 </div>
149 )}
150
151 {adding && (
152 <SubagentProfileForm
153 s={s}
154 tools={tools}
155 existingNames={skills.map((sk) => sk.name)}
156 busy={busy}
157 onCancel={() => setAdding(false)}
158 onSave={(input) =>
159 mutate(() => app.CreateSubagentProfile(input)).then((ok) => {
160 if (ok) setAdding(false);
161 })
162 }
163 />
164 )}
165
166 {editingSkill && (
167 <SubagentProfileForm
168 s={s}
169 tools={tools}
170 existingNames={skills.map((sk) => sk.name)}
171 busy={busy}
172 editingSkill={editingSkill}
173 onCancel={() => setEditingSkill(null)}
174 onSave={(input) =>
175 mutate(() => app.UpdateSubagentProfile(editingSkill.name, editingSkill.scope, input)).then((ok) => {
176 if (ok) setEditingSkill(null);
177 })
178 }
179 />
180 )}
181
182 {!formOpen && (
183 <>
184 <section className="subagents-profile-group" aria-labelledby="subagents-custom-title">
185 <div className="cap-skills-head">
186 <div className="cap-skills-head__copy">
187 <h3 id="subagents-custom-title" className="cap-skills-head__title">{t("subagents.customTitle")}</h3>
188 <div className="cap-skills-head__summary">{t("subagents.customHint")}</div>
189 </div>
190 </div>
191 {custom.length === 0 && external.length === 0 ? (
192 <div className="mem-empty">{query.trim() ? t("subagents.noMatches") : t("subagents.noCustom")}</div>
193 ) : (
194 <div className="cap-skills">
195 {custom.map((sk) => (
196 <CustomSubagentRow
197 key={`${sk.scope}:${sk.name}`}
198 skill={sk}
199 busy={busy}
200 onEdit={() => { setAdding(false); setEditingSkill(sk); }}
201 onDelete={() => void mutate(() => app.DeleteSubagentProfile(sk.name, sk.scope))}
202 onUseInChat={onUseInChat}
203 />
204 ))}
205 {external.map((sk) => (
206 <CustomSubagentRow key={`${sk.scope}:${sk.name}`} skill={sk} busy={busy} externallyManaged onUseInChat={onUseInChat} />
207 ))}
208 </div>
209 )}
210 </section>
211
212 <section className="subagents-profile-group" aria-labelledby="subagents-builtin-title">
213 <div className="cap-skills-head">
214 <div className="cap-skills-head__copy">
215 <h3 id="subagents-builtin-title" className="cap-skills-head__title">{t("subagents.builtinTitle")}</h3>
216 <div className="cap-skills-head__summary">{t("subagents.builtinHint")}</div>
217 </div>
218 </div>
219 {builtins.length > 0 && (
220 <div className="cap-skills">
221 {builtins.map((sk) => (
222 <BuiltinSubagentRow
223 key={sk.name}
224 skill={sk}
225 s={s}
226 busy={busy}
227 onSetModel={(ref) => void mutate(() => app.SetSubagentProfileModel(sk.name, ref))}
228 onSetEffort={(level) => void mutate(() => app.SetSubagentProfileEffort(sk.name, level))}
229 onReset={() => void mutate(async () => {
230 if (sk.configuredModel) await app.SetSubagentProfileModel(sk.name, "");
231 if (sk.configuredEffort) await app.SetSubagentProfileEffort(sk.name, "");
232 })}
233 onUseInChat={onUseInChat}
234 />
235 ))}
236 </div>
237 )}
238 </section>
239 </>
240 )}
241 </section>
242 );
243 }
244
245 function SubagentInvocation({ name, onUseInChat }: { name: string; onUseInChat: (command: string) => void }) {
246 const t = useT();
247 const command = `/${name} `;
248 const example = t("subagents.invocationExample", { name });
249 return (
250 <div className="subagents-invocation">
251 <div className="subagents-invocation__command">
252 <span>{t("subagents.invocationLabel")}</span>
253 <code>{example}</code>
254 <CopyButton text={example} label={t("subagents.copyInvocation")} className="subagents-invocation__copy" />
255 </div>
256 <button className="btn btn--small" type="button" onClick={() => onUseInChat(command)}>
257 {t("subagents.useInChat")}
258 </button>
259 </div>
260 );
261 }
262
263 function EffortPicker({
264 value,
265 inheritedValue,
266 disabled,
267 ariaLabel,
268 onPick,
269 }: {
270 value: string;
271 inheritedValue: string;
272 disabled: boolean;
273 ariaLabel: string;
274 onPick: (level: string) => void;
275 }) {
276 const t = useT();
277 const [open, setOpen] = useState(false);
278 const triggerRef = useRef<HTMLButtonElement>(null);
279 const selectedLabel = value || t("subagents.inheritDefault");
280 const effectiveValue = value || inheritedValue;
281 const pick = (level: string) => {
282 setOpen(false);
283 if (level !== value) onPick(level);
284 };
285
286 return (
287 <div className="settings-model-picker subagents-effort-picker">
288 <button
289 ref={triggerRef}
290 type="button"
291 className="settings-model-picker__trigger"
292 disabled={disabled}
293 aria-label={ariaLabel}
294 aria-haspopup="listbox"
295 aria-expanded={open}
296 onClick={() => setOpen((next) => !next)}
297 >
298 <span className="settings-model-picker__selected">
299 <span>{selectedLabel}</span>
300 <small>{t("subagents.effectiveValue", { value: effectiveValue })}</small>
301 </span>
302 <ChevronDown size={16} className={`settings-model-picker__chev${open ? " settings-model-picker__chev--open" : ""}`} />
303 </button>
304 <AnchoredPopover
305 open={open && !disabled}
306 anchorRef={triggerRef}
307 onClose={() => setOpen(false)}
308 className="settings-model-picker__menu subagents-effort-picker__menu"
309 placement="bottom"
310 style={{ width: triggerRef.current?.getBoundingClientRect().width }}
311 >
312 <div className="settings-model-picker__list" role="listbox">
313 <button
314 type="button"
315 role="option"
316 aria-selected={value === ""}
317 className={`settings-model-picker__option settings-model-picker__option--pinned${value === "" ? " settings-model-picker__option--selected" : ""}`}
318 onClick={() => pick("")}
319 >
320 <span>
321 <strong>{t("subagents.inheritDefault")}</strong>
322 <small>{t("subagents.effectiveValue", { value: inheritedValue })}</small>
323 </span>
324 {value === "" && <Check size={14} />}
325 </button>
326 {EFFORT_PRESETS.map((level) => (
327 <button
328 key={level}
329 type="button"
330 role="option"
331 aria-selected={level === value}
332 className={`settings-model-picker__option${level === value ? " settings-model-picker__option--selected" : ""}`}
333 onClick={() => pick(level)}
334 >
335 <span>
336 <strong>{level}</strong>
337 <small>{t("subagents.effectiveValue", { value: level })}</small>
338 </span>
339 {level === value && <Check size={14} />}
340 </button>
341 ))}
342 </div>
343 </AnchoredPopover>
344 </div>
345 );
346 }
347
348 function BuiltinSubagentRow({
349 skill,
350 s,
351 busy,
352 onSetModel,
353 onSetEffort,
354 onReset,
355 onUseInChat,
356 }: {
357 skill: SkillView;
358 s: SettingsView;
359 busy: boolean;
360 onSetModel: (ref: string) => void;
361 onSetEffort: (level: string) => void;
362 onReset: () => void;
363 onUseInChat: (command: string) => void;
364 }) {
365 const t = useT();
366 const toolsLabel = toolsSummaryLabel(skill.allowedTools, t);
367 const inheritedModel = shortModelRef(toRef(s.subagentModel || s.defaultModel, s)) || t("common.auto");
368 const inheritedEffort = s.subagentEffort || t("common.auto");
369 const overridden = Boolean(skill.configuredModel || skill.configuredEffort);
370 return (
371 <div className="cap-skill-card subagents-builtin-card">
372 <div className="cap-skill-card__top">
373 <span className="cap-skill-card__head">
374 <span className="cap-skill-card__icon">/</span>
375 <span className="cap-skill-card__main">
376 <span className="cap-skill-card__command">{skill.name}</span>
377 <span className="cap-skill-card__badges">
378 <span className="cap-skill-badge cap-skill-badge--builtin">{t("caps.skillScopeBuiltin")}</span>
379 <Tooltip label={(skill.allowedTools ?? []).join(", ") || t("subagents.allTools")}>
380 <span className="cap-skill-badge">{toolsLabel}</span>
381 </Tooltip>
382 </span>
383 </span>
384 </span>
385 </div>
386 <div className="cap-skill-card__desc">{builtinDescription(skill.name, skill.description, t)}</div>
387 <SubagentInvocation name={skill.name} onUseInChat={onUseInChat} />
388 <div className="subagents-builtin-overrides">
389 <div className="subagents-builtin-overrides__field">
390 <span className="subagents-builtin-overrides__field-label">{t("subagents.model")}</span>
391 <ModelPicker
392 s={s}
393 refs={allRefs(s)}
394 value={toRef(skill.configuredModel ?? "", s)}
395 disabled={busy}
396 ariaLabel={`${skill.name}: ${t("subagents.model")}`}
397 emptyOptionLabel={t("subagents.inheritDefault")}
398 emptyOptionHint={t("subagents.effectiveValue", { value: inheritedModel })}
399 onPick={onSetModel}
400 />
401 </div>
402 <div className="subagents-builtin-overrides__field">
403 <span className="subagents-builtin-overrides__field-label">{t("subagents.effort")}</span>
404 <EffortPicker
405 value={skill.configuredEffort ?? ""}
406 disabled={busy}
407 inheritedValue={inheritedEffort}
408 ariaLabel={`${skill.name}: ${t("subagents.effort")}`}
409 onPick={onSetEffort}
410 />
411 </div>
412 <div className="subagents-builtin-overrides__status">
413 {overridden ? (
414 <button className="btn btn--small subagents-reset-override" type="button" disabled={busy} onClick={onReset}>
415 <span className="subagents-reset-override__state">{t("subagents.overridden")}</span>
416 <span aria-hidden="true">·</span>
417 <span>{t("subagents.resetOverride")}</span>
418 </button>
419 ) : (
420 <Tooltip label={t("subagents.builtinReadOnlyHint")}>
421 <span className="subagents-inherit-badge">{t("subagents.inherited")}</span>
422 </Tooltip>
423 )}
424 </div>
425 </div>
426 </div>
427 );
428 }
429
430 function CustomSubagentRow({
431 skill,
432 busy,
433 onEdit,
434 onDelete,
435 externallyManaged = false,
436 onUseInChat,
437 }: {
438 skill: SkillView;
439 busy: boolean;
440 onEdit?: () => void;
441 onDelete?: () => void;
442 externallyManaged?: boolean;
443 onUseInChat: (command: string) => void;
444 }) {
445 const t = useT();
446 const toolsLabel = toolsSummaryLabel(skill.allowedTools, t);
447 const accent = projectColorValue(skill.color);
448 return (
449 <div className="cap-skill-card">
450 <div className="cap-skill-card__top">
451 <span className="cap-skill-card__head">
452 {accent && <span className="subagents-color-dot" style={{ "--project-accent": accent } as CSSProperties} aria-hidden="true" />}
453 <span className="cap-skill-card__main">
454 <span className="cap-skill-card__command">/{skill.name}</span>
455 <span className="cap-skill-card__badges">
456 <span className={`cap-skill-badge cap-skill-badge--${skill.scope}`}>{subagentScopeLabel(skill.scope, t)}</span>
457 {skill.model && <span className="cap-skill-badge">{skill.model}</span>}
458 <Tooltip label={(skill.allowedTools ?? []).join(", ") || t("subagents.allTools")}>
459 <span className="cap-skill-badge">{toolsLabel}</span>
460 </Tooltip>
461 </span>
462 </span>
463 </span>
464 {externallyManaged ? (
465 <Tooltip label={t("subagents.externalManagedHint")}>
466 <span className="cap-skill-badge">{t("subagents.externalManaged")}</span>
467 </Tooltip>
468 ) : (
469 <span className="subagents-row-actions">
470 <button className="btn btn--small" type="button" disabled={busy} onClick={() => onEdit?.()}>
471 {t("common.edit")}
472 </button>
473 <InlineConfirmButton
474 label={t("common.delete")}
475 confirmLabel={t("subagents.confirmDelete")}
476 cancelLabel={t("common.cancel")}
477 disabled={busy}
478 danger
479 onConfirm={() => onDelete?.()}
480 />
481 </span>
482 )}
483 </div>
484 <div className="cap-skill-card__desc">{skill.description}</div>
485 <SubagentInvocation name={skill.name} onUseInChat={onUseInChat} />
486 </div>
487 );
488 }
489
490 function ColorSwatchPicker({ value, onChange }: { value: ProjectColorKey; onChange: (key: ProjectColorKey) => void }) {
491 const t = useT();
492 return (
493 <div className="subagents-color-grid" role="group" aria-label={t("subagents.color")}>
494 {PROJECT_COLOR_OPTIONS.filter((opt) => opt.key !== "").map((opt) => (
495 <button
496 key={opt.key}
497 type="button"
498 className={`subagents-color-swatch${value === opt.key ? " subagents-color-swatch--selected" : ""}`}
499 style={{ "--project-accent": opt.value } as CSSProperties}
500 aria-pressed={value === opt.key}
501 aria-label={opt.key}
502 onClick={() => onChange(value === opt.key ? "" : opt.key)}
503 />
504 ))}
505 </div>
506 );
507 }
508
509 function ToolMultiSelect({
510 tools,
511 selected,
512 onChange,
513 }: {
514 tools: MCPToolView[];
515 selected: Set<string>;
516 onChange: (next: Set<string>) => void;
517 }) {
518 const t = useT();
519 const selectedToolCount = tools.reduce((count, tool) => count + Number(selected.has(tool.name)), 0);
520 const allSelected = tools.length > 0 && selectedToolCount === tools.length;
521 const toggle = (name: string, checked: boolean) => {
522 const next = new Set(selected);
523 if (checked) next.add(name);
524 else next.delete(name);
525 onChange(next);
526 };
527 return (
528 <div className="subagents-tool-grid" role="group" aria-label={t("subagents.customToolsOption")}>
529 <div className="subagents-tool-grid__actions">
530 <span>{t("subagents.selectedToolCount", { n: selectedToolCount, total: tools.length })}</span>
531 <button type="button" disabled={allSelected} onClick={() => onChange(new Set(tools.map((tool) => tool.name)))}>
532 {t("subagents.selectAllTools")}
533 </button>
534 <button type="button" disabled={selected.size === 0} onClick={() => onChange(new Set())}>
535 {t("subagents.clearTools")}
536 </button>
537 </div>
538 {tools.map((tool) => (
539 <Tooltip key={tool.name} label={tool.description}>
540 <label className="subagents-tool-option">
541 <input type="checkbox" checked={selected.has(tool.name)} onChange={(e) => toggle(tool.name, e.target.checked)} />
542 <span>{tool.name}</span>
543 </label>
544 </Tooltip>
545 ))}
546 </div>
547 );
548 }
549
550 export function selectToolsOnFirstCustomUse(
551 selected: ReadonlySet<string>,
552 tools: MCPToolView[],
553 hasUsedCustomMode: boolean,
554 ): Set<string> {
555 if (hasUsedCustomMode) return new Set(selected);
556 return new Set(tools.map((tool) => tool.name));
557 }
558
559 function SubagentProfileForm({
560 s,
561 tools,
562 existingNames,
563 busy,
564 editingSkill,
565 onCancel,
566 onSave,
567 }: {
568 s: SettingsView;
569 tools: MCPToolView[];
570 existingNames: string[];
571 busy: boolean;
572 editingSkill?: SkillView;
573 onCancel: () => void;
574 onSave: (input: SubagentProfileInput) => Promise<unknown>;
575 }) {
576 const t = useT();
577 const formRef = useRef<HTMLDivElement>(null);
578 const isEditing = Boolean(editingSkill);
579 const [name, setName] = useState(editingSkill?.name ?? "");
580 const [description, setDescription] = useState(editingSkill?.description ?? "");
581 const [color, setColor] = useState<ProjectColorKey>((editingSkill?.color as ProjectColorKey) ?? "");
582 const [model, setModel] = useState(editingSkill?.model ?? "");
583 const [effort, setEffort] = useState(editingSkill?.effort ?? "");
584 const [toolMode, setToolMode] = useState<"all" | "custom">(
585 editingSkill?.allowedTools && editingSkill.allowedTools.length > 0 ? "custom" : "all",
586 );
587 const [selectedTools, setSelectedTools] = useState<Set<string>>(() => new Set(editingSkill?.allowedTools ?? []));
588 const hasUsedCustomMode = useRef(Boolean(editingSkill?.allowedTools?.length));
589 const [systemPrompt, setSystemPrompt] = useState(editingSkill?.body ?? "");
590 const [readOnly, setReadOnly] = useState(Boolean(editingSkill?.readOnly));
591 const [scope, setScope] = useState<"global" | "project">(editingSkill?.scope === "project" ? "project" : "global");
592 const [tryTask, setTryTask] = useState("");
593 const [tryRunning, setTryRunning] = useState(false);
594 const [tryResult, setTryResult] = useState<string | null>(null);
595 const [tryError, setTryError] = useState<string | null>(null);
596
597 useEffect(() => {
598 formRef.current?.scrollIntoView({ block: "start" });
599 }, []);
600
601 const trimmedName = name.trim();
602 // Editing keeps its own name fixed, so it can never collide with itself.
603 const otherNames = isEditing ? existingNames.filter((n) => n !== editingSkill?.name) : existingNames;
604 const nameTaken = trimmedName !== "" && otherNames.some((n) => n.toLowerCase() === trimmedName.toLowerCase());
605 const nameValid = trimmedName === "" || NAME_PATTERN.test(trimmedName);
606 const promptReady = systemPrompt.trim() !== "";
607 const toolsReady = toolMode === "all" || selectedTools.size > 0;
608 const ready = trimmedName !== "" && nameValid && !nameTaken && description.trim() !== "" && promptReady && toolsReady;
609
610 const currentInput = (): SubagentProfileInput => ({
611 name: trimmedName,
612 description: description.trim(),
613 systemPrompt: systemPrompt.trim(),
614 color: color || undefined,
615 model,
616 effort,
617 allowedTools: toolMode === "custom" ? Array.from(selectedTools) : [],
618 readOnly,
619 scope,
620 });
621
622 const submit = () => {
623 void onSave(currentInput());
624 };
625
626 const runTry = async () => {
627 setTryRunning(true);
628 setTryError(null);
629 setTryResult(null);
630 try {
631 setTryResult(await app.TrySubagentProfile(currentInput(), tryTask.trim()));
632 } catch (e) {
633 setTryError(String((e as Error)?.message ?? e));
634 } finally {
635 setTryRunning(false);
636 }
637 };
638
639 return (
640 <div ref={formRef} className="prov-card prov-card--edit">
641 <button className="subagents-form-back" type="button" onClick={onCancel} disabled={busy}>
642 <span aria-hidden="true">←</span> {t("subagents.backToList")}
643 </button>
644 <div className="cap-skills-head__title">{isEditing ? t("subagents.editTitle") : t("subagents.newTitle")}</div>
645 <label className="set-label">{t("subagents.name")}</label>
646 <input
647 className="mem-input"
648 placeholder={t("subagents.namePlaceholder")}
649 value={name}
650 disabled={isEditing}
651 onChange={(e) => setName(e.target.value)}
652 />
653 {trimmedName !== "" && !nameValid && <div className="subagents-field-error">{t("subagents.nameInvalid")}</div>}
654 {nameTaken && <div className="subagents-field-error">{t("subagents.nameTaken")}</div>}
655
656 <label className="set-label">{t("subagents.color")}</label>
657 <ColorSwatchPicker value={color} onChange={setColor} />
658
659 <label className="set-label">{t("settings.subagentModel")}</label>
660 <ModelPicker
661 s={s}
662 refs={allRefs(s)}
663 value={toRef(model, s)}
664 disabled={busy}
665 emptyOptionLabel={t("settings.subagentModelDefault")}
666 emptyOptionHint={t("common.auto")}
667 onPick={(ref) => setModel(ref)}
668 />
669
670 <label className="set-label">{t("settings.subagentEffort")}</label>
671 <select className="mem-select set-grow" value={effort} disabled={busy} onChange={(e) => setEffort(e.target.value)}>
672 <option value="">{t("settings.subagentEffortDefault")}</option>
673 {EFFORT_PRESETS.map((level) => (
674 <option key={level} value={level}>
675 {level}
676 </option>
677 ))}
678 </select>
679
680 <label className="set-label">{t("subagents.description")}</label>
681 <input
682 className="mem-input"
683 placeholder={t("subagents.descriptionPlaceholder")}
684 value={description}
685 onChange={(e) => setDescription(e.target.value)}
686 />
687
688 <label className="set-label">{t("subagents.tools")}</label>
689 <div className="subagents-tool-scope-row">
690 <select
691 className="mem-select"
692 value={toolMode}
693 disabled={busy}
694 onChange={(e) => {
695 const nextMode = e.target.value === "custom" ? "custom" : "all";
696 if (nextMode === "custom") {
697 setSelectedTools(selectToolsOnFirstCustomUse(selectedTools, tools, hasUsedCustomMode.current));
698 hasUsedCustomMode.current = true;
699 }
700 setToolMode(nextMode);
701 }}
702 >
703 <option value="all">{t("subagents.allToolsOption")}</option>
704 <option value="custom">{t("subagents.customToolsOption")}</option>
705 </select>
706 <span>{t(toolMode === "all" ? "subagents.allToolsHint" : "subagents.customToolsHint")}</span>
707 </div>
708 {toolMode === "custom" && <ToolMultiSelect tools={tools} selected={selectedTools} onChange={setSelectedTools} />}
709 {toolMode === "custom" && !toolsReady && <div className="subagents-field-error">{t("subagents.selectAtLeastOneTool")}</div>}
710
711 <label className="set-label">{t("subagents.readOnly")}</label>
712 <div className="set-seg" role="group" aria-label={t("subagents.readOnly")}>
713 <button
714 type="button"
715 className={`set-seg__btn${!readOnly ? " set-seg__btn--on" : ""}`}
716 disabled={busy}
717 onClick={() => setReadOnly(false)}
718 >
719 {t("subagents.readOnlyOff")}
720 </button>
721 <button
722 type="button"
723 className={`set-seg__btn${readOnly ? " set-seg__btn--on" : ""}`}
724 disabled={busy}
725 onClick={() => setReadOnly(true)}
726 >
727 {t("subagents.readOnlyOn")}
728 </button>
729 </div>
730 <div className="set-hint">{t("subagents.readOnlyHint")}</div>
731
732 <label className="set-label">{t("subagents.systemPrompt")}</label>
733 <textarea
734 className="mem-textarea"
735 rows={6}
736 placeholder={t("subagents.systemPromptPlaceholder")}
737 value={systemPrompt}
738 onChange={(e) => setSystemPrompt(e.target.value)}
739 />
740
741 <label className="set-label">{t("subagents.tryIt")}</label>
742 <div className="subagents-tryit-row">
743 <input
744 className="mem-input"
745 placeholder={t("subagents.tryItPlaceholder")}
746 value={tryTask}
747 disabled={tryRunning}
748 onChange={(e) => setTryTask(e.target.value)}
749 />
750 <button
751 className="btn btn--small"
752 type="button"
753 onClick={() => (tryRunning ? void app.CancelTrySubagentProfile() : void runTry())}
754 disabled={!tryRunning && (!promptReady || tryTask.trim() === "")}
755 >
756 {tryRunning ? t("subagents.cancelRun") : t("subagents.run")}
757 </button>
758 </div>
759 {tryError && <div className="banner banner--error">{tryError}</div>}
760 {tryResult && <pre className="subagents-tryit-result">{tryResult}</pre>}
761
762 <label className="set-label">{t("subagents.scope")}</label>
763 <select
764 className="mem-select set-grow"
765 value={scope}
766 disabled={busy || isEditing}
767 onChange={(e) => setScope(e.target.value === "project" ? "project" : "global")}
768 >
769 <option value="global">{t("caps.skillScopeGlobal")}</option>
770 <option value="project">{t("caps.skillScopeProject")}</option>
771 </select>
772
773 <div className="subagents-hint">{t("subagents.manualInvocationHint", { name: trimmedName || "…" })}</div>
774
775 <div className="prov-card__actions">
776 <button className="btn btn--small" onClick={onCancel} disabled={busy}>
777 {t("common.cancel")}
778 </button>
779 <button className="btn btn--primary btn--small" onClick={submit} disabled={busy || !ready}>
780 {t("common.save")}
781 </button>
782 </div>
783 </div>
784 );
785 }
786
786 lines Plain Text