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:
Peter Steinberger
2026-03-08 02:18:32 +00:00
parent 1e3daa6373
commit c0a7c302f3
4 changed files with 180 additions and 4 deletions

View File

@@ -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),

View File

@@ -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");
});
});

View File

@@ -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;
}
}