feat(web): group the chat sidebar by workspace with an agent-mode toggle (#26)

Co-authored-by: Alice <alice@prismshadow.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-22 22:22:07 +08:00
committed by GitHub
parent 3806e4e441
commit 99f391f379
8 changed files with 656 additions and 130 deletions
+58 -2
View File
@@ -4,9 +4,17 @@
* the first message is sent, and all four selections land faithfully in its meta;
* - the draft auto-caches (body persisted via debounce): after a page reload, both the body and
* the selections are restored, and the cache clears once sending succeeds;
* - the sidebar group header's "+" creates a draft scoped to that group's Agent (explicitly set
* via router state, overriding the cache).
* - the sidebar defaults to grouping by Workspace: auto temp directories merge into one
* "临时工作区" group, a named directory groups under its basename, and that group header's
* "+" pre-fills the draft's Workspace (via router state, applied once per navigation — a
* manual change made afterwards survives a reload instead of being re-overridden);
* - after switching the sidebar to agent mode (toggle persisted in localStorage), the agent
* group header's "+" creates a draft scoped to that group's Agent (explicitly set via router
* state, overriding the cache).
*/
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join } from "node:path";
import { test, expect } from "@playwright/test";
import { provisionAndLogin } from "./auth.mjs";
@@ -115,6 +123,54 @@ test("draft: pick model/approval -> reload restores them -> send creates the ses
// The cache clears as soon as sending succeeds.
await expect.poll(() => page.evaluate((k) => localStorage.getItem(k), draftKey)).toBeNull();
// —— Default grouping: the sidebar groups Sessions by Workspace — the session just created
// used the auto temp directory, so it lands in the merged "临时工作区" group. ——
await expect(page.getByText("临时工作区")).toBeVisible();
// A session in a named Workspace groups under that directory's basename, and its group
// header's "+" pre-fills the draft's Workspace selection with the group's path.
const namedWs = mkdtempSync(join(tmpdir(), "penguin-e2e-ws-"));
const namedRes = await page.request.post(
`${BASE}/api/projects/${projectId}/agents/default_agent/sessions`,
{ data: { workspace: namedWs } },
);
expect(namedRes.ok(), "create session in named workspace").toBeTruthy();
await page.reload();
const wsLabel = basename(namedWs);
const wsHeader = page
.getByText(wsLabel, { exact: true })
.locator("xpath=ancestor::div[contains(@class,'items-center')][1]");
await wsHeader.getByRole("button", { name: "在此工作区新建对话" }).click();
await expect(page.getByRole("heading", { name: "PenguinHarness" })).toBeVisible();
await expect(page.getByLabel("Workspace")).toContainText(wsLabel);
// Regression (review): the route-state prefill applies once per navigation only — after the
// user picks a different directory and reloads, the restored cached choice must win; the
// prefill must NOT re-apply (location.state survives a reload inside history.state, so the
// consumed marker lives in sessionStorage rather than a ref). Browse one level up and select
// it; the path row mirrors the loaded directory, which orders the two clicks deterministically.
await page.getByRole("button", { name: "Workspace", exact: true }).click();
// Match by trailing basename: the server realpaths the browsed directory, so the prefix may differ from the raw mkdtemp path.
await expect(page.getByRole("textbox", { name: "Workspace" })).toHaveValue(
new RegExp(`${wsLabel}$`),
);
await page.getByRole("button", { name: "上级目录" }).click();
await expect(page.getByRole("textbox", { name: "Workspace" })).not.toHaveValue(
new RegExp(`${wsLabel}$`),
);
await page.getByRole("button", { name: "使用此目录" }).click();
const parentLabel = basename(dirname(namedWs));
await expect(page.getByLabel("Workspace")).toContainText(parentLabel);
await page.reload();
await expect(page.getByLabel("Workspace")).toContainText(parentLabel);
await expect(page.getByLabel("Workspace")).not.toContainText(wsLabel);
// —— Switch the sidebar to agent mode via the section-header toggle (persists in localStorage) ——
await page.getByRole("button", { name: "按 Agent 分组" }).click();
await expect
.poll(() => page.evaluate(() => localStorage.getItem("penguin.sidebarGroupMode")))
.toBe("agent");
// —— Sidebar group-header "+": create a draft with that group's Agent (overrides the previously cached Agent) ——
const helperHeader = page.getByText("Helper Agent", { exact: true }).first();
await expect(helperHeader).toBeVisible();