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
+21 -5
View File
@@ -1,7 +1,9 @@
/**
* @file Layout.tsx
* @description Application shell that frames every authenticated route: persistent
* sidebar, main content column, update notifier, and the Sagi assistant overlay.
* sidebar, main content column, the bottom status bar, update notifier, and the
* Sagi assistant overlay. The connection details modal's open state lives here
* because both the sidebar's rail button and the status bar open it.
* The layout is the single parent route in {@link App} — child pages render inside
* React Router's `<Outlet />` so navigation never remounts chrome.
*
@@ -68,6 +70,7 @@ import { useTranslation } from "react-i18next";
import { Sidebar, SIDEBAR_STORAGE_KEY, loadCollapsed } from "./Sidebar";
import { UpdateNotifier } from "./UpdateNotifier";
import { Sagi } from "./Sagi/Sagi";
import { StatusBar } from "./StatusBar";
/** Props for {@link Layout}. */
interface LayoutProps {
@@ -82,6 +85,8 @@ interface LayoutProps {
export function Layout({ wsConnected }: LayoutProps) {
const { t } = useTranslation("nav");
const [collapsed, setCollapsed] = useState(loadCollapsed);
const [connectionOpen, setConnectionOpen] = useState(false);
const sidebarWidth = collapsed ? "4.25rem" : "15rem";
const toggle = useCallback(() => {
setCollapsed((prev) => {
@@ -100,14 +105,20 @@ export function Layout({ wsConnected }: LayoutProps) {
</a>
<UpdateNotifier />
<Sagi />
<Sidebar wsConnected={wsConnected} collapsed={collapsed} onToggle={toggle} />
<Sidebar
wsConnected={wsConnected}
collapsed={collapsed}
onToggle={toggle}
connectionOpen={connectionOpen}
onConnectionOpenChange={setConnectionOpen}
/>
<main
id="main-content"
tabIndex={-1}
className="min-h-screen min-w-0 transition-[margin-left,width] duration-200 outline-none"
className="min-h-screen min-w-0 pb-6 transition-[margin-left,width] duration-200 outline-none"
style={{
marginLeft: collapsed ? "4.25rem" : "15rem",
width: collapsed ? "calc(100% - 4.25rem)" : "calc(100% - 15rem)",
marginLeft: sidebarWidth,
width: `calc(100% - ${sidebarWidth})`,
}}
>
{/* overflow-x-clip (not -hidden) clips horizontal overflow without
@@ -117,6 +128,11 @@ export function Layout({ wsConnected }: LayoutProps) {
<Outlet />
</div>
</main>
<StatusBar
wsConnected={wsConnected}
left={sidebarWidth}
onConnectionClick={() => setConnectionOpen(true)}
/>
</div>
);
}