mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: preserve agents-page selection after config save
Landed from contributor PR #39301 by @MumuTW. Co-authored-by: MumuTW <clothl47364@gmail.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import type { AppViewState } from "./app-view-state.ts";
|
||||
import { loadAgentFileContent, loadAgentFiles, saveAgentFile } from "./controllers/agent-files.ts";
|
||||
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
|
||||
import { loadAgentSkills } from "./controllers/agent-skills.ts";
|
||||
import { loadAgents, loadToolsCatalog } from "./controllers/agents.ts";
|
||||
import { loadAgents, loadToolsCatalog, saveAgentsConfig } from "./controllers/agents.ts";
|
||||
import { loadChannels } from "./controllers/channels.ts";
|
||||
import { loadChatHistory } from "./controllers/chat.ts";
|
||||
import {
|
||||
@@ -712,7 +712,7 @@ export function renderApp(state: AppViewState) {
|
||||
}
|
||||
},
|
||||
onConfigReload: () => loadConfig(state),
|
||||
onConfigSave: () => saveConfig(state),
|
||||
onConfigSave: () => saveAgentsConfig(state),
|
||||
onChannelsRefresh: () => loadChannels(state, false),
|
||||
onCronRefresh: () => state.loadCron(),
|
||||
onSkillsFilterChange: (next) => (state.skillsFilter = next),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { loadToolsCatalog } from "./agents.ts";
|
||||
import type { AgentsState } from "./agents.ts";
|
||||
import { loadAgents, loadToolsCatalog, saveAgentsConfig } from "./agents.ts";
|
||||
import type { AgentsConfigSaveState, AgentsState } from "./agents.ts";
|
||||
|
||||
function createState(): { state: AgentsState; request: ReturnType<typeof vi.fn> } {
|
||||
const request = vi.fn();
|
||||
@@ -20,6 +20,83 @@ function createState(): { state: AgentsState; request: ReturnType<typeof vi.fn>
|
||||
return { state, request };
|
||||
}
|
||||
|
||||
function createSaveState(): {
|
||||
state: AgentsConfigSaveState;
|
||||
request: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const { state, request } = createState();
|
||||
return {
|
||||
state: {
|
||||
...state,
|
||||
configSaving: false,
|
||||
configSnapshot: { hash: "hash-1" },
|
||||
configFormDirty: true,
|
||||
configFormMode: "form",
|
||||
configForm: { agents: { list: [{ id: "main" }] } },
|
||||
configRaw: "{}",
|
||||
configSchema: null,
|
||||
lastError: null,
|
||||
},
|
||||
request,
|
||||
};
|
||||
}
|
||||
|
||||
describe("loadAgents", () => {
|
||||
it("preserves selected agent when it still exists in the list", async () => {
|
||||
const { state, request } = createState();
|
||||
state.agentsSelectedId = "kimi";
|
||||
request.mockResolvedValue({
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "main", name: "main" },
|
||||
{ id: "kimi", name: "kimi" },
|
||||
],
|
||||
});
|
||||
|
||||
await loadAgents(state);
|
||||
|
||||
expect(state.agentsSelectedId).toBe("kimi");
|
||||
});
|
||||
|
||||
it("resets to default when selected agent is removed", async () => {
|
||||
const { state, request } = createState();
|
||||
state.agentsSelectedId = "removed-agent";
|
||||
request.mockResolvedValue({
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "main", name: "main" },
|
||||
{ id: "kimi", name: "kimi" },
|
||||
],
|
||||
});
|
||||
|
||||
await loadAgents(state);
|
||||
|
||||
expect(state.agentsSelectedId).toBe("main");
|
||||
});
|
||||
|
||||
it("sets default when no agent is selected", async () => {
|
||||
const { state, request } = createState();
|
||||
state.agentsSelectedId = null;
|
||||
request.mockResolvedValue({
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "main", name: "main" },
|
||||
{ id: "kimi", name: "kimi" },
|
||||
],
|
||||
});
|
||||
|
||||
await loadAgents(state);
|
||||
|
||||
expect(state.agentsSelectedId).toBe("main");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadToolsCatalog", () => {
|
||||
it("loads catalog and stores result", async () => {
|
||||
const { state, request } = createState();
|
||||
@@ -59,3 +136,80 @@ describe("loadToolsCatalog", () => {
|
||||
expect(state.toolsCatalogLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveAgentsConfig", () => {
|
||||
it("restores the pre-save agent after reload when it still exists", async () => {
|
||||
const { state, request } = createSaveState();
|
||||
state.agentsSelectedId = "kimi";
|
||||
request
|
||||
.mockImplementationOnce(async () => undefined)
|
||||
.mockImplementationOnce(async () => {
|
||||
state.agentsSelectedId = null;
|
||||
return {
|
||||
hash: "hash-2",
|
||||
raw: '{"agents":{"list":[{"id":"main"},{"id":"kimi"}]}}',
|
||||
config: {
|
||||
agents: {
|
||||
list: [{ id: "main" }, { id: "kimi" }],
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
issues: [],
|
||||
};
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
state.agentsSelectedId = null;
|
||||
return {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "main", name: "main" },
|
||||
{ id: "kimi", name: "kimi" },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
await saveAgentsConfig(state);
|
||||
|
||||
expect(request).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"config.set",
|
||||
expect.objectContaining({ baseHash: "hash-1" }),
|
||||
);
|
||||
expect(JSON.parse(request.mock.calls[0]?.[1]?.raw as string)).toEqual({
|
||||
agents: { list: [{ id: "main" }] },
|
||||
});
|
||||
expect(request).toHaveBeenNthCalledWith(2, "config.get", {});
|
||||
expect(request).toHaveBeenNthCalledWith(3, "agents.list", {});
|
||||
expect(state.agentsSelectedId).toBe("kimi");
|
||||
});
|
||||
|
||||
it("falls back to the default agent when the saved agent disappears", async () => {
|
||||
const { state, request } = createSaveState();
|
||||
state.agentsSelectedId = "kimi";
|
||||
request
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockResolvedValueOnce({
|
||||
hash: "hash-2",
|
||||
raw: '{"agents":{"list":[{"id":"main"}]}}',
|
||||
config: {
|
||||
agents: {
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
issues: [],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main", name: "main" }],
|
||||
});
|
||||
|
||||
await saveAgentsConfig(state);
|
||||
|
||||
expect(state.agentsSelectedId).toBe("main");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GatewayBrowserClient } from "../gateway.ts";
|
||||
import type { AgentsListResult, ToolsCatalogResult } from "../types.ts";
|
||||
import { saveConfig } from "./config.ts";
|
||||
|
||||
export type AgentsState = {
|
||||
client: GatewayBrowserClient | null;
|
||||
@@ -13,6 +14,17 @@ export type AgentsState = {
|
||||
toolsCatalogResult: ToolsCatalogResult | null;
|
||||
};
|
||||
|
||||
export type AgentsConfigSaveState = AgentsState & {
|
||||
configSaving: boolean;
|
||||
configSnapshot: { hash?: string | null } | null;
|
||||
configFormDirty: boolean;
|
||||
configFormMode: "form" | "raw";
|
||||
configForm: Record<string, unknown> | null;
|
||||
configRaw: string;
|
||||
configSchema: unknown;
|
||||
lastError: string | null;
|
||||
};
|
||||
|
||||
export async function loadAgents(state: AgentsState) {
|
||||
if (!state.client || !state.connected) {
|
||||
return;
|
||||
@@ -62,3 +74,12 @@ export async function loadToolsCatalog(state: AgentsState, agentId?: string | nu
|
||||
state.toolsCatalogLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveAgentsConfig(state: AgentsConfigSaveState) {
|
||||
const selectedBefore = state.agentsSelectedId;
|
||||
await saveConfig(state);
|
||||
await loadAgents(state);
|
||||
if (selectedBefore && state.agentsList?.agents.some((entry) => entry.id === selectedBefore)) {
|
||||
state.agentsSelectedId = selectedBefore;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user