feat: add dependsOn for conditional params, boolean checkbox support

Add "Create independent agent" checkbox that toggles visibility of
name/emoji/persona fields. Params with dependsOn are hidden when the
referenced boolean is unchecked, and their steps are auto-skipped.
Template substitution now converts "true"/"false" to native booleans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhixian
2026-02-17 23:23:10 +09:00
parent a373a7c068
commit 1a97705f2b
5 changed files with 57 additions and 18 deletions

View File

@@ -3,7 +3,7 @@
{ {
"id": "dedicated-channel-agent", "id": "dedicated-channel-agent",
"name": "Create dedicated Agent for Channel", "name": "Create dedicated Agent for Channel",
"description": "Create an independent agent, set its identity, bind it to a Discord channel, and configure persona", "description": "Create an agent, optionally independent with its own identity and persona, and bind it to a Discord channel",
"version": "1.0.0", "version": "1.0.0",
"tags": ["discord", "agent", "persona"], "tags": ["discord", "agent", "persona"],
"difficulty": "easy", "difficulty": "easy",
@@ -11,12 +11,13 @@
{ "id": "agent_id", "label": "Agent ID", "type": "string", "required": true, "placeholder": "e.g. my-bot" }, { "id": "agent_id", "label": "Agent ID", "type": "string", "required": true, "placeholder": "e.g. my-bot" },
{ "id": "guild_id", "label": "Guild", "type": "discord_guild", "required": true }, { "id": "guild_id", "label": "Guild", "type": "discord_guild", "required": true },
{ "id": "channel_id", "label": "Channel", "type": "discord_channel", "required": true }, { "id": "channel_id", "label": "Channel", "type": "discord_channel", "required": true },
{ "id": "name", "label": "Display Name", "type": "string", "required": false, "placeholder": "e.g. MyBot" }, { "id": "independent", "label": "Create independent agent", "type": "boolean", "required": false },
{ "id": "emoji", "label": "Emoji", "type": "string", "required": false, "placeholder": "e.g. \ud83e\udd16" }, { "id": "name", "label": "Display Name", "type": "string", "required": false, "placeholder": "e.g. MyBot", "dependsOn": "independent" },
{ "id": "persona", "label": "Persona", "type": "textarea", "required": false, "placeholder": "You are..." } { "id": "emoji", "label": "Emoji", "type": "string", "required": false, "placeholder": "e.g. \ud83e\udd16", "dependsOn": "independent" },
{ "id": "persona", "label": "Persona", "type": "textarea", "required": false, "placeholder": "You are...", "dependsOn": "independent" }
], ],
"steps": [ "steps": [
{ "action": "create_agent", "label": "Create independent agent", "args": { "agentId": "{{agent_id}}", "independent": true } }, { "action": "create_agent", "label": "Create agent", "args": { "agentId": "{{agent_id}}", "independent": "{{independent}}" } },
{ "action": "setup_identity", "label": "Set agent identity", "args": { "agentId": "{{agent_id}}", "name": "{{name}}", "emoji": "{{emoji}}" } }, { "action": "setup_identity", "label": "Set agent identity", "args": { "agentId": "{{agent_id}}", "name": "{{name}}", "emoji": "{{emoji}}" } },
{ "action": "bind_channel", "label": "Bind channel to agent", "args": { "channelType": "discord", "peerId": "{{channel_id}}", "agentId": "{{agent_id}}" } }, { "action": "bind_channel", "label": "Bind channel to agent", "args": { "channelType": "discord", "peerId": "{{channel_id}}", "agentId": "{{agent_id}}" } },
{ "action": "config_patch", "label": "Set channel persona", "args": { "patchTemplate": "{\"channels\":{\"discord\":{\"guilds\":{\"{{guild_id}}\":{\"channels\":{\"{{channel_id}}\":{\"systemPrompt\":\"{{persona}}\"}}}}}}}" } } { "action": "config_patch", "label": "Set channel persona", "args": { "patchTemplate": "{\"channels\":{\"discord\":{\"guilds\":{\"{{guild_id}}\":{\"channels\":{\"{{channel_id}}\":{\"systemPrompt\":\"{{persona}}\"}}}}}}}" } }

View File

@@ -28,6 +28,8 @@ pub struct RecipeParam {
pub max_length: Option<usize>, pub max_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>, pub placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub depends_on: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]

View File

@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -91,9 +92,15 @@ export function ParamForm({
return discordGuildChannels.filter((gc) => gc.guildId === guildId); return discordGuildChannels.filter((gc) => gc.guildId === guildId);
}, [discordGuildChannels, values]); }, [discordGuildChannels, values]);
const isParamVisible = (param: RecipeParam) => {
if (!param.dependsOn) return true;
return values[param.dependsOn] === "true";
};
const errors = useMemo(() => { const errors = useMemo(() => {
const next: Record<string, string> = {}; const next: Record<string, string> = {};
for (const param of recipe.params) { for (const param of recipe.params) {
if (!isParamVisible(param)) continue;
const err = validateField(param, values[param.id] || ""); const err = validateField(param, values[param.id] || "");
if (err) { if (err) {
next[param.id] = err; next[param.id] = err;
@@ -104,6 +111,21 @@ export function ParamForm({
const hasError = Object.keys(errors).length > 0; const hasError = Object.keys(errors).length > 0;
function renderParam(param: RecipeParam) { function renderParam(param: RecipeParam) {
if (param.type === "boolean") {
return (
<div className="flex items-center gap-2">
<Checkbox
id={param.id}
checked={values[param.id] === "true"}
onCheckedChange={(checked) => {
onChange(param.id, checked === true ? "true" : "false");
}}
/>
<Label htmlFor={param.id} className="font-normal">{param.label}</Label>
</div>
);
}
if (param.type === "discord_guild") { if (param.type === "discord_guild") {
return ( return (
<Select <Select
@@ -245,15 +267,19 @@ export function ParamForm({
} }
onSubmit(); onSubmit();
}}> }}>
{recipe.params.map((param: RecipeParam) => ( {recipe.params.map((param: RecipeParam) => {
<div key={param.id} className="space-y-1.5"> if (!isParamVisible(param)) return null;
<Label htmlFor={param.id}>{param.label}</Label> const isBool = param.type === "boolean";
{renderParam(param)} return (
{touched[param.id] && errors[param.id] ? ( <div key={param.id} className="space-y-1.5">
<p className="text-sm text-destructive">{errors[param.id]}</p> {!isBool && <Label htmlFor={param.id}>{param.label}</Label>}
) : null} {renderParam(param)}
</div> {touched[param.id] && errors[param.id] ? (
))} <p className="text-sm text-destructive">{errors[param.id]}</p>
) : null}
</div>
);
})}
<Button <Button
type="submit" type="submit"
disabled={hasError} disabled={hasError}

View File

@@ -12,11 +12,20 @@ function renderArgs(
const result: Record<string, unknown> = {}; const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(args)) { for (const [key, value] of Object.entries(args)) {
if (typeof value === "string") { if (typeof value === "string") {
let rendered = value; // If the entire value is a single template like "{{param}}", resolve to native type
for (const [paramId, paramValue] of Object.entries(params)) { const singleMatch = value.match(/^\{\{(\w+)\}\}$/);
rendered = rendered.split(`{{${paramId}}}`).join(paramValue); if (singleMatch) {
const paramValue = params[singleMatch[1]] ?? "";
if (paramValue === "true") result[key] = true;
else if (paramValue === "false" || paramValue === "") result[key] = false;
else result[key] = paramValue;
} else {
let rendered = value;
for (const [paramId, paramValue] of Object.entries(params)) {
rendered = rendered.split(`{{${paramId}}}`).join(paramValue);
}
result[key] = rendered;
} }
result[key] = rendered;
} else { } else {
result[key] = value; result[key] = value;
} }

View File

@@ -16,6 +16,7 @@ export interface RecipeParam {
minLength?: number; minLength?: number;
maxLength?: number; maxLength?: number;
placeholder?: string; placeholder?: string;
dependsOn?: string;
} }
export interface RecipeStep { export interface RecipeStep {