mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(ui): move Agent Defaults entry from the sidebar into the Agents page (#115836)
* refactor(ui): move Agent Defaults entry from the sidebar into the Agents page * fix(ui): keep Agent Defaults searchable
This commit is contained in:
committed by
GitHub
parent
662abec754
commit
af70829cdf
@@ -282,14 +282,14 @@ select it to open the owning Approvals page.
|
||||
</Accordion>
|
||||
<Accordion title="Config">
|
||||
- View/edit `~/.openclaw/openclaw.json` (`config.get`, `config.set`).
|
||||
- Settings navigation starts with Ask OpenClaw, Profile, Appearance, and Notifications up top; Connections (Connection, Channels, Communications, Talk, Devices); Agents & Tools (Agents, AI & Agents, Labs, Models, MCP, Memory, Automation); Privacy & Security (Security, Approvals); and System (Infrastructure, Advanced, Debug, Logs, About). Language leads the Appearance page, model defaults live on Models, and Gateway host details live on Connection.
|
||||
- Settings navigation starts with Ask OpenClaw, Profile, Appearance, and Notifications up top; Connections (Connection, Channels, Communications, Talk, Devices); Agents & Tools (Agents, Labs, Models, MCP, Memory, Automation); Privacy & Security (Security, Approvals); and System (Infrastructure, Advanced, Debug, Logs, About). Language leads the Appearance page, model defaults live on Models, and Gateway host details live on Connection.
|
||||
- Privacy & Security: curated rows for gateway auth, exec policy, browser enablement, tool profile, device auth, and mobile pairing, above the schema-backed `security`/`approvals` sections.
|
||||
- Approvals includes newest-first, 30-day history for resolved exec, plugin, and system-agent requests. Filter by kind or page through older rows to review the decision, reason, source session, and resolver attribution recorded by the Gateway.
|
||||
- Labs exposes shipped experimental switches. Code Mode and Swarm are the current entries and save `tools.codeMode.enabled` and `tools.swarm.enabled` immediately; unshipped experiments do not appear or write speculative config keys.
|
||||
- Notifications: browser web-push status, subscribe/unsubscribe, and a test send.
|
||||
- Advanced: every config section without a curated home, plus the raw JSON5 editor (previously the General page's Advanced mode).
|
||||
- Model Setup (`/settings/model-setup`) is a subpage of Model Providers, launched from its header.
|
||||
- Agents: a settings page (**Settings → Agents**, `/settings/agents`) with per-agent tabs (Overview, Files, Tools, Skills, Channels, Automations, Memory). The Overview tab edits the agent's identity — display name, emoji, and an avatar image that is downscaled and size-bounded in the browser before `agents.update`. Saving stores configured identity fields and mirrors them to the workspace `IDENTITY.md`; configured values take precedence over manual edits to the same file fields.
|
||||
- Agents: a settings page (**Settings → Agents**, `/settings/agents`) with an **Agent defaults** row for the shared template plus per-agent tabs (Overview, Files, Tools, Skills, Channels, Automations, Memory). The Overview tab edits the agent's identity — display name, emoji, and an avatar image that is downscaled and size-bounded in the browser before `agents.update`. Saving stores configured identity fields and mirrors them to the workspace `IDENTITY.md`; configured values take precedence over manual edits to the same file fields.
|
||||
- Profile: a settings page showing the default agent's identity with all-time usage stats — lifetime tokens, peak day, longest session, activity streaks, a year-long token heatmap, top tools, and channel highlights (`usage.cost`, `sessions.usage`).
|
||||
- MCP has a dedicated settings page with server rows (transport, enablement, OAuth/filter/parallel summaries), direct add/enable/disable/remove controls, common operator commands, and the scoped `mcp` config editor. The Plugins page remains the home for one-click connectors and discovery.
|
||||
- Model Providers: a settings page listing every configured model provider with its brand icon, auth state (`models.authStatus`), model availability (`models.list`), live plan/quota/billing data where the provider reports it (`usage.status`), and local session spend for the last 30 days (`sessions.usage`). A Refresh action re-reads credential state and provider usage.
|
||||
|
||||
@@ -62,6 +62,12 @@ describe("sidebar entries", () => {
|
||||
expect(settingsNavigationOwnerRoute("model-setup")).toBe("model-providers");
|
||||
});
|
||||
|
||||
it("keeps Agent Defaults routed as an Agents subpage without a sidebar entry", () => {
|
||||
expect(settingsRoutes).not.toContain("ai-agents");
|
||||
expect(isSettingsNavigationRoute("ai-agents")).toBe(true);
|
||||
expect(settingsNavigationOwnerRoute("ai-agents")).toBe("agents");
|
||||
});
|
||||
|
||||
it("keeps devices in connection settings and drops stale pinned entries", () => {
|
||||
expect(SIDEBAR_NAV_ROUTES).not.toContain("nodes");
|
||||
expect(settingsRoutes).toContain("nodes");
|
||||
|
||||
@@ -132,6 +132,7 @@ const ALL_ROUTES: RouteId[] = Array.from(
|
||||
// Hub tabs and settings subpages route without their own nav entry.
|
||||
"worktrees",
|
||||
"memory-import",
|
||||
"ai-agents",
|
||||
"model-setup",
|
||||
"lobsterdex",
|
||||
...SETTINGS_NAVIGATION_GROUPS.flatMap((group) => group.routes),
|
||||
@@ -787,7 +788,6 @@ describe("SIDEBAR_NAV_ROUTES", () => {
|
||||
"talk",
|
||||
"nodes",
|
||||
"agents",
|
||||
"ai-agents",
|
||||
"labs",
|
||||
"model-providers",
|
||||
"mcp",
|
||||
|
||||
@@ -185,7 +185,7 @@ export const SETTINGS_NAVIGATION_GROUPS = [
|
||||
},
|
||||
{
|
||||
labelKey: "nav.settingsGroupAgents",
|
||||
routes: ["agents", "ai-agents", "labs", "model-providers", "mcp", "memory", "automation"],
|
||||
routes: ["agents", "labs", "model-providers", "mcp", "memory", "automation"],
|
||||
},
|
||||
{
|
||||
labelKey: "nav.settingsGroupSecurity",
|
||||
@@ -200,10 +200,16 @@ export const SETTINGS_NAVIGATION_GROUPS = [
|
||||
// Settings subpages render with settings chrome but stay out of the sidebar.
|
||||
// Subpages with a visible owner keep that owner selected so users retain
|
||||
// location context while completing the nested flow.
|
||||
const SETTINGS_SUBPAGE_ROUTES: readonly NavigationRouteId[] = ["model-setup", "lobsterdex"];
|
||||
const SETTINGS_SUBPAGE_ROUTES: readonly NavigationRouteId[] = [
|
||||
"ai-agents",
|
||||
"model-setup",
|
||||
"lobsterdex",
|
||||
];
|
||||
export const SETTINGS_SEARCHABLE_SUBPAGE_ROUTES: readonly NavigationRouteId[] = ["ai-agents"];
|
||||
const SETTINGS_SUBPAGE_OWNER_ROUTES: Partial<
|
||||
Readonly<Record<NavigationRouteId, NavigationRouteId>>
|
||||
> = {
|
||||
"ai-agents": "agents",
|
||||
"model-setup": "model-providers",
|
||||
};
|
||||
|
||||
|
||||
@@ -235,6 +235,34 @@ describe("settings sidebar search", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("finds Agent Defaults by page name after its sidebar demotion", () => {
|
||||
render(
|
||||
renderSettingsSidebar({
|
||||
basePath: "",
|
||||
activeRouteId: "agents",
|
||||
offline: false,
|
||||
lastError: null,
|
||||
version: "",
|
||||
updateAvailable: null,
|
||||
updateRunning: false,
|
||||
onUpdate: vi.fn(),
|
||||
searchQuery: "agent defaults",
|
||||
onExit: vi.fn(),
|
||||
onRetryConnect: vi.fn(),
|
||||
onNavigate: vi.fn(),
|
||||
onSearchQueryChange: vi.fn(),
|
||||
preloadTimers: new Map(),
|
||||
saveIndicator: saveIndicator(),
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const result = container.querySelector<HTMLAnchorElement>(
|
||||
'.settings-sidebar__item[href="/settings/ai-agents"]',
|
||||
);
|
||||
expect(result?.textContent?.trim()).toBe("Agent Defaults");
|
||||
});
|
||||
|
||||
it("keeps Memory search results on the canonical Settings tab path", () => {
|
||||
const onNavigate = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
navigationIconForRoute,
|
||||
scheduleRoutePreload,
|
||||
SETTINGS_NAVIGATION_GROUPS,
|
||||
SETTINGS_SEARCHABLE_SUBPAGE_ROUTES,
|
||||
settingsNavigationLabelForRoute,
|
||||
settingsNavigationOwnerRoute,
|
||||
settingsSearchTextMatches,
|
||||
@@ -79,8 +80,15 @@ function filterSettingsNavigationGroups(
|
||||
items: group.routes.map((routeId) => ({ routeId, blocks: [] })),
|
||||
}));
|
||||
}
|
||||
const allRoutes = SETTINGS_NAVIGATION_GROUPS.flatMap((group) => group.routes);
|
||||
const directRoutes = allRoutes.filter((routeId) =>
|
||||
const sidebarRoutes = SETTINGS_NAVIGATION_GROUPS.flatMap((group) => group.routes);
|
||||
const searchableRoutes = [
|
||||
...new Set([
|
||||
...sidebarRoutes,
|
||||
...SETTINGS_SEARCHABLE_SUBPAGE_ROUTES,
|
||||
...blockMatches.map((block) => block.routeId),
|
||||
]),
|
||||
];
|
||||
const directRoutes = searchableRoutes.filter((routeId) =>
|
||||
[
|
||||
settingsNavigationLabelForRoute(routeId),
|
||||
titleForRoute(routeId),
|
||||
@@ -128,7 +136,7 @@ function filterSettingsNavigationGroups(
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...allRoutes
|
||||
...searchableRoutes
|
||||
.filter((routeId) => !includedRoutes.has(routeId) && blocksByRoute.has(routeId))
|
||||
.map((routeId) => ({
|
||||
labelKey: null,
|
||||
|
||||
@@ -159,6 +159,16 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
type: "object",
|
||||
title: "Tools",
|
||||
properties: {
|
||||
profile: {
|
||||
type: "string",
|
||||
description: "Controls sandbox access",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
@@ -245,6 +255,7 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
await holdUiProof(page);
|
||||
const settingsLinks = settingsSidebar.locator(".settings-sidebar__item");
|
||||
const allSettingsLabels = await trimmedTextContents(settingsLinks);
|
||||
expect(allSettingsLabels).not.toContain("Agent Defaults");
|
||||
await expect
|
||||
.poll(() =>
|
||||
settingsSearch.evaluate((input) => {
|
||||
@@ -370,6 +381,28 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
await settingsSidebar.getByRole("button", { name: "Clear settings search" }).click();
|
||||
await expect.poll(() => trimmedTextContents(settingsLinks)).toEqual(allSettingsLabels);
|
||||
await holdUiProof(page, 300);
|
||||
await settingsSidebar.getByRole("link", { name: "Agents", exact: true }).click();
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/agents");
|
||||
const agentDefaultsRow = page.getByRole("button", {
|
||||
name: "Agent defaults Defaults every agent inherits unless overridden.",
|
||||
});
|
||||
await expect.poll(() => agentDefaultsRow.isVisible()).toBe(true);
|
||||
await agentDefaultsRow.click();
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/ai-agents");
|
||||
await expect
|
||||
.poll(() =>
|
||||
settingsSidebar
|
||||
.getByRole("link", { name: "Agents", exact: true })
|
||||
.getAttribute("aria-current"),
|
||||
)
|
||||
.toBe("page");
|
||||
await settingsSearch.fill("sandbox access");
|
||||
const toolsResult = settingsSidebar.getByRole("link", { name: "Tools", exact: true });
|
||||
await expect.poll(() => toolsResult.isVisible()).toBe(true);
|
||||
await toolsResult.click();
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/ai-agents");
|
||||
await expect.poll(() => new URL(page.url()).search).toBe("?section=tools&advanced=1");
|
||||
await expect.poll(() => new URL(page.url()).hash).toBe("#config-section-tools");
|
||||
await settingsSearch.fill("channel");
|
||||
await captureSettingsSidebarProof(settingsSidebar, "01e-settings-search-route.png");
|
||||
await holdUiProof(page);
|
||||
|
||||
@@ -871,6 +871,10 @@ export const en: TranslationMap = {
|
||||
},
|
||||
agents: {
|
||||
noAgents: "No agents",
|
||||
defaults: {
|
||||
title: "Agent defaults",
|
||||
description: "Defaults every agent inherits unless overridden.",
|
||||
},
|
||||
copyId: "Copy ID",
|
||||
copyIdTitle: "Copy agent ID to clipboard",
|
||||
default: "Default",
|
||||
|
||||
@@ -937,6 +937,7 @@ class AgentsPage extends OpenClawLightDomElement implements AgentsState {
|
||||
onChannelsRefresh: () => void this.context.channels.refresh(false),
|
||||
onOpenMemoryImport: () => this.context.navigate("memory-import"),
|
||||
onOpenMemorySettings: () => this.context.navigate("memory"),
|
||||
onOpenAgentDefaults: () => this.context.navigate("ai-agents"),
|
||||
onCronRefresh: () => void this.refreshCron(),
|
||||
onCronRunNow: (jobId) => this.runCronJobNow(jobId),
|
||||
onSkillsFilterChange: (next) => (this.skillsFilter = next),
|
||||
|
||||
@@ -153,11 +153,27 @@ function createProps(overrides: Partial<AgentsProps> = {}): AgentsProps {
|
||||
onIdentityAvatarSelect: () => undefined,
|
||||
onIdentitySave: () => undefined,
|
||||
onTogglePinnedAgent: () => undefined,
|
||||
onOpenAgentDefaults: () => undefined,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("renderAgents", () => {
|
||||
it("opens global Agent defaults before the per-agent tabs", () => {
|
||||
const container = document.createElement("div");
|
||||
const onOpenAgentDefaults = vi.fn();
|
||||
render(renderAgents(createProps({ onOpenAgentDefaults })), container);
|
||||
|
||||
const defaultsRow = container.querySelector<HTMLButtonElement>(".settings-row--nav");
|
||||
const tabs = container.querySelector(".agent-tabs");
|
||||
expect(defaultsRow?.textContent).toContain("Agent defaults");
|
||||
expect(defaultsRow?.textContent).toContain("Defaults every agent inherits unless overridden.");
|
||||
expect(defaultsRow?.compareDocumentPosition(tabs!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
|
||||
defaultsRow?.click();
|
||||
expect(onOpenAgentDefaults).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("prefills the identity editor from the fetched agent identity", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
|
||||
@@ -133,6 +133,7 @@ type AgentsProps = {
|
||||
onChannelsRefresh: () => void;
|
||||
onOpenMemoryImport?: () => void;
|
||||
onOpenMemorySettings?: () => void;
|
||||
onOpenAgentDefaults: () => void;
|
||||
onCronRefresh: () => void;
|
||||
onCronRunNow: (jobId: string) => void;
|
||||
onSkillsFilterChange: (next: string) => void;
|
||||
@@ -235,6 +236,13 @@ export function renderAgents(props: AgentsProps) {
|
||||
: nothing}
|
||||
</section>
|
||||
<section class="agents-main">
|
||||
<div class="settings-group">
|
||||
${renderSettingsNavRow({
|
||||
title: t("agents.defaults.title"),
|
||||
description: t("agents.defaults.description"),
|
||||
onClick: props.onOpenAgentDefaults,
|
||||
})}
|
||||
</div>
|
||||
${!selectedAgent
|
||||
? renderSettingsSection(
|
||||
{ title: t("agents.selectTitle") },
|
||||
|
||||
@@ -32,8 +32,8 @@ const INFRASTRUCTURE_SECTION_KEYS = ["gateway", "browser", "nodeHost", "discover
|
||||
const MCP_SECTION_KEYS = ["mcp"] as const;
|
||||
|
||||
// Curated Memory home: engine/backend/add-on rows plus the Dreaming tab render
|
||||
// above the memory schema section (memory.ts). Memory left AI & Agents because
|
||||
// the engine choice and dreaming's global cron are not agent defaults.
|
||||
// above the memory schema section (memory.ts). Memory stays separate from Agent
|
||||
// Defaults because the engine choice and dreaming's global cron are not defaults.
|
||||
const MEMORY_SECTION_KEYS = ["memory"] as const;
|
||||
|
||||
const AI_AGENTS_SECTION_KEYS = ["agents", "skills", "tools", "session"] as const;
|
||||
|
||||
Reference in New Issue
Block a user