import { Icon } from "./Icon"; import { StatusLeds } from "./StatusLeds"; import { cycleThemeMode, getThemeMode, onThemeChange } from "../theme"; import { useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { get, patch, post } from "../api"; import { promptDialog, toast } from "../dialogs"; import { fmtRelative, useT, type Key } from "../i18n"; import { navigate, openBeside } from "../router"; import { useStore } from "../store"; import type { Session } from "../types"; function basename(p: string) { return p.split("/").filter(Boolean).pop() ?? p; } function groupKey(ms: number): Key { const day = 86400000; const startOfToday = new Date().setHours(0, 0, 0, 0); if (ms >= startOfToday) return "sidebar.today"; if (ms >= startOfToday - day) return "sidebar.yesterday"; if (ms >= startOfToday - 7 * day) return "sidebar.week"; return "sidebar.older"; } const GROUP_BY = "codecraft.sidebarGroupBy"; const COLLAPSED = "codecraft.sidebarCollapsed"; const organise = (id: string, body: Partial> & { folder?: string }) => patch(`/sessions/${id}`, body).catch((e) => toast(e.message)); /** One session in the list, with a menu to pin, file or archive it. */ function SessionRow({ s, active, attention, busy, folders }: { s: Session; active: boolean; attention: boolean; busy: boolean; folders: string[] }) { const t = useT(); const [menu, setMenu] = useState(false); useEffect(() => { if (!menu) return; const close = () => setMenu(false); window.addEventListener("click", close); window.addEventListener("keydown", close); return () => { window.removeEventListener("click", close); window.removeEventListener("keydown", close); }; }, [menu]); const moveTo = async () => { const name = await promptDialog({ title: t("sidebar.moveTitle"), message: folders.length ? t("sidebar.moveHintExisting", { folders: folders.join(", ") }) : t("sidebar.moveHint"), defaultValue: s.folder ?? "", placeholder: t("sidebar.folderName"), confirmLabel: t("sidebar.move"), }); if (name !== null) void organise(s.id, { folder: name }); }; return (
{menu && (
e.stopPropagation()}> {!s.archived && ( )} {s.folder && ( )}
)}
); } export function Sidebar({ open, activeIds, section }: { open: boolean; activeIds: string[]; section: string }) { const t = useT(); const sessions = useStore((s) => s.sessions); const busy = useStore((s) => s.busy); const permissions = useStore((s) => s.permissions); const questions = useStore((s) => s.questions); const me = useStore((s) => s.me); const update = useStore((s) => s.update); // Full-text search over titles and messages. const [query, setQuery] = useState(""); const [hits, setHits] = useState<{ id: string; title: string; cwd: string; agent: string; updated_at: number; snippet: string }[]>(); useEffect(() => { const q = query.trim(); if (q.length < 2) { setHits(undefined); return; } let alive = true; const timer = setTimeout(() => { get(`/sessions/search?q=${encodeURIComponent(q)}`) .then((h) => alive && setHits(h ?? [])) .catch(() => alive && setHits([])); }, 250); return () => { alive = false; clearTimeout(timer); }; }, [query]); const attention = useMemo(() => { const ids = new Set(); for (const p of permissions) ids.add(p.root_session_id ?? p.session_id); for (const q of questions) ids.add(q.root_session_id ?? q.session_id); return ids; }, [permissions, questions]); const [groupBy, setGroupBy] = useState<"time" | "project">(() => (localStorage.getItem(GROUP_BY) === "project" ? "project" : "time")); const [collapsed, setCollapsed] = useState>(() => new Set(JSON.parse(localStorage.getItem(COLLAPSED) ?? '["archive"]'))); const toggle = (id: string) => setCollapsed((c) => { const n = new Set(c); if (n.has(id)) n.delete(id); else n.add(id); localStorage.setItem(COLLAPSED, JSON.stringify([...n])); return n; }); const switchGroupBy = () => { const next = groupBy === "time" ? "project" : "time"; localStorage.setItem(GROUP_BY, next); setGroupBy(next); }; const groups = useMemo(() => { const out: { id: string; label: string; icon?: string; items: Session[] }[] = []; const add = (id: string, label: string, s: Session, icon?: string) => { const g = out.find((x) => x.id === id); if (g) g.items.push(s); else out.push({ id, label, icon, items: [s] }); }; const live = sessions.filter((s) => !s.archived); for (const s of live.filter((s) => s.pinned)) add("pinned", t("sidebar.pinned"), s, "pin"); const rest = live.filter((s) => !s.pinned); const folders = [...new Set(rest.map((s) => s.folder).filter(Boolean) as string[])].sort((a, b) => a.localeCompare(b)); for (const f of folders) for (const s of rest.filter((s) => s.folder === f)) add(`folder:${f}`, f, s, "folder"); const loose = rest.filter((s) => !s.folder); if (groupBy === "project") { const byProject = new Map(); for (const s of loose) byProject.set(s.cwd, [...(byProject.get(s.cwd) ?? []), s]); // Projects with the most recent activity first. for (const [cwd, items] of [...byProject].sort((a, b) => b[1][0].updated_at - a[1][0].updated_at)) for (const s of items) add(`project:${cwd}`, basename(cwd) || "/", s, "folders"); } else { for (const s of loose) { const key = groupKey(s.updated_at); add(key, t(key), s); } } for (const s of sessions.filter((s) => s.archived)) add("archive", t("sidebar.archive"), s, "archive"); return out; }, [sessions, groupBy, t]); const folderNames = useMemo(() => [...new Set(sessions.map((s) => s.folder).filter(Boolean) as string[])].sort(), [sessions]); return ( ); } /** Quick light/dark/auto switcher. */ function ThemeModeButton() { const t = useT(); const mode = useSyncExternalStore(onThemeChange, getThemeMode); const icon = ; const label = t(mode === "auto" ? "settings.themeAuto" : mode === "light" ? "settings.themeLight" : "settings.themeDark"); return ( ); }