Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/agent-gateway/web/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,9 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.agentsShowPrompt": "查看 Prompt",
"settings.agentsReady": "可以保存",
"settings.agentsRequired": "名称和 Prompt 为必填项",
"settings.agentsSubagentTemplates": "子代理模板",
"settings.agentsSubagentTemplatesHint":
"选择可供子代理按需引用的模板,可同时选择多个;不会改变主代理当前激活的模板。",

/* ── Settings SSH ── */
"settings.sshTitle": "SSH",
Expand Down Expand Up @@ -3839,6 +3842,9 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.agentsShowPrompt": "View Prompt",
"settings.agentsReady": "Ready to save",
"settings.agentsRequired": "Name and Prompt are required",
"settings.agentsSubagentTemplates": "Subagent Templates",
"settings.agentsSubagentTemplatesHint":
"Choose templates that subagents may reference. Multiple templates can be selected without changing the active main-agent template.",

/* ── Settings SSH ── */
"settings.sshTitle": "SSH",
Expand Down
5 changes: 5 additions & 0 deletions crates/agent-gateway/web/src/lib/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export type AgentPromptTemplate = {
description: string;
prompt: string;
enabled: boolean;
availableToSubagents: boolean;
};

export type SshAuthType = "password" | "privateKey" | "keyboardInteractive";
Expand Down Expand Up @@ -1501,6 +1502,10 @@ export function normalizeAgentPromptTemplate(input: unknown): AgentPromptTemplat
description: normalizeOptionalText(obj.description),
prompt: normalizeOptionalText(obj.prompt),
enabled: obj.enabled === true,
availableToSubagents:
typeof obj.availableToSubagents === "boolean"
? obj.availableToSubagents
: obj.enabled === true,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import { useLocale } from "../../i18n";
import type { AgentPromptTemplate } from "../../lib/settings";
import { useModalMotion } from "../../lib/shared/modalMotion";

type AgentPromptTemplateContent = Pick<AgentPromptTemplate, "name" | "description" | "prompt">;

type AgentPromptTemplateModalProps = {
initialData?: AgentPromptTemplate;
onSave: (data: Omit<AgentPromptTemplate, "id" | "enabled">) => void;
onSave: (data: AgentPromptTemplateContent) => void;
onClose: () => void;
};

Expand Down
134 changes: 132 additions & 2 deletions crates/agent-gateway/web/src/pages/settings/AgentsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { Popover } from "@base-ui/react";
import { useState } from "react";
import { createPortal } from "react-dom";
import { BookOpen, Eye, FileText, Pencil, Plus, Trash2, X } from "../../components/icons";
import {
BookOpen,
Bot,
Check,
ChevronDown,
Eye,
FileText,
Pencil,
Plus,
Trash2,
X,
} from "../../components/icons";

import { Button } from "../../components/ui/button";
import { useLocale } from "../../i18n";
Expand Down Expand Up @@ -33,7 +45,7 @@ export function AgentsSection(props: SettingsSectionProps) {
setEditingTemplate(null);
}

function handleSave(data: Omit<AgentPromptTemplate, "id" | "enabled">) {
function handleSave(data: Pick<AgentPromptTemplate, "name" | "description" | "prompt">) {
setSettings((prev) => {
if (editingTemplate) {
return updateAgents(
Expand All @@ -48,6 +60,7 @@ export function AgentsSection(props: SettingsSectionProps) {
id: createUuid(),
...data,
enabled: false,
availableToSubagents: false,
};
return updateAgents(prev, [...prev.agents, newTemplate]);
});
Expand Down Expand Up @@ -76,8 +89,24 @@ export function AgentsSection(props: SettingsSectionProps) {
);
}

function handleToggleAvailableToSubagents(id: string) {
setSettings((prev) =>
updateAgents(
prev,
prev.agents.map((template) =>
template.id === id
? { ...template, availableToSubagents: !template.availableToSubagents }
: template,
),
),
);
}

const templates = settings.agents;
const enabledCount = templates.filter((template) => template.enabled).length;
const subagentTemplateCount = templates.filter(
(template) => template.availableToSubagents,
).length;

return (
<>
Expand Down Expand Up @@ -112,6 +141,13 @@ export function AgentsSection(props: SettingsSectionProps) {
) : null}
</div>
) : null}
{templates.length > 0 ? (
<SubagentTemplatePicker
templates={templates}
selectedCount={subagentTemplateCount}
onToggle={handleToggleAvailableToSubagents}
/>
) : null}
<Button variant="outline" size="sm" className="gap-1.5" onClick={openAdd}>
<Plus className="h-3.5 w-3.5" />
{t("settings.agentsAdd")}
Expand Down Expand Up @@ -245,6 +281,100 @@ export function AgentsSection(props: SettingsSectionProps) {
);
}

