diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css
index 3192eec831d..50e67582af3 100644
--- a/ui/src/styles/components.css
+++ b/ui/src/styles/components.css
@@ -1820,22 +1820,21 @@
}
.data-table-container {
- overflow-x: auto;
+ overflow: auto;
+ max-height: 70vh;
}
.data-table {
width: 100%;
- border-collapse: collapse;
+ border-collapse: separate;
+ border-spacing: 0;
font-size: 13px;
}
-.data-table thead {
+.data-table th {
position: sticky;
top: 0;
- z-index: 1;
-}
-
-.data-table th {
+ z-index: 3;
padding: 10px 12px;
text-align: left;
font-weight: 600;
@@ -1937,6 +1936,80 @@
background: var(--bg-hover);
}
+/* Checkbox column */
+.data-table-checkbox-col {
+ width: 1%;
+ padding-left: 12px !important;
+ padding-right: 4px !important;
+ text-align: center;
+ vertical-align: middle;
+ position: sticky;
+ left: 0;
+ z-index: 2;
+}
+
+.data-table th.data-table-checkbox-col {
+ z-index: 4;
+ background: var(--bg-elevated);
+}
+
+td.data-table-checkbox-col {
+ background: var(--card);
+}
+
+.data-table tbody tr:hover td.data-table-checkbox-col {
+ background: var(--bg-hover);
+}
+
+.data-table-checkbox-col input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+ margin: 0;
+ accent-color: var(--accent);
+ cursor: pointer;
+ vertical-align: middle;
+}
+
+/* Sticky Key column (second column, after checkbox) */
+.data-table .data-table-key-col {
+ position: sticky;
+ left: 36px;
+ z-index: 2;
+ min-width: 180px;
+}
+
+.data-table th.data-table-key-col {
+ z-index: 4;
+ background: var(--bg-elevated);
+}
+
+td.data-table-key-col {
+ background: var(--card);
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.08);
+}
+
+.data-table tbody tr:hover td.data-table-key-col {
+ background: var(--bg-hover);
+}
+
+/* Bulk action bar */
+.data-table-bulk-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--border);
+ background: var(--accent-subtle);
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--text);
+}
+
+.data-table-bulk-bar .btn svg {
+ width: 14px;
+ height: 14px;
+}
+
/* Pagination */
.data-table-pagination {
display: flex;
@@ -1979,93 +2052,6 @@
cursor: not-allowed;
}
-/* Row actions */
-.data-table-row-actions {
- position: relative;
-}
-
-.data-table-row-actions__trigger {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 32px;
- height: 32px;
- border: 1px solid transparent;
- border-radius: var(--radius-sm);
- background: transparent;
- color: var(--muted);
- cursor: pointer;
- transition:
- background var(--duration-fast) ease,
- color var(--duration-fast) ease,
- border-color var(--duration-fast) ease;
-}
-
-.data-table-row-actions__trigger svg {
- width: 16px;
- height: 16px;
- stroke: currentColor;
- fill: none;
- stroke-width: 2px;
-}
-
-.data-table-row-actions__trigger:hover {
- background: var(--bg-hover);
- color: var(--text);
- border-color: var(--border);
-}
-
-.data-table-row-actions__menu {
- position: absolute;
- right: 0;
- top: 100%;
- z-index: 42;
- min-width: 140px;
- background: var(--popover);
- border: 1px solid var(--border-strong);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-md);
- padding: 4px;
- animation: fade-in var(--duration-fast) ease;
-}
-
-.data-table-row-actions__menu a,
-.data-table-row-actions__menu button {
- display: block;
- width: 100%;
- padding: 8px 12px;
- font-size: 13px;
- text-align: left;
- text-decoration: none;
- color: var(--text);
- background: transparent;
- border: none;
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: background var(--duration-fast) ease;
-}
-
-.data-table-row-actions__menu a:hover,
-.data-table-row-actions__menu button:hover {
- background: var(--bg-hover);
-}
-
-.data-table-row-actions__menu button.danger {
- color: var(--danger);
-}
-
-.data-table-row-actions__menu button.danger:hover {
- background: var(--danger-subtle);
-}
-
-/* Click-away overlay for open menus */
-.data-table-overlay {
- position: fixed;
- inset: 0;
- z-index: 40;
- background: transparent;
-}
-
/* Inline form fields for filter bars */
.field-inline {
display: inline-flex;
@@ -4074,6 +4060,29 @@ details[open] > .ov-expandable-toggle::after {
}
}
+/* ── Shared collapse chevron ── */
+
+.collapse-chevron {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform var(--duration-fast) ease;
+}
+
+.collapse-chevron svg {
+ width: 12px;
+ height: 12px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 1.5px;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.collapse-chevron--collapsed {
+ transform: rotate(-90deg);
+}
+
@media (max-width: 600px) {
.ov-cards {
grid-template-columns: repeat(2, 1fr);
diff --git a/ui/src/styles/layout.mobile.css b/ui/src/styles/layout.mobile.css
index 6d943253804..2828b6a4dfc 100644
--- a/ui/src/styles/layout.mobile.css
+++ b/ui/src/styles/layout.mobile.css
@@ -651,3 +651,113 @@
font-size: 12px;
}
}
+
+/* ===========================================
+ Sessions table – responsive column hiding
+ =========================================== */
+
+/* nth-child is 1-based and includes the checkbox column (col 1). */
+
+/* ≤1400px: hide Verbose (col 9) and Reasoning (col 10) */
+@media (max-width: 1400px) {
+ .data-table th:nth-child(9),
+ .data-table td:nth-child(9),
+ .data-table th:nth-child(10),
+ .data-table td:nth-child(10) {
+ display: none;
+ }
+}
+
+/* ≤1100px: also hide Fast (col 8) and Kind (col 4) */
+@media (max-width: 1100px) {
+ .data-table th:nth-child(4),
+ .data-table td:nth-child(4),
+ .data-table th:nth-child(8),
+ .data-table td:nth-child(8) {
+ display: none;
+ }
+
+ .data-table th,
+ .data-table td {
+ padding: 8px 10px;
+ }
+}
+
+/* ≤900px: also hide Thinking (col 7) and Label (col 3) */
+@media (max-width: 900px) {
+ .data-table th:nth-child(3),
+ .data-table td:nth-child(3),
+ .data-table th:nth-child(7),
+ .data-table td:nth-child(7) {
+ display: none;
+ }
+}
+
+/* ≤768px: also hide Tokens (col 6); tighten padding; stack pagination */
+@media (max-width: 768px) {
+ .data-table th:nth-child(6),
+ .data-table td:nth-child(6) {
+ display: none;
+ }
+
+ .data-table th,
+ .data-table td {
+ padding: 8px 6px;
+ font-size: 12px;
+ }
+
+ .data-table-pagination {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 8px;
+ padding: 10px 8px;
+ }
+
+ .data-table-pagination__info {
+ text-align: center;
+ }
+
+ .data-table-pagination__controls {
+ justify-content: center;
+ }
+
+ .data-table-search input {
+ font-size: 14px;
+ padding: 8px 10px;
+ }
+
+ .data-table-wrapper {
+ border-radius: var(--radius-sm);
+ }
+
+ .session-key-cell {
+ max-width: 200px;
+ }
+
+ .session-key-cell .session-link,
+ .session-key-cell .session-key-display-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow-wrap: normal;
+ word-break: normal;
+ }
+
+ .filters {
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+}
+
+/* ≤500px: tighten further, truncate key column */
+@media (max-width: 500px) {
+ .data-table th,
+ .data-table td {
+ padding: 6px 4px;
+ font-size: 11px;
+ }
+
+ .session-key-cell {
+ max-width: 140px;
+ }
+}
diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts
index c0535cd6c30..7ccc386de0b 100644
--- a/ui/src/ui/app-render.ts
+++ b/ui/src/ui/app-render.ts
@@ -70,7 +70,7 @@ import {
import { loadLogs } from "./controllers/logs.ts";
import { loadNodes } from "./controllers/nodes.ts";
import { loadPresence } from "./controllers/presence.ts";
-import { deleteSessionAndRefresh, loadSessions, patchSession } from "./controllers/sessions.ts";
+import { deleteSessionsAndRefresh, loadSessions, patchSession } from "./controllers/sessions.ts";
import {
installSkill,
loadSkills,
@@ -477,7 +477,7 @@ export function renderApp(state: AppViewState) {
title="${navCollapsed ? t("nav.expand") : t("nav.collapse")}"
aria-label="${navCollapsed ? t("nav.expand") : t("nav.collapse")}"
>
- ${icons.menu}
+ ${navCollapsed ? icons.panelLeftOpen : icons.panelLeftClose}
@@ -506,7 +506,7 @@ export function renderApp(state: AppViewState) {
>
${t(`nav.${group.label}`)}
- ${showItems ? icons.chevronDown : icons.chevronRight}
+ ${icons.chevronDown}
`
@@ -737,7 +737,7 @@ export function renderApp(state: AppViewState) {
sortDir: state.sessionsSortDir,
page: state.sessionsPage,
pageSize: state.sessionsPageSize,
- actionsOpenKey: state.sessionsActionsOpenKey,
+ selectedKeys: state.sessionsSelectedKeys,
onFiltersChange: (next) => {
state.sessionsFilterActive = next.activeMinutes;
state.sessionsFilterLimit = next.limit;
@@ -760,12 +760,45 @@ export function renderApp(state: AppViewState) {
state.sessionsPageSize = s;
state.sessionsPage = 0;
},
- onActionsOpenChange: (key) => {
- state.sessionsActionsOpenKey = key;
- },
onRefresh: () => loadSessions(state),
onPatch: (key, patch) => patchSession(state, key, patch),
- onDelete: (key) => deleteSessionAndRefresh(state, key),
+ onToggleSelect: (key) => {
+ const next = new Set(state.sessionsSelectedKeys);
+ if (next.has(key)) {
+ next.delete(key);
+ } else {
+ next.add(key);
+ }
+ state.sessionsSelectedKeys = next;
+ },
+ onSelectPage: (keys) => {
+ const next = new Set(state.sessionsSelectedKeys);
+ for (const k of keys) {
+ next.add(k);
+ }
+ state.sessionsSelectedKeys = next;
+ },
+ onDeselectPage: (keys) => {
+ const next = new Set(state.sessionsSelectedKeys);
+ for (const k of keys) {
+ next.delete(k);
+ }
+ state.sessionsSelectedKeys = next;
+ },
+ onDeselectAll: () => {
+ state.sessionsSelectedKeys = new Set();
+ },
+ onDeleteSelected: async () => {
+ const keys = [...state.sessionsSelectedKeys];
+ const deleted = await deleteSessionsAndRefresh(state, keys);
+ if (deleted.length > 0) {
+ const next = new Set(state.sessionsSelectedKeys);
+ for (const k of deleted) {
+ next.delete(k);
+ }
+ state.sessionsSelectedKeys = next;
+ }
+ },
onNavigateToChat: (sessionKey) => {
switchChatSession(state, sessionKey);
state.setTab("chat" as import("./navigation.ts").Tab);
diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts
index df806794645..4eedc881fba 100644
--- a/ui/src/ui/app-view-state.ts
+++ b/ui/src/ui/app-view-state.ts
@@ -191,7 +191,7 @@ export type AppViewState = {
sessionsSortDir: "asc" | "desc";
sessionsPage: number;
sessionsPageSize: number;
- sessionsActionsOpenKey: string | null;
+ sessionsSelectedKeys: Set;
usageLoading: boolean;
usageResult: SessionsUsageResult | null;
usageCostSummary: CostUsageSummary | null;
diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts
index a07ed6376a6..8092dfc4e00 100644
--- a/ui/src/ui/app.ts
+++ b/ui/src/ui/app.ts
@@ -288,8 +288,8 @@ export class OpenClawApp extends LitElement {
@state() sessionsSortColumn: "key" | "kind" | "updated" | "tokens" = "updated";
@state() sessionsSortDir: "asc" | "desc" = "desc";
@state() sessionsPage = 0;
- @state() sessionsPageSize = 10;
- @state() sessionsActionsOpenKey: string | null = null;
+ @state() sessionsPageSize = 25;
+ @state() sessionsSelectedKeys: Set = new Set();
@state() usageLoading = false;
@state() usageResult: import("./types.js").SessionsUsageResult | null = null;
diff --git a/ui/src/ui/controllers/sessions.test.ts b/ui/src/ui/controllers/sessions.test.ts
index 4b66916fab3..ad7d8f56485 100644
--- a/ui/src/ui/controllers/sessions.test.ts
+++ b/ui/src/ui/controllers/sessions.test.ts
@@ -1,10 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
-import {
- deleteSession,
- deleteSessionAndRefresh,
- subscribeSessions,
- type SessionsState,
-} from "./sessions.ts";
+import { deleteSessionsAndRefresh, subscribeSessions, type SessionsState } from "./sessions.ts";
type RequestFn = (method: string, params?: unknown) => Promise;
@@ -47,8 +42,8 @@ describe("subscribeSessions", () => {
});
});
-describe("deleteSessionAndRefresh", () => {
- it("refreshes sessions after a successful delete", async () => {
+describe("deleteSessionsAndRefresh", () => {
+ it("deletes multiple sessions and refreshes", async () => {
const request = vi.fn(async (method: string) => {
if (method === "sessions.delete") {
return { ok: true };
@@ -61,39 +56,44 @@ describe("deleteSessionAndRefresh", () => {
const state = createState(request);
vi.spyOn(window, "confirm").mockReturnValue(true);
- const deleted = await deleteSessionAndRefresh(state, "agent:main:test");
+ const deleted = await deleteSessionsAndRefresh(state, ["key-a", "key-b"]);
- expect(deleted).toBe(true);
- expect(request).toHaveBeenCalledTimes(2);
+ expect(deleted).toEqual(["key-a", "key-b"]);
+ expect(request).toHaveBeenCalledTimes(3);
expect(request).toHaveBeenNthCalledWith(1, "sessions.delete", {
- key: "agent:main:test",
+ key: "key-a",
deleteTranscript: true,
});
- expect(request).toHaveBeenNthCalledWith(2, "sessions.list", {
+ expect(request).toHaveBeenNthCalledWith(2, "sessions.delete", {
+ key: "key-b",
+ deleteTranscript: true,
+ });
+ expect(request).toHaveBeenNthCalledWith(3, "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 () => {
+ it("returns empty array when user cancels", async () => {
const request = vi.fn(async () => undefined);
- const state = createState(request, { sessionsError: "existing error" });
+ const state = createState(request);
vi.spyOn(window, "confirm").mockReturnValue(false);
- const deleted = await deleteSessionAndRefresh(state, "agent:main:test");
+ const deleted = await deleteSessionsAndRefresh(state, ["key-a"]);
- expect(deleted).toBe(false);
+ expect(deleted).toEqual([]);
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) => {
+ it("returns partial results when some deletes fail", async () => {
+ const request = vi.fn(async (method: string, params?: unknown) => {
if (method === "sessions.delete") {
- throw new Error("delete boom");
+ const p = params as { key: string };
+ if (p.key === "key-b" || p.key === "key-c") {
+ throw new Error(`delete failed: ${p.key}`);
+ }
+ return { ok: true };
}
if (method === "sessions.list") {
return undefined;
@@ -103,27 +103,20 @@ describe("deleteSessionAndRefresh", () => {
const state = createState(request);
vi.spyOn(window, "confirm").mockReturnValue(true);
- const deleted = await deleteSessionAndRefresh(state, "agent:main:test");
+ const deleted = await deleteSessionsAndRefresh(state, ["key-a", "key-b", "key-c", "key-d"]);
- 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(deleted).toEqual(["key-a", "key-d"]);
+ expect(state.sessionsError).toBe("Error: delete failed: key-b; Error: delete failed: key-c");
expect(state.sessionsLoading).toBe(false);
});
-});
-describe("deleteSession", () => {
- it("returns false when already loading", async () => {
+ it("returns empty array when already loading", async () => {
const request = vi.fn(async () => undefined);
const state = createState(request, { sessionsLoading: true });
- const deleted = await deleteSession(state, "agent:main:test");
+ const deleted = await deleteSessionsAndRefresh(state, ["key-a"]);
- expect(deleted).toBe(false);
+ expect(deleted).toEqual([]);
expect(request).not.toHaveBeenCalled();
});
});
diff --git a/ui/src/ui/controllers/sessions.ts b/ui/src/ui/controllers/sessions.ts
index b2de9e38fae..2f647c578e6 100644
--- a/ui/src/ui/controllers/sessions.ts
+++ b/ui/src/ui/controllers/sessions.ts
@@ -106,37 +106,44 @@ export async function patchSession(
}
}
-export async function deleteSession(state: SessionsState, key: string): Promise {
- if (!state.client || !state.connected) {
- return false;
+export async function deleteSessionsAndRefresh(
+ state: SessionsState,
+ keys: string[],
+): Promise {
+ if (!state.client || !state.connected || keys.length === 0) {
+ return [];
}
if (state.sessionsLoading) {
- return false;
+ return [];
}
+ const noun = keys.length === 1 ? "session" : "sessions";
const confirmed = window.confirm(
- `Delete session "${key}"?\n\nDeletes the session entry and archives its transcript.`,
+ `Delete ${keys.length} ${noun}?\n\nThis will delete the session entries and archive their transcripts.`,
);
if (!confirmed) {
- return false;
+ return [];
}
state.sessionsLoading = true;
state.sessionsError = null;
+ const deleted: string[] = [];
+ const deleteErrors: string[] = [];
try {
- await state.client.request("sessions.delete", { key, deleteTranscript: true });
- return true;
- } catch (err) {
- state.sessionsError = String(err);
- return false;
+ for (const key of keys) {
+ try {
+ await state.client.request("sessions.delete", { key, deleteTranscript: true });
+ deleted.push(key);
+ } catch (err) {
+ deleteErrors.push(String(err));
+ }
+ }
} finally {
state.sessionsLoading = false;
}
-}
-
-export async function deleteSessionAndRefresh(state: SessionsState, key: string): Promise {
- const deleted = await deleteSession(state, key);
- if (!deleted) {
- return false;
+ if (deleted.length > 0) {
+ await loadSessions(state);
}
- await loadSessions(state);
- return true;
+ if (deleteErrors.length > 0) {
+ state.sessionsError = deleteErrors.join("; ");
+ }
+ return deleted;
}
diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts
index 1b40059c8b9..2940f8c0626 100644
--- a/ui/src/ui/views/chat.ts
+++ b/ui/src/ui/views/chat.ts
@@ -662,7 +662,7 @@ function renderPinnedSection(
}}>
${icons.bookmark}
${entries.length} pinned
- ${vs.pinnedExpanded ? icons.chevronDown : icons.chevronRight}
+ ${icons.chevronDown}
${
vs.pinnedExpanded
diff --git a/ui/src/ui/views/sessions.test.ts b/ui/src/ui/views/sessions.test.ts
index 342af136a75..0f18a179b1b 100644
--- a/ui/src/ui/views/sessions.test.ts
+++ b/ui/src/ui/views/sessions.test.ts
@@ -1,5 +1,7 @@
+/* @vitest-environment jsdom */
+
import { render } from "lit";
-import { describe, expect, it } from "vitest";
+import { describe, expect, it, vi } from "vitest";
import type { SessionsListResult } from "../types.ts";
import { renderSessions, type SessionsProps } from "./sessions.ts";
@@ -13,6 +15,16 @@ function buildResult(session: SessionsListResult["sessions"][number]): SessionsL
};
}
+function buildMultiResult(sessions: SessionsListResult["sessions"]): SessionsListResult {
+ return {
+ ts: Date.now(),
+ path: "(multiple)",
+ count: sessions.length,
+ defaults: { modelProvider: null, model: null, contextTokens: null },
+ sessions,
+ };
+}
+
function buildProps(result: SessionsListResult): SessionsProps {
return {
loading: false,
@@ -28,16 +40,19 @@ function buildProps(result: SessionsListResult): SessionsProps {
sortDir: "desc",
page: 0,
pageSize: 10,
- actionsOpenKey: null,
+ selectedKeys: new Set(),
onFiltersChange: () => undefined,
onSearchChange: () => undefined,
onSortChange: () => undefined,
onPageChange: () => undefined,
onPageSizeChange: () => undefined,
- onActionsOpenChange: () => undefined,
onRefresh: () => undefined,
onPatch: () => undefined,
- onDelete: () => undefined,
+ onToggleSelect: () => undefined,
+ onSelectPage: () => undefined,
+ onDeselectPage: () => undefined,
+ onDeselectAll: () => undefined,
+ onDeleteSelected: () => undefined,
};
}
@@ -111,4 +126,43 @@ describe("sessions view", () => {
const fast = selects[1] as HTMLSelectElement | undefined;
expect(fast?.value).toBe("on");
});
+
+ it("deselects only the current page from the header checkbox", async () => {
+ const onSelectPage = vi.fn();
+ const onDeselectPage = vi.fn();
+ const onDeselectAll = vi.fn();
+ const container = document.createElement("div");
+ render(
+ renderSessions({
+ ...buildProps(
+ buildMultiResult([
+ {
+ key: "page-0",
+ kind: "direct",
+ updatedAt: 20,
+ },
+ {
+ key: "page-1",
+ kind: "direct",
+ updatedAt: 10,
+ },
+ ]),
+ ),
+ pageSize: 1,
+ selectedKeys: new Set(["page-0", "off-page"]),
+ onSelectPage,
+ onDeselectPage,
+ onDeselectAll,
+ }),
+ container,
+ );
+ await Promise.resolve();
+
+ const headerCheckbox = container.querySelector("thead input[type=checkbox]");
+ headerCheckbox?.dispatchEvent(new Event("change", { bubbles: true }));
+
+ expect(onDeselectPage).toHaveBeenCalledWith(["page-0"]);
+ expect(onDeselectAll).not.toHaveBeenCalled();
+ expect(onSelectPage).not.toHaveBeenCalled();
+ });
});
diff --git a/ui/src/ui/views/sessions.ts b/ui/src/ui/views/sessions.ts
index e028b7b4c85..89838bce205 100644
--- a/ui/src/ui/views/sessions.ts
+++ b/ui/src/ui/views/sessions.ts
@@ -19,7 +19,7 @@ export type SessionsProps = {
sortDir: "asc" | "desc";
page: number;
pageSize: number;
- actionsOpenKey: string | null;
+ selectedKeys: Set;
onFiltersChange: (next: {
activeMinutes: string;
limit: string;
@@ -30,7 +30,6 @@ export type SessionsProps = {
onSortChange: (column: "key" | "kind" | "updated" | "tokens", dir: "asc" | "desc") => void;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
- onActionsOpenChange: (key: string | null) => void;
onRefresh: () => void;
onPatch: (
key: string,
@@ -42,7 +41,11 @@ export type SessionsProps = {
reasoningLevel?: string | null;
},
) => void;
- onDelete: (key: string) => void;
+ onToggleSelect: (key: string) => void;
+ onSelectPage: (keys: string[]) => void;
+ onDeselectPage: (keys: string[]) => void;
+ onDeselectAll: () => void;
+ onDeleteSelected: () => void;
onNavigateToChat?: (sessionKey: string) => void;
};
@@ -187,11 +190,16 @@ export function renderSessions(props: SessionsProps) {
const page = Math.min(props.page, totalPages - 1);
const paginated = paginateRows(sorted, page, props.pageSize);
- const sortHeader = (col: "key" | "kind" | "updated" | "tokens", label: string) => {
+ const sortHeader = (
+ col: "key" | "kind" | "updated" | "tokens",
+ label: string,
+ extraClass = "",
+ ) => {
const isActive = props.sortColumn === col;
const nextDir = isActive && props.sortDir === "asc" ? ("desc" as const) : ("asc" as const);
return html`