Files
codecraft/web/src/App.tsx
T
Richard ZinkandClaude Opus 5.5 aa1f8cf016 Add API tokens, CLI server API and shared core crate
- Personal API tokens (full/read scope, expiry, revoke) and CLI password login
- Cargo workspace: codecraft-core (types, providers, permission rules) shared by
  server and the upcoming terminal client; server re-exports the old paths
- CLI server API: /cli/config, /cli/skill, /cli/tool (server MCP tools),
  /llm/stream model gateway with budgets and usage per user
- Theme/language preferences mirrored to the server; personal memory endpoint
- Statistics: provider and mode tables stacked; CLI usage labelled
- cli/: work in progress (terminal client is being rewritten as a local TUI)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:18:50 +01:00

106 lines
3.8 KiB
TypeScript

import { lazy, Suspense, useEffect, useState } from "react";
import { connectEvents, get, setUnauthorizedHandler } from "./api";
import { useT } from "./i18n";
import { navigate, useRoute } from "./router";
import { applyEvent, loadSessions, resync, setState } from "./store";
import type { Info } from "./types";
import { Sidebar } from "./components/Sidebar";
import { Login } from "./components/Login";
import { ChatView } from "./components/ChatView";
import { NewSession } from "./components/NewSession";
import { Settings } from "./components/Settings";
import { Stats } from "./components/Stats";
import { Images } from "./components/Images";
import { Jobs } from "./components/Jobs";
import { Spinner, useTitleActivity } from "./components/Activity";
import { DialogHost } from "./dialogs";
import { startPrefsSync } from "./prefsSync";
// The file manager (editor, git) is loaded on demand to keep the chat small.
const Files = lazy(() => import("./components/Files").then((m) => ({ default: m.Files })));
const TerminalPage = lazy(() => import("./components/Terminal").then((m) => ({ default: m.TerminalPage })));
export function App() {
const t = useT();
const route = useRoute();
const [info, setInfo] = useState<Info>();
const [error, setError] = useState<string>();
const [menuOpen, setMenuOpen] = useState(false);
useTitleActivity();
const refresh = async () => {
try {
const i = await get<Info>("/info");
setState(() => ({ me: i.user }));
if (i.user) {
await loadSessions();
void startPrefsSync();
}
setInfo(i);
} catch (e: any) {
setError(e.message);
}
};
useEffect(() => {
setUnauthorizedHandler(() => setInfo((i) => (i ? { ...i, user: null } : i)));
void refresh();
}, []);
const signedIn = !!info?.user;
useEffect(() => {
if (!signedIn) return;
return connectEvents({
onEvent: applyEvent,
onOpen: () => {
setState(() => ({ connected: true }));
void resync().catch(() => undefined);
},
onClose: () => setState(() => ({ connected: false })),
});
}, [signedIn]);
// Close the mobile menu on navigation.
useEffect(() => setMenuOpen(false), [route.join("/")]);
if (error) return <div className="empty">{error}</div>;
if (!info) return <div className="empty">{t("common.loading")}</div>;
if (!info.user)
return (
<>
<Login info={info} onSuccess={refresh} />
<DialogHost />
</>
);
const [section, arg] = route;
let content;
if (section === "s" && arg) content = <ChatView key={arg} sessionId={arg} onMenu={() => setMenuOpen(true)} />;
else if (section === "settings") content = <Settings tab={arg} onMenu={() => setMenuOpen(true)} />;
else if (section === "stats") content = <Stats onMenu={() => setMenuOpen(true)} />;
else if (section === "images") content = <Images onMenu={() => setMenuOpen(true)} />;
else if (section === "jobs") content = <Jobs onMenu={() => setMenuOpen(true)} />;
else if (section === "terminal")
content = (
<Suspense fallback={<div className="empty"><Spinner /></div>}>
<TerminalPage id={arg} onMenu={() => setMenuOpen(true)} />
</Suspense>
);
else if (section === "files")
content = (
<Suspense fallback={<div className="empty"><Spinner /></div>}>
<Files onMenu={() => setMenuOpen(true)} />
</Suspense>
);
else content = <NewSession onMenu={() => setMenuOpen(true)} onCreated={(id) => navigate(`/s/${id}`)} />;
return (
<div className="app">
{menuOpen && <div className="backdrop" onClick={() => setMenuOpen(false)} />}
<Sidebar open={menuOpen} activeId={section === "s" ? arg : undefined} section={section ?? ""} />
<main className="main">{content}</main>
<DialogHost />
</div>
);
}