feat(@projects): add core routing and page scaffolding

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-18 17:12:11 -07:00
parent 8f12844217
commit 23712cdf4e
22 changed files with 603 additions and 72 deletions

View file

@ -1,31 +1,42 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* R1 scaffold entry. Hello-world that proves:
* - Vite serves the SPA on :5173
* - The proxy to FastAPI on :8767 works (we hit /api/v1/health and render the result)
* - Build chain compiles
* Router + theme. Owned by the foundation (not the per-page agents).
*
* Real pages (chat, projects, sessions, broadcast, dashboard) land in R3R5.
* Route table:
* / DashboardPage (R5)
* /chat ChatPage scope=orchestrator (R3)
* /chat/project/:name ChatPage scope=project (R3)
* /chat/session/:uuid ChatPage scope=session (R3)
* /projects ProjectsListPage (R4)
* /projects/:name ProjectDetailPage (R4)
* /sessions SessionsPage (R4)
* /broadcast BroadcastPage (R5)
*
* Agents implement the pages in their respective subdirs. They MUST NOT
* edit this file (route registration) or lib/* (shared infra).
*/
import { useEffect, useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { ThemeProvider } from "styled-components";
import { AppShell } from "./AppShell";
import { clareTheme } from "./theme";
import { ChatPage } from "./chat/ChatPage";
import { ProjectsListPage } from "./projects/ProjectsListPage";
import { ProjectDetailPage } from "./projects/ProjectDetailPage";
import { SessionsPage } from "./sessions/SessionsPage";
import { BroadcastPage } from "./broadcast/BroadcastPage";
import { DashboardPage } from "./dashboard/DashboardPage";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Clare's state is event-sourced + SSE-pushed; aggressive refetch is
// unnecessary. Pages that want push updates use useChatStream or
// their own EventSource.
staleTime: 5_000,
refetchOnWindowFocus: false,
},
},
});
export function App() {
const [health, setHealth] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const ctrl = new AbortController();
fetch("/api/v1/health", { signal: ctrl.signal })
.then((r) => {
if (!r.ok)
throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then((h) => setHealth(h))
.catch((e) => {
if (e.name !== "AbortError") {
setError(String(e));
}
});
return () => ctrl.abort();
}, []);
return (_jsxs("main", { style: { fontFamily: "ui-monospace, monospace", padding: "2rem" }, children: [_jsx("h1", { children: "clare" }), _jsx("p", { children: "R1 scaffold. Vite \u2192 FastAPI proxy check:" }), error ? (_jsxs("pre", { style: { color: "tomato" }, children: ["error: ", error] })) : health ? (_jsx("pre", { children: `status: ${health.status}\nmachine_id: ${health.machine_id}` })) : (_jsx("p", { children: "loading\u2026" })), _jsx("p", { style: { color: "#888", marginTop: "2rem" }, children: "R3: chat surface. R4: projects + sessions. R5: broadcast + dashboard. R6: delete Jinja templates." })] }));
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ThemeProvider, { theme: clareTheme, children: _jsx(BrowserRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsx(AppShell, {}), children: [_jsx(Route, { index: true, element: _jsx(DashboardPage, {}) }), _jsx(Route, { path: "chat", element: _jsx(ChatPage, { scope: "orchestrator" }) }), _jsx(Route, { path: "chat/project/:name", element: _jsx(ChatPage, { scope: "project" }) }), _jsx(Route, { path: "chat/session/:uuid", element: _jsx(ChatPage, { scope: "session" }) }), _jsx(Route, { path: "projects", element: _jsx(ProjectsListPage, {}) }), _jsx(Route, { path: "projects/:name", element: _jsx(ProjectDetailPage, {}) }), _jsx(Route, { path: "sessions", element: _jsx(SessionsPage, {}) }), _jsx(Route, { path: "broadcast", element: _jsx(BroadcastPage, {}) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/", replace: true }) })] }) }) }) }) }));
}

View file

@ -1,55 +1,66 @@
/**
* R1 scaffold entry. Hello-world that proves:
* - Vite serves the SPA on :5173
* - The proxy to FastAPI on :8767 works (we hit /api/v1/health and render the result)
* - Build chain compiles
* Router + theme. Owned by the foundation (not the per-page agents).
*
* Real pages (chat, projects, sessions, broadcast, dashboard) land in R3R5.
* Route table:
* / DashboardPage (R5)
* /chat ChatPage scope=orchestrator (R3)
* /chat/project/:name ChatPage scope=project (R3)
* /chat/session/:uuid ChatPage scope=session (R3)
* /projects ProjectsListPage (R4)
* /projects/:name ProjectDetailPage (R4)
* /sessions SessionsPage (R4)
* /broadcast BroadcastPage (R5)
*
* Agents implement the pages in their respective subdirs. They MUST NOT
* edit this file (route registration) or lib/* (shared infra).
*/
import { useEffect, useState, type ReactElement } from "react";
interface Health {
status: string;
machine_id: string;
}
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { ThemeProvider } from "styled-components";
import type { ReactElement } from "react";
import { AppShell } from "./AppShell";
import { clareTheme } from "./theme";
import { ChatPage } from "./chat/ChatPage";
import { ProjectsListPage } from "./projects/ProjectsListPage";
import { ProjectDetailPage } from "./projects/ProjectDetailPage";
import { SessionsPage } from "./sessions/SessionsPage";
import { BroadcastPage } from "./broadcast/BroadcastPage";
import { DashboardPage } from "./dashboard/DashboardPage";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Clare's state is event-sourced + SSE-pushed; aggressive refetch is
// unnecessary. Pages that want push updates use useChatStream or
// their own EventSource.
staleTime: 5_000,
refetchOnWindowFocus: false,
},
},
});
export function App(): ReactElement {
const [health, setHealth] = useState<Health | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect((): (() => void) => {
const ctrl = new AbortController();
fetch("/api/v1/health", { signal: ctrl.signal })
.then((r: Response): Promise<Health> => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json() as Promise<Health>;
})
.then((h: Health): void => setHealth(h))
.catch((e: unknown): void => {
if ((e as { name?: string }).name !== "AbortError") {
setError(String(e));
}
});
return (): void => ctrl.abort();
}, []);
return (
<main style={{ fontFamily: "ui-monospace, monospace", padding: "2rem" }}>
<h1>clare</h1>
<p>R1 scaffold. Vite FastAPI proxy check:</p>
{error ? (
<pre style={{ color: "tomato" }}>error: {error}</pre>
) : health ? (
<pre>
{`status: ${health.status}\nmachine_id: ${health.machine_id}`}
</pre>
) : (
<p>loading</p>
)}
<p style={{ color: "#888", marginTop: "2rem" }}>
R3: chat surface. R4: projects + sessions. R5: broadcast + dashboard.
R6: delete Jinja templates.
</p>
</main>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={clareTheme}>
<BrowserRouter>
<Routes>
<Route element={<AppShell />}>
<Route index element={<DashboardPage />} />
<Route path="chat" element={<ChatPage scope="orchestrator" />} />
<Route path="chat/project/:name" element={<ChatPage scope="project" />} />
<Route path="chat/session/:uuid" element={<ChatPage scope="session" />} />
<Route path="projects" element={<ProjectsListPage />} />
<Route path="projects/:name" element={<ProjectDetailPage />} />
<Route path="sessions" element={<SessionsPage />} />
<Route path="broadcast" element={<BroadcastPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
);
}

View file

@ -0,0 +1,49 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* AppShell top navigation + content slot. Wrapped by ThemeProvider in App.tsx.
*
* Pages are rendered into the <main> via react-router's <Outlet />.
* The terminal aesthetic comes from @lilith/react-terminal-themes; we wire
* the "nord" theme by default and let pages override via context if they
* want a different vibe (e.g. orchestrator could go "matrix").
*/
import { NavLink, Outlet } from "react-router-dom";
import styled from "styled-components";
const Shell = styled.div `
min-height: 100vh;
display: flex;
flex-direction: column;
background: ${({ theme }) => theme.colors?.bg ?? "#0f1115"};
color: ${({ theme }) => theme.colors?.fg ?? "#d8dee9"};
font: 14px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
`;
const Nav = styled.nav `
display: flex;
gap: 1.5rem;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid ${({ theme }) => theme.colors?.border ?? "#2a2f3a"};
background: ${({ theme }) => theme.colors?.bgAlt ?? "#0a0c11"};
align-items: center;
`;
const Brand = styled(NavLink) `
font-weight: bold;
color: ${({ theme }) => theme.colors?.accent ?? "#88c0d0"};
text-decoration: none;
margin-right: auto;
`;
const NavItem = styled(NavLink) `
color: ${({ theme }) => theme.colors?.dim ?? "#7a8290"};
text-decoration: none;
&.active {
color: ${({ theme }) => theme.colors?.accent ?? "#88c0d0"};
}
`;
const Main = styled.main `
flex: 1 1 auto;
display: flex;
min-height: 0;
`;
export function AppShell() {
return (_jsxs(Shell, { children: [_jsxs(Nav, { children: [_jsx(Brand, { to: "/", children: "clare" }), _jsx(NavItem, { to: "/chat", children: "chat" }), _jsx(NavItem, { to: "/projects", children: "projects" }), _jsx(NavItem, { to: "/sessions", children: "sessions" }), _jsx(NavItem, { to: "/broadcast", children: "broadcast" })] }), _jsx(Main, { children: _jsx(Outlet, {}) })] }));
}

View file

@ -0,0 +1,69 @@
/**
* AppShell top navigation + content slot. Wrapped by ThemeProvider in App.tsx.
*
* Pages are rendered into the <main> via react-router's <Outlet />.
* The terminal aesthetic comes from @lilith/react-terminal-themes; we wire
* the "nord" theme by default and let pages override via context if they
* want a different vibe (e.g. orchestrator could go "matrix").
*/
import { NavLink, Outlet } from "react-router-dom";
import styled from "styled-components";
import type { ReactElement } from "react";
const Shell = styled.div`
min-height: 100vh;
display: flex;
flex-direction: column;
background: ${({ theme }): string => theme.colors?.bg ?? "#0f1115"};
color: ${({ theme }): string => theme.colors?.fg ?? "#d8dee9"};
font: 14px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
`;
const Nav = styled.nav`
display: flex;
gap: 1.5rem;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid ${({ theme }): string => theme.colors?.border ?? "#2a2f3a"};
background: ${({ theme }): string => theme.colors?.bgAlt ?? "#0a0c11"};
align-items: center;
`;
const Brand = styled(NavLink)`
font-weight: bold;
color: ${({ theme }): string => theme.colors?.accent ?? "#88c0d0"};
text-decoration: none;
margin-right: auto;
`;
const NavItem = styled(NavLink)`
color: ${({ theme }): string => theme.colors?.dim ?? "#7a8290"};
text-decoration: none;
&.active {
color: ${({ theme }): string => theme.colors?.accent ?? "#88c0d0"};
}
`;
const Main = styled.main`
flex: 1 1 auto;
display: flex;
min-height: 0;
`;
export function AppShell(): ReactElement {
return (
<Shell>
<Nav>
<Brand to="/">clare</Brand>
<NavItem to="/chat">chat</NavItem>
<NavItem to="/projects">projects</NavItem>
<NavItem to="/sessions">sessions</NavItem>
<NavItem to="/broadcast">broadcast</NavItem>
</Nav>
<Main>
<Outlet />
</Main>
</Shell>
);
}

View file

@ -0,0 +1,4 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export function BroadcastPage() {
return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsx("h2", { children: "broadcast" }), _jsx("p", { style: { color: "#7a8290" }, children: "R5 placeholder." })] }));
}

