mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(ui): gate sessions refresh on successful delete
This commit is contained in:
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/Channels: wire `gateway.channelHealthCheckMinutes` into strict config validation, treat implicit account status as managed for health checks, and harden channel auto-restart flow (preserve restart-attempt caps across crash loops, propagate enabled/configured runtime flags, and stop pending restart backoff after manual stop). Thanks @steipete.
|
||||
- UI/Usage: replace lingering undefined `var(--text-muted)` usage with `var(--muted)` in usage date-range and chart styles to keep muted text visible across themes. (#17975) Thanks @jogelin.
|
||||
- UI/Usage: preserve selected-range totals when timeline data is downsampled by bucket-aggregating timeseries points (instead of dropping intermediate points), so filtered tokens/cost stay accurate. (#17959) Thanks @jogelin.
|
||||
- UI/Sessions: refresh the sessions table only after successful deletes and preserve delete errors on cancel/failure paths, so deleted sessions disappear automatically without masking delete failures. (#18507)
|
||||
- Mattermost: harden reaction handling by requiring an explicit boolean `remove` flag and routing reaction websocket events to the reaction handler, preventing string `"true"` values from being treated as removes and avoiding double-processing of reaction events as posts. (#18608) Thanks @echo931.
|
||||
- Scripts/UI/Windows: fix `pnpm ui:*` spawn `EINVAL` failures by restoring shell-backed launch for `.cmd`/`.bat` runners, narrowing shell usage to launcher types that require it, and rejecting unsafe forwarded shell metacharacters in UI script args. (#18594)
|
||||
- Hooks/Session-memory: recover `/new` conversation summaries when session pointers are reset-path or missing `sessionFile`, and consistently prefer the newest `.jsonl.reset.*` transcript candidate for fallback extraction. (#18088)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
104
ui/src/ui/controllers/sessions.test.ts
Normal file
104
ui/src/ui/controllers/sessions.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user