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,64 @@
import { z } from "zod";
import { ChatWorkspace } from "@/components/chat/ChatWorkspace";
import { apiFetch } from "@/lib/api/http";
import { AgentSchema } from "@/lib/api/schemas";
import {
fetchHistory,
fetchSessions,
type HistoryMessage,
} from "@/lib/api/sessions";
import { decodeSessionKeyParam, formatSessionKey } from "@/lib/url/session-key";
import type { UiMessage } from "@/lib/gateway/transcript";
const SettingsSchema = z.object({ agent: AgentSchema }).passthrough();
function toUiMessage(entry: HistoryMessage): UiMessage | null {
if (entry.role === "system") {
return null;
}
return {
id: entry.id,
role: entry.role,
text: entry.content.text,
steps: entry.steps.map((step) => ({
seq: step.seq,
tool: step.tool_name ?? step.kind,
input: step.input,
output: step.output,
status: step.status,
})),
status: "complete",
};
}
export default async function ChatPage({
params,
}: {
params: Promise<{ clawId: string; sessionKey: string }>;
}) {
const { clawId, sessionKey: rawKey } = await params;
const key = formatSessionKey(decodeSessionKeyParam(rawKey));
const [settings, history, sessions] = await Promise.all([
apiFetch(
SettingsSchema,
`/api/claws/settings/full?clawId=${encodeURIComponent(clawId)}`,
),
fetchHistory(key),
fetchSessions(clawId),
]);
const initialMessages = history
.map(toUiMessage)
.filter((m): m is UiMessage => m !== null);
return (
<ChatWorkspace
agent={settings.agent}
sessionKey={key}
initialMessages={initialMessages}
sessions={sessions}
/>
);
}
@@ -0,0 +1,19 @@
import { redirect } from "next/navigation";
import { createSession, fetchSessions } from "@/lib/api/sessions";
import { encodeSessionKeyParam } from "@/lib/url/session-key";
// Clicking a claw in the rail lands here: resume the most recent session,
// or open a fresh one (sessions are resumable, §6).
export default async function ClawHome({
params,
}: {
params: Promise<{ clawId: string }>;
}) {
const { clawId } = await params;
const sessions = await fetchSessions(clawId);
const target = sessions[0] ?? (await createSession(clawId));
redirect(
`/claws/${clawId}/chat/${encodeSessionKeyParam(target.sessionKey)}`,
);
}
@@ -0,0 +1,17 @@
import { CreateClawForm } from "@/components/wizard/CreateClawForm";
export default function NewClawPage() {
return (
<section className="flex min-h-dvh flex-col items-center justify-center gap-6 px-4 motion-safe:animate-[fade-up_var(--duration-normal)_var(--ease-app)]">
<div className="text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Create your Claw
</h1>
<p className="pt-1 text-sm text-muted-foreground">
Claws help get your work done.
</p>
</div>
<CreateClawForm />
</section>
);
}