View file

@ -0,0 +1,13 @@
/**
* R5 placeholder broadcast form. Owned by the R5 agent.
*/
import type { ReactElement } from "react";
export function BroadcastPage(): ReactElement {
return (
<div style={{ padding: "1.5rem" }}>
<h2>broadcast</h2>
<p style={{ color: "#7a8290" }}>R5 placeholder.</p>
</div>
);
}

View file

@ -0,0 +1,11 @@
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
/**
* R3 placeholder replaced by the orchestrator/project/session chat surface.
* Owned by the R3 agent. Don't edit `App.tsx` or `lib/*` from here.
*/
import { useParams } from "react-router-dom";
export function ChatPage({ scope }) {
const params = useParams();
const ref = scope === "project" ? params.name : scope === "session" ? params.uuid : null;
return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsxs("h2", { children: ["chat \u00B7 ", scope, ref ? ` · ${ref}` : ""] }), _jsx("p", { style: { color: "#7a8290" }, children: "R3 placeholder \u2014 agent will replace this." })] }));
}

View file

@ -0,0 +1,22 @@
/**
* R3 placeholder replaced by the orchestrator/project/session chat surface.
* Owned by the R3 agent. Don't edit `App.tsx` or `lib/*` from here.
*/
import { useParams } from "react-router-dom";
import type { ReactElement } from "react";
import type { ChatScope } from "../lib/types";
export interface ChatPageProps {
scope: ChatScope;
}
export function ChatPage({ scope }: ChatPageProps): ReactElement {
const params = useParams<{ name?: string; uuid?: string }>();
const ref = scope === "project" ? params.name : scope === "session" ? params.uuid : null;
return (
<div style={{ padding: "1.5rem" }}>
<h2>chat · {scope}{ref ? ` · ${ref}` : ""}</h2>
<p style={{ color: "#7a8290" }}>R3 placeholder agent will replace this.</p>
</div>
);
}

