Phase 3 of v2.0. React + Vite SPA at apps/coder/web/ served by the coder Fastify server via @fastify/static with SPA fallback. Chat pane: message list via WS streaming (useSessionStream hook), input bar, POST /api/sessions/:id/messages on submit, markdown rendering via react-markdown + remark-gfm, inline tool-call display. Diff pane: fetches GET /api/sessions/:id/pending, shows pending changes with file path + operation badge (create/edit/delete), before/after diff for edits, Approve/Reject per change and Approve All/Reject All buttons. Layout: fixed two-pane split (chat 60%, diff 40%). Dark theme (bg-zinc-900). Desktop-first for v2.0.0. Session picker (Home page): lists projects and sessions from the shared DB. No CRUD — use BooChat's UI for that. Dockerfile updated: builds web app in builder stage, copies dist to runtime. index.ts registers fastifyStatic + SPA fallback route. Tailwind v4, React 18, TypeScript strict. ~20 new files, ~370KB built output. Functional developer tool UI, not polished consumer product — Phase 7 (v2.0.3) handles polish. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
2.4 KiB
TypeScript
87 lines
2.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { ArrowLeft } from 'lucide-react';
|
|
import type { Chat } from '@/api/types';
|
|
import { api } from '@/api/client';
|
|
import { useSessionStream } from '@/hooks/useSessionStream';
|
|
import { ChatPane } from '@/components/ChatPane';
|
|
import { DiffPane } from '@/components/DiffPane';
|
|
import { Layout } from '@/components/Layout';
|
|
|
|
export function Session() {
|
|
const { sessionId } = useParams<{ sessionId: string }>();
|
|
const navigate = useNavigate();
|
|
const [chat, setChat] = useState<Chat | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const { messages, connected, isStreaming, onPendingChange } =
|
|
useSessionStream(sessionId);
|
|
|
|
// Get or create a chat for this session
|
|
useEffect(() => {
|
|
if (!sessionId) return;
|
|
|
|
api.chats
|
|
.listForSession(sessionId)
|
|
.then((chats) => {
|
|
// Use the first open chat, or create one
|
|
const openChat = chats.find((c) => c.status === 'open');
|
|
if (openChat) {
|
|
setChat(openChat);
|
|
} else {
|
|
// Create a new chat
|
|
return api.chats.create(sessionId).then((newChat) => {
|
|
setChat(newChat);
|
|
});
|
|
}
|
|
})
|
|
.catch(console.error)
|
|
.finally(() => setLoading(false));
|
|
}, [sessionId]);
|
|
|
|
if (!sessionId) {
|
|
navigate('/');
|
|
return null;
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
|
<div className="text-zinc-500">Loading session...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!chat) {
|
|
return (
|
|
<div className="min-h-screen bg-zinc-900 flex flex-col items-center justify-center gap-4">
|
|
<div className="text-zinc-500">Could not load chat for this session.</div>
|
|
<button
|
|
onClick={() => navigate('/')}
|
|
className="flex items-center gap-2 text-sm text-blue-400 hover:text-blue-300"
|
|
>
|
|
<ArrowLeft size={14} />
|
|
Back to projects
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Layout
|
|
chatPane={
|
|
<ChatPane
|
|
sessionId={sessionId}
|
|
chatId={chat.id}
|
|
messages={messages}
|
|
isStreaming={isStreaming}
|
|
connected={connected}
|
|
/>
|
|
}
|
|
diffPane={
|
|
<DiffPane sessionId={sessionId} onPendingChange={onPendingChange} />
|
|
}
|
|
/>
|
|
);
|
|
}
|