mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
feat: make workspace files panel collapsible
Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-workbench--workspace-collapsed {
|
||||
grid-template-columns: minmax(0, 1fr) 52px;
|
||||
}
|
||||
|
||||
.chat-split-container {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
@@ -45,6 +49,12 @@
|
||||
background: color-mix(in srgb, var(--panel) 84%, transparent);
|
||||
}
|
||||
|
||||
.chat-workspace-rail--collapsed {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -75,14 +85,27 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__refresh {
|
||||
.chat-workspace-rail__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__refresh,
|
||||
.chat-workspace-rail__collapse-toggle {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__collapse-toggle {
|
||||
flex: 0 0 auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__refresh svg,
|
||||
.chat-workspace-rail__collapse-toggle svg,
|
||||
.chat-workspace-rail__file-icon svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
@@ -93,6 +116,25 @@
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__collapsed-icon {
|
||||
color: var(--muted);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__collapsed-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__path {
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border) 42%, transparent);
|
||||
|
||||
@@ -680,6 +680,7 @@ const lazyWorkboard = createLazyView(() => import("./views/workboard.ts"), notif
|
||||
type ChatWorkspaceFilesState = {
|
||||
activeName: string | null;
|
||||
agentId: string;
|
||||
collapsed: boolean;
|
||||
error: string | null;
|
||||
list: AgentsFilesListResult | null;
|
||||
loading: boolean;
|
||||
@@ -700,6 +701,7 @@ function getChatWorkspaceFilesState(state: AppViewState, agentId: string): ChatW
|
||||
const next = {
|
||||
activeName: null,
|
||||
agentId,
|
||||
collapsed: false,
|
||||
error: null,
|
||||
list: null,
|
||||
loading: false,
|
||||
@@ -2098,6 +2100,7 @@ export function renderApp(state: AppViewState) {
|
||||
};
|
||||
if (
|
||||
isChat &&
|
||||
!chatWorkspaceFiles.collapsed &&
|
||||
state.connected &&
|
||||
state.agentsList &&
|
||||
!chatWorkspaceFiles.loading &&
|
||||
@@ -2106,6 +2109,13 @@ export function renderApp(state: AppViewState) {
|
||||
) {
|
||||
loadChatWorkspaceFiles();
|
||||
}
|
||||
const toggleChatWorkspaceFilesCollapsed = () => {
|
||||
chatWorkspaceFiles.collapsed = !chatWorkspaceFiles.collapsed;
|
||||
if (!chatWorkspaceFiles.collapsed && chatWorkspaceFiles.list?.agentId !== chatAgentId) {
|
||||
loadChatWorkspaceFiles();
|
||||
}
|
||||
requestHostUpdate?.();
|
||||
};
|
||||
const refreshChatWorkspaceFiles = () => {
|
||||
loadChatWorkspaceFiles({ force: true });
|
||||
};
|
||||
@@ -3534,6 +3544,7 @@ export function renderApp(state: AppViewState) {
|
||||
sessions: state.sessionsResult,
|
||||
composerControls: renderGuardedChatControls(state),
|
||||
workspaceFiles: {
|
||||
collapsed: chatWorkspaceFiles.collapsed,
|
||||
agentId: chatAgentId,
|
||||
list:
|
||||
chatWorkspaceFiles.list?.agentId === chatAgentId
|
||||
@@ -3542,6 +3553,7 @@ export function renderApp(state: AppViewState) {
|
||||
loading: chatWorkspaceFiles.loading,
|
||||
error: chatWorkspaceFiles.error,
|
||||
activeName: chatWorkspaceFiles.activeName,
|
||||
onToggleCollapsed: toggleChatWorkspaceFilesCollapsed,
|
||||
onRefresh: refreshChatWorkspaceFiles,
|
||||
onOpenFile: openChatWorkspaceFile,
|
||||
},
|
||||
|
||||
@@ -221,6 +221,51 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("collapses the workspace files panel from its header control", async () => {
|
||||
const context = await browser.newContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"agents.files.list": {
|
||||
agentId: "main",
|
||||
files: [{ name: "AGENTS.md", path: "/workspace/AGENTS.md", size: 2048 }],
|
||||
workspace: "/workspace",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}chat`);
|
||||
await page.getByRole("button", { name: "Collapse workspace files" }).waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await page.getByText("AGENTS.md").waitFor({ timeout: 10_000 });
|
||||
expect(await gateway.getRequests("agents.files.list")).toHaveLength(1);
|
||||
|
||||
await page.getByRole("button", { name: "Collapse workspace files" }).click();
|
||||
await page.getByRole("button", { name: "Expand workspace files" }).waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
expect(await page.locator(".chat-workspace-rail__file").count()).toBe(0);
|
||||
expect(await page.locator(".chat-workspace-rail__collapsed-icon svg").count()).toBe(1);
|
||||
|
||||
await page.getByRole("button", { name: "Expand workspace files" }).click();
|
||||
await page.getByRole("button", { name: "Collapse workspace files" }).waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await page.getByText("AGENTS.md").waitFor({ timeout: 10_000 });
|
||||
|
||||
await page.setViewportSize({ height: 900, width: 1000 });
|
||||
expect(await page.locator(".chat-workspace-rail").isHidden()).toBe(true);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders stable markdown during a streaming chat turn and finalizes the tail", async () => {
|
||||
const context = await browser.newContext({
|
||||
locale: "en-US",
|
||||
|
||||
@@ -496,6 +496,13 @@ export const icons = {
|
||||
<path d="M10 10l-3 2 3 2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
`,
|
||||
panelRightClose: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<path d="M15 3v18" stroke-linecap="round" />
|
||||
<path d="M8 10l3 2-3 2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
`,
|
||||
maximize: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
|
||||
@@ -875,12 +875,14 @@ describe("chat goal status", () => {
|
||||
});
|
||||
|
||||
describe("chat composer workbench", () => {
|
||||
it("renders session controls in the composer and workspace files in the rail", () => {
|
||||
it("renders session controls in the composer and workspace files in the expanded rail", () => {
|
||||
const onToggleCollapsed = vi.fn();
|
||||
const onRefresh = vi.fn();
|
||||
const onOpenFile = vi.fn();
|
||||
const container = renderChatView({
|
||||
composerControls: html`<button class="test-composer-control">Model</button>`,
|
||||
workspaceFiles: {
|
||||
collapsed: false,
|
||||
agentId: "main",
|
||||
list: {
|
||||
agentId: "main",
|
||||
@@ -897,6 +899,7 @@ describe("chat composer workbench", () => {
|
||||
loading: false,
|
||||
error: null,
|
||||
activeName: "AGENTS.md",
|
||||
onToggleCollapsed,
|
||||
onRefresh,
|
||||
onOpenFile,
|
||||
},
|
||||
@@ -913,8 +916,41 @@ describe("chat composer workbench", () => {
|
||||
expect(file?.textContent).toContain("2 KB");
|
||||
|
||||
file?.click();
|
||||
container
|
||||
.querySelector<HTMLButtonElement>('button[aria-label="Collapse workspace files"]')
|
||||
?.click();
|
||||
|
||||
expect(onOpenFile).toHaveBeenCalledWith("AGENTS.md");
|
||||
expect(onToggleCollapsed).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelector('button[aria-label="Workspace files"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps the workspace files rail reachable from the collapsed strip", () => {
|
||||
const onToggleCollapsed = vi.fn();
|
||||
const container = renderChatView({
|
||||
workspaceFiles: {
|
||||
collapsed: true,
|
||||
agentId: "main",
|
||||
list: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
activeName: null,
|
||||
onToggleCollapsed,
|
||||
onRefresh: () => undefined,
|
||||
onOpenFile: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelector(".chat-workspace-rail__list")).toBeNull();
|
||||
expect(container.querySelector(".chat-workspace-rail__collapsed-icon")).not.toBeNull();
|
||||
const toggle = container.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Expand workspace files"]',
|
||||
);
|
||||
expect(toggle?.getAttribute("aria-expanded")).toBe("false");
|
||||
|
||||
toggle?.click();
|
||||
|
||||
expect(onToggleCollapsed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps the secondary New session and Export controls suppressed in the composer", () => {
|
||||
|
||||
+53
-11
@@ -199,11 +199,13 @@ export type ChatProps = {
|
||||
basePath?: string;
|
||||
composerControls?: TemplateResult | typeof nothing | ReturnType<typeof guard>;
|
||||
workspaceFiles?: {
|
||||
collapsed: boolean;
|
||||
agentId: string;
|
||||
list: AgentsFilesListResult | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
activeName: string | null;
|
||||
onToggleCollapsed: () => void;
|
||||
onRefresh: () => void;
|
||||
onOpenFile: (name: string) => void;
|
||||
};
|
||||
@@ -1028,6 +1030,28 @@ function renderWorkspaceFileRail(
|
||||
if (!workspaceFiles) {
|
||||
return nothing;
|
||||
}
|
||||
if (workspaceFiles.collapsed) {
|
||||
return html`
|
||||
<aside
|
||||
class="chat-workspace-rail chat-workspace-rail--collapsed"
|
||||
aria-label="Workspace files"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="nav-collapse-toggle chat-workspace-rail__collapse-toggle"
|
||||
title="Expand workspace files"
|
||||
aria-label="Expand workspace files"
|
||||
aria-expanded="false"
|
||||
@click=${workspaceFiles.onToggleCollapsed}
|
||||
>
|
||||
<span class="nav-collapse-toggle__icon" aria-hidden="true">${icons.panelRightOpen}</span>
|
||||
</button>
|
||||
<span class="chat-workspace-rail__collapsed-icon" aria-hidden="true"
|
||||
>${icons.fileText}</span
|
||||
>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
const files = workspaceFiles.list?.files ?? [];
|
||||
return html`
|
||||
<aside class="chat-workspace-rail" aria-label="Workspace files">
|
||||
@@ -1036,16 +1060,30 @@ function renderWorkspaceFileRail(
|
||||
<span class="chat-workspace-rail__eyebrow">Workspace</span>
|
||||
<strong>Files</strong>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn--ghost btn--sm chat-workspace-rail__refresh"
|
||||
type="button"
|
||||
title="Refresh files"
|
||||
aria-label="Refresh files"
|
||||
?disabled=${workspaceFiles.loading}
|
||||
@click=${workspaceFiles.onRefresh}
|
||||
>
|
||||
${icons.refresh}
|
||||
</button>
|
||||
<div class="chat-workspace-rail__actions">
|
||||
<button
|
||||
class="btn btn--ghost btn--sm chat-workspace-rail__refresh"
|
||||
type="button"
|
||||
title="Refresh files"
|
||||
aria-label="Refresh files"
|
||||
?disabled=${workspaceFiles.loading}
|
||||
@click=${workspaceFiles.onRefresh}
|
||||
>
|
||||
${icons.refresh}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="nav-collapse-toggle chat-workspace-rail__collapse-toggle"
|
||||
title="Collapse workspace files"
|
||||
aria-label="Collapse workspace files"
|
||||
aria-expanded="true"
|
||||
@click=${workspaceFiles.onToggleCollapsed}
|
||||
>
|
||||
<span class="nav-collapse-toggle__icon" aria-hidden="true"
|
||||
>${icons.panelRightClose}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${workspaceFiles.list?.workspace
|
||||
? html`<div class="chat-workspace-rail__path" title=${workspaceFiles.list.workspace}>
|
||||
@@ -2058,7 +2096,11 @@ export function renderChat(props: ChatProps) {
|
||||
: nothing}
|
||||
${renderSearchBar(requestUpdate)} ${renderPinnedSection(props, pinned, requestUpdate)}
|
||||
|
||||
<div class="chat-workbench">
|
||||
<div
|
||||
class="chat-workbench ${props.workspaceFiles?.collapsed
|
||||
? "chat-workbench--workspace-collapsed"
|
||||
: ""}"
|
||||
>
|
||||
<div class="chat-split-container ${sidebarOpen ? "chat-split-container--open" : ""}">
|
||||
<div
|
||||
class="chat-main"
|
||||
|
||||
Reference in New Issue
Block a user