View file

@ -0,0 +1,4 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export function DashboardPage() {
return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsx("h2", { children: "dashboard" }), _jsx("p", { style: { color: "#7a8290" }, children: "R5 placeholder." })] }));
}

View file

@ -0,0 +1,13 @@
/**
* R5 placeholder landing dashboard. Owned by the R5 agent.
*/
import type { ReactElement } from "react";
export function DashboardPage(): ReactElement {
return (
<div style={{ padding: "1.5rem" }}>
<h2>dashboard</h2>
<p style={{ color: "#7a8290" }}>R5 placeholder.</p>
</div>
);
}

View file

@ -0,0 +1,128 @@
/**
* Typed fetch wrapper for Clare's JSON API.
*
* Every page calls these never raw `fetch`. Centralizes:
* - URL construction (relative paths; Vite proxies in dev, same-origin in prod)
* - error translation: non-2xx `ApiError(status, detail)`
* - JSON parsing with typed return
* - AbortSignal threading so callers can cancel
*/
export class ApiError extends Error {
status;
detail;
constructor(status, detail) {
super(`${status}: ${detail}`);
this.status = status;
this.detail = detail;
this.name = "ApiError";
}
}
async function request(method, path, options = {}) {
const url = buildUrl(path, options.params);
const init = {
method,
signal: options.signal,
headers: options.body !== undefined ? { "content-type": "application/json" } : undefined,
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
};
const resp = await fetch(url, init);
if (!resp.ok) {
let detail = resp.statusText;
try {
const j = await resp.json();
if (j && typeof j === "object" && "detail" in j) {
detail = String(j.detail);
}
}
catch {
// body wasn't json — keep statusText
}
throw new ApiError(resp.status, detail);
}
// 204 / empty body → cast to T (caller must declare void in that case)
if (resp.status === 204)
return undefined;
return (await resp.json());
}
function buildUrl(path, params) {
if (!params)
return path;
const usp = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null)
usp.append(k, String(v));
}
const q = usp.toString();
return q ? `${path}?${q}` : path;
}
// ---------------------------------------------------------------------------
// Projects
// ---------------------------------------------------------------------------
export async function listProjects(signal) {
const r = await request("GET", "/api/v1/projects", { signal });
return r.projects;
}
export async function createProject(body, signal) {
return request("POST", "/api/v1/projects", { body, signal });
}
export async function getProject(nameOrId, signal) {
return request("GET", `/api/v1/projects/${encodeURIComponent(nameOrId)}`, { signal });
}
// ---------------------------------------------------------------------------
// Tasks
// ---------------------------------------------------------------------------
export async function listTasks(params = {}, signal) {
const r = await request("GET", "/api/v1/tasks", { params, signal });
return r.tasks;
}
export async function createTask(body, signal) {
return request("POST", "/api/v1/tasks", { body, signal });
}
export async function updateTask(taskId, body, signal) {
return request("POST", `/api/v1/tasks/${encodeURIComponent(taskId)}`, { body, signal });
}
// ---------------------------------------------------------------------------
// Assignments
// ---------------------------------------------------------------------------
export async function listAssignments(active = true, signal) {
const r = await request("GET", "/api/v1/assignments", {
params: { active: active ? 1 : 0 }, signal,
});
return r.assignments;
}
export async function createAssignment(body, signal) {
return request("POST", "/api/v1/assignments", { body, signal });
}
// ---------------------------------------------------------------------------
// Sessions + pull
// ---------------------------------------------------------------------------
export async function listSessions(signal) {
const r = await request("GET", "/api/v1/sessions", { signal });
return r.sessions;
}
export async function pull(signal) {
return request("POST", "/api/v1/pull", { signal });
}
// ---------------------------------------------------------------------------
// Broadcast
// ---------------------------------------------------------------------------
export async function broadcast(body, signal) {
return request("POST", "/api/v1/broadcast", { body, signal });
}
// ---------------------------------------------------------------------------
// Chat
// ---------------------------------------------------------------------------
export async function listChatMessages(params, signal) {
const r = await request("GET", "/api/v1/chat", { params, signal });
return r.messages;
}
export async function postChatMessage(body, signal) {
return request("POST", "/api/v1/chat", { body, signal });
}
// ---------------------------------------------------------------------------
// Autocomplete
// ---------------------------------------------------------------------------
export async function autocomplete(params, signal) {
const r = await request("GET", "/api/v1/autocomplete", { params, signal });
return r.hits;
}

