mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-08-18 21:05:32 +00:00
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.
This commit is contained in:
parent
9dab4f2460
commit
13ecab24ee
12 changed files with 621 additions and 397 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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/<profile-id>` and apply
|
||||
across projects. Project-scoped agents and customizations live under
|
||||
`usr/projects/<project>/.a0proj/agents/<profile-id>`, 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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<p>This changes every ${kind} currently shown by your filters.</p>`,
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -50,13 +50,13 @@
|
|||
</span>
|
||||
<strong x-text="$store.agentEditor.activeProfile().title || $store.agentEditor.activeProfile().id"></strong>
|
||||
</span>
|
||||
<button type="button" class="button icon-button" title="Edit" :aria-label="`Edit ${$store.agentEditor.activeProfile().title || $store.agentEditor.activeProfile().id}`" @click="$store.agentEditor.loadEditor($store.agentEditor.activeProfile().id, false)"><x-icon class="icon" name="edit"></x-icon></button>
|
||||
<button type="button" class="button icon-button" x-show="$store.agentEditor.activeProfile().id !== 'default'" title="Edit" :aria-label="`Edit ${$store.agentEditor.activeProfile().title || $store.agentEditor.activeProfile().id}`" @click="$store.agentEditor.loadEditor($store.agentEditor.activeProfile().id, false)"><x-icon class="icon" name="edit"></x-icon></button>
|
||||
</div>
|
||||
</template>
|
||||
<button type="button" class="button agent-manager-create" @click="$store.agentEditor.loadEditor('new-agent', true)"><x-icon class="icon" name="add"></x-icon>Create agent</button>
|
||||
</div>
|
||||
<div class="agent-manager-list">
|
||||
<template x-for="profile in $store.agentEditor.profiles" :key="profile.id">
|
||||
<template x-for="profile in $store.agentEditor.visibleProfiles" :key="profile.id">
|
||||
<article class="agent-manager-card">
|
||||
<div class="agent-manager-avatar" :style="`background:${$store.agentEditor.profileVisual(profile).color}`">
|
||||
<img x-show="$store.agentEditor.profileVisual(profile).url" :src="$store.agentEditor.profileVisual(profile).url" :alt="`${profile.title || profile.id} avatar`">
|
||||
|
|
@ -144,12 +144,26 @@
|
|||
<button type="button" class="text-button" x-show="$store.agentEditor.state.profile.metadata.avatar.has_override || $store.agentEditor.draft.avatar" @click="$store.agentEditor.resetAvatar()">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-field agent-name-field">
|
||||
<label for="agent-editor-name" class="agent-field-label">Agent name</label>
|
||||
<input id="agent-editor-name" type="text" x-model="$store.agentEditor.draft.title" @input="$store.agentEditor.onNameInput(); $store.agentEditor.markMetadataSet('title')" required autocomplete="off" :aria-invalid="$store.agentEditor.fieldIssue('name') ? 'true' : null" :aria-describedby="$store.agentEditor.fieldIssue('name') ? 'agent-editor-name-error' : null">
|
||||
<div class="agent-id-feedback" x-show="$store.agentEditor.fieldIssue('name')">
|
||||
<span id="agent-editor-name-error" class="field-error" role="alert" x-text="$store.agentEditor.fieldIssue('name')?.message"></span>
|
||||
<button type="button" class="text-button" x-show="$store.agentEditor.profileConflict" @click="$store.agentEditor.openConflictingProfile()">Open existing agent</button>
|
||||
<div class="agent-easy-identity-fields">
|
||||
<div class="agent-field agent-name-field">
|
||||
<label for="agent-editor-name" class="agent-field-label">Agent name</label>
|
||||
<input id="agent-editor-name" type="text" x-model="$store.agentEditor.draft.title" @input="$store.agentEditor.onNameInput(); $store.agentEditor.markMetadataSet('title')" required autocomplete="off" :aria-invalid="$store.agentEditor.fieldIssue('name') ? 'true' : null" :aria-describedby="$store.agentEditor.fieldIssue('name') ? 'agent-editor-name-error' : null">
|
||||
<div class="agent-id-feedback" x-show="$store.agentEditor.fieldIssue('name')">
|
||||
<span id="agent-editor-name-error" class="field-error" role="alert" x-text="$store.agentEditor.fieldIssue('name')?.message"></span>
|
||||
<button type="button" class="text-button" x-show="$store.agentEditor.profileConflict" @click="$store.agentEditor.openConflictingProfile()">Open existing agent</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-field agent-model-preset">
|
||||
<label for="agent-editor-easy-model-preset" class="agent-field-label">Model preset</label>
|
||||
<div class="agent-model-preset-picker">
|
||||
<select id="agent-editor-easy-model-preset" x-model="$store.agentEditor.draft.modelPreset">
|
||||
<option value="" x-text="`Use current preset (${$store.agentEditor.state.model_preset.effective})`"></option>
|
||||
<template x-for="preset in $store.agentEditor.state.model_presets" :key="preset.name">
|
||||
<option :value="preset.name" x-text="preset.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button type="button" class="button" @click="$store.agentEditor.openPresetManager()"><x-icon class="icon" name="tune"></x-icon>Edit Presets</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -165,23 +179,52 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="agent-easy-field agent-easy-tools">
|
||||
<div class="agent-field-heading">
|
||||
<div><div class="agent-field-label">Tools <span class="field-count" x-text="`— ${$store.agentEditor.toolCatalog.length} ${$store.agentEditor.toolCatalog.length === 1 ? 'tool' : 'tools'}`"></span></div><p>Choose which tools this agent can use.</p></div>
|
||||
<section class="agent-easy-field agent-easy-capabilities">
|
||||
<div class="agent-field-heading"><div><div class="agent-field-label">Capabilities</div><p>Choose the default access, or set individual items On or Off.</p></div></div>
|
||||
<div class="capability-policy-group">
|
||||
<div class="future-policy-control"><strong>Allow tools by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('tool') === 'allow'" @change="$store.agentEditor.setPolicyDefault('tool', $event.target.checked ? 'allow' : 'block')" aria-label="Allow tools by default"><span class="toggler"></span></label></div>
|
||||
<details class="capability-accordion">
|
||||
<summary><span>Choose individual tools</span><x-icon name="expand_more"></x-icon></summary>
|
||||
<div class="policy-items" role="list" aria-label="Tools this agent can use">
|
||||
<template x-for="tool in $store.agentEditor.standardToolCatalog" :key="tool.id">
|
||||
<div class="policy-item" role="listitem"><div class="policy-item-copy"><strong x-text="tool.label"></strong><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p></div><div class="policy-state-control" role="group" :aria-label="`${tool.label} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'allow'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'default'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('tool') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'block'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'block')">Off</button></div></div>
|
||||
</template>
|
||||
<p class="policy-empty" x-show="!$store.agentEditor.standardToolCatalog.length">No configurable tools are available.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="easy-tool-list" role="list" aria-label="Tools this agent can use">
|
||||
<template x-for="tool in $store.agentEditor.toolCatalog" :key="tool.id">
|
||||
<label class="policy-item" role="listitem"><input type="checkbox" :checked="$store.agentEditor.isToolAllowed(tool)" :aria-label="`Allow ${tool.label}`" @change="$store.agentEditor.setEasyToolAllowed(tool.id, $event.target.checked)"><div><strong x-text="tool.label"></strong><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p></div></label>
|
||||
</template>
|
||||
<p class="policy-empty" x-show="!$store.agentEditor.toolCatalog.length">No configurable tools are available.</p>
|
||||
|
||||
<div class="capability-policy-group">
|
||||
<div class="future-policy-control"><strong>Allow MCPs by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('mcp') === 'allow'" @change="$store.agentEditor.setPolicyDefault('mcp', $event.target.checked ? 'allow' : 'block')" aria-label="Allow MCPs by default"><span class="toggler"></span></label></div>
|
||||
<details class="capability-accordion">
|
||||
<summary><span>Choose individual MCPs</span><x-icon name="expand_more"></x-icon></summary>
|
||||
<div class="policy-items" role="list" aria-label="MCP tools this agent can use">
|
||||
<template x-for="tool in $store.agentEditor.mcpCatalog" :key="tool.id">
|
||||
<div class="policy-item" role="listitem"><div class="policy-item-copy"><strong x-text="tool.label"></strong><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p></div><div class="policy-state-control" role="group" :aria-label="`${tool.label} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'allow'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'default'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('mcp') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'block'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'block')">Off</button></div></div>
|
||||
</template>
|
||||
<p class="policy-empty" x-show="!$store.agentEditor.mcpCatalog.length">No MCP tools are available.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="capability-policy-group">
|
||||
<div class="future-policy-control"><strong>Allow skills by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('skill') === 'allow'" @change="$store.agentEditor.setPolicyDefault('skill', $event.target.checked ? 'allow' : 'block')" aria-label="Allow skills by default"><span class="toggler"></span></label></div>
|
||||
<details class="capability-accordion">
|
||||
<summary><span>Choose individual skills</span><x-icon name="expand_more"></x-icon></summary>
|
||||
<div class="policy-items" role="list" aria-label="Skills this agent can use">
|
||||
<template x-for="skill in $store.agentEditor.skillCatalog" :key="skill.path">
|
||||
<div class="policy-item" role="listitem"><div class="policy-item-copy"><strong x-text="skill.name"></strong><p class="policy-item-description" x-show="skill.description" x-text="skill.description"></p><em x-show="$store.agentEditor.skillWarnings(skill).length" x-text="`Expects blocked tool: ${$store.agentEditor.skillWarnings(skill).join(', ')}`"></em></div><div class="policy-state-control" role="group" :aria-label="`${skill.name} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'allow'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'default'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('skill') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'block'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'block')">Off</button></div></div>
|
||||
</template>
|
||||
<p class="policy-empty" x-show="!$store.agentEditor.skillCatalog.length">No configurable skills are available.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<p class="easy-skills-hint">To enable or disable skills, click Advanced.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="agent-advanced" x-show="$store.agentEditor.mode === 'advanced'">
|
||||
<nav class="agent-advanced-nav" aria-label="Advanced editor sections">
|
||||
<template x-for="item in [{id:'1',label:'Identity'},{id:'2',label:'Prompt files'},{id:'3',label:'Tools'},{id:'4',label:'Skills'},{id:'5',label:'Review'}]" :key="item.id">
|
||||
<template x-for="item in [{id:'1',label:'Identity'},{id:'2',label:'Prompt files'},{id:'3',label:'Tools'},{id:'4',label:'MCPs'},{id:'5',label:'Skills'},{id:'6',label:'Review'}]" :key="item.id">
|
||||
<button type="button" :class="{ active: $store.agentEditor.section === item.id }" :aria-current="$store.agentEditor.section === item.id ? 'step' : null" @click="$store.agentEditor.setSection(item.id)">
|
||||
<span class="section-number" x-text="item.id"></span><span x-text="item.label"></span>
|
||||
<span class="agent-status-badge compact" :class="$store.agentEditor.sectionIssues(item.id).length ? 'is-error' : 'is-unsaved'" x-show="$store.agentEditor.sectionIssues(item.id).length || $store.agentEditor.sectionDirty(item.id)"><x-icon :name="$store.agentEditor.sectionIssues(item.id).length ? 'error' : 'edit_note'"></x-icon><span x-text="$store.agentEditor.sectionIssues(item.id).length ? 'Needs attention' : 'Changed'"></span></span>
|
||||
|
|
@ -232,7 +275,7 @@
|
|||
</section>
|
||||
|
||||
<section x-show="$store.agentEditor.section === '2'" data-agent-editor-section="2" tabindex="-1" aria-labelledby="agent-section-2-title">
|
||||
<header class="advanced-section-heading"><h3 id="agent-section-2-title">Prompt files</h3><p>Customize this agent’s prompt files. You always see the inherited version next to your version.</p></header>
|
||||
<header class="advanced-section-heading"><h3 id="agent-section-2-title">Prompt files</h3><p>Choose a file and edit its prompt.</p></header>
|
||||
<div class="prompt-workspace">
|
||||
<aside class="prompt-browser">
|
||||
<label class="compact-search"><span class="sr-only">Search prompt files</span><x-icon name="search"></x-icon><input type="search" x-model="$store.agentEditor.promptFileSearch" placeholder="Search files"></label>
|
||||
|
|
@ -252,61 +295,47 @@
|
|||
</aside>
|
||||
<div class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0" x-show="$store.agentEditor.selectedPromptDraft">
|
||||
<div class="prompt-editor-header">
|
||||
<div><strong x-text="$store.agentEditor.selectedPrompt"></strong><div class="source-chain" x-text="$store.agentEditor.promptSourceChain($store.agentEditor.selectedPromptDraft)"></div></div>
|
||||
<div class="prompt-file-heading">
|
||||
<strong x-text="$store.agentEditor.selectedPrompt"></strong>
|
||||
<div class="prompt-customization-path"><code x-text="$store.agentEditor.promptCustomizationPath()"></code><button type="button" class="button" title="Copy customization path" aria-label="Copy customization path" @click="$store.agentEditor.copyPromptPath()"><x-icon name="content_copy"></x-icon></button></div>
|
||||
</div>
|
||||
<div class="prompt-actions">
|
||||
<button type="button" class="btn btn-action-header cancel" x-show="$store.agentEditor.promptEditPending($store.agentEditor.selectedPromptDraft)" title="Discard current edit" aria-label="Discard current edit" @click="$store.agentEditor.discardPromptEdit($store.agentEditor.selectedPrompt)"><x-icon name="close"></x-icon></button>
|
||||
<button type="button" class="btn btn-action-header confirm" x-show="$store.agentEditor.promptEditPending($store.agentEditor.selectedPromptDraft)" title="Accept current edit" aria-label="Accept current edit" @click="$store.agentEditor.acceptPromptEdit($store.agentEditor.selectedPrompt)"><x-icon name="check"></x-icon></button>
|
||||
<button type="button" class="button" x-show="$store.agentEditor.selectedPromptDraft.has_override || $store.agentEditor.promptDirty($store.agentEditor.selectedPromptDraft)" @click="$store.agentEditor.resetPrompt($store.agentEditor.selectedPrompt)">Reset to default</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-customization-path"><code x-text="$store.agentEditor.promptCustomizationPath()"></code><button type="button" class="button" title="Copy customization path" aria-label="Copy customization path" @click="$store.agentEditor.copyPromptPath()"><x-icon name="content_copy"></x-icon></button></div>
|
||||
<div class="agent-editor-note" x-show="$store.agentEditor.selectedPromptDraft.dynamic_processor">This file is generated dynamically at runtime. Python processors are read-only and are not executed by the editor.</div>
|
||||
<div class="prompt-view-tabs" role="tablist" aria-label="Prompt view">
|
||||
<button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === ''" :class="{ active: $store.agentEditor.comparePrompt === '' }" @click="$store.agentEditor.comparePrompt = ''">Your version</button>
|
||||
<button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === 'inherited'" :class="{ active: $store.agentEditor.comparePrompt === 'inherited' }" @click="$store.agentEditor.comparePrompt = 'inherited'" x-text="$store.agentEditor.inheritedLabel"></button>
|
||||
<button type="button" role="tab" :aria-selected="$store.agentEditor.comparePrompt === 'compare'" :class="{ active: $store.agentEditor.comparePrompt === 'compare' }" @click="$store.agentEditor.comparePrompt = 'compare'">Compare</button>
|
||||
</div>
|
||||
<div class="prompt-find"><label><x-icon name="search"></x-icon><span class="sr-only">Search within prompt</span><input type="search" x-model="$store.agentEditor.promptTextSearch" placeholder="Find in file" @keydown.enter.prevent="$store.agentEditor.findInPrompt($event.shiftKey ? -1 : 1)"></label><span x-text="`${$store.agentEditor.promptMatchCount()} matches`"></span><button type="button" class="button icon prompt-match-action" aria-label="Previous match" @click="$store.agentEditor.findInPrompt(-1)"><x-icon name="keyboard_arrow_up"></x-icon></button><button type="button" class="button icon prompt-match-action" aria-label="Next match" @click="$store.agentEditor.findInPrompt(1)"><x-icon name="keyboard_arrow_down"></x-icon></button></div>
|
||||
<div class="prompt-panes" :class="{ compare: $store.agentEditor.comparePrompt === 'compare' }">
|
||||
<div class="prompt-pane" x-show="$store.agentEditor.comparePrompt !== 'inherited'"><label for="agent-editor-prompt-text">Your version</label><textarea id="agent-editor-prompt-text" spellcheck="false" x-model="$store.agentEditor.selectedPromptDraft.value" @input="$store.agentEditor.onPromptInput($store.agentEditor.selectedPrompt)" aria-label="Prompt Markdown" :aria-invalid="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions') ? 'true' : null" :aria-describedby="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions') ? 'agent-editor-prompt-error' : null"></textarea><span id="agent-editor-prompt-error" class="field-error" role="alert" x-show="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions')" x-text="$store.agentEditor.fieldIssue('instructions')?.message"></span></div>
|
||||
<div class="prompt-pane inherited" x-show="$store.agentEditor.comparePrompt"><div class="prompt-pane-title" x-text="$store.agentEditor.inheritedLabel"></div><pre x-text="$store.agentEditor.selectedPromptDraft.inherited || '(empty)'" tabindex="0"></pre></div>
|
||||
</div>
|
||||
<div id="agent-editor-prompt-ace" class="prompt-ace" :aria-invalid="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions') ? 'true' : null" :aria-describedby="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions') ? 'agent-editor-prompt-error' : null"></div>
|
||||
<div class="agent-editor-note" role="alert" x-show="$store.agentEditor.aceUnavailable">Prompt editor is unavailable. Reload Agent Zero and try again.</div>
|
||||
<span id="agent-editor-prompt-error" class="field-error" role="alert" x-show="$store.agentEditor.selectedPrompt === 'agent.system.main.specifics.md' && $store.agentEditor.fieldIssue('instructions')" x-text="$store.agentEditor.fieldIssue('instructions')?.message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-show="$store.agentEditor.section === '3'" data-agent-editor-section="3" tabindex="-1" aria-labelledby="agent-section-3-title">
|
||||
<header class="advanced-section-heading"><h3 id="agent-section-3-title">Tools</h3><p>Choose which tools this agent can use.</p></header>
|
||||
<div class="policy-mode-row"><label><input type="radio" name="tool-policy-mode" :checked="$store.agentEditor.draft.toolPolicy.mode === 'inherit'" @change="$store.agentEditor.useStandardTools()"><span><span x-text="$store.agentEditor.projectName ? 'Use inherited tool access' : 'Use standard tool access'"></span> <small x-text="`(${$store.agentEditor.toolCatalog.length} ${$store.agentEditor.toolCatalog.length === 1 ? 'tool' : 'tools'})`"></small></span></label><label><input type="radio" name="tool-policy-mode" :checked="$store.agentEditor.draft.toolPolicy.mode === 'custom'" @change="$store.agentEditor.chooseTools()">Choose tools</label></div>
|
||||
<fieldset class="policy-editor" :disabled="$store.agentEditor.draft.toolPolicy.mode !== 'custom'">
|
||||
<legend class="sr-only">Tool access selection</legend>
|
||||
<div class="policy-filters tools"><label class="policy-search"><x-icon name="search"></x-icon><span class="sr-only">Search tools</span><input type="search" x-model="$store.agentEditor.toolSearch" placeholder="Search tools"></label><label>Category <select x-model="$store.agentEditor.toolCategory"><option value="all">All</option><option value="local">Local</option><option value="plugin">Plugin</option><option value="mcp">MCP</option></select></label><label>Origin <select x-model="$store.agentEditor.toolOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.toolOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
|
||||
<div class="policy-lists">
|
||||
<section class="policy-list" aria-labelledby="allowed-tools-title"><header><div><h4 id="allowed-tools-title">Allowed</h4><span x-text="`${$store.agentEditor.filteredTools(true).length} ${$store.agentEditor.filteredTools(true).length === 1 ? 'tool' : 'tools'}`"></span></div><button type="button" class="button policy-bulk" x-show="$store.agentEditor.draft.toolPolicy.mode === 'custom' && $store.agentEditor.filteredTools(true).length" @click="$store.agentEditor.moveAllVisibleTools(false)" x-text="`Block ${$store.agentEditor.filteredTools(true).length} shown`"></button></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredTools(true).length">No allowed tools — select items on the right, or allow new tools automatically.</p><template x-for="tool in $store.agentEditor.filteredTools(true)" :key="tool.id"><label class="policy-item"><input type="checkbox" :value="tool.id" x-model="$store.agentEditor.selectedAllowedTools" :aria-label="`Select ${tool.label}`"><div><strong x-text="tool.label"></strong><small x-text="tool.id"></small><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p><em x-show="!tool.available">Unavailable — kept in your settings</em></div></label></template></div></section>
|
||||
<div class="policy-transfer-actions" role="group" aria-label="Move selected tools"><button type="button" class="button icon" :disabled="!$store.agentEditor.selectedAllowedTools.length" aria-label="Block selected tools" @click="$store.agentEditor.moveTools($store.agentEditor.selectedAllowedTools, false)"><x-icon name="arrow_forward"></x-icon></button><button type="button" class="button icon" :disabled="!$store.agentEditor.selectedBlockedTools.length" aria-label="Allow selected tools" @click="$store.agentEditor.moveTools($store.agentEditor.selectedBlockedTools, true)"><x-icon name="arrow_back"></x-icon></button></div>
|
||||
<section class="policy-list" aria-labelledby="blocked-tools-title"><header><div><h4 id="blocked-tools-title">Blocked</h4><span x-text="`${$store.agentEditor.filteredTools(false).length} ${$store.agentEditor.filteredTools(false).length === 1 ? 'tool' : 'tools'}`"></span></div><button type="button" class="button policy-bulk" x-show="$store.agentEditor.draft.toolPolicy.mode === 'custom' && $store.agentEditor.filteredTools(false).length" @click="$store.agentEditor.moveAllVisibleTools(true)" x-text="`Allow ${$store.agentEditor.filteredTools(false).length} shown`"></button></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredTools(false).length" x-text="$store.agentEditor.draft.toolPolicy.mode === 'custom' ? 'No blocked tools — select items on the left, or block new tools until reviewed.' : 'Choose tools to customize standard access.'"></p><template x-for="tool in $store.agentEditor.filteredTools(false)" :key="tool.id"><label class="policy-item"><input type="checkbox" :value="tool.id" x-model="$store.agentEditor.selectedBlockedTools" :aria-label="`Select ${tool.label}`"><div><strong x-text="tool.label"></strong><small x-text="tool.id"></small><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p><em x-show="!tool.available">Unavailable — kept in your settings</em></div></label></template></div></section>
|
||||
</div>
|
||||
<fieldset class="future-default"><legend>When new tools are installed later</legend><label><input type="radio" name="tool-future-default" value="allow" :checked="$store.agentEditor.draft.toolPolicy.default === 'allow'" @change="$store.agentEditor.setPolicyDefault('tool','allow')">Allow automatically</label><label><input type="radio" name="tool-future-default" value="block" :checked="$store.agentEditor.draft.toolPolicy.default === 'block'" @change="$store.agentEditor.setPolicyDefault('tool','block')">Block until reviewed</label></fieldset>
|
||||
</fieldset>
|
||||
<div class="future-policy-control"><strong>Allow tools by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('tool') === 'allow'" @change="$store.agentEditor.setPolicyDefault('tool', $event.target.checked ? 'allow' : 'block')" aria-label="Allow tools by default"><span class="toggler"></span></label></div>
|
||||
<div class="policy-filters tools"><label class="policy-search"><x-icon name="search"></x-icon><span class="sr-only">Search tools</span><input type="search" x-model="$store.agentEditor.toolSearch" placeholder="Search tools"></label><label>Origin <select x-model="$store.agentEditor.toolOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.toolOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
|
||||
<section class="policy-group" aria-labelledby="tools-title"><header><h4 id="tools-title">Tools</h4><span x-text="$store.agentEditor.filteredTools('tool').length"></span></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredTools('tool').length">No tools match these filters.</p><template x-for="tool in $store.agentEditor.filteredTools('tool')" :key="tool.id"><div class="policy-item"><div class="policy-item-copy"><strong x-text="tool.label"></strong><small x-text="tool.id"></small><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p><em x-show="tool.available === false">Unavailable — kept in your settings</em></div><div class="policy-state-control" role="group" :aria-label="`${tool.label} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'allow'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'default'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('tool') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('tool', tool.id) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('tool', tool.id) === 'block'" @click="$store.agentEditor.setPolicyItem('tool', tool.id, 'block')">Off</button></div></div></template></div></section>
|
||||
</section>
|
||||
|
||||
<section x-show="$store.agentEditor.section === '4'" data-agent-editor-section="4" tabindex="-1" aria-labelledby="agent-section-4-title">
|
||||
<header class="advanced-section-heading"><h3 id="agent-section-4-title">Skills</h3><p>Choose which skills this agent can find and use.</p></header>
|
||||
<div class="policy-mode-row"><label><input type="radio" name="skill-policy-mode" :checked="$store.agentEditor.draft.skillPolicy.mode === 'inherit'" @change="$store.agentEditor.useStandardSkills()"><span><span x-text="$store.agentEditor.projectName ? 'Use inherited skill access' : 'Use standard skill access'"></span> <small x-text="`(${$store.agentEditor.skillCatalog.length} ${$store.agentEditor.skillCatalog.length === 1 ? 'skill' : 'skills'})`"></small></span></label><label><input type="radio" name="skill-policy-mode" :checked="$store.agentEditor.draft.skillPolicy.mode === 'custom'" @change="$store.agentEditor.chooseSkills()">Choose skills</label></div>
|
||||
<fieldset class="policy-editor" :disabled="$store.agentEditor.draft.skillPolicy.mode !== 'custom'">
|
||||
<legend class="sr-only">Skill access selection</legend>
|
||||
<div class="policy-filters skills"><label class="policy-search"><x-icon name="search"></x-icon><span class="sr-only">Search skills</span><input type="search" x-model="$store.agentEditor.skillSearch" placeholder="Search skills"></label><label>Origin <select x-model="$store.agentEditor.skillOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.skillOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
|
||||
<div class="policy-lists">
|
||||
<section class="policy-list" aria-labelledby="allowed-skills-title"><header><div><h4 id="allowed-skills-title">Allowed</h4><span x-text="`${$store.agentEditor.filteredSkills(true).length} ${$store.agentEditor.filteredSkills(true).length === 1 ? 'skill' : 'skills'}`"></span></div><button type="button" class="button policy-bulk" x-show="$store.agentEditor.draft.skillPolicy.mode === 'custom' && $store.agentEditor.filteredSkills(true).length" @click="$store.agentEditor.moveAllVisibleSkills(false)" x-text="`Block ${$store.agentEditor.filteredSkills(true).length} shown`"></button></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredSkills(true).length">No allowed skills — select items on the right, or allow new skills automatically.</p><template x-for="skill in $store.agentEditor.filteredSkills(true)" :key="skill.path"><label class="policy-item"><input type="checkbox" :value="skill.name" x-model="$store.agentEditor.selectedAllowedSkills" :aria-label="`Select ${skill.name}`"><div><strong x-text="skill.name"></strong><small x-text="skill.origin"></small><p class="policy-item-description" x-show="skill.description" x-text="skill.description"></p><em x-show="skill.available === false">Unavailable — kept in your settings</em><em x-show="$store.agentEditor.skillWarnings(skill).length" x-text="`Expects blocked tool: ${$store.agentEditor.skillWarnings(skill).join(', ')}`"></em></div></label></template></div></section>
|
||||
<div class="policy-transfer-actions" role="group" aria-label="Move selected skills"><button type="button" class="button icon" :disabled="!$store.agentEditor.selectedAllowedSkills.length" aria-label="Block selected skills" @click="$store.agentEditor.moveSkills($store.agentEditor.selectedAllowedSkills, false)"><x-icon name="arrow_forward"></x-icon></button><button type="button" class="button icon" :disabled="!$store.agentEditor.selectedBlockedSkills.length" aria-label="Allow selected skills" @click="$store.agentEditor.moveSkills($store.agentEditor.selectedBlockedSkills, true)"><x-icon name="arrow_back"></x-icon></button></div>
|
||||
<section class="policy-list" aria-labelledby="blocked-skills-title"><header><div><h4 id="blocked-skills-title">Blocked</h4><span x-text="`${$store.agentEditor.filteredSkills(false).length} ${$store.agentEditor.filteredSkills(false).length === 1 ? 'skill' : 'skills'}`"></span></div><button type="button" class="button policy-bulk" x-show="$store.agentEditor.draft.skillPolicy.mode === 'custom' && $store.agentEditor.filteredSkills(false).length" @click="$store.agentEditor.moveAllVisibleSkills(true)" x-text="`Allow ${$store.agentEditor.filteredSkills(false).length} shown`"></button></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredSkills(false).length" x-text="$store.agentEditor.draft.skillPolicy.mode === 'custom' ? 'No blocked skills — select items on the left, or block new skills until reviewed.' : 'Choose skills to customize standard access.'"></p><template x-for="skill in $store.agentEditor.filteredSkills(false)" :key="skill.path"><label class="policy-item"><input type="checkbox" :value="skill.name" x-model="$store.agentEditor.selectedBlockedSkills" :aria-label="`Select ${skill.name}`"><div><strong x-text="skill.name"></strong><small x-text="skill.origin"></small><p class="policy-item-description" x-show="skill.description" x-text="skill.description"></p><em x-show="skill.available === false">Unavailable — kept in your settings</em></div></label></template></div></section>
|
||||
</div>
|
||||
<fieldset class="future-default"><legend>When new skills are installed later</legend><label><input type="radio" name="skill-future-default" :checked="$store.agentEditor.draft.skillPolicy.default === 'allow'" @change="$store.agentEditor.setPolicyDefault('skill','allow')">Allow automatically</label><label><input type="radio" name="skill-future-default" :checked="$store.agentEditor.draft.skillPolicy.default === 'block'" @change="$store.agentEditor.setPolicyDefault('skill','block')">Block until reviewed</label></fieldset>
|
||||
</fieldset>
|
||||
<header class="advanced-section-heading"><h3 id="agent-section-4-title">MCPs</h3><p>Choose which MCP tools this agent can use.</p></header>
|
||||
<div class="future-policy-control"><strong>Allow MCPs by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('mcp') === 'allow'" @change="$store.agentEditor.setPolicyDefault('mcp', $event.target.checked ? 'allow' : 'block')" aria-label="Allow MCPs by default"><span class="toggler"></span></label></div>
|
||||
<div class="policy-filters tools"><label class="policy-search"><x-icon name="search"></x-icon><span class="sr-only">Search MCPs</span><input type="search" x-model="$store.agentEditor.toolSearch" placeholder="Search MCPs"></label><label>Origin <select x-model="$store.agentEditor.toolOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.toolOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
|
||||
<section class="policy-group" aria-labelledby="mcps-title"><header><h4 id="mcps-title">MCPs</h4><span x-text="$store.agentEditor.filteredTools('mcp').length"></span></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredTools('mcp').length">No MCP tools match these filters.</p><template x-for="tool in $store.agentEditor.filteredTools('mcp')" :key="tool.id"><div class="policy-item"><div class="policy-item-copy"><strong x-text="tool.label"></strong><small x-text="tool.id"></small><p class="policy-item-description" x-show="tool.description" x-text="tool.description"></p><em x-show="tool.available === false">Unavailable — kept in your settings</em></div><div class="policy-state-control" role="group" :aria-label="`${tool.label} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'allow'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'default'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('mcp') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('mcp', tool.id) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('mcp', tool.id) === 'block'" @click="$store.agentEditor.setPolicyItem('mcp', tool.id, 'block')">Off</button></div></div></template></div></section>
|
||||
</section>
|
||||
|
||||
<section x-show="$store.agentEditor.section === '5'" data-agent-editor-section="5" tabindex="-1" aria-labelledby="agent-section-5-title">
|
||||
<header class="advanced-section-heading"><h3 id="agent-section-5-title">Review</h3><p>Saving will change exactly these files — nothing else.</p></header>
|
||||
<header class="advanced-section-heading"><h3 id="agent-section-5-title">Skills</h3><p>Choose which skills this agent can find and use.</p></header>
|
||||
<div class="future-policy-control"><strong>Allow skills by default</strong><label class="toggle"><input type="checkbox" :checked="$store.agentEditor.policyDefault('skill') === 'allow'" @change="$store.agentEditor.setPolicyDefault('skill', $event.target.checked ? 'allow' : 'block')" aria-label="Allow skills by default"><span class="toggler"></span></label></div>
|
||||
<div class="policy-filters skills"><label class="policy-search"><x-icon name="search"></x-icon><span class="sr-only">Search skills</span><input type="search" x-model="$store.agentEditor.skillSearch" placeholder="Search skills"></label><label>Origin <select x-model="$store.agentEditor.skillOrigin"><option value="all">All</option><template x-for="origin in $store.agentEditor.skillOrigins" :key="origin"><option :value="origin" x-text="origin"></option></template></select></label></div>
|
||||
<section class="policy-group" aria-labelledby="skills-title"><header><h4 id="skills-title">Skills</h4><span x-text="$store.agentEditor.filteredSkills().length"></span></header><div class="policy-items"><p class="policy-empty" x-show="!$store.agentEditor.filteredSkills().length">No skills match these filters.</p><template x-for="skill in $store.agentEditor.filteredSkills()" :key="skill.path"><div class="policy-item"><div class="policy-item-copy"><strong x-text="skill.name"></strong><small x-text="skill.origin"></small><p class="policy-item-description" x-show="skill.description" x-text="skill.description"></p><em x-show="skill.available === false">Unavailable — kept in your settings</em><em x-show="$store.agentEditor.skillWarnings(skill).length" x-text="`Expects blocked tool: ${$store.agentEditor.skillWarnings(skill).join(', ')}`"></em></div><div class="policy-state-control" role="group" :aria-label="`${skill.name} access`"><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'allow' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'allow'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'allow')">On</button><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'default' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'default'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'default')" x-text="`Default (${$store.agentEditor.policyDefault('skill') === 'allow' ? 'on' : 'off'})`"></button><button type="button" :class="{ active: $store.agentEditor.policyItemState('skill', skill.name) === 'block' }" :aria-pressed="$store.agentEditor.policyItemState('skill', skill.name) === 'block'" @click="$store.agentEditor.setPolicyItem('skill', skill.name, 'block')">Off</button></div></div></template></div></section>
|
||||
</section>
|
||||
|
||||
<section x-show="$store.agentEditor.section === '6'" data-agent-editor-section="6" tabindex="-1" aria-labelledby="agent-section-6-title">
|
||||
<header class="advanced-section-heading"><h3 id="agent-section-6-title">Review</h3><p>Saving will change exactly these files — nothing else.</p></header>
|
||||
<div class="review-plan-status" role="status" x-show="$store.agentEditor.planStatus === 'loading'"><x-icon class="spinning" name="progress_activity"></x-icon><span>Computing the exact change plan…</span></div>
|
||||
<div class="review-plan-status" x-show="$store.agentEditor.planStatus === 'error'"><span>The change plan could not be computed.</span><button type="button" class="text-button" @click="$store.agentEditor.previewPlan()">Retry</button></div>
|
||||
<div class="review-blocked" role="alert" x-show="$store.agentEditor.planStatus === 'blocked'">
|
||||
|
|
@ -366,11 +395,7 @@
|
|||
.agent-editor h2,.agent-editor h3,.agent-editor h4,.agent-editor p { margin: 0; }
|
||||
.agent-editor button,.agent-editor input,.agent-editor textarea,.agent-editor select { font: inherit; }
|
||||
.agent-editor input[type="search"],.agent-editor select { min-width:0; min-height:2.25rem; box-sizing:border-box; padding:.5rem .6rem; border:1px solid var(--color-border); border-radius:7px; background:var(--color-input); color:var(--color-text); }
|
||||
.agent-editor input[type="checkbox"],.agent-editor input[type="radio"] { appearance:none; flex:0 0 auto; width:1.5rem; height:1.5rem; margin:.05rem 0; border:1px solid var(--color-border); background:var(--color-input); }
|
||||
.agent-editor input[type="checkbox"] { display:grid; place-content:center; border-radius:4px; }
|
||||
.agent-editor input[type="checkbox"]::before { content:""; width:.55rem; height:.3rem; border:solid white; border-width:0 0 2px 2px; opacity:0; transform:translateY(-1px) rotate(-45deg); }
|
||||
.agent-editor input[type="checkbox"]:checked { border-color:var(--agent-editor-action); background:var(--agent-editor-action); }
|
||||
.agent-editor input[type="checkbox"]:checked::before { opacity:1; }
|
||||
.agent-editor input[type="radio"] { appearance:none; flex:0 0 auto; width:1.5rem; height:1.5rem; margin:.05rem 0; border:1px solid var(--color-border); background:var(--color-input); }
|
||||
.agent-editor input[type="radio"] { border-radius:50%; }
|
||||
.agent-editor input[type="radio"]:checked { border-color:var(--agent-editor-action); box-shadow:inset 0 0 0 4px var(--color-input); background:var(--agent-editor-action); }
|
||||
.agent-editor-error { display:flex; gap:.55rem; align-items:flex-start; margin:0 0 .8rem; padding:.7rem .8rem; border:1px solid color-mix(in srgb,var(--agent-editor-danger) 55%,var(--color-border)); border-radius:8px; background:color-mix(in srgb,var(--agent-editor-danger) 10%,var(--color-panel)); }
|
||||
|
|
@ -390,7 +415,8 @@
|
|||
.agent-mode-switch button { border:0; border-radius:6px; padding:.42rem .8rem; color:var(--color-text-secondary); background:transparent; }
|
||||
.agent-mode-switch button.active { color:var(--color-text); background:var(--color-panel); box-shadow:0 1px 4px rgba(0,0,0,.2); }
|
||||
.agent-easy { max-width:43rem; margin:0 auto; display:flex; flex-direction:column; gap:1.35rem; padding:.25rem .25rem 1rem; }
|
||||
.agent-easy-identity { position:relative; display:grid; grid-template-columns:7.2rem 1fr; gap:1rem; align-items:center; }
|
||||
.agent-easy-identity { position:relative; display:grid; grid-template-columns:7.2rem 1fr; gap:1rem; align-items:start; }
|
||||
.agent-easy-identity-fields { width:100%; min-width:0; display:flex; flex-direction:column; gap:.9rem; }
|
||||
.agent-avatar-wrap { display:flex; flex-direction:column; align-items:center; gap:.45rem; }
|
||||
.agent-avatar { width:5rem; aspect-ratio:1; position:relative; display:grid; place-items:center; border-radius:16px; overflow:hidden; color:white; font-size:1.35rem; font-weight:700; box-shadow:inset 0 0 0 1px rgba(255,255,255,.15); }
|
||||
.agent-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
|
|
@ -401,9 +427,9 @@
|
|||
.avatar-action-icon x-icon { font-size:1.05rem; }
|
||||
.avatar-color-action input,.avatar-upload-action input { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }
|
||||
.agent-field { display:flex; flex-direction:column; gap:.35rem; min-width:0; }
|
||||
.agent-name-field { align-self:start; }
|
||||
.agent-name-field { width:100%; }
|
||||
.agent-field-label { font-weight:650; font-size:.92rem; }
|
||||
.agent-field input,.agent-field textarea,.agent-easy textarea,.prompt-find input,.policy-filters input,.policy-filters select,.compact-search input { width:100%; box-sizing:border-box; }
|
||||
.agent-field input,.agent-field textarea,.agent-easy textarea,.policy-filters input,.policy-filters select,.compact-search input { width:100%; box-sizing:border-box; }
|
||||
.agent-field small,.agent-field-heading p,.field-status { color:var(--color-text-secondary); font-size:.79rem; }
|
||||
.field-count { color:var(--color-text-secondary); font-size:.79rem; font-weight:400; }
|
||||
.field-status { display:block; margin-top:.15rem; }
|
||||
|
|
@ -415,16 +441,22 @@
|
|||
.agent-editor .text-button:hover { text-decoration:underline; }
|
||||
.agent-easy textarea { min-height:11rem; resize:vertical; }
|
||||
.restore-action { --agent-editor-action:var(--color-primary); display:inline-flex; align-items:center; gap:.25rem; margin-top:.4rem; }
|
||||
.easy-tool-list { display:flex; flex-direction:column; padding:.2rem .35rem .2rem 0; }
|
||||
.easy-tool-list .policy-item { cursor:pointer; }
|
||||
.easy-skills-hint { margin:.55rem 0 0; color:var(--color-text-secondary); font-size:.79rem; }
|
||||
.agent-advanced { display:grid; grid-template-columns:14rem minmax(0,1fr); gap:1rem; min-height:0; }
|
||||
.agent-easy-capabilities { display:flex; flex-direction:column; gap:.65rem; }
|
||||
.future-policy-control { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:.45rem .2rem; }
|
||||
.capability-policy-group + .capability-policy-group { border-top:1px solid var(--color-border); padding-top:.55rem; }
|
||||
.capability-accordion summary { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.65rem .2rem .75rem; cursor:pointer; color:var(--color-text-secondary); font-weight:500; }
|
||||
.capability-accordion summary x-icon { color:var(--color-text-secondary); transition:transform .15s ease; }
|
||||
.capability-accordion[open] summary x-icon { transform:rotate(180deg); }
|
||||
.capability-accordion .policy-items { max-height:none; overflow:visible; padding:0 0 .45rem; }
|
||||
.agent-advanced { flex:1 1 auto; display:grid; grid-template-columns:14rem minmax(0,1fr); gap:1rem; min-height:0; }
|
||||
.agent-advanced-nav { display:flex; flex-direction:column; gap:.3rem; padding:.45rem; border:1px solid var(--color-border); border-radius:10px; background:var(--color-input); align-self:start; position:sticky; top:0; }
|
||||
.agent-advanced-nav button { display:grid; grid-template-columns:1.7rem 1fr auto; align-items:center; gap:.45rem; border:0; border-radius:7px; padding:.65rem; color:var(--color-text-secondary); background:transparent; text-align:left; }
|
||||
.agent-advanced-nav button.active { color:var(--color-text); background:var(--color-panel); }
|
||||
.section-number { display:grid; place-items:center; width:1.55rem; height:1.55rem; border:1px solid var(--color-border); border-radius:50%; font-size:.75rem; }
|
||||
.agent-advanced-content,.agent-editor-workspace,.agent-manager { min-width:0; }
|
||||
.agent-advanced-content { min-height:0; overflow:auto; }
|
||||
.agent-advanced-content > section { outline:none; display:flex; flex-direction:column; gap:1rem; }
|
||||
.agent-advanced-content > section[data-agent-editor-section="2"] { height:100%; min-height:0; }
|
||||
.advanced-section-heading { display:flex; flex-direction:column; gap:.25rem; padding-bottom:.8rem; border-bottom:1px solid var(--color-border); }
|
||||
.advanced-section-heading h3 { font-size:1.2rem; }
|
||||
.advanced-section-heading p { max-width:48rem; color:var(--color-text-secondary); font-size:.84rem; }
|
||||
|
|
@ -439,12 +471,12 @@
|
|||
.agent-model-preset-picker { width:100%; display:grid; grid-template-columns:minmax(13rem,18rem) auto; align-items:center; justify-content:space-between; gap:1rem; }
|
||||
.agent-model-preset-picker select { width:100%; }
|
||||
.agent-model-preset-picker > .button { display:inline-flex; align-items:center; gap:.35rem; }
|
||||
.prompt-workspace { display:grid; grid-template-columns:minmax(13rem,16rem) minmax(0,1fr); gap:.8rem; min-width:0; height:clamp(34rem,65vh,58rem); }
|
||||
.prompt-workspace { flex:1 1 auto; display:grid; grid-template-columns:minmax(13rem,16rem) minmax(0,1fr); gap:.8rem; min-width:0; min-height:0; }
|
||||
.prompt-browser { display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
|
||||
.compact-search,.policy-search,.prompt-find label { display:grid; grid-template-columns:1.25rem minmax(0,1fr); align-items:center; gap:.3rem; min-width:0; height:2.125rem; padding:0 .45rem; border:1px solid color-mix(in srgb,var(--color-border) 42%,transparent); border-radius:7px; background:color-mix(in srgb,var(--color-text) 10%,transparent); color:var(--color-text-secondary); }
|
||||
.compact-search,.policy-search { display:grid; grid-template-columns:1.25rem minmax(0,1fr); align-items:center; gap:.3rem; min-width:0; height:2.125rem; padding:0 .45rem; border:1px solid color-mix(in srgb,var(--color-border) 42%,transparent); border-radius:7px; background:color-mix(in srgb,var(--color-text) 10%,transparent); color:var(--color-text-secondary); }
|
||||
.compact-search { margin:.45rem; }
|
||||
.compact-search:focus-within,.policy-search:focus-within,.prompt-find label:focus-within { border-color:color-mix(in srgb,var(--color-primary) 46%,var(--color-border)); background:color-mix(in srgb,var(--color-text) 13%,transparent); }
|
||||
.agent-editor .compact-search input,.agent-editor .policy-search input,.agent-editor .prompt-find label input { height:100%; min-height:0; padding:0; border:0; outline:0; appearance:none; background:transparent; }
|
||||
.compact-search:focus-within,.policy-search:focus-within { border-color:color-mix(in srgb,var(--color-primary) 46%,var(--color-border)); background:color-mix(in srgb,var(--color-text) 13%,transparent); }
|
||||
.agent-editor .compact-search input,.agent-editor .policy-search input { height:100%; min-height:0; padding:0; border:0; outline:0; appearance:none; background:transparent; }
|
||||
.prompt-file-list { flex:1; overflow:auto; padding:.35rem; }
|
||||
.prompt-file-group { min-width:0; margin-bottom:.55rem; }
|
||||
.prompt-file-group h4 { padding:.35rem .5rem .25rem; color:var(--color-text-secondary); font-size:.7rem; font-weight:650; letter-spacing:.03em; text-transform:uppercase; }
|
||||
|
|
@ -453,57 +485,36 @@
|
|||
.prompt-file-name { grid-column:1/-1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:monospace; font-size:.76rem; }
|
||||
.prompt-file-state { color:var(--color-text-secondary); font-size:.7rem; }
|
||||
.prompt-empty { padding:.65rem; color:var(--color-text-secondary); font-size:.78rem; }
|
||||
.prompt-editor { display:flex; flex-direction:column; gap:.55rem; min-width:0; max-width:100%; max-height:100%; overflow:auto; }
|
||||
.prompt-editor { display:flex; flex-direction:column; gap:.55rem; min-width:0; min-height:0; max-width:100%; overflow:hidden; }
|
||||
.prompt-editor-header { display:flex; justify-content:space-between; gap:.7rem; align-items:flex-start; }
|
||||
.prompt-editor-header > div:first-child { min-width:0; overflow-wrap:anywhere; }
|
||||
.source-chain { margin-top:.2rem; color:var(--color-text-secondary); font-size:.75rem; }
|
||||
.prompt-file-heading { display:flex; flex-direction:column; gap:.25rem; }
|
||||
.prompt-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:.35rem; min-width:0; }
|
||||
.prompt-actions .button { max-width:100%; white-space:normal; overflow-wrap:anywhere; }
|
||||
.prompt-customization-path { display:flex; align-items:center; justify-content:flex-end; gap:.4rem; min-width:0; }
|
||||
.prompt-customization-path code { min-width:0; color:var(--color-text-secondary); font-size:.72rem; overflow-wrap:anywhere; text-align:right; }
|
||||
.prompt-customization-path { display:flex; align-items:center; gap:.4rem; min-width:0; }
|
||||
.prompt-customization-path code { min-width:0; color:var(--color-text-secondary); font-size:.72rem; overflow-wrap:anywhere; }
|
||||
.prompt-customization-path .button { width:2.125rem; height:2.125rem; flex:0 0 auto; padding:0; }
|
||||
.prompt-view-tabs { display:flex; flex-wrap:wrap; gap:.25rem; padding-bottom:.35rem; border-bottom:1px solid var(--color-border); }
|
||||
.prompt-view-tabs button { min-height:2rem; padding:.35rem .65rem; border:0; border-radius:6px; background:transparent; color:var(--color-text-secondary); }
|
||||
.prompt-view-tabs button.active { background:var(--color-input); color:var(--color-text); }
|
||||
.prompt-find { display:flex; flex-wrap:wrap; align-items:center; gap:.35rem; min-width:0; }
|
||||
.prompt-find label { flex:1 1 10rem; min-width:0; }
|
||||
.prompt-find span { color:var(--color-text-secondary); font-size:.75rem; white-space:nowrap; }
|
||||
.prompt-find .prompt-match-action { flex:0 0 2.125rem; width:2.125rem; height:2.125rem; min-height:2.125rem; padding:0; border-radius:7px; justify-content:center; }
|
||||
.prompt-panes { display:grid; grid-template-columns:1fr; gap:.65rem; min-height:18rem; }
|
||||
.prompt-panes.compare { grid-template-columns:1fr 1fr; }
|
||||
.prompt-pane { min-width:0; min-height:0; display:flex; flex-direction:column; gap:.35rem; }
|
||||
.prompt-pane label,.prompt-pane-title { color:var(--color-text-secondary); font-size:.75rem; }
|
||||
.prompt-pane textarea,.prompt-pane pre { flex:1; box-sizing:border-box; width:100%; min-height:0; margin:0; padding:.75rem; overflow:auto; border:1px solid var(--color-border); border-radius:8px; background:var(--color-input); color:var(--color-text); font:13px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace; white-space:pre-wrap; tab-size:2; resize:vertical; }
|
||||
.prompt-pane textarea:focus-visible { outline-offset:-2px; }
|
||||
.prompt-pane.inherited pre { opacity:.86; }
|
||||
.prompt-ace { flex:1; min-height:0; border:1px solid var(--color-border); border-radius:8px; overflow:hidden; font:13px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace; }
|
||||
.profile-maintenance summary { display:flex; align-items:center; min-height:1.5rem; cursor:pointer; }
|
||||
.policy-mode-row,.future-default { display:flex; flex-wrap:wrap; align-items:center; gap:.7rem 1rem; padding:.7rem; border:1px solid var(--color-border); border-radius:9px; }
|
||||
.policy-mode-row label,.future-default label { display:flex; gap:.35rem; align-items:center; }
|
||||
.policy-mode-row small { color:var(--color-text-secondary); }
|
||||
.policy-editor { min-width:0; margin:.75rem 0 0; padding:0; border:0; transition:opacity .15s ease; }
|
||||
.policy-editor:disabled { opacity:.48; }
|
||||
.future-default { margin-top:.8rem; }
|
||||
.future-default legend { padding:0 .35rem; color:var(--color-text-secondary); font-size:.8rem; }
|
||||
.policy-filters { display:grid; grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto) minmax(8rem,auto); gap:.6rem; margin-bottom:.7rem; }
|
||||
.policy-filters { display:grid; grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto); gap:.6rem; }
|
||||
.policy-filters.skills { grid-template-columns:minmax(12rem,1fr) minmax(8rem,auto); }
|
||||
.policy-filters label { display:flex; align-items:center; gap:.35rem; font-size:.78rem; color:var(--color-text-secondary); }
|
||||
.policy-filters select { flex:1; }
|
||||
.policy-lists { display:grid; grid-template-columns:minmax(0,1fr) 2.5rem minmax(0,1fr); gap:.55rem; }
|
||||
.policy-transfer-actions { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.5rem; }
|
||||
.policy-transfer-actions .button { width:2.5rem; height:2.5rem; padding:0; justify-content:center; }
|
||||
.policy-list { display:flex; flex-direction:column; min-width:0; min-height:28rem; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
|
||||
.policy-list header { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.5rem; min-height:3rem; box-sizing:border-box; padding:.7rem; border-bottom:1px solid var(--color-border); background:var(--color-input); }
|
||||
.policy-list h4 { display:inline; margin-right:.35rem; }
|
||||
.policy-list header span { color:var(--color-text-secondary); font-size:.72rem; }
|
||||
.policy-items { flex:1; max-height:32rem; overflow:auto; padding:.35rem; }
|
||||
.policy-item { display:flex; gap:.55rem; align-items:flex-start; padding:.55rem; border-radius:7px; }
|
||||
.policy-group { display:flex; flex-direction:column; min-width:0; border:1px solid var(--color-border); border-radius:10px; overflow:hidden; }
|
||||
.policy-group header { display:flex; align-items:center; justify-content:space-between; gap:.5rem; padding:.65rem .8rem; border-bottom:1px solid var(--color-border); background:var(--color-input); }
|
||||
.policy-group header span { color:var(--color-text-secondary); font-size:.72rem; }
|
||||
.policy-items { flex:1; max-height:24rem; overflow:auto; padding:.35rem; }
|
||||
.policy-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:.75rem; align-items:start; padding:.55rem; border-radius:7px; }
|
||||
.policy-item:hover { background:var(--color-input); }
|
||||
.policy-item > div { display:flex; flex:1; flex-direction:column; gap:.15rem; min-width:0; }
|
||||
.policy-item-copy { display:flex; flex-direction:column; gap:.15rem; min-width:0; }
|
||||
.policy-items small { color:var(--color-text-secondary); overflow-wrap:anywhere; }
|
||||
.policy-items em { color:var(--color-warning); font-size:.72rem; font-style:normal; }
|
||||
.policy-item-description { margin:0; color:var(--color-text-secondary); font-size:.75rem; white-space:pre-wrap; overflow-wrap:anywhere; }
|
||||
.policy-state-control { display:inline-flex; align-self:center; overflow:hidden; border:1px solid var(--color-border); border-radius:var(--border-radius-sm); background:var(--color-input); }
|
||||
.policy-state-control button { min-height:2rem; padding:.35rem .6rem; border:0; border-left:1px solid var(--color-border); border-radius:0; background:transparent; color:var(--color-text-secondary); cursor:pointer; font-size:.74rem; white-space:nowrap; }
|
||||
.policy-state-control button:first-child { border-left:0; }
|
||||
.policy-state-control button.active { color:var(--color-text); background:color-mix(in srgb,var(--agent-editor-action) 16%,var(--color-input)); box-shadow:inset 0 0 0 1px var(--agent-editor-action); }
|
||||
.policy-empty { padding:1rem .7rem; color:var(--color-text-secondary); font-size:.8rem; text-align:center; }
|
||||
.agent-editor .policy-bulk { min-height:1.5rem; padding:.1rem .45rem; font-size:.72rem; line-height:1.15; white-space:normal; }
|
||||
.review-identity { display:grid; grid-template-columns:3.25rem minmax(0,1fr); align-items:center; gap:.8rem; padding:.8rem; border-bottom:1px solid var(--color-border); }
|
||||
.review-identity-avatar { width:3.25rem; aspect-ratio:1; display:grid; place-items:center; overflow:hidden; border-radius:10px; color:#fff; font-weight:700; }
|
||||
.review-identity-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
|
|
@ -562,9 +573,9 @@
|
|||
.agent-editor :focus-visible { outline:2px solid var(--agent-editor-action); outline-offset:2px; }
|
||||
.modal-inner.agent-editor-easy { width:min(720px,calc(100vw - 2rem)); max-width:720px; }
|
||||
.modal-inner.agent-editor-advanced { width:92vw; max-width:none; height:min(calc(100vh - 4rem),80rem); max-height:calc(100vh - 4rem); }
|
||||
.modal-inner.agent-editor-advanced .modal-scroll,.modal-inner.agent-editor-advanced .modal-bd,.modal-inner.agent-editor-advanced .agent-editor,.modal-inner.agent-editor-advanced .agent-editor-workspace { display:flex; flex:1 1 auto; min-height:0; }
|
||||
.modal-inner.agent-editor-advanced .modal-scroll { max-height:none; }
|
||||
.modal-inner.agent-editor-advanced .modal-bd { min-height:0; }
|
||||
.modal-inner.agent-editor-advanced .agent-editor,.modal-inner.agent-editor-advanced .agent-editor-workspace { min-height:100%; }
|
||||
.modal-inner.agent-editor-advanced .agent-editor,.modal-inner.agent-editor-advanced .agent-editor-workspace { flex-direction:column; }
|
||||
@media (max-width: 760px) {
|
||||
.modal-inner.agent-editor-easy,.modal-inner.agent-editor-advanced { width:100vw; max-width:none; height:100vh; max-height:none; border-radius:0; }
|
||||
.agent-editor-topbar { align-items:flex-start; flex-wrap:wrap; }
|
||||
|
|
@ -572,21 +583,22 @@
|
|||
.agent-editor-topbar-actions { width:100%; flex-wrap:wrap; justify-content:flex-end; }
|
||||
.agent-editor-scope { flex:1 1 14rem; }
|
||||
.agent-easy-identity { grid-template-columns:1fr; justify-items:center; padding-top:1.8rem; }
|
||||
.agent-name-field { width:100%; }
|
||||
.agent-id-feedback { width:100%; }
|
||||
.agent-advanced { grid-template-columns:1fr; }
|
||||
.agent-advanced-nav { position:static; flex-direction:row; overflow-x:auto; }
|
||||
.agent-advanced-nav button { grid-template-columns:auto auto auto; white-space:nowrap; }
|
||||
.agent-model-preset-picker { grid-template-columns:1fr; gap:.5rem; }
|
||||
.prompt-workspace { grid-template-columns:1fr; height:auto; }
|
||||
.agent-model-preset-picker { grid-template-columns:minmax(0,1fr) auto; gap:.5rem; }
|
||||
.prompt-workspace { grid-template-columns:1fr; }
|
||||
.prompt-browser { height:22rem; max-height:22rem; }
|
||||
.prompt-editor { max-height:none; overflow:visible; }
|
||||
.prompt-panes.compare,.policy-lists,.change-plan,.advanced-identity-grid,.identity-fields { grid-template-columns:1fr; }
|
||||
.policy-lists .policy-transfer-actions { flex-direction:row; }
|
||||
.policy-lists .policy-transfer-actions x-icon { transform:rotate(90deg); }
|
||||
.prompt-ace { min-height:18rem; }
|
||||
.change-plan,.advanced-identity-grid,.identity-fields { grid-template-columns:1fr; }
|
||||
.identity-fields .agent-field.wide { grid-column:1; }
|
||||
.policy-filters { grid-template-columns:1fr; }
|
||||
.policy-list { min-height:20rem; }
|
||||
.future-policy-control { align-items:flex-start; }
|
||||
.policy-item { grid-template-columns:1fr; }
|
||||
.policy-state-control { width:100%; align-self:stretch; }
|
||||
.policy-state-control button { flex:1; }
|
||||
.agent-manager-list-header { flex-wrap:wrap; }
|
||||
.active-agent-display { width:100%; }
|
||||
.agent-manager-create { margin-left:auto; }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
profile `config.json` files, projects may own project or project-profile
|
||||
configs through the standard plugin scope paths, and the runtime remains
|
||||
authoritative.
|
||||
- Custom configuration stores independent `default` and `mcp_default`
|
||||
fallbacks; explicit canonical IDs remain shared in `allowed` and `blocked`.
|
||||
- Required final-response capability is never disabled.
|
||||
|
||||
## Verification
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
mode: inherit
|
||||
default: allow
|
||||
mcp_default: allow
|
||||
allowed: []
|
||||
blocked: []
|
||||
|
|
|
|||
|
|
@ -329,6 +329,7 @@ def test_plugin_configs_preserve_unowned_keys_and_use_json(user_root: Path) -> N
|
|||
"tool_policy": {
|
||||
"mode": "custom",
|
||||
"default": "block",
|
||||
"mcp_default": "allow",
|
||||
"allowed": ["local:search_engine"],
|
||||
"blocked": ["local:shell"],
|
||||
},
|
||||
|
|
@ -344,7 +345,7 @@ def test_plugin_configs_preserve_unowned_keys_and_use_json(user_root: Path) -> N
|
|||
|
||||
assert json.loads(model.read_text())["manual"] == 1
|
||||
assert set(json.loads(tools.read_text())) >= {
|
||||
"manual", "mode", "default", "allowed", "blocked"
|
||||
"manual", "mode", "default", "mcp_default", "allowed", "blocked"
|
||||
}
|
||||
skill_data = json.loads(skill.read_text())
|
||||
assert skill_data["active_skills"] == [{"name": "existing"}]
|
||||
|
|
@ -379,6 +380,7 @@ def test_model_and_off_tool_choices_write_only_their_json_contracts(
|
|||
assert json.loads(off.changes[tool_path].content) == {
|
||||
"mode": "custom",
|
||||
"default": "block",
|
||||
"mcp_default": "block",
|
||||
"allowed": [],
|
||||
"blocked": [],
|
||||
}
|
||||
|
|
@ -419,6 +421,44 @@ def test_project_tool_policy_reads_effective_access_and_writes_project_scope(
|
|||
]
|
||||
|
||||
|
||||
def test_tri_state_tool_mcp_and_skill_policies_write_at_both_scopes(
|
||||
user_root: Path,
|
||||
project_scope: tuple[editor._EditorContext, Path],
|
||||
) -> None:
|
||||
tool_policy = {
|
||||
"mode": "custom",
|
||||
"default": "block",
|
||||
"mcp_default": "allow",
|
||||
"allowed": ["local:shell"],
|
||||
"blocked": ["mcp:docs:write"],
|
||||
}
|
||||
skill_policy = {
|
||||
"mode": "custom",
|
||||
"default": "allow",
|
||||
"allowed": ["Research"],
|
||||
"blocked": ["Unsafe"],
|
||||
}
|
||||
patch = {
|
||||
"profile_id": "researcher",
|
||||
"tool_policy": tool_policy,
|
||||
"skill_policy": skill_policy,
|
||||
}
|
||||
context, project_agents = project_scope
|
||||
|
||||
for plan, root in (
|
||||
(editor.build_change_plan(patch), user_root),
|
||||
(editor.build_change_plan(patch, context), project_agents),
|
||||
):
|
||||
editor.apply_change_plan(plan)
|
||||
profile_root = root / "researcher" / "plugins"
|
||||
assert json.loads(
|
||||
(profile_root / "_tool_access" / "config.json").read_text()
|
||||
) == tool_policy
|
||||
assert json.loads(
|
||||
(profile_root / "_skills" / "config.json").read_text()
|
||||
)["visibility_policy"] == skill_policy
|
||||
|
||||
|
||||
def test_project_agents_are_scope_owned_and_never_leak_global_writes(
|
||||
user_root: Path,
|
||||
project_scope: tuple[editor._EditorContext, Path],
|
||||
|
|
|
|||
|
|
@ -33,11 +33,16 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
modal,
|
||||
re.DOTALL,
|
||||
).group(0)
|
||||
skill_section = re.search(
|
||||
mcp_section = re.search(
|
||||
r'data-agent-editor-section="4".*?(?=<section x-show="\$store\.agentEditor\.section === \'5\'")',
|
||||
modal,
|
||||
re.DOTALL,
|
||||
).group(0)
|
||||
skill_section = re.search(
|
||||
r'data-agent-editor-section="5".*?(?=<section x-show="\$store\.agentEditor\.section === \'6\'")',
|
||||
modal,
|
||||
re.DOTALL,
|
||||
).group(0)
|
||||
easy_surface = modal.split('<div class="agent-advanced"', 1)[0]
|
||||
|
||||
assert "Create agent" not in switcher
|
||||
|
|
@ -59,43 +64,50 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
"Identity",
|
||||
"Prompt files",
|
||||
"Tools",
|
||||
"MCPs",
|
||||
"Skills",
|
||||
"Review",
|
||||
"Save & test",
|
||||
)
|
||||
)
|
||||
assert 'aria-label="Editor mode"' in modal
|
||||
assert "Allow selected" in modal and "Block selected" in modal
|
||||
assert "Allow selected" not in modal and "Block selected" not in modal
|
||||
assert "No optional tools" not in modal
|
||||
assert 'class="agent-model-preset-picker"' in modal
|
||||
assert 'x-model="$store.agentEditor.draft.modelPreset"' in modal
|
||||
assert modal.count('x-model="$store.agentEditor.draft.modelPreset"') == 2
|
||||
assert 'id="agent-editor-easy-model-preset"' in easy_surface
|
||||
easy_identity = easy_surface[easy_surface.index('<section class="agent-easy-identity">'):easy_surface.index('<section class="agent-easy-field">')]
|
||||
assert easy_identity.index('id="agent-editor-name"') < easy_identity.index('id="agent-editor-easy-model-preset"')
|
||||
assert easy_surface.index('id="agent-editor-easy-model-preset"') < easy_surface.index('id="agent-editor-instructions"')
|
||||
assert '`Use current preset (${$store.agentEditor.state.model_preset.effective})`' in modal
|
||||
assert 'x-for="preset in $store.agentEditor.state.model_presets"' in modal
|
||||
assert "Edit Presets" in modal
|
||||
assert modal.count("Edit Presets") == 2
|
||||
assert "Manage presets" not in modal
|
||||
assert "model-preset-row" not in modal
|
||||
assert 'class="easy-tool-details"' not in modal
|
||||
assert 'x-for="tool in $store.agentEditor.toolCatalog"' in easy_surface
|
||||
assert 'class="field-count"' in easy_surface
|
||||
assert "toolCatalog.length === 1 ? 'tool' : 'tools'" in modal
|
||||
assert "skillCatalog.length === 1 ? 'skill' : 'skills'" in modal
|
||||
assert ':checked="$store.agentEditor.isToolAllowed(tool)"' in easy_surface
|
||||
assert "$store.agentEditor.setEasyToolAllowed(tool.id, $event.target.checked)" in easy_surface
|
||||
assert easy_surface.count('<details class="capability-accordion">') == 3
|
||||
assert re.findall(r'<summary><span>(Choose individual (?:tools|MCPs|skills))</span><x-icon name="expand_more"></x-icon></summary>', easy_surface) == ["Choose individual tools", "Choose individual MCPs", "Choose individual skills"]
|
||||
assert '<details class="capability-accordion" open' not in easy_surface
|
||||
assert 'x-for="tool in $store.agentEditor.standardToolCatalog"' in easy_surface
|
||||
assert 'x-for="skill in $store.agentEditor.skillCatalog"' in easy_surface
|
||||
assert 'x-for="tool in $store.agentEditor.mcpCatalog"' in easy_surface
|
||||
assert "Choose tools in Advanced" not in modal
|
||||
assert "To enable or disable skills, click Advanced." in easy_surface
|
||||
assert 'x-for="skill in $store.agentEditor' not in easy_surface
|
||||
assert 'class="easy-tool-actions"' not in modal
|
||||
assert 'class="policy-editor" :disabled="$store.agentEditor.draft.toolPolicy.mode !== \'custom\'"' in tool_section
|
||||
assert 'class="policy-editor" :disabled="$store.agentEditor.draft.skillPolicy.mode !== \'custom\'"' in skill_section
|
||||
assert 'class="policy-lists"' in tool_section and 'class="policy-lists"' in skill_section
|
||||
assert 'aria-label="Block selected tools"' in tool_section
|
||||
assert 'aria-label="Allow selected tools"' in tool_section
|
||||
assert 'aria-label="Block selected skills"' in skill_section
|
||||
assert 'aria-label="Allow selected skills"' in skill_section
|
||||
assert "Use standard tool access" not in modal
|
||||
assert "Use standard skill access" not in modal
|
||||
assert 'class="policy-lists"' not in modal
|
||||
assert "policy-transfer-actions" not in modal
|
||||
assert tool_section.count('class="policy-group"') == 1
|
||||
assert mcp_section.count('class="policy-group"') == 1
|
||||
assert skill_section.count('class="policy-group"') == 1
|
||||
assert '<label>Category ' not in tool_section
|
||||
assert "indeterminate" not in modal
|
||||
assert modal.count('class="policy-state-control" role="group"') == 6
|
||||
assert modal.count(':aria-pressed="$store.agentEditor.policyItemState') == 18
|
||||
assert '<details class="policy-description"' not in tool_section
|
||||
assert '<details class="policy-description"' not in mcp_section
|
||||
assert '<details class="policy-description"' not in skill_section
|
||||
assert tool_section.count('class="policy-item-description"') == 2
|
||||
assert skill_section.count('class="policy-item-description"') == 2
|
||||
assert tool_section.count('class="policy-item-description"') == 1
|
||||
assert mcp_section.count('class="policy-item-description"') == 1
|
||||
assert skill_section.count('class="policy-item-description"') == 1
|
||||
assert "Your changes override the built-in profile. The original files stay unchanged." in modal
|
||||
assert 'x-model="$store.agentEditor.projectName"' in modal
|
||||
assert 'x-init="$nextTick(() => $el.value = $store.agentEditor.projectName)"' in modal
|
||||
|
|
@ -148,7 +160,11 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
assert "Create agents and customize inherited profiles" not in modal
|
||||
assert "Unavailable — kept in your settings" in modal
|
||||
assert "Customize this file" not in modal
|
||||
assert 'role="tablist" aria-label="Prompt view"' in modal
|
||||
assert "Choose a file and edit its prompt." in modal
|
||||
assert "inherited version next to your version" not in modal
|
||||
assert 'role="tablist" aria-label="Prompt view"' not in modal
|
||||
assert "Your version" not in modal and ">Compare</button>" not in modal
|
||||
assert "Find in file" not in modal and "prompt-match-action" not in modal
|
||||
assert "No prompt files match your search." in modal
|
||||
assert "Saving will change exactly these files — nothing else." in modal
|
||||
assert "Review & test" not in modal
|
||||
|
|
@ -161,15 +177,21 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
assert "<h2" not in modal
|
||||
assert "Section 1" not in modal
|
||||
assert '<textarea id="agent-editor-description" rows="2"' in modal
|
||||
assert "When new tools are installed later" in modal
|
||||
assert "When new skills are installed later" in modal
|
||||
assert "Block until reviewed" in modal
|
||||
assert "No blocked tools" in modal and "No blocked skills" in modal
|
||||
assert "Allow newly installed" not in modal
|
||||
assert modal.count("<strong>Allow tools by default</strong>") == 2
|
||||
assert modal.count("<strong>Allow MCPs by default</strong>") == 2
|
||||
assert modal.count("<strong>Allow skills by default</strong>") == 2
|
||||
assert "Allow tools and MCPs by default" not in modal
|
||||
assert "Applies to Tools and MCPs left on Default." not in modal
|
||||
assert "Applies to Skills left on Default." not in modal
|
||||
assert "No blocked tools" not in modal and "No blocked skills" not in modal
|
||||
assert "policy-description" not in modal and "-webkit-line-clamp:2" not in modal
|
||||
assert 'class="prompt-file-list" role="region" aria-label="Prompt file list" tabindex="0"' in modal
|
||||
assert 'class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0"' in modal
|
||||
assert "Preview combined prompt" not in modal
|
||||
assert 'class="prompt-customization-path"' in modal
|
||||
assert '<strong x-text="$store.agentEditor.selectedPrompt"></strong>\n <div class="prompt-customization-path">' in modal
|
||||
assert "source-chain" not in modal and "promptSourceChain" not in store
|
||||
assert "$store.agentEditor.promptCustomizationPath()" in modal
|
||||
assert 'class="review-identity"' in modal
|
||||
assert 'aria-label="Agent identity"' in modal
|
||||
|
|
@ -178,10 +200,16 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
assert "draft.profileId\"></code>" not in review_identity
|
||||
assert "width:92vw" in modal
|
||||
assert ".modal-inner.agent-editor-advanced .modal-scroll { max-height:none; }" in modal
|
||||
assert '.agent-advanced-content > section[data-agent-editor-section="2"] { height:100%; min-height:0; }' in modal
|
||||
assert ".prompt-workspace { flex:1 1 auto;" in modal
|
||||
assert 'width:1.5rem; height:1.5rem' in modal
|
||||
assert modal.count('class="button icon prompt-match-action"') == 2
|
||||
assert "min-height:3rem" in modal and "font-size:.72rem" in modal
|
||||
assert "moveAllVisibleTools(false)" in modal and "moveAllVisibleSkills(false)" in modal
|
||||
assert 'id="agent-editor-prompt-ace"' in modal
|
||||
assert 'id="agent-editor-prompt-text"' not in modal
|
||||
assert "globalThis.ace.edit(container)" in store
|
||||
assert 'editor.session.setMode("ace/mode/markdown")' in store
|
||||
assert "editor.session.setUseWrapMode(true)" in store
|
||||
assert "showPrintMargin: false" in store and "useWorker: false" in store
|
||||
assert "findInPrompt" not in store and "promptTextSearch" not in store
|
||||
assert 'class="agent-editor-heading"' in modal
|
||||
assert ':aria-invalid=' in modal
|
||||
assert modal.count('role="alert"') >= 4
|
||||
|
|
@ -191,9 +219,8 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
assert 'x-show="$store.agentEditor.view === \'editor\' && !$store.agentEditor.draft?.creating"' in modal
|
||||
assert 'class="btn btn-ok" x-show="$store.agentEditor.view === \'editor\'"' in modal
|
||||
assert "Delete all customizations in" in modal
|
||||
assert 'input[type="checkbox"]' in modal and "appearance:none" in modal
|
||||
assert '.agent-editor input[type="checkbox"]' not in modal
|
||||
assert 'promptDisplayState(prompt)' in modal
|
||||
assert 'promptSourceChain($store.agentEditor.selectedPromptDraft)' in modal
|
||||
assert "Will reset to default on save." in modal
|
||||
assert "metadataProvenance('description')" not in modal
|
||||
assert 'x-show="$store.agentEditor.metadataProvenance(\'title\')"' in modal
|
||||
|
|
@ -203,7 +230,7 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
assert 'aria-label="Discard current edit"' in modal
|
||||
assert 'aria-label="Accept current edit"' in modal
|
||||
assert ':readonly="!$store.agentEditor.isPromptEditing' not in modal
|
||||
assert ".prompt-pane textarea:focus-visible { outline-offset:-2px; }" in modal
|
||||
assert ".prompt-ace { flex:1; min-height:0;" in modal
|
||||
store_source = STORE.read_text(encoding="utf-8")
|
||||
assert "cannot be recovered" in store_source
|
||||
assert "deletionImpactHtml" not in store_source
|
||||
|
|
@ -213,34 +240,39 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
switcher_mixin = SWITCHER_MIXIN.read_text(encoding="utf-8")
|
||||
assert "avatar_url" in switcher_mixin
|
||||
assert "BUILT_IN_AGENT_COLORS" in switcher_mixin
|
||||
assert '!["_example", "default"].includes(profile.id)' in switcher_mixin
|
||||
assert 'activeKey !== "default"' in switcher_mixin
|
||||
assert "customized: !!profile.has_user_overrides" not in switcher_mixin
|
||||
assert 'x-for="profile in $store.agentEditor.visibleProfiles"' in modal
|
||||
assert "$store.agentEditor.activeProfile().id !== 'default'" in modal
|
||||
assert 'name="palette"' in modal and 'name="add_photo_alternate"' in modal
|
||||
assert ".easy-tool-summary" not in modal
|
||||
easy_tool_list_style = re.search(
|
||||
r"\.easy-tool-list\s*\{([^}]*)\}", modal
|
||||
assert ".capability-accordion .policy-items { max-height:none; overflow:visible;" in modal
|
||||
assert ".capability-policy-group + .capability-policy-group { border-top:1px solid var(--color-border);" in modal
|
||||
assert "font-weight:500" in re.search(
|
||||
r"\.capability-accordion summary\s*\{([^}]*)\}", modal
|
||||
).group(1)
|
||||
assert "max-height" not in easy_tool_list_style
|
||||
assert "overflow-y" not in easy_tool_list_style
|
||||
assert "grid-template-columns:minmax(0,1fr) 2.5rem minmax(0,1fr)" in modal
|
||||
assert ".policy-lists .policy-transfer-actions { flex-direction:row; }" in modal
|
||||
assert "easyToolsOpen" not in store_source
|
||||
assert "easySkills" not in store_source
|
||||
assert "get toolMode" not in store_source
|
||||
assert "get easyTools" not in store_source
|
||||
assert "firstSentence" not in store_source
|
||||
assert ".capability-accordion[open] summary x-icon { transform:rotate(180deg); }" in modal
|
||||
assert "toolCategory" not in store_source
|
||||
assert "selectedAllowed" not in store_source and "selectedBlocked" not in store_source
|
||||
assert "moveAllVisible" not in store_source and "confirmBulkMove" not in store_source
|
||||
assert "setPolicyItemState" in store_source and "collapsePolicy" in store_source
|
||||
assert '.agent-editor [aria-invalid="true"]' not in modal
|
||||
assert "color-scheme:dark" not in modal
|
||||
assert "#fff 82%" not in modal
|
||||
assert ".agent-manager-name strong,.agent-manager-copy p { overflow-wrap:anywhere; }" in modal
|
||||
assert ".field-error { display:block; color:var(--color-text)" in modal
|
||||
assert ".prompt-pane { min-width:0; min-height:0;" in modal
|
||||
assert ".prompt-pane" not in modal
|
||||
assert 'callJsonApi("/plugins/_agent_editor/agent_editor"' in switcher_mixin
|
||||
assert "profile.enabled !== false" in switcher_mixin
|
||||
assert 'x-show="!$store.modelConfig.agentProfilesLoading"' in switcher
|
||||
assert "@keydown.ctrl.s.prevent" in modal
|
||||
assert "@media (max-width: 760px)" in modal
|
||||
assert modal.count('<label class="policy-item"><input type="checkbox"') == 4
|
||||
assert '<div class="policy-item"><input type="checkbox"' not in modal
|
||||
assert "tri-state-item" not in modal
|
||||
assert "policy-state-legend" not in modal
|
||||
assert "policy-item-state" not in modal
|
||||
assert modal.count("Default (${$store.agentEditor.policyDefault") == 6
|
||||
assert "border-radius:var(--border-radius-sm)" in modal
|
||||
assert "cyclePolicyItem" not in store_source and "policyAriaChecked" not in store_source
|
||||
assert ".agent-manager-card { grid-template-columns:3rem minmax(0,1fr) auto; align-items:start; }" in modal
|
||||
assert ".agent-manager-actions { grid-column:auto; align-self:stretch; display:grid;" in modal
|
||||
assert ".agent-manager-actions .agent-profile-availability { grid-column:1/-1; }" in modal
|
||||
|
|
@ -309,12 +341,35 @@ const chatsStore = {
|
|||
};
|
||||
const modelConfigStore = {
|
||||
loadAgentProfiles: async force => calls.push({ endpoint: "loadAgentProfiles", payload: force }),
|
||||
openPresetEditor: async preset => calls.push({ endpoint: "openPresetEditor", payload: preset }),
|
||||
selectAgentProfile: async (contextId, profileId) => {
|
||||
calls.push({ endpoint: "selectAgentProfile", payload: { contextId, profileId } });
|
||||
return true;
|
||||
},
|
||||
getAgentProfileVisual: (_id, label) => ({ color: "#123456", url: "", initials: label?.[0] || "A" }),
|
||||
};
|
||||
const aceState = { change: null, find: null, destroyed: false, value: "" };
|
||||
const aceContainer = { textContent: "" };
|
||||
const aceSession = {
|
||||
setMode: value => { aceState.mode = value; },
|
||||
setUseWrapMode: value => { aceState.wrap = value; },
|
||||
on: (name, callback) => { if (name === "change") aceState.change = callback; },
|
||||
off: (name, callback) => { if (name === "change" && aceState.change === callback) aceState.change = null; },
|
||||
};
|
||||
const aceEditor = {
|
||||
container: aceContainer,
|
||||
session: aceSession,
|
||||
textInput: { getElement: () => ({ setAttribute: (key, value) => { aceState[key] = value; } }) },
|
||||
setTheme: value => { aceState.theme = value; },
|
||||
setOptions: value => { aceState.options = value; },
|
||||
setValue: value => { aceState.value = value; aceState.change?.(); },
|
||||
getValue: () => aceState.value,
|
||||
find: (query, options) => { aceState.find = { query, options }; },
|
||||
focus: () => { aceState.focused = true; },
|
||||
resize: () => { aceState.resized = true; },
|
||||
destroy: () => { aceState.destroyed = true; },
|
||||
};
|
||||
globalThis.ace = { edit: container => { aceState.container = container; return aceEditor; } };
|
||||
globalThis.window = globalThis;
|
||||
globalThis.document = {
|
||||
dispatchEvent: (event) => calls.push({ endpoint: "event", payload: event.type }),
|
||||
|
|
@ -350,13 +405,14 @@ store.state = {
|
|||
{ filename: "agent.system.main.communication.md", group: "2.4", group_label: "Communication", effective: "Inherited comm", inherited: "Inherited comm", source_chain: ["Framework", "Researcher"], state: "Inherited", has_override: false },
|
||||
],
|
||||
model_preset: { has_override: false },
|
||||
tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [] },
|
||||
tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [] },
|
||||
skills: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [] },
|
||||
};
|
||||
store.makeDraft(true);
|
||||
if (await store.previewPlan()) throw new Error("invalid plan unexpectedly succeeded");
|
||||
if (store.planStatus !== "blocked" || store.error || store.validationIssues().length !== 2) throw new Error("blocked plan state mismatch");
|
||||
if (store.fieldIssue("name")?.message !== "Agent name is required.") throw new Error("inline name issue missing");
|
||||
if (store.fieldIssue("instructions")?.field !== "agent-editor-instructions") throw new Error("Easy validation targeted the wrong input");
|
||||
await store.save();
|
||||
if (store.error) throw new Error("validation leaked into dismissible error banner");
|
||||
store.state.profile.id = "new-agent";
|
||||
|
|
@ -365,7 +421,8 @@ store.state.profile.metadata.title = { inherited_source: "agents/new-agent/agent
|
|||
if (store.metadataProvenance("title") !== "") throw new Error("new profile showed misleading provenance");
|
||||
store.draft.creating = false;
|
||||
if (store.metadataProvenance("title") !== "Using the default") throw new Error("default provenance mismatch");
|
||||
store.profiles = [{ id: "researcher", title: "Researcher" }];
|
||||
store.profiles = [{ id: "researcher", title: "Researcher" }, { id: "default", title: "Default" }];
|
||||
if (store.visibleProfiles.length !== 1 || store.visibleProfiles[0].id !== "researcher") throw new Error("Default profile remained selectable in Agent Editor");
|
||||
store.state.profile.metadata.title = { inherited_source: "agents/researcher/agent.yaml" };
|
||||
if (store.metadataProvenance("title") !== "Inherited from Researcher") throw new Error("inherited provenance mismatch");
|
||||
store.state.profile.metadata.title.has_override = true;
|
||||
|
|
@ -378,35 +435,50 @@ if (store.promptCustomizationPath() !== "usr/agents/researcher/prompts/agent.sys
|
|||
store.projectName = "demo";
|
||||
if (store.promptCustomizationPath() !== "usr/projects/demo/.a0proj/agents/researcher/prompts/agent.system.main.specifics.md") throw new Error("project prompt customization path mismatch");
|
||||
store.projectName = "";
|
||||
store.state.model_preset.effective = "Current";
|
||||
store.state.model_presets = [{ name: "Codex" }];
|
||||
store.draft.modelPreset = "Codex";
|
||||
if (store.buildPatch().model_preset?.name !== "Codex") throw new Error("Easy model preset was not saved");
|
||||
loadHandler = () => ({ ok: true, state: { model_presets: [{ name: "Codex" }] } });
|
||||
await store.openPresetManager();
|
||||
loadHandler = null;
|
||||
if (!calls.some(call => call.endpoint === "openPresetEditor" && call.payload === "Codex")) throw new Error("Easy preset editor action did not reuse Model Configuration");
|
||||
store.draft.modelPreset = "";
|
||||
store.state.tools.catalog = [
|
||||
{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
|
||||
{ id: "local:gone", name: "gone", label: "Gone", origin: "Unavailable", available: false },
|
||||
{ id: "mcp:docs:read", name: "read", label: "Docs read", origin: "MCP", available: true },
|
||||
];
|
||||
store.draft.toolPolicy = { mode: "custom", default: "allow", allowed: [], blocked: ["local:shell"] };
|
||||
store.draft.toolPolicy = { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] };
|
||||
store.initialDraft.toolPolicy = clone(store.draft.toolPolicy);
|
||||
if (store.standardToolCatalog.length !== 1 || store.mcpCatalog.length !== 1 || store.toolCatalog.length !== 2) throw new Error("Easy tool/MCP grouping mismatch");
|
||||
if (store.filteredTools("tool").length !== 2 || store.filteredTools("mcp").length !== 1) throw new Error("Advanced retained catalog grouping mismatch");
|
||||
if (store.policyItemState("tool", "local:shell") !== "default") throw new Error("initial segmented state mismatch");
|
||||
store.setPolicyItem("tool", "local:shell", "allow");
|
||||
if (store.policyItemState("tool", "local:shell") !== "allow" || !store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("On selection failed");
|
||||
store.setPolicyItem("tool", "local:shell", "block");
|
||||
if (store.policyItemState("tool", "local:shell") !== "block" || !store.draft.toolPolicy.blocked.includes("local:shell")) throw new Error("Off selection failed");
|
||||
if (JSON.stringify(store.skillWarnings({ allowed_tools: ["shell"] })) !== JSON.stringify(["shell"])) throw new Error("live skill warning missing");
|
||||
if (store.toolCatalog.length !== 1 || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("Easy custom tool state mismatch");
|
||||
await store.moveAllVisibleTools(true);
|
||||
if (confirmations.at(-1)?.title !== "Allow 1 shown tool?" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("filtered bulk confirmation mismatch");
|
||||
confirmations.length = 0;
|
||||
store.selectedAllowedTools = ["local:shell"];
|
||||
store.useStandardTools();
|
||||
if (store.draft.toolPolicy.mode !== "inherit" || !store.isToolAllowed(store.state.tools.catalog[0]) || store.filteredTools(true).length !== 1 || store.selectedAllowedTools.length) throw new Error("standard tool state mismatch");
|
||||
store.setEasyToolAllowed("local:shell", false);
|
||||
if (store.draft.toolPolicy.mode !== "custom" || store.draft.toolPolicy.default !== "allow" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("Easy uncheck did not block tool");
|
||||
store.setEasyToolAllowed("local:shell", true);
|
||||
if (store.draft.toolPolicy.mode !== "inherit" || !store.isToolAllowed(store.state.tools.catalog[0]) || store.draft.toolPolicy.blocked.length) throw new Error("Easy recheck did not restore standard access");
|
||||
store.draft.toolPolicy = { mode: "custom", default: "block", allowed: [], blocked: [] };
|
||||
store.setEasyToolAllowed("local:shell", true);
|
||||
if (!store.isToolAllowed(store.state.tools.catalog[0]) || !store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("Easy check ignored block-by-default policy");
|
||||
store.setEasyToolAllowed("local:shell", false);
|
||||
if (store.isToolAllowed(store.state.tools.catalog[0]) || store.draft.toolPolicy.allowed.length) throw new Error("Easy uncheck ignored block-by-default policy");
|
||||
store.useStandardTools();
|
||||
store.chooseTools();
|
||||
if (store.draft.toolPolicy.mode !== "custom" || store.draft.toolPolicy.default !== "allow" || store.section !== "3") throw new Error("custom tool editor did not open");
|
||||
store.useStandardTools();
|
||||
store.state.tools.effective_policy = { mode: "inherit", default: "block", allowed: [], blocked: ["local:shell"] };
|
||||
store.chooseTools();
|
||||
if (store.draft.toolPolicy.default !== "allow" || store.draft.toolPolicy.blocked.length) throw new Error("inactive inherited exceptions leaked into custom policy");
|
||||
store.setPolicyItem("tool", "local:shell", "default");
|
||||
if (store.draft.toolPolicy.mode !== "inherit" || store.policyItemState("tool", "local:shell") !== "default") throw new Error("segmented undo did not collapse to inherit");
|
||||
store.setPolicyDefault("tool", "block");
|
||||
store.setPolicyItem("tool", "local:shell", "allow");
|
||||
store.setPolicyDefault("tool", "allow");
|
||||
if (!store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("explicit On was lost when the default changed");
|
||||
store.setPolicyItem("tool", "local:shell", "block");
|
||||
store.setPolicyDefault("tool", "block");
|
||||
if (!store.draft.toolPolicy.blocked.includes("local:shell")) throw new Error("explicit Off was lost when the default changed");
|
||||
store.setPolicyItem("tool", "local:shell", "default");
|
||||
store.setPolicyDefault("tool", "allow");
|
||||
if (store.draft.toolPolicy.mode !== "inherit") throw new Error("default undo did not collapse to inherit");
|
||||
store.setPolicyDefault("mcp", "block");
|
||||
if (store.policyDefault("tool") !== "allow" || store.policyDefault("mcp") !== "block") throw new Error("tool and MCP defaults were not independent");
|
||||
if (!store.isToolAllowed(store.state.tools.catalog[0]) || store.isToolAllowed(store.state.tools.catalog[2])) throw new Error("MCP default affected the wrong catalog group");
|
||||
store.setPolicyItem("mcp", "mcp:docs:read", "allow");
|
||||
store.setPolicyDefault("mcp", "allow");
|
||||
if (!store.draft.toolPolicy.allowed.includes("mcp:docs:read")) throw new Error("explicit MCP On was lost when its default changed");
|
||||
store.setPolicyItem("mcp", "mcp:docs:read", "default");
|
||||
if (store.draft.toolPolicy.mode !== "inherit") throw new Error("MCP default undo did not collapse to inherit");
|
||||
store.projectName = "demo";
|
||||
store.intent = { ...store.intent, projectName: "demo" };
|
||||
if (!store.currentChatUsesScope() || !store.isProfileActive("researcher")) throw new Error("active project profile state mismatch");
|
||||
|
|
@ -468,26 +540,29 @@ confirmResult = false;
|
|||
store.projectName = "other";
|
||||
if (store.currentChatUsesScope() || store.isProfileActive("default")) throw new Error("foreign project profile appeared active");
|
||||
store.projectName = "demo";
|
||||
store.state.tools.effective_policy = { mode: "custom", default: "allow", allowed: [], blocked: ["local:shell"] };
|
||||
store.useStandardTools();
|
||||
store.state.tools.effective_policy = { mode: "custom", default: "allow", mcp_default: "allow", allowed: [], blocked: ["local:shell"] };
|
||||
if (store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope ignored inherited tool restriction");
|
||||
store.setEasyToolAllowed("local:shell", true);
|
||||
store.setPolicyItem("tool", "local:shell", "default");
|
||||
if (store.draft.toolPolicy.mode !== "custom" || !store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope did not customize inherited policy");
|
||||
store.setEasyToolAllowed("local:shell", false);
|
||||
store.setPolicyItem("tool", "local:shell", "allow");
|
||||
if (!store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("project scope did not pin explicit On");
|
||||
store.setPolicyItem("tool", "local:shell", "block");
|
||||
if (store.draft.toolPolicy.mode !== "inherit" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope did not restore inherited policy");
|
||||
store.projectName = "";
|
||||
store.state.tools.effective_policy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
|
||||
store.state.tools.effective_policy = { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] };
|
||||
store.state.skills.catalog = [
|
||||
{ name: "Research", path: "skills/research/SKILL.md", origin: "Agent Zero", description: "Research sources", available: true, tags: [], allowed_tools: [] },
|
||||
{ name: "Gone", path: "skills/gone/SKILL.md", origin: "Unavailable", description: "Missing skill", available: false, tags: [], allowed_tools: [] },
|
||||
];
|
||||
store.draft.skillPolicy = { mode: "custom", default: "allow", allowed: [], blocked: ["Research"] };
|
||||
if (store.skillCatalog.length !== 1 || store.filteredSkills(false).length !== 1 || store.filteredSkills(true).length !== 1) throw new Error("custom skill catalog mismatch");
|
||||
store.selectedBlockedSkills = ["Research"];
|
||||
store.useStandardSkills();
|
||||
if (store.draft.skillPolicy.mode !== "inherit" || store.filteredSkills(true).length !== 1 || store.filteredSkills(false).length || store.selectedBlockedSkills.length) throw new Error("standard skill summary mismatch");
|
||||
store.chooseSkills();
|
||||
if (store.draft.skillPolicy.mode !== "custom" || store.draft.skillPolicy.default !== "allow") throw new Error("custom skill editor did not open");
|
||||
store.draft.skillPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
|
||||
store.initialDraft.skillPolicy = clone(store.draft.skillPolicy);
|
||||
if (store.skillCatalog.length !== 1 || store.filteredSkills().length !== 2) throw new Error("Easy/Advanced skill catalog mismatch");
|
||||
store.setPolicyItem("skill", "Research", "allow");
|
||||
if (!store.draft.skillPolicy.allowed.includes("Research")) throw new Error("skill On selection failed");
|
||||
store.setPolicyItem("skill", "Research", "block");
|
||||
if (!store.draft.skillPolicy.blocked.includes("Research")) throw new Error("skill Off selection failed");
|
||||
store.setPolicyItem("skill", "Research", "default");
|
||||
if (store.draft.skillPolicy.mode !== "inherit") throw new Error("skill sparse undo did not collapse");
|
||||
store.draft.title = "Preserved Agent";
|
||||
store.onNameInput();
|
||||
store.instructions.value = "Preserved instructions";
|
||||
|
|
@ -499,6 +574,7 @@ store.mode = "advanced";
|
|||
store.instructions.value = "";
|
||||
const callsBeforeInvalidAdvancedCreate = calls.length;
|
||||
if (!store.fieldIssue("instructions")) throw new Error("Advanced create accepted empty instructions");
|
||||
if (store.fieldIssue("instructions")?.field !== "agent-editor-prompt-ace") throw new Error("Advanced validation targeted the wrong editor");
|
||||
await store.save();
|
||||
if (calls.length !== callsBeforeInvalidAdvancedCreate) throw new Error("Advanced create submitted empty instructions");
|
||||
store.instructions.value = "Preserved instructions";
|
||||
|
|
@ -525,7 +601,7 @@ store.state = {
|
|||
],
|
||||
model_preset: { has_override: false, effective: "Default" },
|
||||
model_presets: [],
|
||||
tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
|
||||
tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
|
||||
{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
|
||||
{ id: "local:old", name: "old", label: "Old", origin: "Old scope", available: true },
|
||||
] },
|
||||
|
|
@ -537,6 +613,25 @@ store.state = {
|
|||
store.view = "editor";
|
||||
store.intent = { ...store.intent, view: "create", projectName: "" };
|
||||
store.makeDraft(true);
|
||||
store.root = {
|
||||
querySelector: selector => selector === "#agent-editor-prompt-ace" ? aceContainer : null,
|
||||
contains: node => node === aceContainer,
|
||||
closest: () => null,
|
||||
};
|
||||
store.mode = "advanced";
|
||||
store.section = "2";
|
||||
const draftBeforeAce = JSON.stringify(store.draft);
|
||||
store.initPromptEditor();
|
||||
if (aceState.mode !== "ace/mode/markdown" || aceState.wrap !== true || aceState.options?.showPrintMargin !== false || aceState.options?.useWorker !== false) throw new Error("ACE configuration mismatch");
|
||||
if (aceState["aria-label"] !== "Prompt Markdown" || JSON.stringify(store.draft) !== draftBeforeAce) throw new Error("ACE initialization created a false edit");
|
||||
aceState.value = "Edited in ACE";
|
||||
aceState.change();
|
||||
if (store.instructions.value !== "Edited in ACE" || !store.promptEditPending(store.instructions)) throw new Error("ACE change did not update the prompt draft");
|
||||
store.selectPrompt("agent.system.main.communication.md");
|
||||
if (aceState.value !== "Old comm" || store.instructions.value !== "Edited in ACE") throw new Error("ACE file switch lost a draft");
|
||||
store.selectPrompt("agent.system.main.specifics.md");
|
||||
store.destroyPromptEditor();
|
||||
if (!aceState.destroyed || aceState.change) throw new Error("ACE instance was not destroyed cleanly");
|
||||
store.draft.title = "Scoped Agent";
|
||||
store.onNameInput();
|
||||
store.draft.description = "Scoped description";
|
||||
|
|
@ -546,10 +641,9 @@ store.markPromptSet("agent.system.main.specifics.md");
|
|||
store.draft.prompts["agent.system.main.communication.md"].value = "Authored communication";
|
||||
store.acceptPromptEdit("agent.system.main.communication.md");
|
||||
store.chooseAvatarColor("#ABCDEF");
|
||||
store.setEasyToolAllowed("local:shell", false);
|
||||
store.setPolicyItem("tool", "local:shell", "block");
|
||||
store.setPolicyDefault("tool", "block");
|
||||
store.chooseSkills();
|
||||
store.moveSkills(["Research"], false);
|
||||
store.setPolicyItem("skill", "Research", "block");
|
||||
const projectState = {
|
||||
profile: { id: "new-agent", avatar_url: "", metadata: { title: {}, description: {}, context: {}, avatar: { effective: { kind: "color", value: "#222222" } } } },
|
||||
prompts: [
|
||||
|
|
@ -558,7 +652,7 @@ const projectState = {
|
|||
],
|
||||
model_preset: { has_override: false, effective: "Default" },
|
||||
model_presets: [],
|
||||
tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
|
||||
tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
|
||||
{ id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
|
||||
{ id: "local:new", name: "new", label: "New", origin: "Project", available: true },
|
||||
] },
|
||||
|
|
@ -569,7 +663,7 @@ const projectState = {
|
|||
};
|
||||
loadHandler = () => ({ ok: true, state: projectState });
|
||||
store.mode = "advanced";
|
||||
store.section = "5";
|
||||
store.section = "6";
|
||||
store.projectName = "demo";
|
||||
store.intent = { ...store.intent, projectName: "" };
|
||||
calls.length = 0;
|
||||
|
|
@ -577,7 +671,7 @@ await store.onScopeChanged();
|
|||
if (store.state !== projectState || store.draft.title !== "Scoped Agent" || store.draft.profileId !== "scoped-agent") throw new Error("create scope rebase lost identity");
|
||||
if (store.draft.description !== "Scoped description" || store.draft.context !== "Use for scoped work") throw new Error("create scope rebase lost authored metadata");
|
||||
if (store.instructions.value !== "Authored instructions" || store.instructions.source !== "project-source") throw new Error("create scope rebase kept stale prompt provenance");
|
||||
if (store.draft.avatar?.value !== "#ABCDEF" || store.isToolAllowed(projectState.tools.catalog[0]) || !store.isToolAllowed(projectState.tools.catalog[1]) || store.draft.toolPolicy.default !== "block") throw new Error("create scope rebase lost avatar or explicit tool decision");
|
||||
if (store.draft.avatar?.value !== "#ABCDEF" || store.isToolAllowed(projectState.tools.catalog[0]) || store.isToolAllowed(projectState.tools.catalog[1]) || store.draft.toolPolicy.default !== "block") throw new Error("create scope rebase lost avatar or tool policy");
|
||||
if (store.isSkillAllowed(projectState.skills.catalog[0]) || !store.isSkillAllowed(projectState.skills.catalog[1])) throw new Error("create scope rebase lost explicit skill decision");
|
||||
if (store.draft.prompts["agent.system.main.communication.md"].value !== "Authored communication" || store.draft.prompts["agent.system.main.communication.md"].source !== "project-source" || store.promptEditPending(store.draft.prompts["agent.system.main.communication.md"])) throw new Error("create scope rebase lost an accepted prompt edit or kept stale provenance");
|
||||
if (calls.at(-1)?.payload?.action !== "plan" || calls.at(-1)?.payload?.project_name !== "demo" || store.planStatus !== "ready" || !store.plan.written[0].startsWith("usr/projects/demo/.a0proj/agents/scoped-agent/")) throw new Error("Review plan was not recomputed after scope change");
|
||||
|
|
@ -602,14 +696,13 @@ store.onPromptInput(communication.filename);
|
|||
store.acceptPromptEdit(communication.filename);
|
||||
if (store.promptEditPending(communication)) throw new Error("prompt edit was not accepted");
|
||||
if (store.promptDisplayState(communication) !== "Customized by you") throw new Error("customized state missing");
|
||||
if (store.promptSourceChain(communication) !== "Customized by you") throw new Error("customized provenance missing");
|
||||
store.resetPrompt(communication.filename);
|
||||
if (store.promptEditPending(communication) || store.promptDisplayState(communication) !== "Will use the default") throw new Error("reset state mismatch");
|
||||
const draftBeforeModes = JSON.stringify(store.draft);
|
||||
store.setMode("advanced", "2");
|
||||
store.setMode("easy");
|
||||
if (store.section !== "2" || JSON.stringify(store.draft) !== draftBeforeModes) throw new Error("mode switch lost draft");
|
||||
store.setMode("advanced", "5");
|
||||
store.setMode("advanced", "6");
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
if (store.planStatus !== "ready" || calls.at(-1).payload.action !== "plan") throw new Error("review plan was not computed on entry");
|
||||
|
|
@ -634,7 +727,7 @@ if (calls.length || !store.error.includes("Save or discard")) throw new Error("d
|
|||
store.initialDraft = clone(store.draft);
|
||||
store.error = "";
|
||||
await store.planRemoval(true);
|
||||
if (!store.pendingMutation?.destructive || store.section !== "5" || store.planStatus !== "ready") throw new Error("removal plan was replaced");
|
||||
if (!store.pendingMutation?.destructive || store.section !== "6" || store.planStatus !== "ready") throw new Error("removal plan was replaced");
|
||||
if (calls.at(-1).payload.action !== "plan_remove_changes") throw new Error("removal plan request missing");
|
||||
if (calls.at(-1).payload.project_name !== "demo") throw new Error("removal request lost selected scope");
|
||||
const callsBeforePendingSave = calls.length;
|
||||
|
|
@ -704,9 +797,13 @@ const store = { ...switcherState, ...switcherMethods };
|
|||
const older = store.loadAgentProfiles(true);
|
||||
const newer = store.loadAgentProfiles(true);
|
||||
if (pending.length !== 2 || !store.agentProfilesLoading) throw new Error("overlapping profile loads did not start");
|
||||
pending[1]({ profiles: [{ id: "new", title: "New", enabled: true }] });
|
||||
pending[1]({ profiles: [
|
||||
{ id: "default", title: "Default", enabled: true },
|
||||
{ id: "new", title: "New", enabled: true },
|
||||
] });
|
||||
await newer;
|
||||
if (store.agentProfiles[0]?.key !== "new" || store.agentProfilesLoading || !store.agentProfilesLoaded) throw new Error("newest profile load did not settle");
|
||||
if (store.agentProfiles.length !== 1 || store.getAgentProfileList("default", "Default").some(profile => profile.key === "default")) throw new Error("Default profile remained selectable in the chat popover");
|
||||
pending[0]({ profiles: [{ id: "old", title: "Old", enabled: true }] });
|
||||
await older;
|
||||
if (store.agentProfiles[0]?.key !== "new" || store.agentProfilesLoading || !store.agentProfilesLoaded) throw new Error("stale profile load replaced newer state");
|
||||
|
|
|
|||
|
|
@ -53,10 +53,11 @@ def _prompt_paths(root: Path):
|
|||
return get_paths
|
||||
|
||||
|
||||
def _custom_policy(*, default: str, allowed=(), blocked=()):
|
||||
def _custom_policy(*, default: str, mcp_default: str = "allow", allowed=(), blocked=()):
|
||||
return {
|
||||
"mode": "custom",
|
||||
"default": default,
|
||||
"mcp_default": mcp_default,
|
||||
"allowed": list(allowed),
|
||||
"blocked": list(blocked),
|
||||
}
|
||||
|
|
@ -157,6 +158,25 @@ def test_required_response_survives_default_block(monkeypatch, tmp_path: Path) -
|
|||
assert tool_policy.get_tool_catalog(agent) == []
|
||||
|
||||
|
||||
def test_tool_and_mcp_defaults_are_independent(monkeypatch, tmp_path: Path) -> None:
|
||||
agent = _Agent(tmp_path)
|
||||
monkeypatch.setattr(
|
||||
tool_policy,
|
||||
"get_policy",
|
||||
lambda _agent: _custom_policy(
|
||||
default="block",
|
||||
mcp_default="allow",
|
||||
allowed=["local:pinned"],
|
||||
blocked=["mcp:docs:delete"],
|
||||
),
|
||||
)
|
||||
|
||||
assert tool_policy.resolve_tool(agent, "shell", canonical_id="local:shell").allowed is False
|
||||
assert tool_policy.resolve_tool(agent, "read", canonical_id="mcp:docs:read").allowed is True
|
||||
assert tool_policy.resolve_tool(agent, "pinned", canonical_id="local:pinned").allowed is True
|
||||
assert tool_policy.resolve_tool(agent, "delete", canonical_id="mcp:docs:delete").allowed is False
|
||||
|
||||
|
||||
def test_catalog_comes_from_executable_tools_not_prompt_names(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue