feat(ui): sidebar project tree, status bar and a tabbed workbench for workspaces

The Workspace page's lane strip moves into the sidebar as a project tree:
workspaces grouped by git repository (new GET /api/projects, cached in
memory, POST /api/projects/refresh), untracked worktrees with Adopt, inline
rename, drag/Alt+arrow ordering and a right-click row menu. The selected
workspace is the URL's ?lane=, one shared lane list feeds the tree, the new
bottom status bar and the page. The Changes panel gains an activity bar
(Files / Changes / Sessions, with resume), and split panes get a header.
This commit is contained in:
2026-10-01 16:33:25 +07:00
parent 5b3884b309
commit ba2c4e1914
39 changed files with 2431 additions and 1439 deletions
@@ -14,7 +14,13 @@ import { eventBus } from "../../lib/eventBus";
function renderSidebar(wsConnected: boolean, collapsed = false) {
return render(
<MemoryRouter>
<Sidebar wsConnected={wsConnected} collapsed={collapsed} onToggle={() => {}} />
<Sidebar
wsConnected={wsConnected}
collapsed={collapsed}
onToggle={() => {}}
connectionOpen={false}
onConnectionOpenChange={() => {}}
/>
</MemoryRouter>
);
}
@@ -45,22 +51,24 @@ describe("Sidebar", () => {
expect(screen.queryByText("Activity Feed")).toBeNull();
});
it('should show "Live" when WebSocket is connected', () => {
// The live/version line moved to the status bar (StatusBar.test.tsx); the
// rail keeps an icon-only button for the connection details.
it("keeps the connection button in the rail only", () => {
renderSidebar(true);
expect(screen.getByText("Live")).toBeInTheDocument();
expect(screen.queryByText("Live")).toBeNull();
expect(screen.queryByRole("button", { name: "Connection details" })).toBeNull();
});
it('should show "Disconnected" when WebSocket is not connected', () => {
renderSidebar(false);
expect(screen.getByText("Disconnected")).toBeInTheDocument();
it("offers the connection details from the collapsed rail", () => {
renderSidebar(false, true);
expect(screen.getByRole("button", { name: "Connection details" })).toBeInTheDocument();
});
it("should show version number", () => {
// `__APP_VERSION__` is injected by Vite from the repo-root package.json
// (see vite.config.ts) and replaced at transform time in tests too, so this
// stays correct as the project version changes.
it("renders the project tree when expanded and its icon in the rail", () => {
renderSidebar(true);
expect(screen.getByText(`v${__APP_VERSION__}`)).toBeInTheDocument();
expect(screen.getByTestId("project-tree")).toBeInTheDocument();
renderSidebar(true, true);
expect(screen.getAllByRole("button", { name: "Projects" }).length).toBeGreaterThan(0);
});
it("should have correct navigation hrefs", () => {