fix(ui): gate sessions refresh on successful delete

This commit is contained in:
Sebastian
2026-02-16 21:46:04 -05:00
parent 9c5f08244e
commit 3df8305cb6
4 changed files with 124 additions and 11 deletions

View File

@@ -1,10 +1,10 @@
import { html, nothing } from "lit";
import type { AppViewState } from "./app-view-state.ts";
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
import { t } from "../i18n/index.ts";
import { refreshChatAvatar } from "./app-chat.ts";
import { renderUsageTab } from "./app-render-usage-tab.ts";
import { renderChatControls, renderTab, renderThemeToggle } from "./app-render.helpers.ts";
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";
@@ -44,7 +44,7 @@ import {
import { loadLogs } from "./controllers/logs.ts";
import { loadNodes } from "./controllers/nodes.ts";
import { loadPresence } from "./controllers/presence.ts";
import { deleteSession, loadSessions, patchSession } from "./controllers/sessions.ts";
import { deleteSessionAndRefresh, loadSessions, patchSession } from "./controllers/sessions.ts";
import {
installSkill,
loadSkills,
@@ -301,10 +301,7 @@ export function renderApp(state: AppViewState) {
},
onRefresh: () => loadSessions(state),
onPatch: (key, patch) => patchSession(state, key, patch),
onDelete: async (key) => {
await deleteSession(state, key);
await loadSessions(state);
},
onDelete: (key) => deleteSessionAndRefresh(state, key),
})
: nothing
}

View File

@@ -0,0 +1,104 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { deleteSession, deleteSessionAndRefresh, type SessionsState } from "./sessions.ts";
type RequestFn = (method: string, params?: unknown) => Promise<unknown>;
function createState(request: RequestFn, overrides: Partial<SessionsState> = {}): SessionsState {
return {
client: { request } as unknown as SessionsState["client"],
connected: true,
sessionsLoading: false,
sessionsResult: null,
sessionsError: null,
sessionsFilterActive: "0",
sessionsFilterLimit: "0",
sessionsIncludeGlobal: true,
sessionsIncludeUnknown: true,
...overrides,
};
}
afterEach(() => {
vi.restoreAllMocks();
});
describe("deleteSessionAndRefresh", () => {
it("refreshes sessions after a successful delete", async () => {
const request = vi.fn(async (method: string) => {
if (method === "sessions.delete") {
return { ok: true };
}
if (method === "sessions.list") {
return undefined;
}
throw new Error(`unexpected method: ${method}`);
});
const state = createState(request);
vi.spyOn(window, "confirm").mockReturnValue(true);
const deleted = await deleteSessionAndRefresh(state, "agent:main:test");
expect(deleted).toBe(true);
expect(request).toHaveBeenCalledTimes(2);
expect(request).toHaveBeenNthCalledWith(1, "sessions.delete", {
key: "agent:main:test",
deleteTranscript: true,
});
expect(request).toHaveBeenNthCalledWith(2, "sessions.list", {
includeGlobal: true,
includeUnknown: true,
});
expect(state.sessionsError).toBeNull();
expect(state.sessionsLoading).toBe(false);
});
it("does not refresh sessions when user cancels delete", async () => {
const request = vi.fn(async () => undefined);
const state = createState(request, { sessionsError: "existing error" });
vi.spyOn(window, "confirm").mockReturnValue(false);
const deleted = await deleteSessionAndRefresh(state, "agent:main:test");
expect(deleted).toBe(false);
expect(request).not.toHaveBeenCalled();
expect(state.sessionsError).toBe("existing error");
expect(state.sessionsLoading).toBe(false);
});
it("does not refresh sessions when delete fails and preserves the delete error", async () => {
const request = vi.fn(async (method: string) => {
if (method === "sessions.delete") {
throw new Error("delete boom");
}
if (method === "sessions.list") {
return undefined;
}
throw new Error(`unexpected method: ${method}`);
});
const state = createState(request);
vi.spyOn(window, "confirm").mockReturnValue(true);
const deleted = await deleteSessionAndRefresh(state, "agent:main:test");
expect(deleted).toBe(false);
expect(request).toHaveBeenCalledTimes(1);
expect(request).toHaveBeenCalledWith("sessions.delete", {
key: "agent:main:test",
deleteTranscript: true,
});
expect(state.sessionsError).toContain("delete boom");
expect(state.sessionsLoading).toBe(false);
});
});
describe("deleteSession", () => {
it("returns false when already loading", async () => {
const request = vi.fn(async () => undefined);
const state = createState(request, { sessionsLoading: true });
const deleted = await deleteSession(state, "agent:main:test");
expect(deleted).toBe(false);
expect(request).not.toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,6 @@
import { toNumber } from "../format.ts";
import type { GatewayBrowserClient } from "../gateway.ts";
import type { SessionsListResult } from "../types.ts";
import { toNumber } from "../format.ts";
export type SessionsState = {
client: GatewayBrowserClient | null;
@@ -91,26 +91,37 @@ export async function patchSession(
}
}
export async function deleteSession(state: SessionsState, key: string) {
export async function deleteSession(state: SessionsState, key: string): Promise<boolean> {
if (!state.client || !state.connected) {
return;
return false;
}
if (state.sessionsLoading) {
return;
return false;
}
const confirmed = window.confirm(
`Delete session "${key}"?\n\nDeletes the session entry and archives its transcript.`,
);
if (!confirmed) {
return;
return false;
}
state.sessionsLoading = true;
state.sessionsError = null;
try {
await state.client.request("sessions.delete", { key, deleteTranscript: true });
return true;
} catch (err) {
state.sessionsError = String(err);
return false;
} finally {
state.sessionsLoading = false;
}
}
export async function deleteSessionAndRefresh(state: SessionsState, key: string): Promise<boolean> {
const deleted = await deleteSession(state, key);
if (!deleted) {
return false;
}
await loadSessions(state);
return true;
}