P1 exit: chat UI, sessions column, agent create — 11 E2E journeys green

- Stream layer: incremental SSE parser tested against bytes captured from
  the REAL gateway (chunking-independent, UTF-8-safe, heartbeat-tolerant);
  Zod gateway event schemas; pure transcript reducer (optimistic send,
  delta streaming, step traces, resume dedupe, error states)
- Same-origin /api proxy route: httpOnly cookie -> bearer, unbuffered SSE
  passthrough; NuqsAdapter in root layout
- Chat workspace: route /claws/{id}/chat/{key} (RSC history + settings),
  WelcomeState with suggested prompts, MessageList (right user bubbles,
  left agent messages, blink caret), collapsible StepTrace, Composer
  (Enter sends, Shift+Enter newline)
- SessionsColumn in 208px SlidePanel (?sessions=1): search, relative
  times, active coral border, new session; /claws/{id} resumes latest or
  opens fresh; minimal create-claw form; rail + button and roster links
- P1 exit E2E: scripted reply streams, tool step trace survives reload,
  separate transcripts across sessions with column switching, create claw
  and chat immediately

83 Rust + 56 frontend unit/component tests + 11 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-09 23:29:27 -05:00
co-authored by Claude Fable 5
parent 32008c9ef0
commit 9f9f507c15
29 changed files with 1774 additions and 4 deletions
@@ -0,0 +1,89 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useQueryStates } from "nuqs";
import type { Agent } from "@/lib/api/schemas";
import type { Session } from "@/lib/api/sessions";
import { useChat } from "@/lib/gateway/use-chat";
import type { UiMessage } from "@/lib/gateway/transcript";
import { encodeSessionKeyParam } from "@/lib/url/session-key";
import { panelParsers } from "@/lib/url/panel-params";
import { SlidePanel } from "@/components/ui/SlidePanel";
import { SessionsColumn } from "@/components/sessions/SessionsColumn";
import { ChatHeader } from "./ChatHeader";
import { Composer } from "./Composer";
import { MessageList } from "./MessageList";
import { WelcomeState } from "./WelcomeState";
interface ChatWorkspaceProps {
agent: Agent;
sessionKey: string;
initialMessages: UiMessage[];
sessions: Session[];
}
/** The chat column orchestrator (§5/§6). Search-param state is client-only
* and shallow; navigating sessions is a real path navigation. */
export function ChatWorkspace({
agent,
sessionKey,
initialMessages,
sessions,
}: ChatWorkspaceProps) {
const router = useRouter();
const { state, send } = useChat(agent.id, sessionKey, initialMessages);
const [draft, setDraft] = useState("");
const [{ sessions: showSessions }] = useQueryStates(panelParsers, {
shallow: true,
});
function openSession(session: Session) {
router.push(
`/claws/${agent.id}/chat/${encodeSessionKeyParam(session.sessionKey)}?sessions=1`,
);
}
async function newSession() {
const res = await fetch("/api/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ clawId: agent.id, title: "" }),
});
if (res.ok) {
const session = (await res.json()) as Session;
router.push(
`/claws/${agent.id}/chat/${encodeSessionKeyParam(session.sessionKey)}`,
);
}
}
return (
<div className="flex h-dvh">
<SlidePanel open={showSessions} width={208} label="Sessions">
<SessionsColumn
sessions={sessions}
activeSessionKey={sessionKey}
onOpen={openSession}
onNewSession={newSession}
/>
</SlidePanel>
<section className="flex min-w-0 flex-1 flex-col">
<ChatHeader agent={agent} onNewSession={newSession} />
{state.messages.length === 0 ? (
<WelcomeState agent={agent} onPick={setDraft} />
) : (
<MessageList messages={state.messages} agent={agent} />
)}
<Composer
agentName={agent.name}
disabled={state.streaming}
onSend={send}
draft={draft}
onDraftChange={setDraft}
/>
</section>
</div>
);
}