diff --git a/docs/web/dashboard-architecture.md b/docs/web/dashboard-architecture.md
index 025e65cc347e..48529d91e724 100644
--- a/docs/web/dashboard-architecture.md
+++ b/docs/web/dashboard-architecture.md
@@ -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.
diff --git a/docs/web/dashboards.md b/docs/web/dashboards.md
index 6c0746ce2973..a5493e569a4e 100644
--- a/docs/web/dashboards.md
+++ b/docs/web/dashboards.md
@@ -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
diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts
index a6d0e8c2ba4f..3c73dbe193b2 100644
--- a/ui/src/i18n/locales/en.ts
+++ b/ui/src/i18n/locales/en.ts
@@ -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",
diff --git a/ui/src/lib/board/settings.ts b/ui/src/lib/board/settings.ts
index 4718b9448133..159a0181e813 100644
--- a/ui/src/lib/board/settings.ts
+++ b/ui/src/lib/board/settings.ts
@@ -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;
diff --git a/ui/src/pages/chat/board-session-surface.test.ts b/ui/src/pages/chat/board-session-surface.test.ts
index 6c97f245ae4d..f1a9499e0407 100644
--- a/ui/src/pages/chat/board-session-surface.test.ts
+++ b/ui/src/pages/chat/board-session-surface.test.ts
@@ -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`
chat
`,
divider: html`
`,
@@ -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("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`chat
`,
divider: html`
`,
@@ -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`chat
`,
divider: html`
`,
@@ -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(".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`chat
`,
divider: html`
`,
@@ -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);
diff --git a/ui/src/pages/chat/board-session-surface.ts b/ui/src/pages/chat/board-session-surface.ts
index 1bac543e4188..0be9f07f59ae 100644
--- a/ui/src/pages/chat/board-session-surface.ts
+++ b/ui/src/pages/chat/board-session-surface.ts
@@ -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 | null = null;
@@ -60,93 +59,94 @@ export async function ensureBoardViewElement(): Promise {
return true;
}
-export function renderBoardFaceToggle(
- hasBoard: boolean,
- face: BoardFace,
- onChange: (face: BoardFace) => void,
-) {
- if (!hasBoard) {
- return nothing;
- }
- return html`
-
- ${renderSettingsSegmented({
- 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),
- })}
-
- `;
-}
+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({
+ 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({
+ 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`
-
+
+ ${segmented}
+ ${showDockCaret && visibleDock
+ ? html`
+ ) => {
+ const value = event.detail.item.value;
+ if (value === "left" || value === "right" || value === "bottom") {
+ props.onDockSideChange(value);
+ }
+ }}
+ >
+
+ ${icons.chevronDown}
+
+ ${(["left", "right", "bottom"] as const).map(
+ (candidate) => html`
+
+ ${dockLabel(candidate)}
+
+ `,
+ )}
+
+ `
+ : nothing}
+
`;
}
@@ -186,17 +186,6 @@ export function renderBoardSessionSurface(props: BoardSessionSurfaceProps) {
${renderBoardView(props)}
${props.dock === "bottom" ? html`${props.divider}${renderChatDock(props)}` : nothing}
- props.onDockChange(props.reopenDock)}
- >
- ${icons.messageSquare}${t("chat.board.chatFace")}
-
`;
}
diff --git a/ui/src/pages/chat/chat-pane-header.ts b/ui/src/pages/chat/chat-pane-header.ts
index f9ffc69f1b95..f8eff7089571 100644
--- a/ui/src/pages/chat/chat-pane-header.ts
+++ b/ui/src/pages/chat/chat-pane-header.ts
@@ -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"
>`
: 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,
diff --git a/ui/src/pages/chat/chat-pane-render.ts b/ui/src/pages/chat/chat-pane-render.ts
index ef64d6054402..98140bedbabf 100644
--- a/ui/src/pages/chat/chat-pane-render.ts
+++ b/ui/src/pages/chat/chat-pane-render.ts
@@ -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());
diff --git a/ui/src/pages/chat/components/chat-pane-header.ts b/ui/src/pages/chat/components/chat-pane-header.ts
index 2a0983e33fc3..c107ea5ace6b 100644
--- a/ui/src/pages/chat/components/chat-pane-header.ts
+++ b/ui/src/pages/chat/components/chat-pane-header.ts
@@ -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)}
- ${props.boardDockAction ?? nothing} ${props.terminalAction} ${props.discussionAction}
+ ${props.terminalAction} ${props.discussionAction}
${props.catalog
? nothing
: html`${props.diffAction} ${props.backgroundTasksAction} ${props.workspaceAction}`}
diff --git a/ui/src/styles/chat/board.css b/ui/src/styles/chat/board.css
index 8382c5d68697..8afd26dd94dc 100644
--- a/ui/src/styles/chat/board.css
+++ b/ui/src/styles/chat/board.css
@@ -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;
diff --git a/ui/src/styles/layout.mobile.css b/ui/src/styles/layout.mobile.css
index 23cf82a4c37f..db5df80d880b 100644
--- a/ui/src/styles/layout.mobile.css
+++ b/ui/src/styles/layout.mobile.css
@@ -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;
}