Files
boocode/apps/coder/web/src/pages/Session.tsx
indifferentketchup 78455b7efc v2.0.0: BooCoder frontend — chat pane + diff pane + session picker
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>
2026-05-25 03:04:52 +00:00

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} />
}
/>
);
}