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:
co-authored by
Claude Fable 5
parent
32008c9ef0
commit
9f9f507c15
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
// Client chat engine: optimistic sends through the same-origin gateway
|
||||
// proxy, incremental SSE parsing, reducer-owned streaming state.
|
||||
|
||||
import { useCallback, useReducer, useRef } from "react";
|
||||
|
||||
import { parseEnvelope } from "./events";
|
||||
import { createSseParser } from "./stream-parser";
|
||||
import {
|
||||
initialTranscript,
|
||||
transcriptReducer,
|
||||
type TranscriptState,
|
||||
type UiMessage,
|
||||
} from "./transcript";
|
||||
|
||||
export interface ChatHandle {
|
||||
state: TranscriptState;
|
||||
send: (text: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useChat(
|
||||
clawId: string,
|
||||
sessionKey: string,
|
||||
initialMessages: UiMessage[],
|
||||
): ChatHandle {
|
||||
const [state, dispatch] = useReducer(
|
||||
transcriptReducer,
|
||||
undefined,
|
||||
(): TranscriptState => ({
|
||||
...initialTranscript(),
|
||||
messages: initialMessages,
|
||||
}),
|
||||
);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const send = useCallback(
|
||||
async (text: string) => {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
dispatch({ kind: "send", text });
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/gateway?clawId=${encodeURIComponent(clawId)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sessionKey, message: text }),
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok || !response.body) {
|
||||
dispatch({
|
||||
kind: "transport_error",
|
||||
message: `gateway returned ${response.status}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const parser = createSseParser();
|
||||
const reader = response.body.getReader();
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
const frames = done ? parser.flush() : parser.push(value);
|
||||
for (const frame of frames) {
|
||||
dispatch({
|
||||
kind: "gateway",
|
||||
envelope: parseEnvelope(frame.id, frame.data),
|
||||
});
|
||||
}
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) {
|
||||
dispatch({ kind: "transport_error", message: String(error) });
|
||||
}
|
||||
}
|
||||
},
|
||||
[clawId, sessionKey],
|
||||
);
|
||||
|
||||
return { state, send };
|
||||
}
|
||||
Reference in New Issue
Block a user