From 13ecab24ee8bc8ceacea9cc139652b5d1c4481af Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:22:37 +0200 Subject: [PATCH] Unify Agent Editor capability and prompt controls Give Tools, MCPs, and Skills explicit default switches with segmented per-item policy controls while preserving sparse overrides. Simplify Advanced prompt editing around the full-height ACE surface and align the editor contracts and regression coverage. --- helpers/tool_policy.py | 8 +- helpers/tool_policy.py.dox.md | 5 +- plugins/_agent_editor/AGENTS.md | 27 +- plugins/_agent_editor/README.md | 8 + plugins/_agent_editor/helpers/editor.py | 16 +- .../_agent_editor/webui/agent-editor-store.js | 348 ++++++++++-------- plugins/_agent_editor/webui/main.html | 246 +++++++------ plugins/_tool_access/AGENTS.md | 2 + plugins/_tool_access/default_config.yaml | 1 + tests/test_agent_editor.py | 42 ++- tests/test_agent_editor_webui.py | 293 ++++++++++----- tests/test_tool_policy.py | 22 +- 12 files changed, 621 insertions(+), 397 deletions(-) diff --git a/helpers/tool_policy.py b/helpers/tool_policy.py index a448ccd3b..1b98a22c0 100644 --- a/helpers/tool_policy.py +++ b/helpers/tool_policy.py @@ -28,8 +28,10 @@ def normalize_policy(config: Any) -> dict[str, Any]: raw = dict(config) if isinstance(config, dict) else {} mode = str(raw.get("mode") or "inherit").strip().lower() default = str(raw.get("default") or "allow").strip().lower() + mcp_default = str(raw.get("mcp_default") or "allow").strip().lower() raw["mode"] = "custom" if mode == "custom" else "inherit" raw["default"] = "block" if default == "block" else "allow" + raw["mcp_default"] = "block" if mcp_default == "block" else "allow" raw["allowed"] = _normalize_ids(raw.get("allowed")) raw["blocked"] = _normalize_ids(raw.get("blocked")) return raw @@ -49,7 +51,8 @@ def get_policy(agent: Any) -> dict[str, Any]: ): config = files.read_file_json(asset["path"]) if not isinstance(config, dict) or not any( - key in config for key in ("mode", "default", "allowed", "blocked") + key in config + for key in ("mode", "default", "mcp_default", "allowed", "blocked") ): continue policy = normalize_policy(config) @@ -171,7 +174,8 @@ def resolve_tool( if tool_id in policy["allowed"]: return ToolPolicyDecision(True, tool_id, "scoped-policy", "custom") - is_allowed = policy["default"] == "allow" + default_key = "mcp_default" if tool_id.startswith("mcp:") else "default" + is_allowed = policy[default_key] == "allow" return ToolPolicyDecision( is_allowed, tool_id, diff --git a/helpers/tool_policy.py.dox.md b/helpers/tool_policy.py.dox.md index 1c7a1d4d4..1d5729df1 100644 --- a/helpers/tool_policy.py.dox.md +++ b/helpers/tool_policy.py.dox.md @@ -31,8 +31,9 @@ active project, user profile, bundled/plugin profile, then default. `get_policy` selects the first custom policy; unknown-only and explicit-inherit files remain on disk but defer to the next lower layer. -- Missing policy inherits standard access; custom policy always records whether - future tools default to allowed or blocked. +- Missing policy inherits standard access. A custom policy records independent + defaults for local/plugin tools and canonical MCP tools; explicit allowed or + blocked IDs take precedence over either default. - The `response` capability is a framework-required invariant: profile policy cannot disable it, and the editor does not list it as a configurable tool. - `vision_load` remains owned by the active chat model's vision configuration; diff --git a/plugins/_agent_editor/AGENTS.md b/plugins/_agent_editor/AGENTS.md index 9676d6191..b32322fe5 100644 --- a/plugins/_agent_editor/AGENTS.md +++ b/plugins/_agent_editor/AGENTS.md @@ -44,18 +44,25 @@ Removing or deleting in project scope never mutates those inherited layers; only agents created in the selected scope are deletable. - Bundled `agents/` files are read-only. -- Advanced prompt text is directly editable; per-file close/check actions - discard or accept the current edit checkpoint, while the editor's global save - remains the only persistence boundary. +- Advanced prompt text is edited with a full-height bundled ACE editor in + Markdown mode. The selected file's customization path sits below its name; + per-file close/check actions discard or accept the current edit checkpoint, + while the editor's global save remains the only persistence boundary. - New profiles require a display name and non-empty agent instructions in both Easy and Advanced; existing Advanced prompt edits retain per-file semantics. -- The configurable tool catalog is visible in both modes; Easy provides direct - allow/block checkboxes and points to Advanced for skill access. Skills remain - Advanced-only. Advanced keeps both complete selectors visible but disabled - for inherited access and interactive for custom access. Framework-required - tools remain absent from the tool catalog. -- Model selection reuses `_model_config`'s compact preset dropdown and preset - editor; Agent Editor persists only the scoped preset reference. +- Easy and Advanced share the same segmented capability controls: Default + removes the item from `allowed` and `blocked`, On stores it in `allowed`, and + Off stores it in `blocked`. Tools, canonical MCP entries, and Skills expose + independent default switches; explicit choices remain pinned when a default + changes, and opening then undoing an inherited policy produces no write. Easy + places each initially closed native accordion directly below its default + switch. Advanced gives Tools, MCPs, and Skills separate sections while keeping + unavailable retained IDs reviewable. Framework-required tools remain absent. +- Model selection in both modes reuses `_model_config`'s compact preset dropdown + and preset editor; Agent Editor persists only the scoped preset reference. +- The exact `default` profile is an internal baseline and is omitted only from + selectable and editable UI rows. Runtime discovery remains unchanged, and an + existing chat using it may still report it as current status. - Manage agents reuses the plugin-settings project vocabulary: Global or one existing project. The active chat profile appears once above the list; each row exposes scoped availability, duplication, restore for inherited profiles, diff --git a/plugins/_agent_editor/README.md b/plugins/_agent_editor/README.md index 588d56210..4a9ceac77 100644 --- a/plugins/_agent_editor/README.md +++ b/plugins/_agent_editor/README.md @@ -8,6 +8,12 @@ The editor never invokes a model. Tool and skill controls are backed by the central runtime policy owners, and every save is previewed as exact file writes and deletions before the same validated plan is applied. +Easy and Advanced use the same segmented capability policy: On pins an item +allowed, Off pins it blocked, and Default follows the profile's Tools, MCPs, or +Skills default switch. Easy places each chooser below its default switch; +Advanced gives Tools, MCPs, and Skills separate searchable sections and adds +retained unavailable entries plus ACE-based prompt editing. + Global agents and customizations live under `usr/agents/` and apply across projects. Project-scoped agents and customizations live under `usr/projects//.a0proj/agents/`, inherit the Global layer, @@ -17,3 +23,5 @@ Manage agents can duplicate the effective profile into the selected scope and toggle whether each profile is available there. Project availability reuses `.a0proj/agents.json`; Global availability is a sparse profile override. The selected scope must always keep at least one profile available. +The bundled `default` profile remains the internal inheritance baseline and is +not offered as a selectable or editable profile. diff --git a/plugins/_agent_editor/helpers/editor.py b/plugins/_agent_editor/helpers/editor.py index 11ad405a8..59105f57d 100644 --- a/plugins/_agent_editor/helpers/editor.py +++ b/plugins/_agent_editor/helpers/editor.py @@ -32,7 +32,7 @@ RESERVED_PROFILE_IDS = {"_example"} NON_PROMPT_MARKDOWN = {"AGENTS.md"} USER_AGENTS_ROOT = Path(files.get_abs_path(subagents.USER_AGENTS_DIR)) STAGED_AVATAR_ROOT = Path(files.get_abs_path("tmp", "agent-editor")) -_POLICY_KEYS = ("mode", "default", "allowed", "blocked") +_TOOL_POLICY_KEYS = ("mode", "default", "mcp_default", "allowed", "blocked") _MUTATION_LOCK = threading.RLock() @@ -319,7 +319,7 @@ def build_editor_state( "tools": { "policy": tool_policy.normalize_policy(tool_scope), "effective_policy": tool_policy.get_policy(agent), - "has_override": any(key in tool_scope for key in _POLICY_KEYS), + "has_override": any(key in tool_scope for key in _TOOL_POLICY_KEYS), "catalog": tool_catalog, }, "skills": { @@ -1020,11 +1020,17 @@ def _plan_tool_policy(plan: ChangePlan, value: Any) -> None: data = _read_mapping_strict(path, "tool policy configuration") if mode == "inherit": - for key in _POLICY_KEYS: + for key in _TOOL_POLICY_KEYS: data.pop(key, None) else: if mode == "off": - policy = {"mode": "custom", "default": "block", "allowed": [], "blocked": []} + policy = { + "mode": "custom", + "default": "block", + "mcp_default": "block", + "allowed": [], + "blocked": [], + } elif mode == "custom": policy = tool_policy.normalize_policy(section) allowed = set(policy["allowed"]) @@ -1036,7 +1042,7 @@ def _plan_tool_policy(plan: ChangePlan, value: Any) -> None: raise ValueError(f'Invalid canonical tool ID "{tool_id}".') else: raise ValueError("Tool policy mode must be inherit, off, or custom.") - data.update({key: policy[key] for key in _POLICY_KEYS}) + data.update({key: policy[key] for key in _TOOL_POLICY_KEYS}) _plan_json_mapping(plan, path, data) diff --git a/plugins/_agent_editor/webui/agent-editor-store.js b/plugins/_agent_editor/webui/agent-editor-store.js index 727b9674b..b0e4712b6 100644 --- a/plugins/_agent_editor/webui/agent-editor-store.js +++ b/plugins/_agent_editor/webui/agent-editor-store.js @@ -29,7 +29,7 @@ export function slugifyProfileName(value) { .replace(/[-_]+$/g, ""); } -function policyFromState(value, hasOverride) { +function policyFromState(value, hasOverride, includeMcpDefault = false) { const policy = value && typeof value === "object" ? value : {}; const normalized = { mode: policy.mode === "custom" ? "custom" : "inherit", @@ -37,6 +37,9 @@ function policyFromState(value, hasOverride) { allowed: unique(policy.allowed), blocked: unique(policy.blocked), }; + if (includeMcpDefault) { + normalized.mcp_default = policy.mcp_default === "block" ? "block" : "allow"; + } if (!hasOverride || normalized.mode !== "custom") normalized.mode = "inherit"; return normalized; } @@ -45,27 +48,42 @@ function policyAllows(policy, id) { if (!policy || policy.mode !== "custom") return true; if (policy.blocked.includes(id)) return false; if (policy.allowed.includes(id)) return true; - return policy.default === "allow"; + const fallback = String(id || "").startsWith("mcp:") ? policy.mcp_default : policy.default; + return fallback === "allow"; } -function policyBehavior(policy) { +function policyItemState(policy, id) { + if (!policy || policy.mode !== "custom") return "default"; + if (policy?.blocked?.includes(id)) return "block"; + if (policy?.allowed?.includes(id)) return "allow"; + return "default"; +} + +function policyBehavior(policy, includeMcpDefault = false) { const value = policy || {}; if (value.mode !== "custom") { - return { default: "allow", allowed: [], blocked: [] }; + return { + default: "allow", + ...(includeMcpDefault ? { mcp_default: "allow" } : {}), + allowed: [], + blocked: [], + }; } return { default: value.default === "block" ? "block" : "allow", + ...(includeMcpDefault + ? { mcp_default: value.mcp_default === "block" ? "block" : "allow" } + : {}), allowed: unique(value.allowed).sort(), blocked: unique(value.blocked).sort(), }; } -function movePolicyItem(policy, id, allow) { +function setPolicyItemState(policy, id, state) { policy.allowed = policy.allowed.filter((item) => item !== id); policy.blocked = policy.blocked.filter((item) => item !== id); - const exceptions = allow ? "allowed" : "blocked"; - const matchesDefault = allow === (policy.default === "allow"); - if (!matchesDefault) policy[exceptions].push(id); + if (state === "allow") policy.allowed.push(id); + if (state === "block") policy.blocked.push(id); } function escapeHtml(value) { @@ -93,17 +111,14 @@ const model = { initialDraft: null, selectedPrompt: SPECIFICS, promptFileSearch: "", - promptTextSearch: "", - comparePrompt: "", toolSearch: "", - toolCategory: "all", toolOrigin: "all", - selectedAllowedTools: [], - selectedBlockedTools: [], skillSearch: "", skillOrigin: "all", - selectedAllowedSkills: [], - selectedBlockedSkills: [], + promptEditor: null, + promptEditorChangeHandler: null, + settingPromptEditorValue: false, + aceUnavailable: false, promptEditBaselines: {}, plan: { written: [], deleted: [], warnings: [] }, planLoading: false, @@ -136,10 +151,15 @@ const model = { : String(options.projectName || ""), }; this.suppressClosePrompt = false; - return await openModal(MODAL, () => this.beforeClose()); + try { + return await openModal(MODAL, () => this.beforeClose()); + } finally { + this.destroyPromptEditor(); + } }, async mount(root) { + this.destroyPromptEditor(); this.revokePreview(); this.root = root; this.draft = null; @@ -248,6 +268,10 @@ const model = { return this.profiles.find((profile) => this.isProfileActive(profile.id)) || null; }, + get visibleProfiles() { + return this.profiles.filter((profile) => profile.id !== "default"); + }, + async setProfileEnabled(profile, enabled) { if (!profile?.id || this.profileAvailabilitySaving) return; const previous = !!profile.enabled; @@ -343,12 +367,11 @@ const model = { this.projectName = previous; return; } - const reviewActive = this.mode === "advanced" && this.section === "5"; + const reviewActive = this.mode === "advanced" && this.section === "6"; const creatingDraft = this.draft?.creating ? this.draft : null; const creatingInitial = creatingDraft ? this.initialDraft : null; const creatingState = creatingDraft ? this.state : null; const selectedPrompt = this.selectedPrompt; - const comparePrompt = this.comparePrompt; const promptChanges = creatingDraft ? Object.values(creatingDraft.prompts).flatMap((prompt) => { const initial = creatingInitial?.prompts?.[prompt.filename]; @@ -376,13 +399,14 @@ const model = { key, idKey, default: policy.default, + mcpDefault: policy.mcp_default, choices: (oldState.catalog || []) .map((item) => [ item[idKey], - policyAllows(policy, item[idKey]), - policyAllows(oldState.effective_policy, item[idKey]), + policyItemState(policy, item[idKey]), + policyItemState(oldState.effective_policy, item[idKey]), ]) - .filter(([, allowed, inherited]) => allowed !== inherited), + .filter(([, state, inherited]) => state !== inherited), }); } } @@ -426,7 +450,6 @@ const model = { if (change.baseline) this.promptEditBaselines[change.filename] = change.baseline; } if (this.draft.prompts[selectedPrompt]) this.selectedPrompt = selectedPrompt; - this.comparePrompt = comparePrompt; if (avatarChanged) { this.draft.avatar = clone(creatingDraft.avatar); this.draft.avatarToken = creatingDraft.avatarToken; @@ -438,12 +461,16 @@ const model = { if (modelPresetChanged) this.draft.modelPreset = creatingDraft.modelPreset; for (const change of policyChanges) { this.customizePolicy(change.kind); - this.setPolicyDefault(change.kind, change.default); + this.draft[change.key].default = change.default; + if (change.kind === "tool") { + this.draft[change.key].mcp_default = change.mcpDefault; + } const catalog = this.state[change.kind === "tool" ? "tools" : "skills"].catalog || []; const nextIds = new Set(catalog.map((item) => item[change.idKey])); - for (const [id, allowed] of change.choices) { - if (nextIds.has(id)) movePolicyItem(this.draft[change.key], id, allowed); + for (const [id, state] of change.choices) { + if (nextIds.has(id)) setPolicyItemState(this.draft[change.key], id, state); } + this.collapsePolicy(change.kind); } } if (reviewActive) await this.previewPlan(); @@ -524,23 +551,19 @@ const model = { modelPreset: this.state.model_preset.has_override ? String(this.state.model_preset.override || "") : "", - toolPolicy: policyFromState(this.state.tools.policy, this.state.tools.has_override), + toolPolicy: policyFromState(this.state.tools.policy, this.state.tools.has_override, true), skillPolicy: policyFromState(this.state.skills.policy, this.state.skills.has_override), }; this.initialDraft = clone(this.draft); this.selectedPrompt = SPECIFICS; - this.comparePrompt = ""; this.planStatus = "idle"; - this.selectedAllowedTools = []; - this.selectedBlockedTools = []; - this.selectedAllowedSkills = []; - this.selectedBlockedSkills = []; this.promptEditBaselines = Object.fromEntries( Object.values(prompts).map((prompt) => [prompt.filename, { value: prompt.value, reset: prompt.reset, }]), ); + this.schedulePromptEditor(); }, get dirty() { @@ -564,6 +587,14 @@ const model = { return (this.state?.tools?.catalog || []).filter((item) => item.available !== false); }, + get standardToolCatalog() { + return this.toolCatalog.filter((item) => !String(item.id || "").startsWith("mcp:")); + }, + + get mcpCatalog() { + return this.toolCatalog.filter((item) => String(item.id || "").startsWith("mcp:")); + }, + get toolOrigins() { return unique((this.state?.tools?.catalog || []).map((item) => item.origin)).sort(); }, @@ -599,8 +630,8 @@ const model = { ); } if (String(section) === "2") return Object.values(this.draft.prompts).some((prompt) => this.promptDirty(prompt)); - if (String(section) === "3") return !same(this.draft.toolPolicy, this.initialDraft.toolPolicy); - if (String(section) === "4") return !same(this.draft.skillPolicy, this.initialDraft.skillPolicy); + if (["3", "4"].includes(String(section))) return !same(this.draft.toolPolicy, this.initialDraft.toolPolicy); + if (String(section) === "5") return !same(this.draft.skillPolicy, this.initialDraft.skillPolicy); return this.dirty; }, @@ -622,6 +653,7 @@ const model = { }, enterManager() { + this.destroyPromptEditor(); this.revokePreview(); this.draft = null; this.initialDraft = null; @@ -638,12 +670,13 @@ const model = { setMode(mode, section = "", preview = true) { this.mode = mode === "advanced" ? "advanced" : "easy"; if (section) this.setSection(section, preview); - else if (preview && this.mode === "advanced" && this.section === "5") this.previewPlan(); + else if (preview && this.mode === "advanced" && this.section === "6") this.previewPlan(); this.syncSurface(); if (this.mode === "advanced") { requestAnimationFrame(() => { this.root?.querySelector(`[data-agent-editor-section="${this.section}"]`)?.focus(); }); + this.schedulePromptEditor(); } }, @@ -652,7 +685,8 @@ const model = { try { localStorage.setItem(LAST_SECTION_KEY, this.section); } catch {} - if (preview && this.section === "5") this.previewPlan(); + if (preview && this.section === "6") this.previewPlan(); + if (this.section === "2") this.schedulePromptEditor(); }, savedSection() { @@ -791,6 +825,7 @@ const model = { prompt.value = String(prompt.inherited || ""); prompt.reset = true; this.acceptPromptEdit(prompt.filename); + this.syncPromptEditor(); }, markPromptSet(filename) { @@ -830,6 +865,7 @@ const model = { if (!prompt || !baseline) return; prompt.value = baseline.value; prompt.reset = baseline.reset; + this.syncPromptEditor(); }, resetPrompt(filename) { @@ -838,6 +874,7 @@ const model = { prompt.value = prompt.inherited; prompt.reset = true; this.acceptPromptEdit(filename); + this.syncPromptEditor(); }, promptDisplayState(prompt) { @@ -847,25 +884,10 @@ const model = { return this.projectName ? "Inherited" : "Default"; }, - promptSourceChain(prompt) { - if (!prompt?.reset && (prompt?.has_override || this.promptDirty(prompt))) return "Customized by you"; - if (this.projectName) return "Inherited from Global"; - const source = [...(prompt?.source_chain || [])] - .filter((item) => item !== "Your override") - .at(-1); - const current = String( - this.state?.profile?.metadata?.title?.effective || this.state?.profile?.id || "", - ); - return !source || source.toLowerCase() === current.toLowerCase() - ? "Default" - : `Inherited from ${source}`; - }, - selectPrompt(filename) { if (!this.draft?.prompts?.[filename]) return; this.selectedPrompt = filename; - this.promptTextSearch = ""; - this.comparePrompt = ""; + this.schedulePromptEditor(); }, filteredPromptFiles(group = "") { @@ -880,26 +902,68 @@ const model = { return Boolean(prompt?.reset || prompt?.value !== prompt?.initialValue); }, - promptMatchCount() { - const query = this.promptTextSearch; - const text = this.selectedPromptDraft?.value || ""; - if (!query) return 0; - return text.toLowerCase().split(query.toLowerCase()).length - 1; + schedulePromptEditor() { + if (this.mode !== "advanced" || this.section !== "2") return; + requestAnimationFrame(() => requestAnimationFrame(() => this.initPromptEditor())); }, - findInPrompt(direction = 1) { - const textarea = this.root?.querySelector("#agent-editor-prompt-text"); - const query = this.promptTextSearch; - const text = this.selectedPromptDraft?.value || ""; - if (!textarea || !query) return; - const lower = text.toLowerCase(); - const needle = query.toLowerCase(); - const start = direction > 0 ? textarea.selectionEnd : Math.max(0, textarea.selectionStart - 1); - let index = direction > 0 ? lower.indexOf(needle, start) : lower.lastIndexOf(needle, start); - if (index < 0) index = direction > 0 ? lower.indexOf(needle) : lower.lastIndexOf(needle); - if (index < 0) return; - textarea.focus(); - textarea.setSelectionRange(index, index + query.length); + initPromptEditor() { + const container = this.root?.querySelector("#agent-editor-prompt-ace"); + if (!container) return; + if (this.promptEditor && !this.root?.contains?.(this.promptEditor.container)) { + this.destroyPromptEditor(); + } + if (this.promptEditor) { + this.syncPromptEditor(); + this.promptEditor.resize?.(true); + return; + } + if (!globalThis.ace?.edit) { + this.aceUnavailable = true; + return; + } + const editor = globalThis.ace.edit(container); + const darkMode = globalThis.localStorage?.getItem("darkMode"); + editor.setTheme(darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/github"); + editor.session.setMode("ace/mode/markdown"); + editor.session.setUseWrapMode(true); + editor.setOptions({ showPrintMargin: false, useWorker: false }); + editor.setValue(this.selectedPromptDraft?.value || "", -1); + this.promptEditorChangeHandler = () => { + if (this.settingPromptEditorValue || !this.selectedPromptDraft) return; + this.selectedPromptDraft.value = editor.getValue(); + this.onPromptInput(this.selectedPrompt); + }; + editor.session.on("change", this.promptEditorChangeHandler); + editor.textInput?.getElement?.()?.setAttribute("aria-label", "Prompt Markdown"); + this.promptEditor = editor; + this.aceUnavailable = false; + }, + + syncPromptEditor() { + if (!this.promptEditor) { + this.schedulePromptEditor(); + return; + } + const value = String(this.selectedPromptDraft?.value || ""); + if (this.promptEditor.getValue() !== value) { + this.settingPromptEditorValue = true; + this.promptEditor.setValue(value, -1); + this.settingPromptEditorValue = false; + } + this.promptEditor.resize?.(true); + }, + + destroyPromptEditor() { + if (this.promptEditor?.session && this.promptEditorChangeHandler) { + this.promptEditor.session.off?.("change", this.promptEditorChangeHandler); + } + const container = this.promptEditor?.container; + this.promptEditor?.destroy?.(); + if (container) container.textContent = ""; + this.promptEditor = null; + this.promptEditorChangeHandler = null; + this.settingPromptEditorValue = false; }, promptCustomizationPath() { @@ -914,55 +978,61 @@ const model = { globalThis.justToast?.("Path copied", "success", 1200, "agent-editor-copy"); }, - useStandardTools() { - this.draft.toolPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] }; - this.selectedAllowedTools = []; - this.selectedBlockedTools = []; - }, - customizePolicy(kind) { - const key = kind === "tool" ? "toolPolicy" : "skillPolicy"; + const isSkill = kind === "skill"; + const key = isSkill ? "skillPolicy" : "toolPolicy"; if (this.draft[key].mode === "custom") return; - const state = kind === "tool" ? this.state.tools : this.state.skills; - this.draft[key] = { mode: "custom", ...policyBehavior(state.effective_policy) }; + const state = isSkill ? this.state.skills : this.state.tools; + this.draft[key] = { + mode: "custom", + ...policyBehavior(state.effective_policy, !isSkill), + }; }, - chooseTools() { - this.customizePolicy("tool"); - this.setMode("advanced", "3"); + activePolicy(kind) { + const isSkill = kind === "skill"; + const key = isSkill ? "skillPolicy" : "toolPolicy"; + const state = isSkill ? this.state?.skills : this.state?.tools; + return this.draft?.[key]?.mode === "custom" ? this.draft[key] : state?.effective_policy; }, - setEasyToolAllowed(id, allow) { - this.customizePolicy("tool"); - this.moveTools([id], allow); - const policy = this.draft.toolPolicy; - if (this.initialDraft?.toolPolicy.mode !== "custom" - && same(policyBehavior(policy), policyBehavior(this.state.tools.effective_policy))) { - this.useStandardTools(); + policyItemState(kind, id) { + return policyItemState(this.activePolicy(kind), id); + }, + + setPolicyItem(kind, id, state) { + this.customizePolicy(kind); + const key = kind === "skill" ? "skillPolicy" : "toolPolicy"; + setPolicyItemState(this.draft[key], id, state); + this.collapsePolicy(kind); + }, + + collapsePolicy(kind) { + const isSkill = kind === "skill"; + const key = isSkill ? "skillPolicy" : "toolPolicy"; + const state = isSkill ? this.state.skills : this.state.tools; + if (this.initialDraft?.[key]?.mode !== "custom" + && same( + policyBehavior(this.draft[key], !isSkill), + policyBehavior(state.effective_policy, !isSkill), + )) { + this.draft[key] = clone(this.initialDraft[key]); } }, - useStandardSkills() { - this.draft.skillPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] }; - this.selectedAllowedSkills = []; - this.selectedBlockedSkills = []; - }, - - chooseSkills() { - this.customizePolicy("skill"); - }, - setPolicyDefault(kind, nextDefault) { - const policy = kind === "tool" ? this.draft.toolPolicy : this.draft.skillPolicy; - const catalog = kind === "tool" ? this.state.tools.catalog : this.state.skills.catalog; - const ids = catalog.map((item) => - kind === "tool" ? item.id : item.name, - ); - const current = new Map(ids.map((id) => [id, policyAllows(policy, id)])); - policy.default = nextDefault === "block" ? "block" : "allow"; - policy.allowed = []; - policy.blocked = []; - for (const [id, allowed] of current) movePolicyItem(policy, id, allowed); + this.customizePolicy(kind); + const key = kind === "skill" ? "skillPolicy" : "toolPolicy"; + const policy = this.draft[key]; + const field = kind === "mcp" ? "mcp_default" : "default"; + policy[field] = nextDefault === "block" ? "block" : "allow"; + this.collapsePolicy(kind); + }, + + policyDefault(kind) { + const policy = this.activePolicy(kind); + const field = kind === "mcp" ? "mcp_default" : "default"; + return policy?.mode === "custom" && policy[field] === "block" ? "block" : "allow"; }, isToolAllowed(item) { @@ -979,73 +1049,26 @@ const model = { return policyAllows(policy, item.name); }, - filteredTools(allowed) { + filteredTools(group = "tool") { const query = this.toolSearch.trim().toLowerCase(); return (this.state?.tools?.catalog || []).filter((item) => { - if (this.draft.toolPolicy.mode !== "custom" && item.available === false) return false; - if (this.isToolAllowed(item) !== allowed) return false; - const category = item.id.split(":", 1)[0]; - if (this.toolCategory !== "all" && category !== this.toolCategory) return false; + const isMcp = String(item.id || "").startsWith("mcp:"); + if ((group === "mcp") !== isMcp) return false; if (this.toolOrigin !== "all" && item.origin !== this.toolOrigin) return false; return !query || [item.label, item.name, item.id, item.description, item.origin] .join(" ").toLowerCase().includes(query); }); }, - moveTools(ids, allow) { - for (const id of unique(ids)) movePolicyItem(this.draft.toolPolicy, id, allow); - this.selectedAllowedTools = []; - this.selectedBlockedTools = []; - }, - - moveAllVisibleTools(allow) { - return this.confirmBulkMove( - "tool", - this.filteredTools(!allow).map((item) => item.id), - allow, - ); - }, - - filteredSkills(allowed) { + filteredSkills() { const query = this.skillSearch.trim().toLowerCase(); return (this.state?.skills?.catalog || []).filter((item) => { - if (this.draft.skillPolicy.mode !== "custom" && item.available === false) return false; - if (this.isSkillAllowed(item) !== allowed) return false; if (this.skillOrigin !== "all" && item.origin !== this.skillOrigin) return false; return !query || [item.name, item.description, item.origin, ...(item.tags || [])] .join(" ").toLowerCase().includes(query); }); }, - moveSkills(ids, allow) { - for (const id of unique(ids)) movePolicyItem(this.draft.skillPolicy, id, allow); - this.selectedAllowedSkills = []; - this.selectedBlockedSkills = []; - }, - - moveAllVisibleSkills(allow) { - return this.confirmBulkMove( - "skill", - this.filteredSkills(!allow).map((item) => item.name), - allow, - ); - }, - - async confirmBulkMove(kind, ids, allow) { - if (!ids.length) return; - const action = allow ? "Allow" : "Block"; - const plural = `${kind}${ids.length === 1 ? "" : "s"}`; - const confirmed = await showConfirmDialog({ - title: `${action} ${ids.length} shown ${plural}?`, - message: `

This changes every ${kind} currently shown by your filters.

`, - confirmText: `${action} shown ${plural}`, - type: "warning", - }); - if (!confirmed) return; - if (kind === "tool") this.moveTools(ids, allow); - else this.moveSkills(ids, allow); - }, - skillWarnings(skill) { const warnings = []; for (const toolName of skill.allowed_tools || []) { @@ -1090,7 +1113,10 @@ const model = { const message = this.draft.creating ? "Instructions are required for a new agent." : `Instructions can’t be empty. Use ${fallback} instructions instead.`; - issues.push({ key: "instructions", section: "2", field: "agent-editor-prompt-text", label: "Instructions", message }); + const field = this.mode === "advanced" + ? "agent-editor-prompt-ace" + : "agent-editor-instructions"; + issues.push({ key: "instructions", section: "2", field, label: "Instructions", message }); } if (this.avatarUploading) { issues.push({ key: "avatar", section: "1", field: "agent-editor-advanced-name", label: "Avatar", message: "Wait for the avatar upload to finish." }); @@ -1296,7 +1322,7 @@ const model = { this.plan = data; this.planStatus = "ready"; this.pendingMutation = { destructive }; - this.setMode("advanced", "5", false); + this.setMode("advanced", "6", false); } catch (error) { this.error = error.message || String(error); } finally { diff --git a/plugins/_agent_editor/webui/main.html b/plugins/_agent_editor/webui/main.html index 1cda936f1..1a3e91d7c 100644 --- a/plugins/_agent_editor/webui/main.html +++ b/plugins/_agent_editor/webui/main.html @@ -50,13 +50,13 @@ - +
-