View file

@ -0,0 +1,67 @@
/**
* useChatStream subscribe to the SSE channel for a chat scope.
*
* The FastAPI endpoint at `/chat/stream?scope=...&scope_ref=...&after_rowid=N`
* emits `event: chat\ndata: <html>` frames whenever new chat_messages rows
* appear. The HTML payload was for HTMX; we need parsed ChatMessage objects.
*
* The cleanest path is a sibling JSON SSE channel but plumbing a second
* stream is out of scope here. Instead the hook re-fetches `listChatMessages`
* with the latest known rowid each time a `chat` event fires (and on mount).
* The SSE channel is used only as a wake-up signal.
*
* Hand-rolled hook (no react-query streaming) to keep the dependency surface
* minimal and the wake-up logic explicit.
*/
import { useEffect, useState, useRef } from "react";
import { listChatMessages } from "./api";
export function useChatStream({ scope, scopeRef }) {
const [messages, setMessages] = useState([]);
const [connected, setConnected] = useState(false);
const lastRowidRef = useRef(0);
useEffect(() => {
let cancelled = false;
const ctrl = new AbortController();
async function refresh() {
try {
const rows = await listChatMessages({ scope, scope_ref: scopeRef, after_rowid: lastRowidRef.current, limit: 200 }, ctrl.signal);
if (cancelled || rows.length === 0)
return;
lastRowidRef.current = rows[rows.length - 1].rowid;
setMessages((prev) => mergeById(prev, rows));
}
catch (e) {
if (e.name === "AbortError")
return;
// Network blips during reconnect are common; swallow rather than
// surface — the SSE channel will fire another wake-up.
}
}
// Initial backfill (rowid 0 → everything).
void refresh();
const params = new URLSearchParams({ scope, scope_ref: scopeRef ?? "", after_rowid: "0" });
const es = new EventSource(`/chat/stream?${params.toString()}`);
es.addEventListener("open", () => setConnected(true));
es.addEventListener("error", () => setConnected(false));
es.addEventListener("chat", () => void refresh());
return () => {
cancelled = true;
ctrl.abort();
es.close();
};
}, [scope, scopeRef]);
function ingest(msgs) {
if (msgs.length === 0)
return;
lastRowidRef.current = Math.max(lastRowidRef.current, msgs.reduce((m, r) => Math.max(m, r.rowid), 0));
setMessages((prev) => mergeById(prev, msgs));
}
return { messages, connected, ingest };
}
function mergeById(prev, next) {
const seen = new Set(prev.map((m) => m.rowid));
const additions = next.filter((m) => !seen.has(m.rowid));
if (additions.length === 0)
return prev;
return [...prev, ...additions].sort((a, b) => a.rowid - b.rowid);
}

