feat(frontend): viewer humain fil-par-paire en lecture seule (LS7)

Frontend pur, lecture seule, consomme read_conversation_page (LS6) ; aucun changement backend.
- adapters : conversation.ts (gateway) + conversationNormalization.ts.
- features/conversations : useConversationThread, ConversationViewer, index.
- domain (types LS7), ports (port + Gateways.conversation), adapters/index, mock (+ clé inventaire).
- features/workstate : ProjectWorkStatePanel prop onOpenConversation.
- features/projects : ProjectsView swap viewer.
- tests (QA, verts) : conversationNormalization, mock/conversationGateway,
  useConversationThread, ConversationViewer, ProjectsView.ls7. tsc 0, vitest 443/443 (+36).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 17:30:51 +02:00
parent 40ca3e522f
commit c19e375849
17 changed files with 1552 additions and 5 deletions

View File

@ -39,6 +39,7 @@ import { MemoryPanel } from "@/features/memory";
import { EmbedderSettings } from "@/features/embedder";
import { PermissionsPanel } from "@/features/permissions";
import { ProjectWorkStatePanel } from "@/features/workstate";
import { ConversationViewer } from "@/features/conversations";
import { GitPanel, GitGraphView } from "@/features/git";
import { Button, Input, Panel, Tabs, cn } from "@/shared";
import { useGateways } from "@/app/di";
@ -79,6 +80,12 @@ export function ProjectsView() {
// truth. `kind` decides whether the main area is the terminal grid or the git
// graph view.
const [activeLayout, setActiveLayout] = useState<LayoutInfo | null>(null);
// When set, the main area swaps the terminal grid for the read-only
// conversation viewer (LS7) — pure local UI state, same mechanic as the
// terminal↔gitGraph swap; **not** a backend layout kind.
const [viewerConversationId, setViewerConversationId] = useState<
string | null
>(null);
const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null;
@ -89,6 +96,7 @@ export function ProjectsView() {
// loading it against the new project's store fails with "not found: layout X".
useEffect(() => {
setActiveLayout(null);
setViewerConversationId(null);
}, [active?.id]);
const activeLayoutKind = activeLayout?.kind ?? "terminal";
@ -287,7 +295,10 @@ export function ProjectsView() {
{/* Work-state panel */}
{sidebarTab === "work" && active && (
<ProjectWorkStatePanel projectId={active.id} />
<ProjectWorkStatePanel
projectId={active.id}
onOpenConversation={setViewerConversationId}
/>
)}
{sidebarTab === "work" && !active && (
<p className="text-sm text-muted">Open a project to view work state.</p>
@ -348,7 +359,14 @@ export function ProjectsView() {
{/* ── Main: terminal grid or git graph (fills remaining height) ── */}
<main className="flex flex-1 flex-col overflow-hidden">
{active ? (
{active && viewerConversationId ? (
<ConversationViewer
key={`${active.id}-${viewerConversationId}`}
projectId={active.id}
conversationId={viewerConversationId}
onClose={() => setViewerConversationId(null)}
/>
) : active ? (
<>
<LayoutTabs
projectId={active.id}