mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-08-21 06:15:11 +00:00
Fix Agent Editor profile lifecycle state
Base reset visibility on the sparse removal plan, preserve unrelated profile files, and surface completion feedback without waiting for model-profile refreshes. Keep saved preset controls and fresh-chat selection synchronized across global and project scopes, including the shared sidebar context race, with focused regressions and updated DOX.
This commit is contained in:
parent
13ecab24ee
commit
c327701c83
9 changed files with 67 additions and 16 deletions
|
|
@ -68,7 +68,8 @@
|
|||
row exposes scoped availability, duplication, restore for inherited profiles,
|
||||
icon-only Edit, and Delete for profiles owned by that scope. Duplicate
|
||||
materializes the effective source profile into the selected writable layer
|
||||
with a collision-free ID and title.
|
||||
with a collision-free ID and title. Restore visibility follows the sparse
|
||||
removal plan, ignores unrelated profile files, and reports successful reset.
|
||||
Availability changes quietly refresh the adjacent profile switcher catalog
|
||||
without a success toast.
|
||||
- The same project selector is available inside Create and Edit. Create keeps
|
||||
|
|
|
|||
|
|
@ -166,12 +166,6 @@ def _profile_root(profile_id: str, project_name: str = "") -> Path:
|
|||
)
|
||||
|
||||
|
||||
def _scope_has_files(root: Path) -> bool:
|
||||
return root.is_dir() and any(
|
||||
path.is_file() or path.is_symlink() for path in root.rglob("*")
|
||||
)
|
||||
|
||||
|
||||
def _scope_owns_custom_profile(profile_id: str, project_name: str) -> bool:
|
||||
if not _profile_root(profile_id, project_name).is_dir():
|
||||
return False
|
||||
|
|
@ -222,7 +216,10 @@ def list_profiles(context: Any | None = None) -> list[dict[str, Any]]:
|
|||
"origin": state["origin"],
|
||||
"origin_chain": state["origin_chain"],
|
||||
"built_in": state["built_in"],
|
||||
"scope_has_overrides": state["scope_has_overrides"],
|
||||
"scope_has_overrides": bool(
|
||||
profile_exists(profile_id, context)
|
||||
and plan_remove_changes(profile_id, context).changes
|
||||
),
|
||||
"deletable": state["deletable"],
|
||||
"avatar": state["avatar"]["effective"],
|
||||
"avatar_url": effective_avatar_url(profile_id, context),
|
||||
|
|
@ -348,7 +345,10 @@ def build_profile_state(
|
|||
"origin": metadata.pop("origin"),
|
||||
"origin_chain": metadata.pop("origin_chain"),
|
||||
"built_in": metadata.pop("built_in"),
|
||||
"scope_has_overrides": metadata.pop("scope_has_overrides"),
|
||||
"scope_has_overrides": bool(
|
||||
profile_exists(profile_id, context)
|
||||
and plan_remove_changes(profile_id, context).changes
|
||||
),
|
||||
"deletable": metadata.pop("deletable"),
|
||||
"metadata": metadata,
|
||||
"avatar_url": effective_avatar_url(profile_id, context),
|
||||
|
|
@ -378,7 +378,6 @@ def metadata_state(profile_id: str, context: Any | None = None) -> dict[str, Any
|
|||
"inherited_source": _relative_source(inherited_source),
|
||||
}
|
||||
|
||||
scope_root = _profile_root(profile_id, project_name)
|
||||
built_in = (Path(files.get_abs_path("agents")) / profile_id).is_dir()
|
||||
plugin_origin = any(layer.kind == "plugin" for layer in layers)
|
||||
state.update(
|
||||
|
|
@ -386,7 +385,6 @@ def metadata_state(profile_id: str, context: Any | None = None) -> dict[str, Any
|
|||
"origin": "Built-in" if built_in else "Plugin" if plugin_origin else "Custom",
|
||||
"origin_chain": list(dict.fromkeys(layer.kind for layer in layers)),
|
||||
"built_in": built_in,
|
||||
"scope_has_overrides": _scope_has_files(scope_root),
|
||||
"deletable": _scope_owns_custom_profile(profile_id, project_name),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -346,6 +346,10 @@ const model = {
|
|||
...this.scopeInput(),
|
||||
});
|
||||
await this.loadProfiles();
|
||||
globalThis.justToast?.(
|
||||
`${profile.title || profile.id} reset to default.`,
|
||||
"success", 1800, "agent-profile-reset",
|
||||
);
|
||||
await modelConfigStore.loadAgentProfiles(true);
|
||||
if (editorState) {
|
||||
await this.loadEditor(profile.id, false);
|
||||
|
|
@ -1275,7 +1279,7 @@ const model = {
|
|||
context_id: created.ctxid,
|
||||
...(scopeProject ? { name: scopeProject } : {}),
|
||||
});
|
||||
await callJsonApi("/agent_profile_set", {
|
||||
const selectedProfile = await callJsonApi("/agent_profile_set", {
|
||||
context_id: created.ctxid,
|
||||
agent_profile: profileId,
|
||||
});
|
||||
|
|
@ -1288,6 +1292,10 @@ const model = {
|
|||
try { sessionStorage.setItem(READY_NOTE_KEY, created.ctxid); } catch {}
|
||||
}
|
||||
await chatsStore.selectChat(created.ctxid);
|
||||
if (chatsStore.selectedContext) {
|
||||
chatsStore.selectedContext.agent_profile = selectedProfile.agent_profile || profileId;
|
||||
chatsStore.selectedContext.agent_profile_label = selectedProfile.agent_profile_label || profileId;
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent("chat-created", { detail: { ctxid: created.ctxid } }));
|
||||
return created.ctxid;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@
|
|||
<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">
|
||||
<select id="agent-editor-easy-model-preset" x-model="$store.agentEditor.draft.modelPreset" x-effect="$nextTick(() => { $el.value = $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>
|
||||
|
|
@ -259,7 +259,7 @@
|
|||
<div class="agent-field wide agent-model-preset">
|
||||
<label for="agent-editor-model-preset" class="agent-field-label">Model preset</label>
|
||||
<div class="agent-model-preset-picker">
|
||||
<select id="agent-editor-model-preset" x-model="$store.agentEditor.draft.modelPreset">
|
||||
<select id="agent-editor-model-preset" x-model="$store.agentEditor.draft.modelPreset" x-effect="$nextTick(() => { $el.value = $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>
|
||||
|
|
|
|||
|
|
@ -351,6 +351,15 @@ def test_plugin_configs_preserve_unowned_keys_and_use_json(user_root: Path) -> N
|
|||
assert skill_data["active_skills"] == [{"name": "existing"}]
|
||||
assert skill_data["visibility_policy"]["default"] == "block"
|
||||
|
||||
editor.apply_change_plan(editor.plan_remove_changes("researcher"))
|
||||
|
||||
assert json.loads(model.read_text()) == {"manual": 1}
|
||||
assert json.loads(tools.read_text()) == {"manual": 2}
|
||||
assert json.loads(skill.read_text()) == {
|
||||
"active_skills": [{"name": "existing"}]
|
||||
}
|
||||
assert editor.build_profile_state("researcher")["scope_has_overrides"] is False
|
||||
|
||||
|
||||
def test_model_and_off_tool_choices_write_only_their_json_contracts(
|
||||
user_root: Path,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls
|
|||
assert "No optional tools" not in modal
|
||||
assert 'class="agent-model-preset-picker"' in modal
|
||||
assert modal.count('x-model="$store.agentEditor.draft.modelPreset"') == 2
|
||||
assert modal.count('$el.value = $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"')
|
||||
|
|
@ -304,13 +305,18 @@ def test_local_slugging_and_fresh_chat_profile_selection_are_deterministic() ->
|
|||
harness = r"""
|
||||
const calls = [];
|
||||
const confirmations = [];
|
||||
const toasts = [];
|
||||
let setEnabledHandler = null;
|
||||
let loadHandler = null;
|
||||
let profilesHandler = null;
|
||||
let confirmResult = false;
|
||||
const createStore = (_name, value) => value;
|
||||
const callJsonApi = async (endpoint, payload) => {
|
||||
calls.push({ endpoint, payload });
|
||||
if (payload?.action === "load" && loadHandler) return loadHandler(payload);
|
||||
if (payload?.action === "list") return profilesHandler
|
||||
? profilesHandler(payload)
|
||||
: { ok: true, profiles: [] };
|
||||
if (payload?.action === "set_enabled") return setEnabledHandler
|
||||
? setEnabledHandler(payload)
|
||||
: { ok: true, active_profile: "default", active_profile_label: "Default" };
|
||||
|
|
@ -328,6 +334,11 @@ const callJsonApi = async (endpoint, payload) => {
|
|||
title: "Researcher 1",
|
||||
profiles: [{ id: "researcher" }, { id: "researcher-1" }],
|
||||
};
|
||||
if (endpoint === "/agent_profile_set") return {
|
||||
ok: true,
|
||||
agent_profile: payload.agent_profile,
|
||||
agent_profile_label: "Researcher",
|
||||
};
|
||||
return endpoint === "/chat_create" ? { ok: true, ctxid: "fresh-chat" } : { ok: true };
|
||||
};
|
||||
const fetchApi = async () => ({ ok: true, json: async () => ({}) });
|
||||
|
|
@ -337,7 +348,11 @@ const showConfirmDialog = async options => { confirmations.push(options); return
|
|||
const chatsStore = {
|
||||
selected: "old-chat",
|
||||
selectedContext: { project: { name: "demo" }, agent_profile: "researcher" },
|
||||
selectChat: async (id) => calls.push({ endpoint: "selectChat", payload: id }),
|
||||
selectChat: async (id) => {
|
||||
calls.push({ endpoint: "selectChat", payload: id });
|
||||
chatsStore.selected = id;
|
||||
chatsStore.selectedContext = { agent_profile: "default", agent_profile_label: "Default" };
|
||||
},
|
||||
};
|
||||
const modelConfigStore = {
|
||||
loadAgentProfiles: async force => calls.push({ endpoint: "loadAgentProfiles", payload: force }),
|
||||
|
|
@ -382,6 +397,7 @@ globalThis.sessionStorage = { setItem: () => {}, getItem: () => "", removeItem:
|
|||
globalThis.localStorage = { setItem: () => {}, getItem: () => "" };
|
||||
globalThis.requestAnimationFrame = callback => callback();
|
||||
globalThis.confirm = () => true;
|
||||
globalThis.justToast = (...args) => toasts.push(args);
|
||||
"""
|
||||
checks = r"""
|
||||
if (slugifyProfileName(" Crème Brûlée__Lab ") !== "creme-brulee-lab") throw new Error("slug mismatch");
|
||||
|
|
@ -519,9 +535,15 @@ confirmResult = false;
|
|||
await store.restoreProfile({ id: "researcher", title: "Researcher", scope_has_overrides: true, deletable: false });
|
||||
if (calls.length || confirmations.at(-1)?.title !== "Reset Researcher to default?") throw new Error("restore cancellation failed");
|
||||
confirmResult = true;
|
||||
profilesHandler = () => ({
|
||||
ok: true,
|
||||
profiles: [{ id: "researcher", title: "Researcher", scope_has_overrides: false }],
|
||||
});
|
||||
await store.restoreProfile({ id: "researcher", title: "Researcher", scope_has_overrides: true, deletable: false });
|
||||
if (!calls.some(item => item.payload?.action === "remove_changes" && item.payload.profile_id === "researcher" && item.payload.destructive === false)) throw new Error("reset to default did not use sparse removal");
|
||||
if (!calls.some(item => item.endpoint === "loadAgentProfiles" && item.payload === true) || store.saving) throw new Error("reset to default did not refresh profile state");
|
||||
if (store.profiles[0]?.scope_has_overrides || toasts.at(-1)?.[0] !== "Researcher reset to default.") throw new Error("reset to default kept its action or omitted feedback");
|
||||
profilesHandler = null;
|
||||
let editorReload = null;
|
||||
const loadEditor = store.loadEditor;
|
||||
const setMode = store.setMode;
|
||||
|
|
@ -716,6 +738,7 @@ if (calls[1].payload.action !== "deactivate") throw new Error("global test chat
|
|||
if (calls[2].payload.agent_profile !== "researcher") throw new Error("profile not selected");
|
||||
if (calls[3].payload.action !== "clear") throw new Error("chat preset override not cleared");
|
||||
if (store.readyNoteContext !== "fresh-chat") throw new Error("ready note missing");
|
||||
if (chatsStore.selectedContext.agent_profile !== "researcher" || chatsStore.selectedContext.agent_profile_label !== "Researcher") throw new Error("fresh chat showed a stale profile");
|
||||
calls.length = 0;
|
||||
store.projectName = "demo";
|
||||
await store.openFreshChat("researcher", false);
|
||||
|
|
|
|||
|
|
@ -69,6 +69,14 @@ const chats = [
|
|||
{{ id: "c", created_at: 10 }},
|
||||
];
|
||||
|
||||
reset(chats, "b");
|
||||
globalThis.__context = "a";
|
||||
await model.selectChat("a");
|
||||
assert(
|
||||
model.selected === "a" && model.selectedContext?.id === "a",
|
||||
"selection state must catch up when the low-level context already switched",
|
||||
);
|
||||
|
||||
let resolveDelete;
|
||||
globalThis.__sendJsonData = () => new Promise((resolve) => {{ resolveDelete = resolve; }});
|
||||
reset(chats, "a");
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
- Process-detail preference changes must use the message renderer's async expansion hooks and honor an explicit chat-history render target so staged pages are ready before an atomic swap.
|
||||
- The utility-message preference controls both individual utility steps and utility-only process-group chrome so hidden utility runs cannot leave empty headers in the transcript.
|
||||
- Chat deletion removes the sidebar row optimistically in the same render batch as fallback selection. Keep successful local deletion tombstones for the page session so out-of-order poll or push snapshots cannot reinsert rows; restore the row and clear its tombstone if the delete request fails.
|
||||
- Chat selection must synchronize the sidebar store even when the low-level context has already switched to the requested ID.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,10 @@ const model = {
|
|||
if (!id || this.deletedContextIds[id]) return;
|
||||
|
||||
const currentContext = getContext();
|
||||
if (id === currentContext) return; // already selected
|
||||
if (id === currentContext) {
|
||||
this.setSelected(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Proceed with context selection
|
||||
setContext(id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue