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