feat(workstate): UI live-state des conversations/délégations (Lot A frontend)

Ajoute le port et l'adaptateur workState (+ mock) côté frontend, le type domaine
associé, et la feature `workstate` (panneau ProjectWorkStatePanel + hook
useProjectWorkState) consommant le read-model live exposé par le backend.
Intègre le panneau dans ProjectsView. Tests verts (workstate + projects).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:06:28 +02:00
parent aae18499a9
commit 17685a08e1
11 changed files with 397 additions and 1 deletions

View File

@ -38,6 +38,7 @@ import { SkillsPanel } from "@/features/skills";
import { MemoryPanel } from "@/features/memory";
import { EmbedderSettings } from "@/features/embedder";
import { PermissionsPanel } from "@/features/permissions";
import { ProjectWorkStatePanel } from "@/features/workstate";
import { GitPanel, GitGraphView } from "@/features/git";
import { Button, Input, Panel, Tabs, cn } from "@/shared";
import { useGateways } from "@/app/di";
@ -47,6 +48,7 @@ import { useProjects } from "./useProjects";
type SidebarTab =
| "projects"
| "context"
| "work"
| "agents"
| "templates"
| "skills"
@ -57,6 +59,7 @@ type SidebarTab =
const SIDEBAR_TABS: { id: SidebarTab; label: string }[] = [
{ id: "projects", label: "Projects" },
{ id: "context", label: "Context" },
{ id: "work", label: "Work" },
{ id: "agents", label: "Agents" },
{ id: "templates", label: "Templates" },
{ id: "skills", label: "Skills" },
@ -282,6 +285,14 @@ export function ProjectsView() {
<p className="text-sm text-muted">Open a project to edit context.</p>
)}
{/* Work-state panel */}
{sidebarTab === "work" && active && (
<ProjectWorkStatePanel projectId={active.id} />
)}
{sidebarTab === "work" && !active && (
<p className="text-sm text-muted">Open a project to view work state.</p>
)}
{/* Agents panel — only rendered when active project exists */}
{sidebarTab === "agents" && active && (
<AgentsPanel projectId={active.id} projectRoot={active.root} />

View File

@ -12,7 +12,7 @@ import {
fireEvent,
} from "@testing-library/react";
import { MockAgentGateway, MockGitGateway, MockProfileGateway, MockProjectGateway, MockSystemGateway, MockTemplateGateway } from "@/adapters/mock";
import { MockAgentGateway, MockGitGateway, MockProfileGateway, MockProjectGateway, MockSystemGateway, MockTemplateGateway, MockWorkStateGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { ProjectsView } from "./ProjectsView";
@ -33,6 +33,7 @@ function renderView(
profile: new MockProfileGateway(),
template: new MockTemplateGateway(agentGateway),
git: new MockGitGateway(),
workState: new MockWorkStateGateway(),
} as unknown as Gateways;
return {
project,
@ -130,6 +131,20 @@ describe("ProjectsView (with MockProjectGateway)", () => {
expect([a.id, b.id]).toHaveLength(2);
});
it("shows and renders the Work sidebar tab for an active project", async () => {
const project = new MockProjectGateway();
await project.createProject("alpha", "/p/a");
renderView(project);
await screen.findByText("/p/a");
fireEvent.click(screen.getByRole("button", { name: "Open" }));
await screen.findByRole("tab", { name: "alpha" });
fireEvent.click(screen.getByRole("button", { name: "Work" }));
expect(await screen.findByText("No agent work state.")).toBeTruthy();
});
it("closing a tab removes it from the tab bar", async () => {
renderView();
await createProject("alpha", "/home/me/alpha");