function SubagentTemplatePicker(props: {
templates: AgentPromptTemplate[];
selectedCount: number;
onToggle: (id: string) => void;
}) {
const { templates, selectedCount, onToggle } = props;
const { t } = useLocale();
const [open, setOpen] = useState(false);

return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger
render={
<Button
variant="outline"
size="sm"
className="gap-1.5"
aria-label={t("settings.agentsSubagentTemplates")}
/>
}
>
<Bot className="h-3.5 w-3.5" />
<span>{t("settings.agentsSubagentTemplates")}</span>
<span className="rounded-full bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-sky-600 dark:text-sky-400">
{selectedCount}
</span>
<ChevronDown
className={`h-3.5 w-3.5 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}`}
/>
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner
side="bottom"
align="end"
sideOffset={6}
collisionPadding={8}
className="z-[9999]"
>
<Popover.Popup
aria-label={t("settings.agentsSubagentTemplates")}
className="w-[min(22rem,calc(100vw-1rem))] overflow-hidden rounded-xl border bg-popover text-popover-foreground shadow-lg outline-none"
>
<div className="border-b border-border/60 px-3 py-2.5">
<p className="text-xs font-semibold">{t("settings.agentsSubagentTemplates")}</p>
<p className="mt-0.5 text-[11px] leading-relaxed text-muted-foreground">
{t("settings.agentsSubagentTemplatesHint")}
</p>
</div>
<div className="max-h-72 space-y-1 overflow-y-auto p-1.5">
{templates.map((template) => {
const checked = template.availableToSubagents;
return (
<label
key={template.id}
className={`flex w-full cursor-pointer items-start gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-muted/70 focus-within:ring-2 focus-within:ring-ring ${
checked ? "bg-sky-500/[0.07]" : ""
}`}
>
<input
type="checkbox"
checked={checked}
className="sr-only"
onChange={() => onToggle(template.id)}
/>
<span
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors ${
checked
? "border-sky-500 bg-sky-500 text-white"
: "border-border bg-background"
}`}
>
{checked ? <Check className="h-3 w-3" /> : null}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-xs font-medium text-foreground">
{template.name}
</span>
{template.description ? (
<span className="mt-0.5 block line-clamp-2 text-[11px] leading-relaxed text-muted-foreground">
{template.description}
</span>
) : null}
</span>
</label>
);
})}
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}

type AgentPromptViewModalProps = {
template: AgentPromptTemplate;
onClose: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,25 @@ fn load_agents(conn: &Connection) -> Result<Option<Value>, String> {
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, i64>(4)?,
row.get::<_, i64>(5)?,
))
})
.map_err(|e| format!("读取 {AGENT_PROMPT_TEMPLATES_TABLE} 失败:{e}"))?;

let mut templates = Vec::new();
for row in rows {
let (template_id, name, description, prompt, enabled) =
let (template_id, name, description, prompt, enabled, available_to_subagents) =
row.map_err(|e| format!("读取 {AGENT_PROMPT_TEMPLATES_TABLE} 行失败:{e}"))?;
templates.push(Value::Object(Map::from_iter([
("id".to_string(), Value::String(template_id)),
("name".to_string(), Value::String(name)),
("description".to_string(), Value::String(description)),
("prompt".to_string(), Value::String(prompt)),
("enabled".to_string(), Value::Bool(enabled != 0)),
(
"availableToSubagents".to_string(),
Value::Bool(available_to_subagents != 0),
),
])));
}

Expand Down Expand Up @@ -73,6 +78,16 @@ fn save_agents(conn: &mut Connection, payload: Value) -> Result<(), String> {
}
enabled_template_id = Some(template_id.clone());
}
let available_to_subagents = match template.get("availableToSubagents") {
Some(Value::Bool(value)) => *value,
Some(Value::Null) | None => enabled,
Some(_) => {
return Err(
"settings_save_agents payload[].availableToSubagents 必须是布尔值"
.to_string(),
);
}
};

tx.execute(
AGENT_PROMPT_TEMPLATES_INSERT_SQL,
Expand All @@ -82,6 +97,7 @@ fn save_agents(conn: &mut Connection, payload: Value) -> Result<(), String> {
description,
prompt,
if enabled { 1_i64 } else { 0_i64 },
if available_to_subagents { 1_i64 } else { 0_i64 },
sort_index as i64,
updated_at
],
Expand Down
31 changes: 31 additions & 0 deletions crates/agent-gui/src-tauri/src/commands/config/settings/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub(crate) fn initialize_schema(conn: &Connection) -> Result<(), String> {
description TEXT NOT NULL,
prompt TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0,
available_to_subagents INTEGER NOT NULL DEFAULT 0,
sort_index INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
Expand Down Expand Up @@ -105,6 +106,36 @@ pub(crate) fn initialize_schema(conn: &Connection) -> Result<(), String> {
",
)
.map_err(|e| format!("初始化设置表失败:{e}"))?;

let has_subagent_template_column = {
let mut stmt = conn
.prepare("PRAGMA table_info(agent_prompt_templates)")
.map_err(|e| format!("检查 {AGENT_PROMPT_TEMPLATES_TABLE} 表结构失败:{e}"))?;
let columns = stmt
.query_map([], |row| row.get::<_, String>(1))
.map_err(|e| format!("读取 {AGENT_PROMPT_TEMPLATES_TABLE} 表结构失败:{e}"))?;
let mut found = false;
for column in columns {
if column.map_err(|e| format!("读取 {AGENT_PROMPT_TEMPLATES_TABLE} 列失败:{e}"))?
== "available_to_subagents"
{
found = true;
break;
}
}
found
};
if !has_subagent_template_column {
conn.execute_batch(
"
ALTER TABLE agent_prompt_templates
ADD COLUMN available_to_subagents INTEGER NOT NULL DEFAULT 0;
UPDATE agent_prompt_templates
SET available_to_subagents = enabled;
",
)
.map_err(|e| format!("迁移 {AGENT_PROMPT_TEMPLATES_TABLE} 子代理模板字段失败:{e}"))?;
}
Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ const MCP_SETTINGS_INSERT_SQL: &str = "
const MCP_SETTINGS_DELETE_SQL: &str = "DELETE FROM mcp_settings";

const AGENT_PROMPT_TEMPLATES_SELECT_SQL: &str = "
SELECT template_id, name, description, prompt, enabled
SELECT template_id, name, description, prompt, enabled, available_to_subagents
FROM agent_prompt_templates
ORDER BY sort_index ASC, template_id ASC
";
const AGENT_PROMPT_TEMPLATES_INSERT_SQL: &str = "
INSERT INTO agent_prompt_templates
(template_id, name, description, prompt, enabled, sort_index, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
(template_id, name, description, prompt, enabled, available_to_subagents, sort_index, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
";
const AGENT_PROMPT_TEMPLATES_DELETE_SQL: &str = "DELETE FROM agent_prompt_templates";

Expand Down
Loading
Loading