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>
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
import { useState } from 'react';
|
|
import { Code2, MessageSquare, GitPullRequest } from 'lucide-react';
|
|
|
|
interface Props {
|
|
chatPane: React.ReactNode;
|
|
diffPane: React.ReactNode;
|
|
}
|
|
|
|
export function Layout({ chatPane, diffPane }: Props) {
|
|
const [activeTab, setActiveTab] = useState<'chat' | 'diff'>('chat');
|
|
|
|
return (
|
|
<div className="flex flex-col h-screen bg-zinc-900">
|
|
{/* Top bar */}
|
|
<header className="flex items-center gap-3 px-4 py-2 border-b border-zinc-800 bg-zinc-900/95">
|
|
<Code2 size={20} className="text-blue-400" />
|
|
<h1 className="text-sm font-semibold text-zinc-200">BooCoder</h1>
|
|
</header>
|
|
|
|
{/* Mobile tab bar (visible below lg breakpoint) */}
|
|
<div className="lg:hidden flex border-b border-zinc-800">
|
|
<button
|
|
onClick={() => setActiveTab('chat')}
|
|
className={`flex-1 flex items-center justify-center gap-1.5 py-2 text-sm ${
|
|
activeTab === 'chat'
|
|
? 'text-blue-400 border-b-2 border-blue-400'
|
|
: 'text-zinc-500'
|
|
}`}
|
|
>
|
|
<MessageSquare size={14} />
|
|
Chat
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('diff')}
|
|
className={`flex-1 flex items-center justify-center gap-1.5 py-2 text-sm ${
|
|
activeTab === 'diff'
|
|
? 'text-blue-400 border-b-2 border-blue-400'
|
|
: 'text-zinc-500'
|
|
}`}
|
|
>
|
|
<GitPullRequest size={14} />
|
|
Changes
|
|
</button>
|
|
</div>
|
|
|
|
{/* Desktop split layout */}
|
|
<div className="flex-1 hidden lg:flex overflow-hidden">
|
|
<div className="w-[60%] border-r border-zinc-800 overflow-hidden">
|
|
{chatPane}
|
|
</div>
|
|
<div className="w-[40%] overflow-hidden">
|
|
{diffPane}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile: show only the active tab */}
|
|
<div className="flex-1 lg:hidden overflow-hidden">
|
|
{activeTab === 'chat' ? chatPane : diffPane}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|