mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
fix(ui): keep mobile session actions reachable (#126124)
* fix(ui): keep mobile session actions reachable Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com> * test(ui): use placement startup capability Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com> --------- Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com> Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,13 @@ import { syncDropdownItemRadio } from "./web-awesome.ts";
|
||||
|
||||
type SessionOwnerAssignment = Pick<SessionOwnerOption, "type" | "id">;
|
||||
|
||||
type SessionOwnerMenuParams = {
|
||||
ownerOptions: readonly SessionOwnerOption[];
|
||||
currentOwnerId: string | null;
|
||||
disabled: boolean;
|
||||
disabledReason?: string;
|
||||
};
|
||||
|
||||
export function sessionOwnerAssignmentFromMenuValue(value: string): SessionOwnerAssignment | null {
|
||||
if (!value.startsWith("assign-owner:")) {
|
||||
return null;
|
||||
@@ -16,13 +23,43 @@ export function sessionOwnerAssignmentFromMenuValue(value: string): SessionOwner
|
||||
return (type === "human" || type === "agent") && id ? { type, id } : null;
|
||||
}
|
||||
|
||||
export function renderSessionOwnerAssignmentMenu(params: {
|
||||
ownerOptions: readonly SessionOwnerOption[];
|
||||
selfOwner: SessionOwnerOption | null;
|
||||
currentOwnerId: string | null;
|
||||
disabled: boolean;
|
||||
disabledReason?: string;
|
||||
}) {
|
||||
export function renderSessionOwnerAssignmentOptions(
|
||||
params: SessionOwnerMenuParams,
|
||||
inline = false,
|
||||
) {
|
||||
const title = params.disabledReason ?? nothing;
|
||||
return params.ownerOptions.map((owner) => {
|
||||
const checked = owner.id === params.currentOwnerId;
|
||||
return html`
|
||||
<wa-dropdown-item
|
||||
slot=${inline ? nothing : "submenu"}
|
||||
class="session-menu__item"
|
||||
value=${`assign-owner:${owner.type}:${encodeURIComponent(owner.id)}`}
|
||||
role="menuitemradio"
|
||||
aria-checked=${String(checked)}
|
||||
${ref((element) => syncDropdownItemRadio(element, checked))}
|
||||
?disabled=${params.disabled || checked}
|
||||
title=${title}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true"
|
||||
>${renderSessionOwnerMenuAvatar(owner)}</span
|
||||
>
|
||||
<span class="session-menu__text">${owner.label ?? owner.id}</span>
|
||||
${checked
|
||||
? html`<span slot="details" class="session-menu__check" aria-hidden="true"
|
||||
>${icons.check}</span
|
||||
>`
|
||||
: nothing}
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
export function renderSessionOwnerAssignmentMenu(
|
||||
params: SessionOwnerMenuParams & {
|
||||
selfOwner: SessionOwnerOption | null;
|
||||
},
|
||||
) {
|
||||
const title = params.disabledReason ?? nothing;
|
||||
return html`
|
||||
${params.selfOwner
|
||||
@@ -44,31 +81,7 @@ export function renderSessionOwnerAssignmentMenu(params: {
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.users}</span>
|
||||
<span class="session-menu__text">${t("sessionsView.assignTo")}</span>
|
||||
${params.ownerOptions.map((owner) => {
|
||||
const checked = owner.id === params.currentOwnerId;
|
||||
return html`
|
||||
<wa-dropdown-item
|
||||
slot="submenu"
|
||||
class="session-menu__item"
|
||||
value=${`assign-owner:${owner.type}:${encodeURIComponent(owner.id)}`}
|
||||
role="menuitemradio"
|
||||
aria-checked=${String(checked)}
|
||||
${ref((element) => syncDropdownItemRadio(element, checked))}
|
||||
?disabled=${params.disabled || checked}
|
||||
title=${title}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true"
|
||||
>${renderSessionOwnerMenuAvatar(owner)}</span
|
||||
>
|
||||
<span class="session-menu__text">${owner.label ?? owner.id}</span>
|
||||
${checked
|
||||
? html`<span slot="details" class="session-menu__check" aria-hidden="true"
|
||||
>${icons.check}</span
|
||||
>`
|
||||
: nothing}
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
})}
|
||||
${renderSessionOwnerAssignmentOptions(params)}
|
||||
</wa-dropdown-item>`
|
||||
: nothing}
|
||||
`;
|
||||
|
||||
@@ -207,22 +207,22 @@ suite.define(() => {
|
||||
const app = document.querySelector("openclaw-app") as HTMLElement & {
|
||||
runtime?: {
|
||||
context: {
|
||||
cloudStartup: {
|
||||
placementStartup: {
|
||||
get: (sessionKey: string) => { error?: string; phase: string } | null;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
const cloudStartup = app.runtime?.context.cloudStartup;
|
||||
if (!cloudStartup) {
|
||||
reject(new Error("cloud startup runtime unavailable"));
|
||||
const placementStartup = app.runtime?.context.placementStartup;
|
||||
if (!placementStartup) {
|
||||
reject(new Error("session placement startup unavailable"));
|
||||
return;
|
||||
}
|
||||
let settled = false;
|
||||
const subscription: { stop?: () => void } = {};
|
||||
const resolveFailed = () => {
|
||||
const status = cloudStartup.get(key);
|
||||
const status = placementStartup.get(key);
|
||||
if (settled || status?.phase !== "failed") {
|
||||
return;
|
||||
}
|
||||
@@ -230,7 +230,7 @@ suite.define(() => {
|
||||
subscription.stop?.();
|
||||
resolve(status.error ?? "");
|
||||
};
|
||||
subscription.stop = cloudStartup.subscribe(resolveFailed);
|
||||
subscription.stop = placementStartup.subscribe(resolveFailed);
|
||||
if (settled) {
|
||||
subscription.stop();
|
||||
} else {
|
||||
|
||||
@@ -273,10 +273,15 @@ describe("chat header session menu", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders quick actions directly in the compact menu", async () => {
|
||||
it("drills into compact menu groups without rendering side flyouts", async () => {
|
||||
const showTasks = vi.fn();
|
||||
const onSettingsChange = vi.fn<(patch: Partial<UiSettings>) => void>();
|
||||
const onAction = vi.fn<(action: HeaderMenuAction) => void>();
|
||||
const ada = { type: "human", id: "profile-ada", label: "Ada" } as const;
|
||||
const research = { type: "agent", id: "research:one", label: "Research" } as const;
|
||||
const menu = await mountMenu({
|
||||
compact: true,
|
||||
worktreePath: "/work/openclaw",
|
||||
panelActions: [
|
||||
{
|
||||
id: "background-tasks",
|
||||
@@ -286,15 +291,71 @@ describe("chat header session menu", () => {
|
||||
onActivate: showTasks,
|
||||
},
|
||||
],
|
||||
layoutActions: [
|
||||
{
|
||||
id: "split-right",
|
||||
label: "Split right",
|
||||
icon: icons.panelRightOpen,
|
||||
onActivate: vi.fn(),
|
||||
},
|
||||
],
|
||||
ownerOptions: [ada, research],
|
||||
selfOwner: ada,
|
||||
currentOwnerId: research.id,
|
||||
onSettingsChange,
|
||||
onAction,
|
||||
});
|
||||
|
||||
expect(menu.querySelector(".session-menu__section-label")?.textContent?.trim()).toBe("Panels");
|
||||
const rootLabels = Array.from(
|
||||
menu.querySelectorAll<MenuItemElement>(":scope > wa-dropdown > wa-dropdown-item"),
|
||||
).map(itemLabel);
|
||||
expect(rootLabels).toEqual([
|
||||
"Open in",
|
||||
"Panels",
|
||||
"Layout",
|
||||
"Rename…",
|
||||
"Assign to…",
|
||||
"View",
|
||||
"Fork",
|
||||
"Continue in terminal…",
|
||||
"Archive session",
|
||||
"Delete…",
|
||||
]);
|
||||
expect(menu.querySelector("[slot='submenu']")).toBeNull();
|
||||
|
||||
select(menu, "compact:open-view");
|
||||
await menu.updateComplete;
|
||||
expect(
|
||||
Array.from(
|
||||
menu.querySelectorAll<MenuItemElement>(":scope > wa-dropdown > wa-dropdown-item"),
|
||||
).map(itemLabel),
|
||||
).toEqual(["Back", "Reasoning", "Tool calls", "Keep commentary"]);
|
||||
expect(menu.querySelector("[slot='submenu']")).toBeNull();
|
||||
select(menu, "view:reasoning");
|
||||
expect(onSettingsChange).toHaveBeenCalledWith({ chatShowThinking: false });
|
||||
|
||||
select(menu, "compact:back");
|
||||
await menu.updateComplete;
|
||||
select(menu, "compact:open-panels");
|
||||
await menu.updateComplete;
|
||||
const action = item(menu, "Show background tasks");
|
||||
expect(action.getAttribute("slot")).toBeNull();
|
||||
expect(action.querySelector('[slot="details"]')?.textContent?.trim()).toBe("2");
|
||||
|
||||
select(menu, "quick:panels:background-tasks");
|
||||
expect(showTasks).toHaveBeenCalledOnce();
|
||||
|
||||
select(menu, "compact:open-assign-owner");
|
||||
await menu.updateComplete;
|
||||
expect(
|
||||
Array.from(
|
||||
menu.querySelectorAll<MenuItemElement>(":scope > wa-dropdown > wa-dropdown-item"),
|
||||
).map(itemLabel),
|
||||
).toEqual(["Back", "Ada", "Research"]);
|
||||
select(menu, "assign-owner:human:profile-ada");
|
||||
expect(onAction).toHaveBeenCalledWith({
|
||||
kind: "assign-owner",
|
||||
owner: { type: "human", id: "profile-ada" },
|
||||
});
|
||||
});
|
||||
|
||||
it("pins and disables onboarding view preferences", async () => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { UiSettings } from "../../../app/settings.ts";
|
||||
import { icons } from "../../../components/icons.ts";
|
||||
import { activateMenuShortcut, menuShortcutHint } from "../../../components/menu-shortcuts.ts";
|
||||
import type { SessionOwnerOption } from "../../../components/session-owner-chip.ts";
|
||||
import {
|
||||
renderSessionOwnerAssignmentMenu,
|
||||
renderSessionOwnerAssignmentOptions,
|
||||
sessionOwnerAssignmentFromMenuValue,
|
||||
} from "../../../components/session-owner-menu.ts";
|
||||
import { t } from "../../../i18n/index.ts";
|
||||
@@ -33,6 +34,17 @@ export type HeaderMenuQuickAction = {
|
||||
|
||||
const EMPTY_SETTINGS = {} as UiSettings;
|
||||
|
||||
type CompactMenuView = "root" | "open-in" | "panels" | "layout" | "assign-owner" | "view";
|
||||
|
||||
const COMPACT_MENU_VIEW_BY_VALUE: Record<string, CompactMenuView> = {
|
||||
"compact:back": "root",
|
||||
"compact:open-assign-owner": "assign-owner",
|
||||
"compact:open-layout": "layout",
|
||||
"compact:open-open-in": "open-in",
|
||||
"compact:open-panels": "panels",
|
||||
"compact:open-view": "view",
|
||||
};
|
||||
|
||||
class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
@property({ attribute: false }) sessionLabel = "";
|
||||
@property({ attribute: false }) worktreePath: string | null = null;
|
||||
@@ -56,6 +68,7 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
@property({ attribute: false }) onOpen: () => void = () => {};
|
||||
@property({ attribute: false }) onSettingsChange: (patch: Partial<UiSettings>) => void = () => {};
|
||||
@property({ attribute: false }) onAction: (action: HeaderMenuAction) => void = () => {};
|
||||
@state() private compactView: CompactMenuView = "root";
|
||||
|
||||
private actionDisabled(kind: HeaderMenuActionKind, extra = false): boolean {
|
||||
return extra || Boolean(this.actionDisabledReasons[kind]);
|
||||
@@ -70,6 +83,15 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const compactView = COMPACT_MENU_VIEW_BY_VALUE[value];
|
||||
if (compactView) {
|
||||
event.preventDefault();
|
||||
this.compactView = compactView;
|
||||
void this.updateComplete.then(() => {
|
||||
this.querySelector<HTMLElement>("wa-dropdown-item:not([disabled])")?.focus();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (value.startsWith("quick:")) {
|
||||
const [, group, id] = value.split(":");
|
||||
const actions = group === "panels" ? this.panelActions : this.layoutActions;
|
||||
@@ -124,30 +146,33 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
}
|
||||
};
|
||||
|
||||
private renderEditorSubmenu() {
|
||||
private renderEditorSubmenu(inline = false) {
|
||||
return EDITOR_IDS.map(
|
||||
(editor) => html`
|
||||
<wa-dropdown-item slot="submenu" class="session-menu__item" value=${`open-in:${editor}`}>
|
||||
<wa-dropdown-item
|
||||
slot=${inline ? nothing : "submenu"}
|
||||
class="session-menu__item"
|
||||
value=${`open-in:${editor}`}
|
||||
>
|
||||
<span class="session-menu__text">${EDITOR_LABELS[editor]}</span>
|
||||
</wa-dropdown-item>
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
private renderQuickActions(group: "panels" | "layout", actions: HeaderMenuQuickAction[]) {
|
||||
if (actions.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const label = t(group === "panels" ? "chat.sessionHeader.panels" : "chat.sessionHeader.layout");
|
||||
const icon = group === "panels" ? icons.panelRightOpen : icons.columns2;
|
||||
const items = actions.map((action) => {
|
||||
private renderQuickActionItems(
|
||||
group: "panels" | "layout",
|
||||
actions: HeaderMenuQuickAction[],
|
||||
inline = false,
|
||||
) {
|
||||
return actions.map((action) => {
|
||||
const detail =
|
||||
typeof action.badge === "number" && action.badge > 0
|
||||
? html`<span slot="details" class="session-menu__sub">${action.badge}</span>`
|
||||
: nothing;
|
||||
return html`
|
||||
<wa-dropdown-item
|
||||
slot=${this.compact ? nothing : "submenu"}
|
||||
slot=${inline ? nothing : "submenu"}
|
||||
class="session-menu__item"
|
||||
value=${`quick:${group}:${action.id}`}
|
||||
type=${action.active === undefined ? nothing : "checkbox"}
|
||||
@@ -159,30 +184,55 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
private renderCompactNavigationItem(
|
||||
view: Exclude<CompactMenuView, "root">,
|
||||
label: string,
|
||||
icon: TemplateResult,
|
||||
disabled = false,
|
||||
) {
|
||||
return html`
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value=${`compact:open-${view}`}
|
||||
?disabled=${disabled}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icon}</span>
|
||||
<span class="session-menu__text">${label}</span>
|
||||
<span slot="details" class="session-menu__icon session-menu__chevron" aria-hidden="true"
|
||||
>${icons.chevronRight}</span
|
||||
>
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderQuickActions(group: "panels" | "layout", actions: HeaderMenuQuickAction[]) {
|
||||
if (actions.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const label = t(group === "panels" ? "chat.sessionHeader.panels" : "chat.sessionHeader.layout");
|
||||
const icon = group === "panels" ? icons.panelRightOpen : icons.columns2;
|
||||
if (this.compact) {
|
||||
return html`
|
||||
<div class="session-menu__section-label">${label}</div>
|
||||
${items}
|
||||
<div class="session-menu__separator" role="separator"></div>
|
||||
`;
|
||||
return this.renderCompactNavigationItem(group, label, icon);
|
||||
}
|
||||
return html`
|
||||
<wa-dropdown-item class="session-menu__item">
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icon}</span>
|
||||
<span class="session-menu__text">${label}</span>
|
||||
${items}
|
||||
${this.renderQuickActionItems(group, actions)}
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderViewSubmenu() {
|
||||
private renderViewSubmenu(inline = false) {
|
||||
const showThinking = this.onboarding ? false : this.settings.chatShowThinking;
|
||||
const showToolCalls = this.onboarding ? true : this.settings.chatShowToolCalls;
|
||||
const persistCommentary = this.settings.chatPersistCommentary !== false;
|
||||
const disabledTitle = this.onboarding ? t("chat.onboardingDisabled") : nothing;
|
||||
const item = (value: string, label: string, checked: boolean) => html`
|
||||
<wa-dropdown-item
|
||||
slot="submenu"
|
||||
slot=${inline ? nothing : "submenu"}
|
||||
class="session-menu__item"
|
||||
type="checkbox"
|
||||
value=${`view:${value}`}
|
||||
@@ -198,13 +248,177 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
${item("tool-calls", t("chat.view.toolCalls"), showToolCalls)}
|
||||
${item("commentary", t("chat.view.commentary"), persistCommentary)}
|
||||
${this.preferencesBrowserOnly
|
||||
? html`<div slot="submenu" class="session-menu__info" role="note">
|
||||
? html`<div slot=${inline ? nothing : "submenu"} class="session-menu__info" role="note">
|
||||
${t("quickSettings.personal.browserOnly")}
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private compactOwnerOptions(): readonly SessionOwnerOption[] {
|
||||
if (!this.selfOwner || this.ownerOptions.some((owner) => owner.id === this.selfOwner?.id)) {
|
||||
return this.ownerOptions;
|
||||
}
|
||||
return [this.selfOwner, ...this.ownerOptions];
|
||||
}
|
||||
|
||||
private renderCompactView() {
|
||||
const back = html`
|
||||
<wa-dropdown-item class="session-menu__item session-menu__back" value="compact:back">
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.arrowLeft}</span>
|
||||
<span class="session-menu__text">${t("common.back")}</span>
|
||||
</wa-dropdown-item>
|
||||
<div class="session-menu__separator" role="separator"></div>
|
||||
`;
|
||||
const body =
|
||||
this.compactView === "open-in"
|
||||
? this.renderEditorSubmenu(true)
|
||||
: this.compactView === "panels"
|
||||
? this.renderQuickActionItems("panels", this.panelActions, true)
|
||||
: this.compactView === "layout"
|
||||
? this.renderQuickActionItems("layout", this.layoutActions, true)
|
||||
: this.compactView === "assign-owner"
|
||||
? renderSessionOwnerAssignmentOptions(
|
||||
{
|
||||
ownerOptions: this.compactOwnerOptions(),
|
||||
currentOwnerId: this.currentOwnerId,
|
||||
disabled: this.actionDisabled("assign-owner"),
|
||||
disabledReason: this.actionDisabledReasons["assign-owner"],
|
||||
},
|
||||
true,
|
||||
)
|
||||
: this.renderViewSubmenu(true);
|
||||
return html`${back}${body}`;
|
||||
}
|
||||
|
||||
private renderRootView() {
|
||||
return html`
|
||||
${this.worktreePath
|
||||
? html`
|
||||
${this.compact
|
||||
? this.renderCompactNavigationItem(
|
||||
"open-in",
|
||||
t("sessionsView.openInEditorMenu"),
|
||||
icons.externalLink,
|
||||
)
|
||||
: html`<wa-dropdown-item class="session-menu__item">
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true"
|
||||
>${icons.externalLink}</span
|
||||
>
|
||||
<span class="session-menu__text">${t("sessionsView.openInEditorMenu")}</span>
|
||||
${this.renderEditorSubmenu()}
|
||||
</wa-dropdown-item>`}
|
||||
<div class="session-menu__separator" role="separator"></div>
|
||||
`
|
||||
: nothing}
|
||||
${this.renderQuickActions("panels", this.panelActions)}
|
||||
${this.renderQuickActions("layout", this.layoutActions)}
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value="rename"
|
||||
data-shortcut="r"
|
||||
aria-keyshortcuts="R"
|
||||
?disabled=${this.actionDisabled("rename")}
|
||||
title=${this.actionTitle("rename")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.edit}</span>
|
||||
<span class="session-menu__text">${t("sessionsView.renameSessionMenu")}</span>
|
||||
${menuShortcutHint("r")}
|
||||
</wa-dropdown-item>
|
||||
${this.compact
|
||||
? this.compactOwnerOptions().length > 0
|
||||
? this.renderCompactNavigationItem(
|
||||
"assign-owner",
|
||||
t("sessionsView.assignTo"),
|
||||
icons.users,
|
||||
this.actionDisabled("assign-owner"),
|
||||
)
|
||||
: nothing
|
||||
: renderSessionOwnerAssignmentMenu({
|
||||
ownerOptions: this.ownerOptions,
|
||||
selfOwner: this.selfOwner,
|
||||
currentOwnerId: this.currentOwnerId,
|
||||
disabled: this.actionDisabled("assign-owner"),
|
||||
disabledReason: this.actionDisabledReasons["assign-owner"],
|
||||
})}
|
||||
${this.compact
|
||||
? this.renderCompactNavigationItem("view", t("chat.view.menu"), icons.eye)
|
||||
: html`<wa-dropdown-item class="session-menu__item">
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.eye}</span>
|
||||
<span class="session-menu__text">${t("chat.view.menu")}</span>
|
||||
${this.renderViewSubmenu()}
|
||||
</wa-dropdown-item>`}
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value="fork"
|
||||
data-shortcut="f"
|
||||
aria-keyshortcuts="F"
|
||||
?disabled=${this.actionDisabled("fork", this.forkDisabled)}
|
||||
title=${this.actionTitle("fork")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.copy}</span>
|
||||
<span class="session-menu__text"
|
||||
>${t(
|
||||
this.forkFromLastCompleted
|
||||
? "sessionsView.forkFromLastCompleted"
|
||||
: "sessionsView.forkSession",
|
||||
)}</span
|
||||
>
|
||||
${menuShortcutHint("f")}
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value="continue-in-terminal"
|
||||
?disabled=${this.actionDisabled("continue-in-terminal")}
|
||||
title=${this.actionTitle("continue-in-terminal")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.terminal}</span>
|
||||
<span class="session-menu__text">${t("chat.sessionHeader.continueInTerminal.action")}</span>
|
||||
</wa-dropdown-item>
|
||||
<div class="session-menu__separator" role="separator"></div>
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value="toggle-archived"
|
||||
data-shortcut="a"
|
||||
aria-keyshortcuts="A"
|
||||
?disabled=${this.actionDisabled("toggle-archived", !this.archived && !this.archiveAllowed)}
|
||||
title=${this.actionTitle("toggle-archived")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true"
|
||||
>${this.archived ? icons.archiveRestore : icons.archive}</span
|
||||
>
|
||||
<span class="session-menu__text"
|
||||
>${this.archived
|
||||
? t("sessionsView.restoreSession")
|
||||
: t("sessionsView.archiveSession")}</span
|
||||
>
|
||||
${menuShortcutHint("a")}
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item session-menu__item--destructive"
|
||||
value="delete"
|
||||
variant="danger"
|
||||
data-shortcut="d"
|
||||
aria-keyshortcuts="D"
|
||||
?disabled=${this.actionDisabled("delete", !this.deleteAllowed)}
|
||||
title=${this.actionTitle("delete")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.trash}</span>
|
||||
<span class="session-menu__text">${t("sessionsView.deleteSessionMenu")}</span>
|
||||
${menuShortcutHint("d")}
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly handleShow = () => {
|
||||
this.compactView = "root";
|
||||
this.onOpen();
|
||||
};
|
||||
|
||||
private readonly handleAfterHide = () => {
|
||||
this.compactView = "root";
|
||||
};
|
||||
|
||||
override render() {
|
||||
const menuLabel = t("chat.sidebar.sessionMenu", { session: this.sessionLabel });
|
||||
return html`
|
||||
@@ -213,7 +427,8 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
placement="bottom-end"
|
||||
aria-label=${menuLabel}
|
||||
@keydown=${(event: KeyboardEvent) => activateMenuShortcut(this, event)}
|
||||
@wa-show=${this.onOpen}
|
||||
@wa-show=${this.handleShow}
|
||||
@wa-after-hide=${this.handleAfterHide}
|
||||
@wa-select=${this.handleSelect}
|
||||
>
|
||||
<button
|
||||
@@ -225,108 +440,9 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
>
|
||||
${icons.moreHorizontal}
|
||||
</button>
|
||||
${this.worktreePath
|
||||
? html`
|
||||
<wa-dropdown-item class="session-menu__item">
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true"
|
||||
>${icons.externalLink}</span
|
||||
>
|
||||
<span class="session-menu__text">${t("sessionsView.openInEditorMenu")}</span>
|
||||
${this.renderEditorSubmenu()}
|
||||
</wa-dropdown-item>
|
||||
<div class="session-menu__separator" role="separator"></div>
|
||||
`
|
||||
: nothing}
|
||||
${this.renderQuickActions("panels", this.panelActions)}
|
||||
${this.renderQuickActions("layout", this.layoutActions)}
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value="rename"
|
||||
data-shortcut="r"
|
||||
aria-keyshortcuts="R"
|
||||
?disabled=${this.actionDisabled("rename")}
|
||||
title=${this.actionTitle("rename")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.edit}</span>
|
||||
<span class="session-menu__text">${t("sessionsView.renameSessionMenu")}</span>
|
||||
${menuShortcutHint("r")}
|
||||
</wa-dropdown-item>
|
||||
${renderSessionOwnerAssignmentMenu({
|
||||
ownerOptions: this.ownerOptions,
|
||||
selfOwner: this.selfOwner,
|
||||
currentOwnerId: this.currentOwnerId,
|
||||
disabled: this.actionDisabled("assign-owner"),
|
||||
disabledReason: this.actionDisabledReasons["assign-owner"],
|
||||
})}
|
||||
<wa-dropdown-item class="session-menu__item">
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.eye}</span>
|
||||
<span class="session-menu__text">${t("chat.view.menu")}</span>
|
||||
${this.renderViewSubmenu()}
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value="fork"
|
||||
data-shortcut="f"
|
||||
aria-keyshortcuts="F"
|
||||
?disabled=${this.actionDisabled("fork", this.forkDisabled)}
|
||||
title=${this.actionTitle("fork")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.copy}</span>
|
||||
<span class="session-menu__text"
|
||||
>${t(
|
||||
this.forkFromLastCompleted
|
||||
? "sessionsView.forkFromLastCompleted"
|
||||
: "sessionsView.forkSession",
|
||||
)}</span
|
||||
>
|
||||
${menuShortcutHint("f")}
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value="continue-in-terminal"
|
||||
?disabled=${this.actionDisabled("continue-in-terminal")}
|
||||
title=${this.actionTitle("continue-in-terminal")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.terminal}</span>
|
||||
<span class="session-menu__text"
|
||||
>${t("chat.sessionHeader.continueInTerminal.action")}</span
|
||||
>
|
||||
</wa-dropdown-item>
|
||||
<div class="session-menu__separator" role="separator"></div>
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item"
|
||||
value="toggle-archived"
|
||||
data-shortcut="a"
|
||||
aria-keyshortcuts="A"
|
||||
?disabled=${this.actionDisabled(
|
||||
"toggle-archived",
|
||||
!this.archived && !this.archiveAllowed,
|
||||
)}
|
||||
title=${this.actionTitle("toggle-archived")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true"
|
||||
>${this.archived ? icons.archiveRestore : icons.archive}</span
|
||||
>
|
||||
<span class="session-menu__text"
|
||||
>${this.archived
|
||||
? t("sessionsView.restoreSession")
|
||||
: t("sessionsView.archiveSession")}</span
|
||||
>
|
||||
${menuShortcutHint("a")}
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item
|
||||
class="session-menu__item session-menu__item--destructive"
|
||||
value="delete"
|
||||
variant="danger"
|
||||
data-shortcut="d"
|
||||
aria-keyshortcuts="D"
|
||||
?disabled=${this.actionDisabled("delete", !this.deleteAllowed)}
|
||||
title=${this.actionTitle("delete")}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.trash}</span>
|
||||
<span class="session-menu__text">${t("sessionsView.deleteSessionMenu")}</span>
|
||||
${menuShortcutHint("d")}
|
||||
</wa-dropdown-item>
|
||||
${this.compact && this.compactView !== "root"
|
||||
? this.renderCompactView()
|
||||
: this.renderRootView()}
|
||||
</wa-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -724,11 +724,15 @@ openclaw-chat-pane {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.chat-header-session-menu--compact .session-menu__section-label {
|
||||
padding: 4px 8px 2px;
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-xs);
|
||||
font-weight: 650;
|
||||
.chat-header-session-menu--compact .session-menu__back {
|
||||
z-index: 1;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--wa-color-surface-raised);
|
||||
}
|
||||
|
||||
.chat-header-session-menu--compact .session-menu__chevron {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.chat-header-session-menu--compact::part(menu) {
|
||||
|
||||
Reference in New Issue
Block a user