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