mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
improve(ui): declutter narrow session headers (#121935)
* improve(ui): declutter narrow session headers * test: drop stale unit-fast helper expectations --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
committed by
GitHub
parent
1af6d3051d
commit
4841e4e6b9
@@ -4591,6 +4591,8 @@ export const en: TranslationMap = {
|
||||
renameAria: "Rename session {title}",
|
||||
renameInputAria: "Session title",
|
||||
renameInputPlaceholder: "Session title",
|
||||
panels: "Panels",
|
||||
layout: "Layout",
|
||||
workspaceAria: "Workspace actions for {workspace}",
|
||||
revealFinder: "Reveal in Finder",
|
||||
revealFileExplorer: "Reveal in File Explorer",
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
resolveUiConfiguredMainKey,
|
||||
} from "../../lib/sessions/session-key.ts";
|
||||
import { normalizeOptionalString } from "../../lib/string-coerce.ts";
|
||||
import { isActiveTask } from "../../lib/tasks/data.ts";
|
||||
import { renderBoardViewSwitch } from "./board-session-surface.ts";
|
||||
import { ChatPaneSessionMenu } from "./chat-pane-session-menu.ts";
|
||||
import { readChatSessionActionAccess } from "./chat-session-action-access.ts";
|
||||
@@ -31,7 +32,10 @@ import { renderBackgroundTasksToggle } from "./components/chat-background-tasks-
|
||||
import type { BackgroundTasksProps } from "./components/chat-background-tasks.types.ts";
|
||||
import { isChatRunWorking } from "./components/chat-composer.ts";
|
||||
import "./components/chat-header-session-menu.ts";
|
||||
import type { HeaderMenuAction } from "./components/chat-header-session-menu.ts";
|
||||
import type {
|
||||
HeaderMenuAction,
|
||||
HeaderMenuQuickAction,
|
||||
} from "./components/chat-header-session-menu.ts";
|
||||
import {
|
||||
canRevealSessionWorkspace,
|
||||
renderChatPaneHeader,
|
||||
@@ -157,23 +161,119 @@ export abstract class ChatPaneHeader extends ChatPaneSessionMenu {
|
||||
session: row,
|
||||
})
|
||||
: {};
|
||||
const desktopPanelAction = isDesktopPanelAvailable(this.context.gateway.snapshot)
|
||||
const desktopPanelAvailable = isDesktopPanelAvailable(this.context.gateway.snapshot);
|
||||
const openDesktopPanel = () =>
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<DesktopPanelToggleDetail>(DESKTOP_PANEL_TOGGLE_EVENT, {
|
||||
detail: { open: true },
|
||||
}),
|
||||
);
|
||||
const desktopPanelAction = desktopPanelAvailable
|
||||
? html`<openclaw-tooltip .content=${t("desktop.toggle")}>
|
||||
<button
|
||||
class="btn btn--ghost btn--icon chat-icon-btn chat-desktop-panel-toggle"
|
||||
type="button"
|
||||
aria-label=${t("desktop.toggle")}
|
||||
@click=${() =>
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<DesktopPanelToggleDetail>(DESKTOP_PANEL_TOGGLE_EVENT, {
|
||||
detail: { open: true },
|
||||
}),
|
||||
)}
|
||||
@click=${openDesktopPanel}
|
||||
>
|
||||
${icons.monitor}
|
||||
</button>
|
||||
</openclaw-tooltip>`
|
||||
: nothing;
|
||||
const discussion = this.resolveSessionDiscussionAction();
|
||||
const sessionRailMode = this.selectedSessionRailMode(this.state?.sessionKey ?? "");
|
||||
const toggleSessionRail = () => this.requestSessionRail("toggle");
|
||||
const panelMenuActions: HeaderMenuQuickAction[] = [];
|
||||
if (sessionWorkspace.onToggleTerminal) {
|
||||
panelMenuActions.push({
|
||||
id: "terminal",
|
||||
label: t("terminal.toggle"),
|
||||
icon: icons.terminal,
|
||||
onActivate: sessionWorkspace.onToggleTerminal,
|
||||
});
|
||||
}
|
||||
if (desktopPanelAvailable) {
|
||||
panelMenuActions.push({
|
||||
id: "desktop",
|
||||
label: t("desktop.toggle"),
|
||||
icon: icons.monitor,
|
||||
onActivate: openDesktopPanel,
|
||||
});
|
||||
}
|
||||
if (discussion) {
|
||||
panelMenuActions.push({
|
||||
id: "discussion",
|
||||
label: discussion.label,
|
||||
icon: icons.messageSquare,
|
||||
active: discussion.active,
|
||||
onActivate: discussion.onToggle,
|
||||
});
|
||||
}
|
||||
if (sessionWorkspace.onOpenDiff) {
|
||||
panelMenuActions.push({
|
||||
id: "changes",
|
||||
label: t("chat.sessionDiff.show"),
|
||||
icon: icons.fileDiff,
|
||||
disabledReason: sessionWorkspace.diffNotGit ? t("chat.sessionDiff.notGit") : undefined,
|
||||
onActivate: sessionWorkspace.onOpenDiff,
|
||||
});
|
||||
}
|
||||
if (backgroundTasks) {
|
||||
panelMenuActions.push({
|
||||
id: "background-tasks",
|
||||
label: t(
|
||||
backgroundTasks.collapsed ? "chat.backgroundTasks.show" : "chat.backgroundTasks.collapse",
|
||||
),
|
||||
icon: icons.listChecks,
|
||||
active: !backgroundTasks.collapsed,
|
||||
badge: backgroundTasks.tasks?.filter(isActiveTask).length ?? 0,
|
||||
onActivate: backgroundTasks.onToggleCollapsed,
|
||||
});
|
||||
}
|
||||
panelMenuActions.push({
|
||||
id: "session-files",
|
||||
label: t(
|
||||
sessionWorkspace.collapsed
|
||||
? "chat.workspaceFiles.showFiles"
|
||||
: "chat.workspaceFiles.collapse",
|
||||
),
|
||||
icon: icons.fileText,
|
||||
active: !sessionWorkspace.collapsed,
|
||||
badge: sessionWorkspace.list?.files.filter((file) => file.kind === "modified").length ?? 0,
|
||||
onActivate: sessionWorkspace.onToggleCollapsed,
|
||||
});
|
||||
panelMenuActions.push({
|
||||
id: "session-companion",
|
||||
label: t(sessionRailMode === "expanded" ? "chat.rail.collapse" : "chat.rail.show"),
|
||||
icon: icons.spark,
|
||||
active: sessionRailMode === "expanded",
|
||||
onActivate: toggleSessionRail,
|
||||
});
|
||||
const layoutMenuActions: HeaderMenuQuickAction[] = [];
|
||||
if (this.onOpenSplitView) {
|
||||
layoutMenuActions.push({
|
||||
id: "open-split-view",
|
||||
label: t("chat.splitView.open"),
|
||||
icon: icons.columns2,
|
||||
onActivate: this.onOpenSplitView,
|
||||
});
|
||||
}
|
||||
if (!this.narrow && this.onSplitDown) {
|
||||
layoutMenuActions.push({
|
||||
id: "split-down",
|
||||
label: t("chat.splitView.splitDown"),
|
||||
icon: icons.panelBottomOpen,
|
||||
onActivate: () => this.onSplitDown?.(this.paneId),
|
||||
});
|
||||
}
|
||||
if (!this.narrow && this.onSplitRight) {
|
||||
layoutMenuActions.push({
|
||||
id: "split-right",
|
||||
label: t("chat.splitView.splitRight"),
|
||||
icon: icons.panelRightOpen,
|
||||
onActivate: () => this.onSplitRight?.(this.paneId),
|
||||
});
|
||||
}
|
||||
return renderChatPaneHeader({
|
||||
paneId: this.paneId,
|
||||
narrow: this.narrow,
|
||||
@@ -206,12 +306,12 @@ export abstract class ChatPaneHeader extends ChatPaneSessionMenu {
|
||||
this.catalogSession,
|
||||
sessionWorkspace.onToggleTerminal,
|
||||
)}${desktopPanelAction}`,
|
||||
discussionAction: this.renderSessionDiscussionAction(),
|
||||
discussionAction: this.renderSessionDiscussionAction(discussion),
|
||||
diffAction: renderSessionDiffToggle(sessionWorkspace),
|
||||
backgroundTasksAction: renderBackgroundTasksToggle(backgroundTasks),
|
||||
sessionRailAction: renderSessionRailToggle({
|
||||
mode: this.selectedSessionRailMode(this.state?.sessionKey ?? ""),
|
||||
onToggle: () => this.requestSessionRail("toggle"),
|
||||
mode: sessionRailMode,
|
||||
onToggle: toggleSessionRail,
|
||||
}),
|
||||
workspaceAction: renderSessionWorkspaceToggle(sessionWorkspace),
|
||||
presence:
|
||||
@@ -298,7 +398,10 @@ export abstract class ChatPaneHeader extends ChatPaneSessionMenu {
|
||||
.onboarding=${this.onboarding}
|
||||
.preferencesBrowserOnly=${this.context.runtimeConfig?.state.connected &&
|
||||
this.context.runtimeConfig.canPatch === false}
|
||||
.compact=${this.narrow}
|
||||
.settings=${this.state.settings}
|
||||
.panelActions=${panelMenuActions}
|
||||
.layoutActions=${layoutMenuActions}
|
||||
.actionDisabledReasons=${actionDisabledReasons}
|
||||
.forkDisabled=${this.state.sessionsLoading || row.modelSelectionLocked === true}
|
||||
.archiveAllowed=${archiveAllowed}
|
||||
@@ -508,7 +611,11 @@ export abstract class ChatPaneHeader extends ChatPaneSessionMenu {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected renderSessionDiscussionAction() {
|
||||
private resolveSessionDiscussionAction(): {
|
||||
active: boolean;
|
||||
label: string;
|
||||
onToggle: () => void;
|
||||
} | null {
|
||||
const state = this.state;
|
||||
const sessionKey = state?.sessionKey.trim() ?? "";
|
||||
const known = sessionKey ? this.sessionDiscussionStates.get(sessionKey) : undefined;
|
||||
@@ -520,26 +627,37 @@ export abstract class ChatPaneHeader extends ChatPaneSessionMenu {
|
||||
known === "none" ||
|
||||
isGatewayMethodAdvertised(this.context.gateway.snapshot, "session.discussion.info") !== true
|
||||
) {
|
||||
return nothing;
|
||||
return null;
|
||||
}
|
||||
if (!this.buildSessionDiscussionPanel(state, sessionKey)) {
|
||||
return nothing;
|
||||
return null;
|
||||
}
|
||||
const active = state.sidebarLayout.columns.some((column) =>
|
||||
column.panels.some((panel) => panel.slot === "discussion"),
|
||||
);
|
||||
const label = t(active ? "chat.sessionDiscussion.hide" : "chat.sessionDiscussion.show");
|
||||
return {
|
||||
active,
|
||||
label,
|
||||
onToggle: () =>
|
||||
active
|
||||
? state.updateSidebarLayout(closeSlot(state.sidebarLayout, "discussion"))
|
||||
: this.openSessionDiscussionSlot(),
|
||||
};
|
||||
}
|
||||
|
||||
protected renderSessionDiscussionAction(action = this.resolveSessionDiscussionAction()) {
|
||||
if (!action) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<openclaw-tooltip .content=${label}>
|
||||
<openclaw-tooltip .content=${action.label}>
|
||||
<button
|
||||
class="btn btn--ghost btn--icon chat-icon-btn chat-session-discussion-toggle"
|
||||
type="button"
|
||||
aria-label=${label}
|
||||
aria-pressed=${String(active)}
|
||||
@click=${() =>
|
||||
active
|
||||
? state.updateSidebarLayout(closeSlot(state.sidebarLayout, "discussion"))
|
||||
: this.openSessionDiscussionSlot()}
|
||||
aria-label=${action.label}
|
||||
aria-pressed=${String(action.active)}
|
||||
@click=${action.onToggle}
|
||||
>
|
||||
${icons.messageSquare}
|
||||
</button>
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { html, render } from "lit";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { UiSettings } from "../../../app/settings.ts";
|
||||
import { icons } from "../../../components/icons.ts";
|
||||
import type { SessionMenuActionKind } from "../../../components/session-menu.ts";
|
||||
import "./chat-header-session-menu.ts";
|
||||
import type { HeaderMenuAction } from "./chat-header-session-menu.ts";
|
||||
import type { HeaderMenuAction, HeaderMenuQuickAction } from "./chat-header-session-menu.ts";
|
||||
|
||||
type HeaderMenuElement = HTMLElement & { updateComplete: Promise<boolean> };
|
||||
type MenuItemElement = HTMLElement & { checked: boolean; disabled: boolean; submenuOpen?: boolean };
|
||||
@@ -41,7 +42,10 @@ async function mountMenu(
|
||||
archived?: boolean;
|
||||
onboarding?: boolean;
|
||||
preferencesBrowserOnly?: boolean;
|
||||
compact?: boolean;
|
||||
settings?: UiSettings;
|
||||
panelActions?: HeaderMenuQuickAction[];
|
||||
layoutActions?: HeaderMenuQuickAction[];
|
||||
actionDisabledReasons?: Partial<Record<SessionMenuActionKind, string>>;
|
||||
forkDisabled?: boolean;
|
||||
archiveAllowed?: boolean;
|
||||
@@ -61,7 +65,10 @@ async function mountMenu(
|
||||
.archived=${options.archived ?? false}
|
||||
.onboarding=${options.onboarding ?? false}
|
||||
.preferencesBrowserOnly=${options.preferencesBrowserOnly ?? false}
|
||||
.compact=${options.compact ?? false}
|
||||
.settings=${options.settings ?? settings()}
|
||||
.panelActions=${options.panelActions ?? []}
|
||||
.layoutActions=${options.layoutActions ?? []}
|
||||
.actionDisabledReasons=${options.actionDisabledReasons ?? {}}
|
||||
.forkDisabled=${options.forkDisabled ?? false}
|
||||
.archiveAllowed=${options.archiveAllowed ?? true}
|
||||
@@ -165,6 +172,84 @@ describe("chat header session menu", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps panel and layout actions available from the session menu", async () => {
|
||||
const showTasks = vi.fn();
|
||||
const showChanges = vi.fn();
|
||||
const splitRight = vi.fn();
|
||||
const menu = await mountMenu({
|
||||
panelActions: [
|
||||
{
|
||||
id: "background-tasks",
|
||||
label: "Show background tasks",
|
||||
icon: icons.listChecks,
|
||||
active: false,
|
||||
badge: 2,
|
||||
onActivate: showTasks,
|
||||
},
|
||||
{
|
||||
id: "changes",
|
||||
label: "Show session changes",
|
||||
icon: icons.fileDiff,
|
||||
disabledReason: "This session's workspace is not a git checkout.",
|
||||
onActivate: showChanges,
|
||||
},
|
||||
],
|
||||
layoutActions: [
|
||||
{
|
||||
id: "split-right",
|
||||
label: "Split right",
|
||||
icon: icons.panelRightOpen,
|
||||
onActivate: splitRight,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const panels = item(menu, "Panels");
|
||||
const panelItems = Array.from(
|
||||
panels.querySelectorAll<MenuItemElement>("wa-dropdown-item[slot='submenu']"),
|
||||
);
|
||||
expect(panelItems.map(itemLabel)).toEqual(["Show background tasks", "Show session changes"]);
|
||||
expect(panelItems[0]?.checked).toBe(false);
|
||||
expect(panelItems[0]?.querySelector('[slot="details"]')?.textContent?.trim()).toBe("2");
|
||||
expect(panelItems[1]?.disabled).toBe(true);
|
||||
expect(
|
||||
Array.from(
|
||||
item(menu, "Layout").querySelectorAll<MenuItemElement>("wa-dropdown-item[slot='submenu']"),
|
||||
).map(itemLabel),
|
||||
).toEqual(["Split right"]);
|
||||
|
||||
select(menu, "quick:panels:background-tasks");
|
||||
select(menu, "quick:panels:changes");
|
||||
select(menu, "quick:layout:split-right");
|
||||
expect(showTasks).toHaveBeenCalledOnce();
|
||||
expect(showChanges).not.toHaveBeenCalled();
|
||||
expect(splitRight).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("renders quick actions directly in the compact menu", async () => {
|
||||
const showTasks = vi.fn();
|
||||
const menu = await mountMenu({
|
||||
compact: true,
|
||||
panelActions: [
|
||||
{
|
||||
id: "background-tasks",
|
||||
label: "Show background tasks",
|
||||
icon: icons.listChecks,
|
||||
badge: 2,
|
||||
onActivate: showTasks,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(menu.querySelector(".session-menu__section-label")?.textContent?.trim()).toBe("Panels");
|
||||
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();
|
||||
});
|
||||
|
||||
it("pins and disables onboarding view preferences", async () => {
|
||||
const onSettingsChange = vi.fn<(patch: Partial<UiSettings>) => void>();
|
||||
const menu = await mountMenu({ onboarding: true, onSettingsChange });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import type { UiSettings } from "../../../app/settings.ts";
|
||||
import { icons } from "../../../components/icons.ts";
|
||||
@@ -16,6 +16,16 @@ export type HeaderMenuAction =
|
||||
| { kind: "toggle-archived" }
|
||||
| { kind: "delete" };
|
||||
|
||||
export type HeaderMenuQuickAction = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: TemplateResult;
|
||||
active?: boolean;
|
||||
badge?: number;
|
||||
disabledReason?: string;
|
||||
onActivate: () => void;
|
||||
};
|
||||
|
||||
const EMPTY_SETTINGS = {} as UiSettings;
|
||||
|
||||
class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
@@ -24,7 +34,10 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
@property({ attribute: false }) archived = false;
|
||||
@property({ attribute: false }) onboarding = false;
|
||||
@property({ attribute: false }) preferencesBrowserOnly = false;
|
||||
@property({ attribute: false }) compact = false;
|
||||
@property({ attribute: false }) settings: UiSettings = EMPTY_SETTINGS;
|
||||
@property({ attribute: false }) panelActions: HeaderMenuQuickAction[] = [];
|
||||
@property({ attribute: false }) layoutActions: HeaderMenuQuickAction[] = [];
|
||||
@property({ attribute: false }) actionDisabledReasons: Partial<
|
||||
Record<SessionMenuActionKind, string>
|
||||
> = {};
|
||||
@@ -48,6 +61,15 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (value.startsWith("quick:")) {
|
||||
const [, group, id] = value.split(":");
|
||||
const actions = group === "panels" ? this.panelActions : this.layoutActions;
|
||||
const action = actions.find((candidate) => candidate.id === id);
|
||||
if (action && !action.disabledReason) {
|
||||
action.onActivate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value.startsWith("view:")) {
|
||||
event.preventDefault();
|
||||
if (this.onboarding) {
|
||||
@@ -94,6 +116,49 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
);
|
||||
}
|
||||
|
||||
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) => {
|
||||
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"}
|
||||
class="session-menu__item"
|
||||
value=${`quick:${group}:${action.id}`}
|
||||
type=${action.active === undefined ? nothing : "checkbox"}
|
||||
.checked=${action.active ?? false}
|
||||
?disabled=${Boolean(action.disabledReason)}
|
||||
title=${action.disabledReason ?? nothing}
|
||||
>
|
||||
<span slot="icon" class="session-menu__icon" aria-hidden="true">${action.icon}</span>
|
||||
<span class="session-menu__text">${action.label}</span>
|
||||
${detail}
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
});
|
||||
if (this.compact) {
|
||||
return html`
|
||||
<div class="session-menu__section-label">${label}</div>
|
||||
${items}
|
||||
<div class="session-menu__separator" role="separator"></div>
|
||||
`;
|
||||
}
|
||||
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}
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderViewSubmenu() {
|
||||
const showThinking = this.onboarding ? false : this.settings.chatShowThinking;
|
||||
const showToolCalls = this.onboarding ? true : this.settings.chatShowToolCalls;
|
||||
@@ -128,7 +193,7 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
const menuLabel = t("chat.sidebar.sessionMenu", { session: this.sessionLabel });
|
||||
return html`
|
||||
<wa-dropdown
|
||||
class="session-menu chat-header-session-menu"
|
||||
class=${`session-menu chat-header-session-menu${this.compact ? " chat-header-session-menu--compact" : ""}`}
|
||||
placement="bottom-end"
|
||||
aria-label=${menuLabel}
|
||||
@keydown=${(event: KeyboardEvent) => activateMenuShortcut(this, event)}
|
||||
@@ -156,6 +221,8 @@ class ChatHeaderSessionMenu extends OpenClawLightDomElement {
|
||||
<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"
|
||||
|
||||
@@ -253,6 +253,41 @@ describe("chat pane header", () => {
|
||||
expect(actions?.querySelector(".chat-pane__close-pane")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("moves session panel shortcuts out of a narrow header while keeping shell actions", () => {
|
||||
const { container } = mount({
|
||||
narrow: true,
|
||||
mergedChrome: true,
|
||||
panelActions: html`<button data-action="terminal"></button>`,
|
||||
discussionAction: html`<button data-action="discussion"></button>`,
|
||||
diffAction: html`<button data-action="diff"></button>`,
|
||||
backgroundTasksAction: html`<button data-action="tasks"></button>`,
|
||||
workspaceAction: html`<button data-action="workspace"></button>`,
|
||||
sessionRailAction: html`<button data-action="rail"></button>`,
|
||||
sessionMenuAction: html`<button data-action="session-menu"></button>`,
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-action="terminal"]')).toBeNull();
|
||||
expect(container.querySelector('[data-action="discussion"]')).toBeNull();
|
||||
expect(container.querySelector('[data-action="diff"]')).toBeNull();
|
||||
expect(container.querySelector('[data-action="tasks"]')).toBeNull();
|
||||
expect(container.querySelector('[data-action="workspace"]')).toBeNull();
|
||||
expect(container.querySelector('[data-action="rail"]')).toBeNull();
|
||||
expect(container.querySelector('[data-action="session-menu"]')).not.toBeNull();
|
||||
expect(container.querySelector(".chat-pane__nav-toggle")).not.toBeNull();
|
||||
expect(container.querySelector(".chat-pane__palette-open")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("keeps narrow catalog panel shortcuts visible without a session menu", () => {
|
||||
const { container } = mount({
|
||||
narrow: true,
|
||||
catalog: true,
|
||||
session: undefined,
|
||||
panelActions: html`<button data-action="terminal"></button>`,
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-action="terminal"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders an editable title and workspace chip", () => {
|
||||
const { container, props } = mount();
|
||||
const title = container.querySelector<HTMLButtonElement>(".chat-pane__session-title-button");
|
||||
|
||||
@@ -240,6 +240,7 @@ export function renderChatPaneHeader(props: ChatPaneHeaderProps) {
|
||||
: t("chat.sessionHeader.copyBranch");
|
||||
const copied = props.copiedAction === "copy-path" || props.copiedAction === "copy-branch";
|
||||
const drawerLabel = props.navDrawerOpen ? t("nav.collapse") : t("nav.expand");
|
||||
const compactSessionActions = props.narrow && props.sessionMenuAction !== nothing;
|
||||
|
||||
return html`
|
||||
<div class="chat-pane__header" @mousedown=${beginNativeWindowDrag}>
|
||||
@@ -432,8 +433,8 @@ export function renderChatPaneHeader(props: ChatPaneHeaderProps) {
|
||||
: nothing}
|
||||
${renderGatewayPicker(props)}
|
||||
<div class="chat-pane__actions">
|
||||
${props.panelActions} ${props.discussionAction}
|
||||
${props.catalog
|
||||
${compactSessionActions ? nothing : html`${props.panelActions} ${props.discussionAction}`}
|
||||
${props.catalog || compactSessionActions
|
||||
? nothing
|
||||
: html`${props.diffAction} ${props.backgroundTasksAction} ${props.workspaceAction}
|
||||
${props.sessionRailAction}`}
|
||||
|
||||
@@ -517,6 +517,22 @@ openclaw-chat-pane {
|
||||
}
|
||||
}
|
||||
|
||||
.chat-header-session-menu--compact .session-menu__item {
|
||||
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::part(menu) {
|
||||
max-height: calc(100dvh - 16px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Plain web shell chrome overlays the first pane header. Expanded navigation
|
||||
shows toggle + search; collapsed navigation adds new-thread between them. */
|
||||
html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native-web-chrome)
|
||||
|
||||
Reference in New Issue
Block a user