improve(ui): consolidate dashboard chat visibility into one Chat/Split/Dashboard toggle (#120487)

* improve(ui): consolidate dashboard chat visibility into one Chat/Split/Dashboard toggle

The dashboard face spread chat visibility across three separate
affordances: a two-option face toggle, an icon-only dock dropdown among
the header actions, and a floating vertically-rotated "Show chat"
button on the board edge that users could not discover. One concept,
three controls.

The header now renders a single segmented control - Chat | Split |
Dashboard - where Split is dashboard plus docked chat and Dashboard is
board-only (the former "Hide chat"). A compact caret attached to the
pill picks the dock side (left/right/bottom) and only appears in Split
mode when dock mutation is allowed; read-only sessions keep the plain
two-option Chat | Dashboard toggle. The floating reopen button, the
standalone dock menu button, and their CSS (including the mobile
overrides) are deleted, so the header ends up with fewer buttons than
before.

Verified in the mock-gateway dev harness: mode mapping, caret dropdown,
unhide-to-remembered-side, and hidden dock rendering board-only.

* fix(ci): unexport board dock settings union and repair stale plugin-sdk baseline

BoardVisibleChatDock lost its last external consumer when the board
view switch consolidated dock controls; keep it file-local so the
production unused-export scan stays clean.

Also regenerate docs/.generated/plugin-sdk-api-baseline.sha256: the
session-catalog module hash was stale on main (check fails on clean
origin/main after the agent-attribution reverts), which blocks every
PR's check-plugin-sdk-api-baseline job.

* docs(web): describe the Chat/Split/Dashboard switch and dock-side picker
This commit is contained in:
Peter Steinberger
2026-08-08 04:48:55 -07:00
committed by GitHub
parent 90acd2bc90
commit 2d5ff4768c
11 changed files with 273 additions and 223 deletions
+5 -4
View File
@@ -58,10 +58,11 @@ Principles:
- **Graduation:** agent calls `show_widget` in any chat → widget renders inline
in the transcript exactly as today → hover shows **Pin to dashboard** → widget
appears on the session's board. The agent can pass `pin: true` to do the same.
- **Board view:** a session with a board gets a face toggle (Chat / Dashboard).
Board view = tab strip (only when >1 tab) + fluid grid + docked chat pane.
Chat dock is resizable, movable (left/right/bottom), and collapsible exactly
like the sidebar. Per-tab dock state is remembered.
- **Board view:** a session with a board gets a view switch (Chat / Split /
Dashboard). Split = tab strip (only when >1 tab) + fluid grid + docked chat
pane; Dashboard is the same without the chat. The chat dock is resizable and
movable (left/right/bottom) via the switch's dock picker. Per-tab dock state
is remembered.
- **Drag:** user drags widgets; grid auto-compacts (widgets float up, neighbors
reflow). Resize by handle snaps to size steps. No pixel placement — for
anyone.
+8 -6
View File
@@ -8,9 +8,10 @@ title: "Session Dashboards"
Every thread in the Control UI has two faces: the conversation you know, and a
**dashboard** — a grid of live widgets your agent builds for you. A thread with
no widgets is just chat. The moment a widget is pinned, a **Chat | Dashboard**
toggle appears in the header, and the dashboard becomes the main surface with
your chat docked beside it.
no widgets is just chat. The moment a widget is pinned, a **Chat | Split |
Dashboard** switch appears in the header: Chat is the conversation alone, Split
shows the dashboard with your chat docked beside it, and Dashboard gives the
board the whole surface.
There is nothing to set up and no separate app to configure: dashboards are a
core feature, owned by the thread, stored with the agent, and they survive
@@ -59,9 +60,10 @@ never needs the agent.
- **Tabs.** A board can have several pages — say, an overview tab and a
focused tab with one big widget. Each tab remembers its own chat-dock
position.
- **Docked chat.** On the dashboard face, your conversation docks to the
left, right, or bottom, resizes like the sidebar, and can be hidden
entirely — the agent still hears you when you bring it back.
- **Docked chat.** In Split, your conversation docks to the left, right, or
bottom — pick the side from the small arrow on the header switch — and
resizes like the sidebar. Choose Dashboard to hide the chat entirely; the
agent still hears you when you bring it back.
- **Agent parity.** Everything you can do, the agent can do with its
`dashboard` tool: add, update, move, resize, and remove widgets, manage
tabs, switch the visible tab, and move or hide the chat dock. Ask "put the
+1 -2
View File
@@ -4465,14 +4465,13 @@ export const en: TranslationMap = {
board: {
faceLabel: "Thread face",
chatFace: "Chat",
splitFace: "Split",
dashboardFace: "Dashboard",
dockMenu: "Chat dock: {dock}",
dockLeft: "Dock chat left",
dockRight: "Dock chat right",
dockBottom: "Dock chat bottom",
dockHidden: "Hide chat",
resizeDock: "Resize chat dock",
reopenChat: "Show chat",
workboardCard: "Workboard card: {title}, {status}",
defaultTab: "Main",
mockPlaceholder: "Board view seam · {tabs} tabs · {widgets} widgets",
+3 -1
View File
@@ -1,7 +1,9 @@
import type { SessionBoardFace } from "../../../../src/shared/session-types.js";
export type BoardFace = SessionBoardFace;
export type BoardVisibleChatDock = "bottom" | "left" | "right";
// Persisted-settings union; the render-layer equivalent is VisibleBoardDock
// (chat-pane-shared.ts), derived from the protocol BoardTab shape.
type BoardVisibleChatDock = "bottom" | "left" | "right";
export type BoardSessionView = {
activeTabId?: string;
+127 -32
View File
@@ -3,9 +3,8 @@
import { html, render } from "lit";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { boardProviderForSession } from "../../lib/board/provider.ts";
import type { BoardTab } from "../../lib/board/types.ts";
import { installBrowserHistoryIsolation } from "../../test-helpers/browser-history.ts";
import { renderBoardFaceToggle, renderBoardSessionSurface } from "./board-session-surface.ts";
import { renderBoardSessionSurface, renderBoardViewSwitch } from "./board-session-surface.ts";
const containers: HTMLElement[] = [];
@@ -41,7 +40,6 @@ describe("board session shell", () => {
snapshot: provider.snapshot$.value,
activeTabId: "main",
dock: "right" as const,
reopenDock: "right" as const,
dockSize: { height: 300 },
chat: html`<div>chat</div>`,
divider: html`<div></div>`,
@@ -53,7 +51,6 @@ describe("board session shell", () => {
selectTab: () => {},
},
widgetFrameUrl: (name: string, revision: number) => provider.widgetFrameUrl(name, revision),
onDockChange: () => {},
};
render(
@@ -77,35 +74,142 @@ describe("board session shell", () => {
expect(unlinked.querySelector("openclaw-workboard-card-chip")).toBeNull();
});
it("shows the face toggle only when a board exists", () => {
const withoutBoard = createContainer();
const withBoard = createContainer();
it("renders nothing without a board", () => {
const container = createContainer();
render(
renderBoardFaceToggle(false, "chat", () => {}),
withoutBoard,
);
render(
renderBoardFaceToggle(true, "chat", () => {}),
withBoard,
renderBoardViewSwitch({
hasBoard: false,
face: "chat",
dock: "right",
canChangeDock: true,
onSelectMode: () => {},
onDockSideChange: () => {},
}),
container,
);
expect(withoutBoard.querySelector("wa-radio-group")).toBeNull();
expect(withBoard.querySelectorAll("wa-radio")).toHaveLength(2);
expect(container.querySelector(".chat-pane__face-switch")).toBeNull();
expect(container.querySelector("wa-radio-group")).toBeNull();
expect(container.querySelector("wa-dropdown")).toBeNull();
});
it("supports keyboard face selection", () => {
it.each([
["chat", "left", "chat"],
["chat", "right", "chat"],
["chat", "bottom", "chat"],
["chat", "hidden", "chat"],
["dashboard", "left", "split"],
["dashboard", "right", "split"],
["dashboard", "bottom", "split"],
["dashboard", "hidden", "dashboard"],
] as const)("maps %s with a %s dock to the %s mode", (face, dock, activeMode) => {
const container = createContainer();
const onChange = vi.fn();
render(renderBoardFaceToggle(true, "chat", onChange), container);
render(
renderBoardViewSwitch({
hasBoard: true,
face,
dock,
canChangeDock: true,
onSelectMode: () => {},
onDockSideChange: () => {},
}),
container,
);
expect(
[...container.querySelectorAll("wa-radio")].map((radio) => radio.getAttribute("value")),
).toEqual(["chat", "split", "dashboard"]);
expect(
container.querySelector("wa-radio.settings-segmented__btn--active")?.getAttribute("value"),
).toBe(activeMode);
});
it("falls back to the two face options when dock changes are unavailable", () => {
const container = createContainer();
render(
renderBoardViewSwitch({
hasBoard: true,
face: "dashboard",
dock: "right",
canChangeDock: false,
onSelectMode: () => {},
onDockSideChange: () => {},
}),
container,
);
expect(
[...container.querySelectorAll("wa-radio")].map((radio) => radio.getAttribute("value")),
).toEqual(["chat", "dashboard"]);
expect(
container.querySelector("wa-radio.settings-segmented__btn--active")?.getAttribute("value"),
).toBe("dashboard");
expect(container.querySelector("wa-dropdown")).toBeNull();
});
it.each([
["chat", "right", true, false],
["dashboard", "right", true, true],
["dashboard", "hidden", true, false],
["dashboard", "right", false, false],
] as const)(
"shows the dock caret for face=%s dock=%s canChangeDock=%s: %s",
(face, dock, canChangeDock, hasCaret) => {
const container = createContainer();
const onDockSideChange = vi.fn();
render(
renderBoardViewSwitch({
hasBoard: true,
face,
dock,
canChangeDock,
onSelectMode: () => {},
onDockSideChange,
}),
container,
);
const dropdown = container.querySelector("wa-dropdown.chat-pane__dock-caret");
expect(dropdown !== null).toBe(hasCaret);
if (!dropdown) {
return;
}
const items = [...dropdown.querySelectorAll("wa-dropdown-item")];
expect(items.map((item) => item.getAttribute("value"))).toEqual(["left", "right", "bottom"]);
expect(items.find((item) => item.hasAttribute("checked"))?.getAttribute("value")).toBe(
"right",
);
const left = dropdown.querySelector('wa-dropdown-item[value="left"]');
Reflect.set(left ?? {}, "value", "left");
dropdown.dispatchEvent(
new CustomEvent("wa-select", { bubbles: true, detail: { item: left } }),
);
expect(onDockSideChange).toHaveBeenCalledWith("left");
},
);
it("emits the selected view mode", () => {
const container = createContainer();
const onSelectMode = vi.fn();
render(
renderBoardViewSwitch({
hasBoard: true,
face: "chat",
dock: "right",
canChangeDock: true,
onSelectMode,
onDockSideChange: () => {},
}),
container,
);
const group = container.querySelector<HTMLElement & { value: string }>("wa-radio-group");
if (group) {
group.value = "dashboard";
group.value = "split";
group.dispatchEvent(new Event("change", { bubbles: true }));
}
expect(onChange).toHaveBeenCalledWith("dashboard");
expect(onSelectMode).toHaveBeenCalledWith("split");
});
it.each(["left", "right", "bottom"] as const)("lays out the %s dock", (dock) => {
@@ -116,7 +220,6 @@ describe("board session shell", () => {
snapshot: provider.snapshot$.value,
activeTabId: "main",
dock,
reopenDock: "right",
dockSize: { height: 300 },
chat: html`<div data-test-chat>chat</div>`,
divider: html`<div class="board-session-surface__divider" data-test-divider></div>`,
@@ -128,7 +231,6 @@ describe("board session shell", () => {
selectTab: () => {},
},
widgetFrameUrl: (name, revision) => provider.widgetFrameUrl(name, revision),
onDockChange: () => {},
}),
container,
);
@@ -139,16 +241,14 @@ describe("board session shell", () => {
expect(container.querySelector("openclaw-board-view")).not.toBeNull();
});
it("collapses hidden chat to a reopen affordance", () => {
it("renders the hidden dock as board-only", () => {
const container = createContainer();
const provider = boardProviderForSession("agent:main:main");
const onDockChange = vi.fn<(dock: BoardTab["chatDock"]) => void>();
render(
renderBoardSessionSurface({
snapshot: provider.snapshot$.value,
activeTabId: "main",
dock: "hidden",
reopenDock: "left",
dockSize: { height: 300 },
chat: html`<div data-test-chat>chat</div>`,
divider: html`<div class="board-session-surface__divider"></div>`,
@@ -160,16 +260,13 @@ describe("board session shell", () => {
selectTab: () => {},
},
widgetFrameUrl: (name, revision) => provider.widgetFrameUrl(name, revision),
onDockChange,
}),
container,
);
expect(container.querySelector("[data-test-chat]")).toBeNull();
expect(container.querySelector(".board-session-surface--dock-hidden")).not.toBeNull();
const reopen = container.querySelector<HTMLButtonElement>(".board-session-surface__reopen");
reopen?.click();
expect(onDockChange).toHaveBeenCalledWith("left");
expect(container.querySelector("openclaw-board-view")).not.toBeNull();
});
it("preserves the board while the bottom chat mounts only for that dock", () => {
@@ -178,7 +275,6 @@ describe("board session shell", () => {
const props = {
snapshot: provider.snapshot$.value,
activeTabId: "main",
reopenDock: "left" as const,
dockSize: { height: 300 },
chat: html`<div data-test-chat>chat</div>`,
divider: html`<div class="board-session-surface__divider"></div>`,
@@ -190,7 +286,6 @@ describe("board session shell", () => {
selectTab: () => {},
},
widgetFrameUrl: (name: string, revision: number) => provider.widgetFrameUrl(name, revision),
onDockChange: () => {},
};
render(renderBoardSessionSurface({ ...props, dock: "right" }), container);
+77 -88
View File
@@ -5,13 +5,14 @@ import { icons } from "../../components/icons.ts";
import { renderSettingsSegmented } from "../../components/settings-ui.ts";
import { t } from "../../i18n/index.ts";
import { isMockBoardEnabled, type BoardViewCallbacks } from "../../lib/board/provider.ts";
import type { BoardFace, BoardVisibleChatDock } from "../../lib/board/settings.ts";
import type { BoardFace } from "../../lib/board/settings.ts";
import type { BoardTab } from "../../lib/board/types.ts";
import type {
BoardObserverContext,
BoardViewSnapshot,
BoardWidgetFrameUrl,
} from "../../lib/board/view-types.ts";
import type { VisibleBoardDock } from "./chat-pane-shared.ts";
export type BoardChatDockSize = {
height: number;
@@ -28,7 +29,6 @@ type BoardSessionSurfaceProps = {
observer?: BoardObserverContext;
activeTabId: string;
dock: BoardTab["chatDock"];
reopenDock: BoardVisibleChatDock;
dockSize: BoardChatDockSize;
chat: TemplateResult;
divider: TemplateResult;
@@ -37,7 +37,6 @@ type BoardSessionSurfaceProps = {
callbacks: BoardViewCallbacks;
widgetFrameUrl: BoardWidgetFrameUrl;
workboardCardChip?: WorkboardCardChipProps | null;
onDockChange: (dock: BoardTab["chatDock"]) => void;
};
let boardViewLoad: Promise<unknown> | null = null;
@@ -60,93 +59,94 @@ export async function ensureBoardViewElement(): Promise<boolean> {
return true;
}
export function renderBoardFaceToggle(
hasBoard: boolean,
face: BoardFace,
onChange: (face: BoardFace) => void,
) {
if (!hasBoard) {
return nothing;
}
return html`
<div class="chat-pane__face-switch">
${renderSettingsSegmented<BoardFace>({
value: face,
ariaLabel: t("chat.board.faceLabel"),
options: [
{ value: "chat", label: t("chat.board.chatFace") },
{ value: "dashboard", label: t("chat.board.dashboardFace") },
],
onChange: (value) => onChange(value),
})}
</div>
`;
}
type BoardViewMode = "chat" | "split" | "dashboard";
function dockIcon(dock: BoardTab["chatDock"]) {
if (dock === "left") {
return icons.panelLeftOpen;
}
if (dock === "bottom") {
return icons.panelBottomOpen;
}
if (dock === "hidden") {
return icons.eyeOff;
}
return icons.panelRightOpen;
}
function dockLabel(dock: BoardTab["chatDock"]): string {
function dockLabel(dock: VisibleBoardDock): string {
if (dock === "left") {
return t("chat.board.dockLeft");
}
if (dock === "bottom") {
return t("chat.board.dockBottom");
}
if (dock === "hidden") {
return t("chat.board.dockHidden");
}
return t("chat.board.dockRight");
}
export function renderBoardDockMenu(
hasBoard: boolean,
face: BoardFace,
dock: BoardTab["chatDock"],
onChange: (dock: BoardTab["chatDock"]) => void,
) {
if (!hasBoard || face !== "dashboard") {
export function renderBoardViewSwitch(props: {
hasBoard: boolean;
face: BoardFace;
dock: BoardTab["chatDock"];
canChangeDock: boolean;
onSelectMode: (mode: BoardViewMode) => void;
onDockSideChange: (dock: VisibleBoardDock) => void;
}) {
if (!props.hasBoard) {
return nothing;
}
const mode: BoardViewMode =
props.face === "chat" ? "chat" : props.dock === "hidden" ? "dashboard" : "split";
const segmented = props.canChangeDock
? renderSettingsSegmented<BoardViewMode>({
value: mode,
ariaLabel: t("chat.board.faceLabel"),
options: [
{ value: "chat", label: t("chat.board.chatFace") },
{ value: "split", label: t("chat.board.splitFace") },
{ value: "dashboard", label: t("chat.board.dashboardFace") },
],
onChange: (value) => props.onSelectMode(value),
})
: renderSettingsSegmented<BoardFace>({
value: props.face,
ariaLabel: t("chat.board.faceLabel"),
options: [
{ value: "chat", label: t("chat.board.chatFace") },
{ value: "dashboard", label: t("chat.board.dashboardFace") },
],
onChange: (value) => props.onSelectMode(value),
});
const visibleDock = props.dock === "hidden" ? null : props.dock;
const showDockCaret = mode === "split" && props.canChangeDock && visibleDock !== null;
return html`
<wa-dropdown
class="chat-pane__board-dock-menu"
placement="bottom-end"
@wa-select=${(event: CustomEvent<{ item: { value?: string } }>) => {
const value = event.detail.item.value;
if (value === "left" || value === "right" || value === "bottom" || value === "hidden") {
onChange(value);
}
}}
>
<button
slot="trigger"
type="button"
class="btn btn--ghost btn--icon chat-icon-btn"
data-board-dock-menu
title=${dockLabel(dock)}
aria-label=${t("chat.board.dockMenu", { dock: dockLabel(dock) })}
>
${dockIcon(dock)}
</button>
${(["left", "right", "bottom", "hidden"] as const).map(
(candidate) => html`
<wa-dropdown-item value=${candidate} type="checkbox" ?checked=${candidate === dock}>
${dockLabel(candidate)}
</wa-dropdown-item>
`,
)}
</wa-dropdown>
<div class="chat-pane__face-switch ${showDockCaret ? "chat-pane__face-switch--split" : ""}">
${segmented}
${showDockCaret && visibleDock
? html`
<wa-dropdown
class="chat-pane__dock-caret"
placement="bottom-end"
@wa-select=${(event: CustomEvent<{ item: { value?: string } }>) => {
const value = event.detail.item.value;
if (value === "left" || value === "right" || value === "bottom") {
props.onDockSideChange(value);
}
}}
>
<button
slot="trigger"
type="button"
class="btn btn--ghost btn--icon chat-icon-btn chat-pane__dock-caret-trigger"
title=${dockLabel(visibleDock)}
aria-label=${t("chat.board.dockMenu", { dock: dockLabel(visibleDock) })}
>
${icons.chevronDown}
</button>
${(["left", "right", "bottom"] as const).map(
(candidate) => html`
<wa-dropdown-item
value=${candidate}
type="checkbox"
?checked=${candidate === visibleDock}
>
${dockLabel(candidate)}
</wa-dropdown-item>
`,
)}
</wa-dropdown>
`
: nothing}
</div>
`;
}
@@ -186,17 +186,6 @@ export function renderBoardSessionSurface(props: BoardSessionSurfaceProps) {
<div class="board-session-surface board-session-surface--dock-${props.dock}">
${renderBoardView(props)}
${props.dock === "bottom" ? html`${props.divider}${renderChatDock(props)}` : nothing}
<button
type="button"
class="board-session-surface__reopen board-session-surface__reopen--${props.reopenDock}"
aria-label=${t("chat.board.reopenChat")}
title=${t("chat.board.reopenChat")}
?hidden=${props.dock !== "hidden"}
?disabled=${!props.canMutate}
@click=${() => props.onDockChange(props.reopenDock)}
>
${icons.messageSquare}<span>${t("chat.board.chatFace")}</span>
</button>
</div>
`;
}
+32 -10
View File
@@ -19,7 +19,7 @@ import { copyToClipboard } from "../../lib/clipboard.ts";
import { isGatewayMethodAdvertised } from "../../lib/gateway-methods.ts";
import { readSessionMethodAccess } from "../../lib/session-method-access.ts";
import { parseAgentSessionKey } from "../../lib/sessions/session-key.ts";
import { renderBoardDockMenu, renderBoardFaceToggle } from "./board-session-surface.ts";
import { renderBoardViewSwitch } from "./board-session-surface.ts";
import { ChatPaneContext } from "./chat-pane-context.ts";
import { headerPlatformByClient } from "./chat-pane-shared.ts";
import { readChatSessionActionAccess } from "./chat-session-action-access.ts";
@@ -60,6 +60,8 @@ export abstract class ChatPaneHeader extends ChatPaneContext {
workspaceGit: boolean,
) {
const board = this.resolveBoardView();
const canChangeBoardDock =
board.hasBoard && !board.activeTabReadOnly && board.provider.canMutate;
const workspace = resolveChatPaneWorkspace({
session: row,
agentWorkspace: row?.worktree ? undefined : agentWorkspace,
@@ -193,9 +195,35 @@ export abstract class ChatPaneHeader extends ChatPaneContext {
variant="session"
></openclaw-viewer-facepile>`
: nothing,
faceControl: renderBoardFaceToggle(board.hasBoard, board.face, (face) => {
this.syncChatSidebarForDock(face === "dashboard" ? board.dock : "hidden");
this.persistBoardSessionView({ face });
faceControl: renderBoardViewSwitch({
hasBoard: board.hasBoard,
face: board.face,
dock: board.dock,
canChangeDock: canChangeBoardDock,
onSelectMode: (mode) => {
if (!canChangeBoardDock) {
const face = mode === "chat" ? "chat" : "dashboard";
this.syncChatSidebarForDock(face === "dashboard" ? board.dock : "hidden");
this.persistBoardSessionView({ face });
return;
}
if (mode === "chat") {
this.syncChatSidebarForDock("hidden");
this.persistBoardSessionView({ face: "chat" });
return;
}
this.persistBoardSessionView({ face: "dashboard" });
if (mode === "split") {
if (board.dock === "hidden") {
this.handleBoardDockChange(board.reopenDock);
} else {
this.syncChatSidebarForDock(board.dock);
}
} else if (board.dock !== "hidden") {
this.handleBoardDockChange("hidden");
}
},
onDockSideChange: (dock) => this.handleBoardDockChange(dock),
}),
sharingControl: sharingMethodsSupported
? renderChatSessionSharing({
@@ -222,12 +250,6 @@ export abstract class ChatPaneHeader extends ChatPaneContext {
row && void this.setSessionMember(row, identityId, member),
})
: nothing,
boardDockAction: renderBoardDockMenu(
board.hasBoard && !board.activeTabReadOnly && board.provider.canMutate,
board.face,
board.dock,
(dock) => this.handleBoardDockChange(dock),
),
nativeGateways: this.nativeGateways,
gatewaysSnapshot: this.gatewaysSnapshot,
onboarding: this.onboarding,
-2
View File
@@ -581,7 +581,6 @@ export class ChatPane extends ChatPaneHeader {
},
activeTabId: board.activeTabId,
dock: board.dock,
reopenDock: board.reopenDock,
dockSize: this.boardChatDockSize,
chat,
divider: this.renderBoardDivider("bottom"),
@@ -601,7 +600,6 @@ export class ChatPane extends ChatPaneHeader {
} satisfies BoardViewCallbacks,
widgetFrameUrl: (name, revision) => board.provider.widgetFrameUrl(name, revision),
workboardCardChip,
onDockChange: (dock) => this.handleBoardDockChange(dock),
})
: chat;
const discussion = this.buildSessionDiscussionPanel(state, state.sessionKey.trim());
@@ -52,7 +52,6 @@ type ChatPaneHeaderProps = {
presence?: TemplateResult | typeof nothing;
faceControl?: TemplateResult | typeof nothing;
sharingControl?: TemplateResult | typeof nothing;
boardDockAction?: TemplateResult | typeof nothing;
nativeGateways?: NativeGatewaysCapability | null;
gatewaysSnapshot?: NativeGatewaysSnapshot | null;
onboarding?: boolean;
@@ -432,7 +431,7 @@ export function renderChatPaneHeader(props: ChatPaneHeaderProps) {
: nothing}
${renderGatewayPicker(props)}
<div class="chat-pane__actions">
${props.boardDockAction ?? nothing} ${props.terminalAction} ${props.discussionAction}
${props.terminalAction} ${props.discussionAction}
${props.catalog
? nothing
: html`${props.diffAction} ${props.backgroundTasksAction} ${props.workspaceAction}`}
+19 -64
View File
@@ -3,12 +3,18 @@
flex: 0 0 auto;
align-items: center;
margin-left: auto;
border-radius: var(--radius-full);
background: color-mix(in srgb, var(--panel-strong) 76%, transparent);
}
.chat-pane__face-switch .settings-segmented {
flex-wrap: nowrap;
border-radius: var(--radius-full);
background: color-mix(in srgb, var(--panel-strong) 76%, transparent);
background: transparent;
}
.chat-pane__face-switch--split .settings-segmented {
border-radius: var(--radius-full) 0 0 var(--radius-full);
}
.chat-pane__face-switch .settings-segmented__btn {
@@ -25,12 +31,21 @@
margin-left: 0;
}
.chat-pane__board-dock-menu {
.chat-pane__dock-caret {
display: inline-flex;
}
.chat-pane__board-dock-menu svg,
.board-session-surface__reopen svg {
.chat-pane__dock-caret-trigger {
width: 24px;
min-width: 24px;
height: 24px;
min-height: 24px;
padding: 0;
border-left: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
border-radius: 0 var(--radius-full) var(--radius-full) 0;
}
.chat-pane__dock-caret svg {
width: 15px;
height: 15px;
fill: none;
@@ -163,66 +178,6 @@ openclaw-workboard-card-chip {
z-index: 2;
}
.board-session-surface__reopen[hidden] {
display: none;
}
.board-session-surface__reopen {
position: absolute;
z-index: 3;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 34px;
min-height: 34px;
padding: 7px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--muted);
background: color-mix(in srgb, var(--panel-strong) 92%, transparent);
box-shadow: var(--shadow-sm);
cursor: pointer;
}
.board-session-surface__reopen:hover,
.board-session-surface__reopen:focus-visible {
color: var(--text);
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
}
.board-session-surface__reopen:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.board-session-surface__reopen span {
font-size: 11px;
font-weight: 600;
writing-mode: vertical-rl;
}
.board-session-surface__reopen--left {
top: 50%;
left: 8px;
transform: translateY(-50%);
}
.board-session-surface__reopen--right {
top: 50%;
right: 8px;
transform: translateY(-50%);
}
.board-session-surface__reopen--bottom {
right: 12px;
bottom: 8px;
}
.board-session-surface__reopen--bottom span {
writing-mode: horizontal-tb;
}
@media (max-width: 560px) {
.chat-pane__face-switch .settings-segmented__btn {
padding-inline: 7px;
-12
View File
@@ -148,18 +148,6 @@ html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native-
box-shadow: 0 -12px 32px color-mix(in srgb, black 16%, transparent);
}
.shell--mobile-nav .board-session-surface__reopen {
top: auto;
right: 12px;
bottom: 8px;
left: auto;
transform: none;
}
.shell--mobile-nav .board-session-surface__reopen span {
writing-mode: horizontal-tb;
}
.shell--mobile-nav .sidebar-resizer {
display: none;
}