mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(ui): remove resting last-active age from sidebar rows, surface it in the session menu (#110524)
This commit is contained in:
@@ -68,6 +68,7 @@ export class SidebarCatalogMenuController {
|
||||
.x=${menu.x}
|
||||
.y=${menu.y}
|
||||
.trigger=${this.trigger}
|
||||
.lastActive=${menu.meta}
|
||||
.terminalDisabled=${!menu.canOpenTerminal || !this.hooks.terminalAvailable()}
|
||||
.onAction=${(action: CatalogSessionMenuAction) => this.handleAction(menu, action)}
|
||||
.onClose=${() => this.close()}
|
||||
|
||||
@@ -405,6 +405,7 @@ export abstract class AppSidebarMenusElement extends AppSidebarSessionGroupsElem
|
||||
category: batchRows ? sharedCategory : (session.category ?? null),
|
||||
}}
|
||||
.selectionCount=${rows.length}
|
||||
.lastActive=${batchRows ? "" : session.meta}
|
||||
.anchor=${menu}
|
||||
.trigger=${this.sessionMenuTrigger}
|
||||
.disabled=${!this.connected}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { searchForSession } from "../lib/sessions/index.ts";
|
||||
import type { NewSessionTarget } from "../pages/new-session/location.ts";
|
||||
import { shouldHandleNavigationClick } from "./app-sidebar-nav-menus.ts";
|
||||
import { sidebarSessionMetaId } from "./app-sidebar-session-types.ts";
|
||||
import { icons } from "./icons.ts";
|
||||
|
||||
export function formatSidebarTimestamp(timestampMs: number | null | undefined): string {
|
||||
@@ -60,6 +59,7 @@ export type CatalogSessionMenuRequest = {
|
||||
key: CatalogSessionKey;
|
||||
search: string;
|
||||
canOpenTerminal: boolean;
|
||||
meta: string;
|
||||
};
|
||||
|
||||
/** Stamps a freshly adopted session key onto its catalog row so the sidebar
|
||||
@@ -363,7 +363,6 @@ function renderCatalogSessionRow(
|
||||
const key = session.openClawSessionKey ?? buildCatalogSessionKey(catalogKey);
|
||||
const label = session.name || session.threadId;
|
||||
const meta = formatSidebarTimestamp(timestamp);
|
||||
const metaId = meta ? sidebarSessionMetaId(key) : undefined;
|
||||
const search = searchForSession(key);
|
||||
const href = `${pathForRoute("chat", params.basePath)}${search}`;
|
||||
const active = params.routeSessionKey !== "" && key === params.routeSessionKey;
|
||||
@@ -371,7 +370,7 @@ function renderCatalogSessionRow(
|
||||
const openTerminal = () => params.onOpenTerminal(catalogKey);
|
||||
const openMenu = (x: number, y: number, trigger?: HTMLElement) =>
|
||||
params.onOpenMenu(
|
||||
{ key: catalogKey, search, canOpenTerminal: session.canOpenTerminal === true },
|
||||
{ key: catalogKey, search, canOpenTerminal: session.canOpenTerminal === true, meta },
|
||||
x,
|
||||
y,
|
||||
trigger,
|
||||
@@ -393,7 +392,6 @@ function renderCatalogSessionRow(
|
||||
class="sidebar-recent-session__link"
|
||||
title=${`${label} · ${host.label}`}
|
||||
aria-current=${active ? "page" : nothing}
|
||||
aria-describedby=${metaId ?? nothing}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
@@ -411,7 +409,6 @@ function renderCatalogSessionRow(
|
||||
</span>
|
||||
</a>
|
||||
<span class="sidebar-recent-session__aside session-row-aside">
|
||||
<span class="session-row-trail" id=${metaId ?? nothing}>${meta}</span>
|
||||
<span class="session-row-actions">
|
||||
<button
|
||||
class="session-action"
|
||||
|
||||
@@ -85,7 +85,9 @@ export abstract class AppSidebarSessionListElement extends AppSidebarMenusElemen
|
||||
? session.subtitle
|
||||
: undefined;
|
||||
const meta = display?.meta ?? session.meta;
|
||||
const metaId = meta ? sidebarSessionMetaId(session.key) : undefined;
|
||||
const hasTrail = session.isChild && (session.runtimeMs != null || session.startedAt != null);
|
||||
const metaId = hasTrail ? sidebarSessionMetaId(session.key) : undefined;
|
||||
const menuSession = display ? { ...session, meta } : session;
|
||||
const title = display?.title ?? [label, meta].filter(Boolean).join(" · ");
|
||||
const rowClass = [
|
||||
"sidebar-recent-session",
|
||||
@@ -123,7 +125,7 @@ export abstract class AppSidebarSessionListElement extends AppSidebarMenusElemen
|
||||
? nothing
|
||||
: (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
this.openSessionMenuForRow(session, event.clientX, event.clientY);
|
||||
this.openSessionMenuForRow(menuSession, event.clientX, event.clientY);
|
||||
}}
|
||||
@mouseenter=${(event: MouseEvent) => startHoverMarquee(event.currentTarget as HTMLElement)}
|
||||
@mouseleave=${(event: MouseEvent) => stopHoverMarquee(event.currentTarget as HTMLElement)}
|
||||
@@ -189,7 +191,7 @@ export abstract class AppSidebarSessionListElement extends AppSidebarMenusElemen
|
||||
.startMs=${session.startedAt}
|
||||
.endMs=${session.endedAt ?? null}
|
||||
></openclaw-elapsed-time>`
|
||||
: meta}</span
|
||||
: nothing}</span
|
||||
>
|
||||
${session.isChild
|
||||
? nothing
|
||||
@@ -225,7 +227,7 @@ export abstract class AppSidebarSessionListElement extends AppSidebarMenusElemen
|
||||
}
|
||||
const trigger = event.currentTarget as HTMLElement;
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
this.openSessionMenuForRow(session, rect.right, rect.bottom + 4, trigger);
|
||||
this.openSessionMenuForRow(menuSession, rect.right, rect.bottom + 4, trigger);
|
||||
}}
|
||||
>
|
||||
${icons.moreHorizontal}
|
||||
|
||||
@@ -710,6 +710,9 @@ describe("AppSidebar agent chip", () => {
|
||||
| (HTMLElement & { startMs: number })
|
||||
| null
|
||||
)?.startMs;
|
||||
const childTrail = childRows[0]?.querySelector<HTMLElement>(".session-row-trail");
|
||||
expect(childTrail?.querySelector("openclaw-elapsed-time")).not.toBeNull();
|
||||
expect(childRows[0]?.querySelector("a")?.getAttribute("aria-describedby")).toBe(childTrail?.id);
|
||||
expect(runtimeStartMs).toBeGreaterThan(Date.now() - 31_000);
|
||||
expect(runtimeStartMs).toBeLessThan(Date.now() - 29_000);
|
||||
|
||||
@@ -3490,9 +3493,9 @@ describe("AppSidebar session accessibility", () => {
|
||||
expect(link?.querySelector(".sidebar-recent-session__name")?.textContent).toBe(
|
||||
"Quarterly launch plan",
|
||||
);
|
||||
const descriptionId = link?.getAttribute("aria-describedby");
|
||||
expect(descriptionId).toBeTruthy();
|
||||
expect(descriptionId ? document.getElementById(descriptionId)?.textContent : "").toBe("now");
|
||||
expect(link?.getAttribute("title")).toBe("Quarterly launch plan · now");
|
||||
expect(link?.hasAttribute("aria-describedby")).toBe(false);
|
||||
expect(row?.querySelector(".session-row-trail")?.textContent?.trim()).toBe("");
|
||||
});
|
||||
|
||||
it("renders no chat rows when only the main session exists", async () => {
|
||||
@@ -4308,6 +4311,8 @@ describe("AppSidebar catalog session rows", () => {
|
||||
expect(active[0]?.getAttribute("role")).toBe("listitem");
|
||||
expect(active[0]?.closest('[role="list"]')?.getAttribute("aria-label")).toBe("Local Codex");
|
||||
expect(active[0]?.querySelector("a")?.getAttribute("aria-current")).toBe("page");
|
||||
expect(active[0]?.querySelector("a")?.hasAttribute("aria-describedby")).toBe(false);
|
||||
expect(active[0]?.querySelector(".session-row-trail")).toBeNull();
|
||||
// The raw catalog key must not surface as a synthesized chat row.
|
||||
// Catalogs nest inside the Coding zone, so classify each row by its
|
||||
// closest section rather than any ancestor group.
|
||||
|
||||
@@ -21,7 +21,10 @@ describe("catalog session menu", () => {
|
||||
const container = document.createElement("div");
|
||||
containers.push(container);
|
||||
document.body.append(container);
|
||||
render(html`<openclaw-catalog-session-menu></openclaw-catalog-session-menu>`, container);
|
||||
render(
|
||||
html`<openclaw-catalog-session-menu .lastActive=${"57d"}></openclaw-catalog-session-menu>`,
|
||||
container,
|
||||
);
|
||||
const menu = container.querySelector("openclaw-catalog-session-menu") as CatalogMenuElement;
|
||||
await menu.updateComplete;
|
||||
const dropdown = menu.querySelector<HTMLElement & { open: boolean }>("wa-dropdown");
|
||||
@@ -31,6 +34,7 @@ describe("catalog session menu", () => {
|
||||
expect(dropdown?.open).toBe(true);
|
||||
expect(document.activeElement).toBe(items[0]);
|
||||
expect(items.map((item) => item.getAttribute("value"))).toEqual(["viewer", "terminal"]);
|
||||
expect(menu.querySelector(".session-menu__info")?.textContent?.trim()).toBe("Last active 57d");
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -12,6 +12,7 @@ class CatalogSessionMenu extends OpenClawLightDomElement {
|
||||
@property({ attribute: false }) x = 0;
|
||||
@property({ attribute: false }) y = 0;
|
||||
@property({ attribute: false }) trigger: HTMLElement | null = null;
|
||||
@property({ attribute: false }) lastActive = "";
|
||||
@property({ attribute: false }) terminalDisabled = false;
|
||||
@property({ attribute: false }) onAction: (action: CatalogSessionMenuAction) => void = () => {};
|
||||
@property({ attribute: false }) onClose: () => void = () => {};
|
||||
@@ -69,7 +70,7 @@ class CatalogSessionMenu extends OpenClawLightDomElement {
|
||||
|
||||
override render() {
|
||||
const menuWidth = 240;
|
||||
const menuMaxHeight = 112;
|
||||
const menuMaxHeight = 140;
|
||||
const x = Math.max(8, Math.min(this.x, window.innerWidth - menuWidth - 8));
|
||||
const y = Math.max(8, Math.min(this.y, window.innerHeight - menuMaxHeight - 8));
|
||||
const menuLabel = t("chat.catalog.sessionMenu");
|
||||
@@ -91,6 +92,11 @@ class CatalogSessionMenu extends OpenClawLightDomElement {
|
||||
aria-label=${menuLabel}
|
||||
style="position: fixed; left: ${x}px; top: ${y}px; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
|
||||
></button>
|
||||
${this.lastActive
|
||||
? html`<div class="session-menu__info">
|
||||
${t("sessionsView.lastActive", { time: this.lastActive })}
|
||||
</div>`
|
||||
: ""}
|
||||
<wa-dropdown-item class="session-menu__item" value="viewer">
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true"
|
||||
>${icons.messageSquare}</span
|
||||
|
||||
@@ -14,6 +14,7 @@ type SessionMenuData = {
|
||||
};
|
||||
type SessionMenuElement = HTMLElement & {
|
||||
anchor: { x: number; y: number };
|
||||
lastActive: string;
|
||||
session: SessionMenuData;
|
||||
updateComplete: Promise<boolean>;
|
||||
};
|
||||
@@ -36,6 +37,7 @@ async function mountMenu(
|
||||
archiveAllowed?: boolean;
|
||||
cloudWorkerStopAllowed?: boolean;
|
||||
selectionCount?: number;
|
||||
lastActive?: string;
|
||||
groups?: readonly string[];
|
||||
trigger?: HTMLElement | null;
|
||||
onAction?: (action: SessionMenuAction) => void;
|
||||
@@ -57,6 +59,7 @@ async function mountMenu(
|
||||
html`<openclaw-session-menu
|
||||
.session=${session}
|
||||
.selectionCount=${options.selectionCount ?? 1}
|
||||
.lastActive=${options.lastActive ?? "57d"}
|
||||
.anchor=${{ x: 100, y: 100 }}
|
||||
.trigger=${options.trigger ?? null}
|
||||
.disabled=${false}
|
||||
@@ -105,6 +108,12 @@ function menuItem(menu: ParentNode, label: string): SessionMenuItem {
|
||||
}
|
||||
|
||||
describe("session menu", () => {
|
||||
it("shows when the session was last active", async () => {
|
||||
const menu = await mountMenu({ lastActive: "57d" });
|
||||
|
||||
expect(menu.querySelector(".session-menu__info")?.textContent?.trim()).toBe("Last active 57d");
|
||||
});
|
||||
|
||||
it("renders the full plain-session item set in order", async () => {
|
||||
const menu = await mountMenu();
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ class SessionMenu extends OpenClawLightDomElement {
|
||||
// session (unread/group/archive/delete); `session` then carries aggregated
|
||||
// flags (unread = all unread, category = shared category or null).
|
||||
@property({ attribute: false }) selectionCount = 1;
|
||||
@property({ attribute: false }) lastActive = "";
|
||||
@property({ attribute: false }) anchor: { x: number; y: number } = { x: 0, y: 0 };
|
||||
@property({ attribute: false }) trigger: HTMLElement | null = null;
|
||||
@property({ attribute: false }) disabled = false;
|
||||
@@ -273,6 +274,11 @@ class SessionMenu extends OpenClawLightDomElement {
|
||||
aria-label=${menuLabel}
|
||||
style="position: fixed; left: ${clampedX}px; top: ${clampedY}px; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
|
||||
></button>
|
||||
${!batch && this.lastActive
|
||||
? html`<div class="session-menu__info">
|
||||
${t("sessionsView.lastActive", { time: this.lastActive })}
|
||||
</div>`
|
||||
: nothing}
|
||||
${!batch && this.canOpenChat
|
||||
? html`
|
||||
<wa-dropdown-item
|
||||
|
||||
@@ -615,6 +615,7 @@ export const en: TranslationMap = {
|
||||
provider: "Provider",
|
||||
runtime: "Runtime",
|
||||
runDuration: "Run duration",
|
||||
lastActive: "Last active {time}",
|
||||
surface: "Surface",
|
||||
subject: "Subject",
|
||||
room: "Room",
|
||||
|
||||
@@ -2178,6 +2178,12 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.session-menu__info {
|
||||
padding: 4px 8px 6px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.session-menu__separator {
|
||||
margin: 6px 4px;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||
|
||||
Reference in New Issue
Block a user