import { Dialog } from "@astryxdesign/core/Dialog"; import { DropdownMenu } from "@astryxdesign/core/DropdownMenu"; import type { DropdownMenuOption } from "@astryxdesign/core/DropdownMenu"; import { Tooltip } from "@astryxdesign/core/Tooltip"; import { Archive, ArchiveRestore, ChevronsLeft, MoreHorizontal, Plus, Search, SquarePen, Trash2, X, } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import type { WebSnapshot } from "../../../../protocol/types.ts"; import { OpenPiLogo } from "../../components/OpenPiLogo.tsx"; import { relativeTime, sessionTitle } from "../../lib/format.ts"; import type { WebStoreActions } from "../../store/web-store.ts"; interface SessionSidebarProps { snapshot: WebSnapshot | null; selectedPath: string | null; selectedWorkspace: string | null; collapsed: Set; query: string; searchOpen: boolean; mobileOpen: boolean; actions: WebStoreActions; } type EditTarget = { kind: "workspace" | "session"; path: string; name: string }; type DeleteTarget = { path: string; name: string }; function ActionMenu({ items, label, }: { items: DropdownMenuOption[]; label: string; }) { return ( , isIconOnly: true, size: "sm", variant: "ghost", className: "menu-action-button", }} items={items} menuWidth={190} placement="below" alignment="end" hasChevron={false} /> ); } export function SessionSidebar(props: SessionSidebarProps) { const { t } = useTranslation(); const [editTarget, setEditTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [draft, setDraft] = useState(""); const searchInput = useRef(null); const editInput = useRef(null); const snapshot = props.snapshot; const [archived, setArchived] = useState(false); const [archiveCollapsed, setArchiveCollapsed] = useState>( new Set(), ); const [restoring, setRestoring] = useState>(new Set()); const restoreInFlight = useRef(new Set()); const [restoreError, setRestoreError] = useState(false); const restore = async (path: string) => { if (restoreInFlight.current.has(path)) return; restoreInFlight.current.add(path); setRestoring(new Set(restoreInFlight.current)); setRestoreError(false); try { if (!(await props.actions.unarchiveSession(path))) setRestoreError(true); } finally { restoreInFlight.current.delete(path); setRestoring(new Set(restoreInFlight.current)); } }; useEffect(() => { if (props.searchOpen) searchInput.current?.focus(); }, [props.searchOpen]); useEffect(() => { if (!editTarget) return; editInput.current?.focus(); editInput.current?.select(); }, [editTarget]); const grouped = useMemo(() => { const query = props.query.trim().toLowerCase(); const visible = (workspacePath?: string) => (snapshot?.sessions ?? []).filter( (session) => Boolean(session.archived) === archived && (workspacePath === "__ungrouped__" ? session.ungrouped : session.cwd === workspacePath && !session.ungrouped) && (!query || `${sessionTitle(session, t("untitledSession"))} ${session.cwd}` .toLowerCase() .includes(query)), ); const workspaces = [...(snapshot?.workspaces ?? [])]; for (const session of snapshot?.sessions ?? []) { if ( !session.ungrouped && !workspaces.some((workspace) => workspace.path === session.cwd) ) { workspaces.push({ path: session.cwd, name: session.cwd, current: false, }); } } return [ ...workspaces.map((workspace) => ({ ...workspace, sessions: visible(workspace.path), ungrouped: false, })), { path: "__ungrouped__", name: t("ungrouped"), current: false, sessions: visible("__ungrouped__"), ungrouped: true, }, ].filter( (group) => group.sessions.length > 0 || (!archived && !group.ungrouped && !query), ); }, [archived, props.query, snapshot, t]); const openEdit = (target: EditTarget) => { setDraft(target.name); setEditTarget(target); }; const saveEdit = async () => { const name = draft.trim(); if (!editTarget || !name) return; if (editTarget.kind === "workspace") await props.actions.renameWorkspace(editTarget.path, name); else await props.actions.renameSession(editTarget.path, name); setEditTarget(null); }; return ( ); }