View file

@ -0,0 +1,6 @@
/**
* Wire types mirroring the FastAPI JSON shapes in src/clare/web/api.py.
* Hand-written rather than generated the surface is small and stable.
* If a field is added to a Pydantic model, mirror it here too.
*/
export {};

View file

@ -0,0 +1,9 @@
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
/**
* R4 placeholder single project detail. Owned by the R4 agent.
*/
import { useParams } from "react-router-dom";
export function ProjectDetailPage() {
const { name } = useParams();
return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsxs("h2", { children: ["project \u00B7 ", name] }), _jsx("p", { style: { color: "#7a8290" }, children: "R4 placeholder." })] }));
}

View file

@ -0,0 +1,15 @@
/**
* R4 placeholder single project detail. Owned by the R4 agent.
*/
import { useParams } from "react-router-dom";
import type { ReactElement } from "react";
export function ProjectDetailPage(): ReactElement {
const { name } = useParams<{ name: string }>();
return (
<div style={{ padding: "1.5rem" }}>
<h2>project · {name}</h2>
<p style={{ color: "#7a8290" }}>R4 placeholder.</p>
</div>
);
}

View file

@ -0,0 +1,4 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export function ProjectsListPage() {
return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsx("h2", { children: "projects" }), _jsx("p", { style: { color: "#7a8290" }, children: "R4 placeholder." })] }));
}

