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>
139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Code2, Folder, ArrowRight } from 'lucide-react';
|
|
import type { Project, Session } from '@/api/types';
|
|
import { api } from '@/api/client';
|
|
|
|
export function Home() {
|
|
const navigate = useNavigate();
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [sessions, setSessions] = useState<Session[]>([]);
|
|
const [selectedProject, setSelectedProject] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Fetch projects on mount
|
|
useEffect(() => {
|
|
api.projects
|
|
.list({ status: 'open' })
|
|
.then(setProjects)
|
|
.catch(console.error)
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
// Fetch sessions when a project is selected
|
|
useEffect(() => {
|
|
if (!selectedProject) {
|
|
setSessions([]);
|
|
return;
|
|
}
|
|
api.sessions
|
|
.listForProject(selectedProject, 'open')
|
|
.then(setSessions)
|
|
.catch(console.error);
|
|
}, [selectedProject]);
|
|
|
|
const handleSessionClick = (session: Session) => {
|
|
navigate(`/sessions/${session.id}`);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
|
<div className="text-zinc-500">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-zinc-900 p-6">
|
|
<div className="max-w-2xl mx-auto">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3 mb-8">
|
|
<Code2 size={28} className="text-blue-400" />
|
|
<h1 className="text-2xl font-bold text-zinc-100">BooCoder</h1>
|
|
</div>
|
|
|
|
{/* Projects list */}
|
|
<div className="mb-8">
|
|
<h2 className="text-sm font-medium text-zinc-400 uppercase tracking-wide mb-3">
|
|
Projects
|
|
</h2>
|
|
{projects.length === 0 ? (
|
|
<p className="text-zinc-500 text-sm">
|
|
No projects found. Create one in BooChat first.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{projects.map((project) => (
|
|
<button
|
|
key={project.id}
|
|
onClick={() => setSelectedProject(project.id)}
|
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors ${
|
|
selectedProject === project.id
|
|
? 'bg-blue-600/20 border border-blue-500/40'
|
|
: 'bg-zinc-800/50 border border-zinc-800 hover:bg-zinc-800'
|
|
}`}
|
|
>
|
|
<Folder
|
|
size={16}
|
|
className={
|
|
selectedProject === project.id
|
|
? 'text-blue-400'
|
|
: 'text-zinc-500'
|
|
}
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-zinc-200 truncate">
|
|
{project.name}
|
|
</div>
|
|
<div className="text-xs text-zinc-500 truncate">
|
|
{project.path}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sessions list */}
|
|
{selectedProject && (
|
|
<div>
|
|
<h2 className="text-sm font-medium text-zinc-400 uppercase tracking-wide mb-3">
|
|
Sessions
|
|
</h2>
|
|
{sessions.length === 0 ? (
|
|
<p className="text-zinc-500 text-sm">
|
|
No open sessions. Create one in BooChat first.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{sessions.map((session) => (
|
|
<button
|
|
key={session.id}
|
|
onClick={() => handleSessionClick(session)}
|
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg bg-zinc-800/50 border border-zinc-800 hover:bg-zinc-800 text-left transition-colors group"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-zinc-200 truncate">
|
|
{session.name || 'Untitled session'}
|
|
</div>
|
|
<div className="text-xs text-zinc-500">
|
|
{new Date(session.updated_at).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
<ArrowRight
|
|
size={16}
|
|
className="text-zinc-600 group-hover:text-zinc-400 transition-colors"
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|