Files
openclaw/ui/src/components/session-organizer-controller.ts
Peter Steinberger 9b43f1c82e improve(ui): fix sidebar session row hierarchy and add a message-preview toggle
Pinned sessions inherited the nav zone's muted colour on the session *title*, so a
pinned row's preview line outshone its own name and the same session read dimmer
pinned than unpinned. A title is content in every zone; only glyphs, meta and the
hover fill follow the zone. The subtitle drops to plain --muted, already proven AA
against every surface by theme-contrast.test.ts.

Rows with no preview text reserved a fixed 18px second line anyway, leaving a dead
band or a lone spinner hanging below-right of the title. They now collapse to one
line via a --single-line class plus a CSS variant, so the endcap rides beside the
title. Rows also gained a little vertical air and reclaimed the right-hand gutter
inside the row, leaving the deliberate scrollbar clearance from #124879 intact.

Adds a localStorage-backed "Show message preview" toggle to the session sort menu
(default on). Operator-actionable state is exempt: attention, the queued
concurrency-slot explanation, and critical observer headlines (stuck /
waiting-on-user) always show, because hiding them behind a display preference is
the silent-failure class.

Also registers extensions/codex/src/app-server/run-attempt-tools.test.ts, which
arrived in #126189 with no shard claim and left main red.
2026-08-19 00:08:07 -07:00

778 lines
27 KiB
TypeScript