View file

@ -0,0 +1,13 @@
/**
* R4 placeholder projects list. Owned by the R4 agent.
*/
import type { ReactElement } from "react";
export function ProjectsListPage(): ReactElement {
return (
<div style={{ padding: "1.5rem" }}>
<h2>projects</h2>
<p style={{ color: "#7a8290" }}>R4 placeholder.</p>
</div>
);
}

View file

@ -0,0 +1,4 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export function SessionsPage() {
return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsx("h2", { children: "sessions" }), _jsx("p", { style: { color: "#7a8290" }, children: "R4 placeholder." })] }));
}

View file

@ -0,0 +1,13 @@
/**
* R4 placeholder fleet sessions list. Owned by the R4 agent.
*/
import type { ReactElement } from "react";
export function SessionsPage(): ReactElement {
return (
<div style={{ padding: "1.5rem" }}>
<h2>sessions</h2>
<p style={{ color: "#7a8290" }}>R4 placeholder.</p>
</div>
);
}

View file

@ -0,0 +1,21 @@
/**
* Terminal-styled theme for styled-components.
*
* Hand-tuned to match the Nord-ish look the prior Jinja CSS used.
* @lilith/react-terminal-themes is available too agents can import its
* themes for specific surfaces (e.g. the xterm widget in @lilith/
* react-terminal-ui) without disturbing this base palette.
*/
export const clareTheme = {
colors: {
bg: "#0f1115",
bgAlt: "#0a0c11",
fg: "#d8dee9",
dim: "#7a8290",
accent: "#88c0d0",
border: "#2a2f3a",
warn: "#ebcb8b",
good: "#a3be8c",
bad: "#bf616a",
},
};

View file

@ -0,0 +1,44 @@
/**
* Terminal-styled theme for styled-components.
*
* Hand-tuned to match the Nord-ish look the prior Jinja CSS used.
* @lilith/react-terminal-themes is available too agents can import its
* themes for specific surfaces (e.g. the xterm widget in @lilith/
* react-terminal-ui) without disturbing this base palette.
*/
export interface ClareTheme {
colors: {
bg: string;
bgAlt: string;
fg: string;
dim: string;
accent: string;
border: string;
warn: string;
good: string;
bad: string;
};
}
export const clareTheme: ClareTheme = {
colors: {
bg: "#0f1115",
bgAlt: "#0a0c11",
fg: "#d8dee9",
dim: "#7a8290",
accent: "#88c0d0",
border: "#2a2f3a",
warn: "#ebcb8b",
good: "#a3be8c",
bad: "#bf616a",
},
};
// Augment styled-components' DefaultTheme so `theme.colors.bg` is typed
// throughout the app without explicit casts.
declare module "styled-components" {
export interface DefaultTheme extends ClareTheme {
readonly _brand?: never;
}
}

View file

@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./vite.config.ts"],"version":"5.9.3"}
{"root":["./src/app.tsx","./src/appshell.tsx","./src/main.tsx","./src/theme.ts","./src/broadcast/broadcastpage.tsx","./src/chat/chatpage.tsx","./src/dashboard/dashboardpage.tsx","./src/lib/api.ts","./src/lib/sse.ts","./src/lib/types.ts","./src/projects/projectdetailpage.tsx","./src/projects/projectslistpage.tsx","./src/sessions/sessionspage.tsx","./vite.config.ts"],"version":"5.9.3"}