feat(ui): menus déroulants + fenêtres flottantes, barre unique et sélecteur de projet (#16)
Lot A du sprint UI rework : réorganisation de la navigation racine. - shared/ui/MenuBar.tsx : barre de menus unique avec menus déroulants - shared/ui/FloatingWindow.tsx : primitive de fenêtre flottante réutilisable (socle du lot C #17 fenêtre dédiée gestion tickets) - shared/ui/zIndex.ts : échelle z-index étendue pour les fenêtres flottantes - App : intègre la barre unique et le sélecteur de projet permanent - ProjectsView : sélecteur de projet permanent Tests : tsc --noEmit clean, suite complète 527/527, suites impactées 33/33. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,34 +1,33 @@
|
||||
/**
|
||||
* `ProjectsView` — the top-level project surface (L2 / L11).
|
||||
*
|
||||
* IDE layout (full remaining height):
|
||||
* IDE layout (full remaining height), reworked in ticket #16:
|
||||
*
|
||||
* ┌───────────────────────────────────────────────────────┐
|
||||
* │ PROJECT TABS [ alpha × ][ beta × ] [+] │
|
||||
* ├───────────────┬───────────────────────────────────────┤
|
||||
* │ SIDEBAR ≈320px│ MAIN — LayoutGrid (fills remaining) │
|
||||
* │ [Proj][Agents]│ — or welcome screen when no active │
|
||||
* │ [Tmpl][Git] │ │
|
||||
* │ panel body │ │
|
||||
* └───────────────┴───────────────────────────────────────┘
|
||||
* │ PROJECT TABS [ alpha × ][ beta × ] │
|
||||
* ├───────────────────────────────────────────────────────┤
|
||||
* │ MENU BAR File │ View │
|
||||
* ├───────────────────────────────────────────────────────┤
|
||||
* │ MAIN — LayoutGrid (fills the FULL width) │
|
||||
* │ — or the projects manager / welcome when no project │
|
||||
* └───────────────────────────────────────────────────────┘
|
||||
*
|
||||
* The "Projects" sidebar tab always hosts the create-project form and the
|
||||
* known-projects list, ensuring they are always in the DOM (required by tests).
|
||||
* The former left sidebar is gone: every panel (context, work, tickets, agents,
|
||||
* templates, skills, permissions, memory, git) is now reached from the **View**
|
||||
* menu and shown in a chrome-level {@link FloatingWindow}, freeing the main area
|
||||
* for the terminal grid. Project creation/opening lives under the **File** menu
|
||||
* and, when no project is open, inline in the welcome area (so it is always
|
||||
* reachable without a menu).
|
||||
*
|
||||
* Pure presentation: all behaviour comes from {@link useProjects}. Styling via
|
||||
* `@/shared`; no inline styles, no `invoke()`.
|
||||
* `@/shared`; no inline styles beyond z-index tokens, no `invoke()`.
|
||||
*
|
||||
* **Test-contract** — the following query hooks from `projects.test.tsx` are
|
||||
* always present in the DOM regardless of which sidebar tab is active:
|
||||
* - getByLabelText("project name") / ("project root") — form inputs
|
||||
* - getByRole("button", { name: "Create project" }) / "Refresh" / "Open" / "close <name>"
|
||||
* - getByRole("tablist") + getAllByRole("tab") + aria-selected — project tabs
|
||||
* - getByText("No projects yet.") / "No open tabs." — empty states
|
||||
* - getAllByRole("listitem") — known-projects list
|
||||
* - getByRole("alert") — error display
|
||||
* **Test-contract** — the create-project form + known-projects list are in the
|
||||
* DOM whenever no project is active (welcome area) or when the Projects window is
|
||||
* open; the project tab bar (`role="tablist"`) is always present.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import type { DomainEvent, LayoutInfo } from "@/domain";
|
||||
import { LayoutGrid, LayoutTabs } from "@/features/layout";
|
||||
@ -41,13 +40,25 @@ import { PermissionsPanel } from "@/features/permissions";
|
||||
import { ProjectWorkStatePanel } from "@/features/workstate";
|
||||
import { TicketsView } from "@/features/tickets";
|
||||
import { ConversationViewer } from "@/features/conversations";
|
||||
import { ProfilesSettings } from "@/features/first-run";
|
||||
import { GitPanel, GitGraphView } from "@/features/git";
|
||||
import { Button, Input, Panel, Tabs, cn } from "@/shared";
|
||||
import {
|
||||
Button,
|
||||
FloatingWindow,
|
||||
Input,
|
||||
MenuBar,
|
||||
Panel,
|
||||
Tabs,
|
||||
zIndex,
|
||||
type FloatingWindowSize,
|
||||
type MenuBarMenu,
|
||||
} from "@/shared";
|
||||
import { useGateways } from "@/app/di";
|
||||
import { ProjectContextPanel } from "./ProjectContextPanel";
|
||||
import { useProjects } from "./useProjects";
|
||||
|
||||
type SidebarTab =
|
||||
/** A panel reachable from the menu bar; each opens in a floating window. */
|
||||
type PanelId =
|
||||
| "projects"
|
||||
| "context"
|
||||
| "work"
|
||||
@ -59,18 +70,33 @@ type SidebarTab =
|
||||
| "memory"
|
||||
| "git";
|
||||
|
||||
const SIDEBAR_TABS: { id: SidebarTab; label: string }[] = [
|
||||
{ id: "projects", label: "Projects" },
|
||||
{ id: "context", label: "Context" },
|
||||
{ id: "work", label: "Work" },
|
||||
{ id: "tickets", label: "Tickets" },
|
||||
{ id: "agents", label: "Agents" },
|
||||
{ id: "templates", label: "Templates" },
|
||||
{ id: "skills", label: "Skills" },
|
||||
{ id: "permissions", label: "Perms" },
|
||||
{ id: "memory", label: "Memory" },
|
||||
{ id: "git", label: "Git" },
|
||||
];
|
||||
/** Human titles for each panel's floating window (also its aria-label). */
|
||||
const PANEL_TITLE: Record<PanelId, string> = {
|
||||
projects: "Projects",
|
||||
context: "Project context",
|
||||
work: "Work state",
|
||||
tickets: "Tickets",
|
||||
agents: "Agents",
|
||||
templates: "Templates",
|
||||
skills: "Skills",
|
||||
permissions: "Permissions",
|
||||
memory: "Memory",
|
||||
git: "Git",
|
||||
};
|
||||
|
||||
/** Window width preset per panel (content-heavy panels get more room). */
|
||||
const PANEL_SIZE: Record<PanelId, FloatingWindowSize> = {
|
||||
projects: "md",
|
||||
context: "lg",
|
||||
work: "lg",
|
||||
tickets: "lg",
|
||||
agents: "lg",
|
||||
templates: "lg",
|
||||
skills: "lg",
|
||||
permissions: "md",
|
||||
memory: "lg",
|
||||
git: "lg",
|
||||
};
|
||||
|
||||
interface BackgroundTaskToast {
|
||||
id: string;
|
||||
@ -97,8 +123,12 @@ export function ProjectsView() {
|
||||
const { system } = useGateways();
|
||||
const [name, setName] = useState("");
|
||||
const [root, setRoot] = useState("");
|
||||
const [sidebarTab, setSidebarTab] = useState<SidebarTab>("projects");
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
// Which panel's floating window is open (null = none). Replaces the sidebar.
|
||||
const [openPanel, setOpenPanel] = useState<PanelId | null>(null);
|
||||
// Top-level view switch (#16): when true, the main area shows the AI Profiles
|
||||
// settings instead of the project surface. The single menu bar stays visible
|
||||
// so the user can toggle back from Settings → AI Profiles.
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
// The active layout (id + kind), reported by LayoutTabs — the single source of
|
||||
// truth. `kind` decides whether the main area is the terminal grid or the git
|
||||
// graph view.
|
||||
@ -166,6 +196,220 @@ export function ProjectsView() {
|
||||
if (picked !== null) setRoot(picked);
|
||||
}
|
||||
|
||||
// Opening a conversation viewer takes over the main area — close any panel
|
||||
// window so it doesn't obscure the viewer (LS7).
|
||||
function openConversation(conversationId: string) {
|
||||
setViewerConversationId(conversationId);
|
||||
setOpenPanel(null);
|
||||
}
|
||||
|
||||
// Switch the active project from the always-visible selector: activate its
|
||||
// open tab if present, otherwise open it (adds a tab). Leaving Settings so the
|
||||
// project surface is shown.
|
||||
function selectProject(projectId: string) {
|
||||
setShowSettings(false);
|
||||
if (vm.openTabs.some((t) => t.id === projectId)) {
|
||||
vm.activateTab(projectId);
|
||||
} else {
|
||||
void vm.openProject(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Menus ──────────────────────────────────────────────────────────────
|
||||
const viewItems: { id: PanelId; label: string }[] = [
|
||||
{ id: "context", label: "Context" },
|
||||
{ id: "work", label: "Work" },
|
||||
{ id: "tickets", label: "Tickets" },
|
||||
{ id: "agents", label: "Agents" },
|
||||
{ id: "templates", label: "Templates" },
|
||||
{ id: "skills", label: "Skills" },
|
||||
{ id: "permissions", label: "Perms" },
|
||||
{ id: "memory", label: "Memory" },
|
||||
{ id: "git", label: "Git" },
|
||||
];
|
||||
// Always-visible project selector (left of the menus): shows the active
|
||||
// project and lists every known project so the user can switch without opening
|
||||
// the Projects window (#16 UX pass).
|
||||
const projectMenu: MenuBarMenu = {
|
||||
id: "project",
|
||||
label: `Projet : ${active?.name ?? "—"}`,
|
||||
items:
|
||||
vm.projects.length === 0
|
||||
? [
|
||||
{
|
||||
id: "none",
|
||||
label: "No projects yet",
|
||||
disabled: true,
|
||||
onSelect: () => {},
|
||||
},
|
||||
]
|
||||
: vm.projects.map((p) => ({
|
||||
id: p.id,
|
||||
label: p.name,
|
||||
active: p.id === active?.id,
|
||||
onSelect: () => selectProject(p.id),
|
||||
})),
|
||||
};
|
||||
const menus: MenuBarMenu[] = [
|
||||
projectMenu,
|
||||
{
|
||||
id: "file",
|
||||
label: "File",
|
||||
items: [
|
||||
{
|
||||
id: "projects",
|
||||
label: "Projects…",
|
||||
active: openPanel === "projects",
|
||||
onSelect: () => setOpenPanel("projects"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "view",
|
||||
label: "View",
|
||||
items: viewItems.map((it) => ({
|
||||
id: it.id,
|
||||
label: it.label,
|
||||
active: openPanel === it.id,
|
||||
onSelect: () => setOpenPanel(it.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
label: "Settings",
|
||||
items: [
|
||||
{
|
||||
id: "ai-profiles",
|
||||
label: showSettings ? "Close AI Profiles" : "AI Profiles",
|
||||
active: showSettings,
|
||||
onSelect: () => {
|
||||
setShowSettings((v) => !v);
|
||||
setOpenPanel(null);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// The create-project form + known-projects list. Rendered inline in the
|
||||
// welcome area (no active project) or inside the Projects floating window.
|
||||
const projectsManager: ReactNode = (
|
||||
<div className="flex flex-col gap-4">
|
||||
<form onSubmit={submit} className="flex flex-col gap-2">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-faint">
|
||||
New project
|
||||
</h3>
|
||||
<Input
|
||||
aria-label="project name"
|
||||
placeholder="Project name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
aria-label="project root"
|
||||
placeholder="/absolute/project/root"
|
||||
value={root}
|
||||
onChange={(e) => setRoot(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="browse project folder"
|
||||
onClick={() => void handleBrowse()}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
Browse…
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!canCreate}
|
||||
className="flex-1"
|
||||
>
|
||||
Create project
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => void vm.refresh()}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Panel title="Known projects">
|
||||
{vm.projects.length === 0 ? (
|
||||
<p className="text-sm text-muted">No projects yet.</p>
|
||||
) : (
|
||||
<ul className="flex flex-col divide-y divide-border">
|
||||
{vm.projects.map((p) => (
|
||||
<li
|
||||
key={p.id}
|
||||
className="flex items-center justify-between gap-2 py-2 first:pt-0 last:pb-0"
|
||||
>
|
||||
<span className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="font-medium text-content">{p.name}</span>
|
||||
<code className="truncate text-xs text-muted">{p.root}</code>
|
||||
</span>
|
||||
<Button size="sm" onClick={() => void vm.openProject(p.id)}>
|
||||
Open
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Body of the currently-open panel window (null ⇒ no window).
|
||||
function renderPanel(panel: PanelId): ReactNode {
|
||||
if (panel === "projects") return projectsManager;
|
||||
if (!active) {
|
||||
return (
|
||||
<p className="text-sm text-muted">Open a project to use this panel.</p>
|
||||
);
|
||||
}
|
||||
switch (panel) {
|
||||
case "context":
|
||||
return <ProjectContextPanel projectId={active.id} />;
|
||||
case "work":
|
||||
return (
|
||||
<ProjectWorkStatePanel
|
||||
projectId={active.id}
|
||||
onOpenConversation={openConversation}
|
||||
/>
|
||||
);
|
||||
case "tickets":
|
||||
return <TicketsView projectId={active.id} />;
|
||||
case "agents":
|
||||
return <AgentsPanel projectId={active.id} projectRoot={active.root} />;
|
||||
case "templates":
|
||||
return <TemplatesPanel projectId={active.id} />;
|
||||
case "skills":
|
||||
return <SkillsPanel projectId={active.id} />;
|
||||
case "permissions":
|
||||
return <PermissionsPanel projectId={active.id} />;
|
||||
case "memory":
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<MemoryPanel projectId={active.id} />
|
||||
<EmbedderSettings />
|
||||
</div>
|
||||
);
|
||||
case "git":
|
||||
return <GitPanel projectId={active.id} />;
|
||||
}
|
||||
}
|
||||
|
||||
// Projects manager renders inline in the welcome area only when it is not
|
||||
// already shown in the floating window (avoids duplicate form inputs).
|
||||
const showInlineProjects = !active && openPanel !== "projects";
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* ── Error alert ── */}
|
||||
@ -193,279 +437,96 @@ export function ProjectsView() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── IDE body: sidebar + main ── */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* ── Collapsed sidebar: a thin rail with an expand button ── */}
|
||||
{sidebarCollapsed && (
|
||||
<aside className="flex w-9 shrink-0 flex-col items-center border-r border-border bg-surface py-1">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="expand sidebar"
|
||||
title="Expand sidebar"
|
||||
onClick={() => setSidebarCollapsed(false)}
|
||||
className="flex h-7 w-7 items-center justify-center rounded text-muted hover:bg-raised hover:text-content"
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</aside>
|
||||
)}
|
||||
{/* ── Menu bar (replaces the former left sidebar) ── */}
|
||||
<MenuBar menus={menus} />
|
||||
|
||||
{/* ── Sidebar ── */}
|
||||
<aside
|
||||
className={cn(
|
||||
"flex w-80 shrink-0 flex-col border-r border-border bg-surface",
|
||||
sidebarCollapsed && "hidden",
|
||||
)}
|
||||
>
|
||||
{/* Sidebar tab strip (no role="tablist" to avoid collision with project tabs).
|
||||
The tabs wrap onto multiple rows when they exceed the sidebar width
|
||||
so every label stays visible without scrolling; the collapse button
|
||||
stays pinned to the top-right. */}
|
||||
<div className="flex shrink-0 items-start border-b border-border">
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-stretch">
|
||||
{SIDEBAR_TABS.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
aria-selected={sidebarTab === t.id}
|
||||
onClick={() => setSidebarTab(t.id)}
|
||||
className={cn(
|
||||
"shrink-0 whitespace-nowrap px-3 py-2 text-xs font-medium transition-colors",
|
||||
sidebarTab === t.id
|
||||
? "border-b-2 border-primary text-content"
|
||||
: "text-muted hover:text-content",
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
{/* ── Main: AI Profiles / terminal grid / git graph / welcome ── */}
|
||||
<main className="flex flex-1 flex-col overflow-hidden">
|
||||
{showSettings ? (
|
||||
// Top-level view switch (#16): AI Profiles settings takes over the main
|
||||
// area while the menu bar above stays visible to toggle back.
|
||||
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
||||
<div className="w-full max-w-2xl">
|
||||
<ProfilesSettings />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="collapse sidebar"
|
||||
title="Collapse sidebar"
|
||||
onClick={() => setSidebarCollapsed(true)}
|
||||
className="shrink-0 self-stretch border-l border-border px-2 text-muted hover:text-content"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar panel body */}
|
||||
<div className="flex-1 overflow-auto p-3">
|
||||
{/* Projects panel */}
|
||||
{sidebarTab === "projects" && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Create form */}
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-faint">
|
||||
New project
|
||||
</h3>
|
||||
<Input
|
||||
aria-label="project name"
|
||||
placeholder="Project name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
aria-label="project root"
|
||||
placeholder="/absolute/project/root"
|
||||
value={root}
|
||||
onChange={(e) => setRoot(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="browse project folder"
|
||||
onClick={() => void handleBrowse()}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
Browse…
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!canCreate}
|
||||
className="flex-1"
|
||||
>
|
||||
Create project
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => void vm.refresh()}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Known projects list */}
|
||||
<Panel title="Known projects">
|
||||
{vm.projects.length === 0 ? (
|
||||
<p className="text-sm text-muted">No projects yet.</p>
|
||||
) : (
|
||||
<ul className="flex flex-col divide-y divide-border">
|
||||
{vm.projects.map((p) => (
|
||||
<li
|
||||
key={p.id}
|
||||
className="flex items-center justify-between gap-2 py-2 first:pt-0 last:pb-0"
|
||||
>
|
||||
<span className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="font-medium text-content">{p.name}</span>
|
||||
<code className="truncate text-xs text-muted">{p.root}</code>
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void vm.openProject(p.id)}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Panel>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project context panel */}
|
||||
{sidebarTab === "context" && active && (
|
||||
<ProjectContextPanel projectId={active.id} />
|
||||
)}
|
||||
{sidebarTab === "context" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to edit context.</p>
|
||||
)}
|
||||
|
||||
{/* Work-state panel */}
|
||||
{sidebarTab === "work" && active && (
|
||||
<ProjectWorkStatePanel
|
||||
projectId={active.id}
|
||||
onOpenConversation={setViewerConversationId}
|
||||
/>
|
||||
)}
|
||||
{sidebarTab === "work" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to view work state.</p>
|
||||
)}
|
||||
|
||||
{/* Tickets panel */}
|
||||
{sidebarTab === "tickets" && active && (
|
||||
<TicketsView projectId={active.id} />
|
||||
)}
|
||||
{sidebarTab === "tickets" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to view tickets.</p>
|
||||
)}
|
||||
|
||||
{/* Agents panel — only rendered when active project exists */}
|
||||
{sidebarTab === "agents" && active && (
|
||||
<AgentsPanel projectId={active.id} projectRoot={active.root} />
|
||||
)}
|
||||
{sidebarTab === "agents" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to manage agents.</p>
|
||||
)}
|
||||
|
||||
{/* Templates panel */}
|
||||
{sidebarTab === "templates" && active && (
|
||||
<TemplatesPanel projectId={active.id} />
|
||||
)}
|
||||
{sidebarTab === "templates" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to manage templates.</p>
|
||||
)}
|
||||
|
||||
{/* Skills panel */}
|
||||
{sidebarTab === "skills" && active && (
|
||||
<SkillsPanel projectId={active.id} />
|
||||
)}
|
||||
{sidebarTab === "skills" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to manage skills.</p>
|
||||
)}
|
||||
|
||||
{/* Permissions panel */}
|
||||
{sidebarTab === "permissions" && active && (
|
||||
<PermissionsPanel projectId={active.id} />
|
||||
)}
|
||||
{sidebarTab === "permissions" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to manage permissions.</p>
|
||||
)}
|
||||
|
||||
{/* Memory panel + embedder settings */}
|
||||
{sidebarTab === "memory" && active && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<MemoryPanel projectId={active.id} />
|
||||
<EmbedderSettings />
|
||||
</div>
|
||||
)}
|
||||
{sidebarTab === "memory" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to manage memory.</p>
|
||||
)}
|
||||
|
||||
{/* Git panel */}
|
||||
{sidebarTab === "git" && active && (
|
||||
<GitPanel projectId={active.id} />
|
||||
)}
|
||||
{sidebarTab === "git" && !active && (
|
||||
<p className="text-sm text-muted">Open a project to manage git.</p>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ── Main: terminal grid or git graph (fills remaining height) ── */}
|
||||
<main className="flex flex-1 flex-col overflow-hidden">
|
||||
{active && viewerConversationId ? (
|
||||
<ConversationViewer
|
||||
key={`${active.id}-${viewerConversationId}`}
|
||||
) : active && viewerConversationId ? (
|
||||
<ConversationViewer
|
||||
key={`${active.id}-${viewerConversationId}`}
|
||||
projectId={active.id}
|
||||
conversationId={viewerConversationId}
|
||||
onClose={() => setViewerConversationId(null)}
|
||||
/>
|
||||
) : active ? (
|
||||
<>
|
||||
<LayoutTabs
|
||||
projectId={active.id}
|
||||
conversationId={viewerConversationId}
|
||||
onClose={() => setViewerConversationId(null)}
|
||||
onActiveLayoutChange={setActiveLayout}
|
||||
/>
|
||||
) : active ? (
|
||||
<>
|
||||
<LayoutTabs
|
||||
{activeLayoutKind === "gitGraph" ? (
|
||||
<GitGraphView
|
||||
key={`${active.id}-${activeLayout?.id ?? "default"}-graph`}
|
||||
projectId={active.id}
|
||||
onActiveLayoutChange={setActiveLayout}
|
||||
/>
|
||||
{activeLayoutKind === "gitGraph" ? (
|
||||
<GitGraphView
|
||||
key={`${active.id}-${activeLayout?.id ?? "default"}-graph`}
|
||||
projectId={active.id}
|
||||
/>
|
||||
) : (
|
||||
<LayoutGrid
|
||||
key={`${active.id}-${activeLayout?.id ?? "default"}`}
|
||||
projectId={active.id}
|
||||
cwd={active.root}
|
||||
layoutId={activeLayout?.id}
|
||||
onOpenConversation={openConversation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
||||
<div className="w-full max-w-xl">
|
||||
{showInlineProjects ? (
|
||||
projectsManager
|
||||
) : (
|
||||
<LayoutGrid
|
||||
key={`${active.id}-${activeLayout?.id ?? "default"}`}
|
||||
projectId={active.id}
|
||||
cwd={active.root}
|
||||
layoutId={activeLayout?.id}
|
||||
onOpenConversation={setViewerConversationId}
|
||||
/>
|
||||
<p className="text-sm text-muted">
|
||||
Select or create a project to get started.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-muted">
|
||||
<p className="text-sm">Select or create a project to get started.</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* ── Panel floating window ── */}
|
||||
{openPanel && (
|
||||
<FloatingWindow
|
||||
open
|
||||
title={PANEL_TITLE[openPanel]}
|
||||
size={PANEL_SIZE[openPanel]}
|
||||
onClose={() => setOpenPanel(null)}
|
||||
>
|
||||
{renderPanel(openPanel)}
|
||||
</FloatingWindow>
|
||||
)}
|
||||
|
||||
{/* ── Background-task toasts (above floating windows) ── */}
|
||||
{taskToasts.length > 0 && (
|
||||
<div className="fixed bottom-4 right-4 z-50 flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2">
|
||||
<div
|
||||
className="fixed bottom-4 right-4 flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2"
|
||||
style={{ zIndex: zIndex.toast }}
|
||||
>
|
||||
{taskToasts.map((toast) => (
|
||||
<button
|
||||
key={toast.id}
|
||||
type="button"
|
||||
className="rounded-md border border-border bg-surface px-3 py-2 text-left shadow-lg hover:border-border-strong"
|
||||
onClick={() => {
|
||||
const projectOpen = vm.openTabs.some((tab) => tab.id === toast.projectId);
|
||||
const projectOpen = vm.openTabs.some(
|
||||
(tab) => tab.id === toast.projectId,
|
||||
);
|
||||
if (projectOpen) vm.activateTab(toast.projectId);
|
||||
setSidebarCollapsed(false);
|
||||
setSidebarTab("work");
|
||||
setShowSettings(false);
|
||||
setViewerConversationId(null);
|
||||
setTaskToasts((prev) => prev.filter((item) => item.id !== toast.id));
|
||||
setOpenPanel("work");
|
||||
setTaskToasts((prev) =>
|
||||
prev.filter((item) => item.id !== toast.id),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="block text-sm font-medium text-content">
|
||||
|
||||
Reference in New Issue
Block a user