import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
parseSidebarEntry,
serializeSidebarEntry,
type PersistedSidebarRoute,
} from "../app-navigation.ts";
import { t } from "../i18n/index.ts";
import {
readSessionDragData,
readSidebarSectionDragData,
readSidebarRouteDragData,
sessionDragActive,
sidebarSectionDragActive,
sidebarRouteDragActive,
writeSidebarRouteDragData,
} from "../lib/sessions/drag.ts";
import {
categoryClearReturnsToGroups,
type SidebarSessionsGrouping,
} from "../lib/sessions/grouping.ts";
import {
loadStoredCollapsedSessionSections,
storeSidebarSessionStatusFilter,
storeCollapsedSessionSections,
storeSidebarSessionsGrouping,
storeSidebarSessionsShowCron,
storeSidebarSessionsShowPreview,
storeSidebarSessionsShowSystem,
type SidebarRecentSession,
type SidebarSectionDropTarget,
type SidebarSessionMutationResult,
type SidebarSessionMutationScope,
type SidebarSessionPatch,
type SidebarSessionStatusFilter,
} from "./app-sidebar-session-types.ts";
import type { SessionMenuAction } from "./session-menu.ts";
import type { SessionOrganizerControllerHost } from "./session-organizer-controller-types.ts";
import type { SessionOwnerOption } from "./session-owner-chip.ts";
export type { SessionOrganizerControllerHost } from "./session-organizer-controller-types.ts";
type SessionOrganizerOperations = typeof import("./session-organizer-operations.runtime.ts");
type InputDialogOpener = (typeof import("./input-dialog.ts"))["showInputDialog"];
type SessionGroupDefaultsDialogOpener =
(typeof import("./session-group-defaults-dialog.ts"))["showSessionGroupDefaultsDialog"];
/** Custom session groups, collapse state, and drag-and-drop assignment. */
export class SessionOrganizerController {
collapsedSessionSections = loadStoredCollapsedSessionSections();
draggingSessionKey: string | null = null;
draggingSidebarSection: string | null = null;
sessionDropTarget: string | null = null;
sidebarSectionDropTarget: SidebarSectionDropTarget | null = null;
draggingSidebarEntry: string | null = null;
sidebarZoneDropTarget: {
entry: string;
position: "before" | "after";
} | null = null;
sessionListRemovalDrop = false;
private operationsLoad: Promise<SessionOrganizerOperations> | null = null;
constructor(private readonly host: SessionOrganizerControllerHost) {}
private async loadOperations(
scope: SidebarSessionMutationScope,
): Promise<SessionOrganizerOperations | null> {
const load = (this.operationsLoad ??= import("./session-organizer-operations.runtime.ts"));
try {
return await load;
} catch (error) {
if (this.operationsLoad === load) {
this.operationsLoad = null;
}
if (this.host.sessionData.isSessionMutationScopeCurrent(scope)) {
this.host.sessionData.publishSessionMutationError(scope, error);
}
return null;
}
}
readonly patchSession = async (
session: SidebarRecentSession,
patch: SidebarSessionPatch,
scope: SidebarSessionMutationScope | null = this.host.sessionData.beginSessionMutation(),
): Promise<SidebarSessionMutationResult> => {
if (!scope) {
return "stale";
}
const operations = await this.loadOperations(scope);
if (!operations) {
return this.host.sessionData.isSessionMutationScopeCurrent(scope) ? "failed" : "stale";
}
return operations.patchSession(this.host, session, patch, scope);
};
async patchSessions(
rows: readonly SidebarRecentSession[],
patch: SidebarSessionPatch,
scope: SidebarSessionMutationScope | null = this.host.sessionData.beginSessionMutation(),
): Promise<SidebarSessionMutationResult> {
if (!scope) {
return "stale";
}
const operations = await this.loadOperations(scope);
if (!operations) {
return this.host.sessionData.isSessionMutationScopeCurrent(scope) ? "failed" : "stale";
}
return operations.patchSessions(this.host, rows, patch, scope);
}
async archiveSessionWithUndo(session: SidebarRecentSession): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.archiveSessionWithUndo(this.host, session, scope);
}
async deleteSessionsBatch(rows: readonly SidebarRecentSession[]): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.deleteSessionsBatch(this.host, rows, scope);
}
async runBatchSessionAction(
action: SessionMenuAction,
rows: SidebarRecentSession[],
allUnread: boolean,
): Promise<void> {
if (action.kind === "new-group") {
await this.createSessionGroup(rows);
return;
}
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.runBatchSessionAction(this.host, action, rows, allUnread, scope);
}
async forkSession(session: SidebarRecentSession): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.forkSession(this.host, session, scope);
}
async stopCloudWorker(session: SidebarRecentSession): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.stopCloudWorker(this.host, session, scope);
}
async assignSessionOwner(
session: SidebarRecentSession,
owner: Pick<SessionOwnerOption, "type" | "id">,
): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.assignSessionOwner(this.host, session, owner, scope);
}
async deleteSession(session: SidebarRecentSession): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
// Sidebar is the surface the delete-confirm setting names, so it is the one
// caller allowed to offer the opt-out.
await operations?.deleteSession(this.host, session, scope, { offerSkip: true });
}
startSidebarRouteDrag(event: DragEvent, route: PersistedSidebarRoute) {
if (!event.dataTransfer) {
return;
}
writeSidebarRouteDragData(event.dataTransfer, route);
this.draggingSidebarEntry = serializeSidebarEntry({ type: "route", route });
this.host.requestUpdate();
}
startSidebarWorkboardDrag(event: DragEvent, boardId: string) {
if (!event.dataTransfer) {
return;
}
const entry = serializeSidebarEntry({ type: "workboard", boardId });
writeSidebarRouteDragData(event.dataTransfer, entry);
this.draggingSidebarEntry = entry;
this.host.requestUpdate();
}
finishSidebarEntryDrag() {
this.draggingSidebarEntry = null;
this.host.requestUpdate();
this.draggingSessionKey = null;
this.host.requestUpdate();
this.sidebarZoneDropTarget = null;
this.host.requestUpdate();
this.sessionListRemovalDrop = false;
this.host.requestUpdate();
}
startSessionDrag(session: SidebarRecentSession): void {
this.draggingSessionKey = session.key;
this.host.requestUpdate();
this.draggingSidebarEntry = session.pinned ? `session:${session.key}` : null;
this.host.requestUpdate();
}
finishSessionDrag(): void {
this.finishSidebarEntryDrag();
this.sessionDropTarget = null;
this.host.requestUpdate();
}
startSidebarSectionDrag(sectionId: string): void {
this.draggingSidebarSection = sectionId;
this.host.requestUpdate();
}
finishSidebarSectionDrag(): void {
this.draggingSidebarSection = null;
this.host.requestUpdate();
this.sidebarSectionDropTarget = null;
this.host.requestUpdate();
}
private draggedSidebarEntry(dataTransfer: DataTransfer | null): string | null {
const route = readSidebarRouteDragData(dataTransfer);
const routeEntry = parseSidebarEntry(route ? `route:${route}` : null);
if (routeEntry?.type === "route") {
return serializeSidebarEntry(routeEntry);
}
const dynamicEntry = parseSidebarEntry(route);
if (dynamicEntry?.type === "workboard") {
return serializeSidebarEntry(dynamicEntry);
}
const sessionKey = readSessionDragData(dataTransfer);
return sessionKey ? serializeSidebarEntry({ type: "session", key: sessionKey }) : null;
}
handleSidebarZoneDragOver(event: DragEvent, targetEntry?: string) {
if (!sidebarRouteDragActive(event.dataTransfer) && !sessionDragActive(event.dataTransfer)) {
return;
}
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
if (!targetEntry) {
this.sidebarZoneDropTarget = null;
this.host.requestUpdate();
return;
}
const target = event.currentTarget as HTMLElement;
const bounds = target.getBoundingClientRect();
this.sidebarZoneDropTarget = {
entry: targetEntry,
position: event.clientY < bounds.top + bounds.height / 2 ? "before" : "after",
};
this.host.requestUpdate();
}
handleSidebarZoneDragLeave(event: DragEvent) {
const current = event.currentTarget as HTMLElement;
if (event.relatedTarget instanceof Node && current.contains(event.relatedTarget)) {
return;
}
this.sidebarZoneDropTarget = null;
this.host.requestUpdate();
}
/** Insert `entry` into the freshest canonical order at the captured drop slot. */
private writeSidebarEntryAt(
entry: string,
targetEntry: string | undefined,
position: "before" | "after" | undefined,
) {
const next = this.host
.reconciledSidebarZone()
.sidebarEntries.filter((candidate) => candidate !== entry);
const targetIndex = targetEntry ? next.indexOf(targetEntry) : -1;
const offset = position === "after" ? 1 : 0;
next.splice(targetIndex < 0 ? next.length : targetIndex + offset, 0, entry);
this.host.onUpdateSidebarEntries?.(next);
}
handleSidebarZoneDrop(event: DragEvent, targetEntry?: string) {
const entry = this.draggedSidebarEntry(event.dataTransfer);
if (!entry) {
return;
}
// Consume before the self-drop bailout: an unhandled drop would bubble to
// the zone container and append the entry at the end.
event.preventDefault();
event.stopPropagation();
if (targetEntry === entry) {
this.finishSidebarEntryDrag();
return;
}
const position = this.sidebarZoneDropTarget?.position;
const sessionKey = readSessionDragData(event.dataTransfer);
const session = sessionKey ? this.host.findSidebarSessionByKey(sessionKey) : undefined;
if (session && !session.pinned) {
// Persist the dropped slot only once the pin lands, and recompute
// against the then-current order: a failed patch must not leave an
// unpinned slot behind, and a stale snapshot must not undo zone edits
// that raced the request.
void this.patchSession(session, { pinned: true }).then((result) => {
if (result === "completed") {
this.writeSidebarEntryAt(entry, targetEntry, position);
}
});
} else {
this.writeSidebarEntryAt(entry, targetEntry, position);
}
this.finishSidebarEntryDrag();
}
private removeSidebarEntry(entry: string) {
const next = this.host
.reconciledSidebarZone()
.sidebarEntries.filter((candidate) => candidate !== entry);
this.host.onUpdateSidebarEntries?.(next);
}
handleSessionListDragOver(event: DragEvent) {
const routeDrag = sidebarRouteDragActive(event.dataTransfer);
const sessionKey = readSessionDragData(event.dataTransfer);
const session = sessionKey ? this.host.findSidebarSessionByKey(sessionKey) : undefined;
if (!routeDrag && !session?.pinned) {
return;
}
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
this.sessionListRemovalDrop = true;
this.host.requestUpdate();
}
handleSessionListDragLeave(event: DragEvent) {
const current = event.currentTarget as HTMLElement;
if (!(event.relatedTarget instanceof Node && current.contains(event.relatedTarget))) {
this.sessionListRemovalDrop = false;
this.host.requestUpdate();
}
}
handleSessionListDrop(event: DragEvent) {
const draggedNavigation = readSidebarRouteDragData(event.dataTransfer);
const routeEntry = parseSidebarEntry(draggedNavigation ? `route:${draggedNavigation}` : null);
const dynamicEntry = parseSidebarEntry(draggedNavigation);
const entry =
routeEntry?.type === "route"
? routeEntry
: dynamicEntry?.type === "workboard"
? dynamicEntry
: null;
if (entry) {
event.preventDefault();
this.removeSidebarEntry(serializeSidebarEntry(entry));
this.finishSidebarEntryDrag();
return;
}
const sessionKey = readSessionDragData(event.dataTransfer);
const session = sessionKey ? this.host.findSidebarSessionByKey(sessionKey) : undefined;
if (session?.pinned) {
event.preventDefault();
// patchSession prunes the persisted zone entry once the unpin lands.
void this.patchSession(session, { pinned: false });
}
this.finishSidebarEntryDrag();
}
/** A dialog that never opens still owes the operator a visible outcome. */
private async loadInputDialog(): Promise<InputDialogOpener | null> {
try {
return (await import("./input-dialog.ts")).showInputDialog;
} catch (error) {
const scope = this.host.sessionData.beginSessionMutation();
if (scope) {
this.host.sessionData.publishSessionMutationError(scope, error);
}
return null;
}
}
async renameSession(session: SidebarRecentSession): Promise<void> {
const showInputDialog = await this.loadInputDialog();
const nextLabel =
(await showInputDialog?.({
title: t("sessionsView.renameSessionPrompt"),
// The stored label, not the resolved display name: pre-filling the
// derived string persists it on submit and it then outranks every
// later derivation. Matches the Sessions page rename.
defaultValue: normalizeOptionalString(session.userLabel) ?? "",
})) ?? null;
if (nextLabel === null) {
return;
}
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.renameSession(this.host, session, nextLabel, scope);
}
async createSessionGroup(sessions: readonly SidebarRecentSession[] = []): Promise<void> {
const showInputDialog = await this.loadInputDialog();
await showInputDialog?.({
title: t("sessionsView.newGroupTitle"),
label: t("sessionsView.newGroupPrompt"),
submitLabel: t("sessionsView.newGroupCreate"),
requireValue: true,
submit: (name) => this.writeSessionGroup(name, sessions),
});
}
/**
* Replays the failure the mutation already recorded so the dialog can keep the
* typed name for a retry. A replaced connection confirmed neither the group nor
* the move, so it reports a retryable message too rather than closing on an
* outcome that never landed; resubmitting runs against the new connection.
*/
private async writeSessionGroup(
name: string,
sessions: readonly SidebarRecentSession[],
): Promise<string | null> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return t("sessionsView.newGroupFailed");
}
const operations = await this.loadOperations(scope);
if (!operations) {
return this.host.sessionData.isSessionMutationScopeCurrent(scope)
? this.sessionGroupFailure()
: t("sessionsView.newGroupStale");
}
const result = await operations.createSessionGroup(this.host, name, sessions, scope);
if (result === "failed") {
return this.sessionGroupFailure();
}
return result === "stale" ? t("sessionsView.newGroupStale") : null;
}
private sessionGroupFailure(): string {
return this.host.sessionData.sessionMutationError ?? t("sessionsView.newGroupFailed");
}
async renameSessionGroupFromMenu(group: string): Promise<void> {
const showInputDialog = await this.loadInputDialog();
// requireChange holds the submit closed on the name the group already has,
// so the only rename that reaches the Gateway is one that changes something.
const next = await showInputDialog?.({
title: t("sessionsView.renameGroupTitle", { group }),
label: t("sessionsView.groupNameLabel"),
defaultValue: group,
requireValue: true,
requireChange: true,
});
if (!next) {
return;
}
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
if (!operations || !(await operations.renameSessionGroup(this.host, group, next, scope))) {
return;
}
// Collapse keys follow only a confirmed Gateway rename. A stale completion
// must not rewrite storage owned by the replacement connection.
const from = `category:${group}`;
if (this.collapsedSessionSections.has(from)) {
const collapsed = new Set(this.collapsedSessionSections);
collapsed.delete(from);
collapsed.add(`category:${next}`);
this.saveCollapsedSessionSections(collapsed);
}
this.host.requestUpdate();
}
async deleteSessionGroupFromMenu(group: string): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
if (!operations || !(await operations.deleteSessionGroup(this.host, group, scope))) {
return;
}
const collapsed = new Set(this.collapsedSessionSections);
collapsed.delete(`category:${group}`);
this.saveCollapsedSessionSections(collapsed);
this.host.requestUpdate();
}
async editSessionGroupDefaults(group: string): Promise<void> {
let showDialog: SessionGroupDefaultsDialogOpener;
try {
showDialog = (await import("./session-group-defaults-dialog.ts"))
.showSessionGroupDefaultsDialog;
} catch (error) {
const scope = this.host.sessionData.beginSessionMutation();
if (scope) {
this.host.sessionData.publishSessionMutationError(scope, error);
}
return;
}
const defaults = this.host.sessionGroupDefaults(group);
if (defaults) {
await showDialog({
group,
defaults,
listDirectory: (path) => this.host.listSessionGroupFolders(path),
inspectRepository: (path) => this.host.inspectSessionGroupRepository(path),
submit: async (nextDefaults) => {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope || !this.host.sessionGroupDefaults(group)) {
return t("sessionsView.groupDefaultsStale");
}
const operations = await this.loadOperations(scope);
const result = await operations?.updateSessionGroupDefaults(
this.host,
group,
{ cwd: nextDefaults.cwd || null, worktree: nextDefaults.worktree },
scope,
);
return result === "completed"
? null
: result === "stale"
? t("sessionsView.groupDefaultsStale")
: (this.host.sessionData.sessionMutationError ??
t("sessionsView.groupDefaultsFailed"));
},
});
}
}
saveCollapsedSessionSections(sections: ReadonlySet<string>) {
this.collapsedSessionSections = new Set(sections);
this.host.requestUpdate();
try {
storeCollapsedSessionSections(sections);
} catch {
// Group membership and ordering remain usable without local persistence.
}
}
toggleSection(sectionId: string) {
const collapsed = new Set(this.collapsedSessionSections);
if (collapsed.has(sectionId)) {
collapsed.delete(sectionId);
} else {
collapsed.add(sectionId);
}
this.saveCollapsedSessionSections(collapsed);
}
private async reorderSidebarSection(
sourceSectionId: string,
targetSectionId: string,
position: "before" | "after",
): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.reorderSidebarSection(
this.host,
sourceSectionId,
targetSectionId,
position,
scope,
);
}
async assignSessionCategory(
session: SidebarRecentSession,
category: string | null,
patch: { pinned?: boolean } = {},
): Promise<void> {
const scope = this.host.sessionData.beginSessionMutation();
if (!scope) {
return;
}
const operations = await this.loadOperations(scope);
await operations?.assignSessionCategory(this.host, session, category, scope, patch);
}
private sectionAcceptsSession(
sectionId: string,
category: string | undefined,
session: SidebarRecentSession | undefined,
): boolean {
if (sectionId === "pinned") {
return true;
}
if (
this.host.sessionsGrouping === "category" &&
(sectionId === "ungrouped" || Boolean(category))
) {
return true;
}
return (
sectionId === "groups" &&
Boolean(session && categoryClearReturnsToGroups(session, this.host.sessionsGrouping))
);
}
sectionDragOver(event: DragEvent, sectionId: string, category?: string) {
const dataTransfer = event.dataTransfer;
if (sidebarSectionDragActive(dataTransfer) && this.draggingSidebarSection !== sectionId) {
event.preventDefault();
if (dataTransfer) {
dataTransfer.dropEffect = "move";
}
const target = event.currentTarget as HTMLElement;
const header = target.querySelector<HTMLElement>(":scope > .sidebar-recent-sessions__head");
const bounds = (header ?? target).getBoundingClientRect();
const position = event.clientY < bounds.top + bounds.height / 2 ? "before" : "after";
this.sidebarSectionDropTarget = { sectionId, position };
this.host.requestUpdate();
this.sessionDropTarget = null;
this.host.requestUpdate();
return;
}
if (!sessionDragActive(dataTransfer)) {
return;
}
// Browsers protect transferred data during dragover. Use the key recorded
// at dragstart for hover eligibility; sectionDrop reads the payload itself.
const session = this.draggingSessionKey
? this.host.findSidebarSessionByKey(this.draggingSessionKey)
: undefined;
if (!this.sectionAcceptsSession(sectionId, category, session)) {
event.stopPropagation();
return;
}
event.preventDefault();
if (dataTransfer) {
dataTransfer.dropEffect = "move";
}
this.sessionDropTarget = sectionId;
this.host.requestUpdate();
this.sidebarSectionDropTarget = null;
this.host.requestUpdate();
}
sectionDragLeave(event: DragEvent, sectionId: string, _category?: string) {
const current = event.currentTarget as HTMLElement;
if (event.relatedTarget instanceof Node && current.contains(event.relatedTarget)) {
return;
}
if (this.sessionDropTarget === sectionId) {
this.sessionDropTarget = null;
this.host.requestUpdate();
}
if (this.sidebarSectionDropTarget?.sectionId === sectionId) {
this.sidebarSectionDropTarget = null;
this.host.requestUpdate();
}
}
sectionDrop(event: DragEvent, sectionId: string, category?: string) {
const sourceSectionId = readSidebarSectionDragData(event.dataTransfer);
const sessionKey = readSessionDragData(event.dataTransfer);
if (!sourceSectionId && !sessionKey) {
return;
}
// Rows can be dragged from a browsed agent section, so search all caches.
const session = sessionKey ? this.host.findSidebarSessionByKey(sessionKey) : undefined;
if (!sourceSectionId && !this.sectionAcceptsSession(sectionId, category, session)) {
event.stopPropagation();
return;
}
event.preventDefault();
event.stopPropagation();
if (sourceSectionId && sourceSectionId !== sectionId) {
const position =
this.sidebarSectionDropTarget?.sectionId === sectionId
? this.sidebarSectionDropTarget.position
: "before";
void this.reorderSidebarSection(sourceSectionId, sectionId, position);
} else if (session && sectionId === "pinned") {
if (!session.pinned) {
void this.patchSession(session, { pinned: true });
}
} else if (session) {
const nextCategory = category ?? null;
if (session.category !== nextCategory || session.pinned) {
// The pinned:false leg prunes the persisted zone entry via patchSession.
void this.assignSessionCategory(
session,
nextCategory,
session.pinned ? { pinned: false } : {},
);
}
}
this.finishSidebarEntryDrag();
this.draggingSidebarSection = null;
this.host.requestUpdate();
this.sessionDropTarget = null;
this.host.requestUpdate();
this.sidebarSectionDropTarget = null;
this.host.requestUpdate();
}
setSessionsGrouping(grouping: SidebarSessionsGrouping) {
this.host.sessionsGrouping = grouping;
try {
storeSidebarSessionsGrouping(grouping);
} catch {
// Keep the in-memory preference when storage is unavailable.
}
}
setSessionsShowCron(show: boolean) {
this.host.sessionsShowCron = show;
try {
storeSidebarSessionsShowCron(show);
} catch {
// Keep the in-memory preference when storage is unavailable.
}
}
setSessionsShowPreview(show: boolean) {
this.host.sessionsShowPreview = show;
try {
storeSidebarSessionsShowPreview(show);
} catch {
// Keep the in-memory preference when storage is unavailable.
}
}
setSessionsShowSystem(show: boolean) {
this.host.sessionsShowSystem = show;
try {
storeSidebarSessionsShowSystem(show);
} catch {
// Keep the in-memory preference when storage is unavailable.
}
}
setSessionsStatusFilter(statusFilter: SidebarSessionStatusFilter) {
if (statusFilter === this.host.sessionsStatusFilter) {
return;
}
this.host.sessionsStatusFilter = statusFilter;
this.host.clearSessionSelection();
this.host.sessionData.resetForStatusFilter(statusFilter);
try {
storeSidebarSessionStatusFilter(statusFilter);
} catch {
// Keep the in-memory preference when storage is unavailable.
}
void this.host.sessionData.refreshSidebarSessions();
}
}