import { getErrorMessage } from "@/lib/error-message.js"; import { useState, useEffect, useCallback, useRef } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { LayoutTemplate, Loader2, Plus, Save, Settings2, Star, Trash2, X, } from "lucide-react"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { Badge } from "@/components/badge"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogDescription, } from "@/components/dialog"; import { FOLDER_COLORS } from "@/lib/theme"; import { listWorkspaces, createWorkspace, renameWorkspace, updateWorkspaceContent, deleteWorkspace, duplicateWorkspace, setDefaultWorkspace, unsetDefaultWorkspace, } from "./workspaces-api"; import type { Workspace, WorkspacePayload } from "@/types/ui-types"; function timeAgo(isoStr: string): string { const ms = Date.now() - new Date(isoStr).getTime(); if (!Number.isFinite(ms)) return ""; const sec = Math.floor(ms / 1000); if (sec < 60) return `${sec}s ago`; const min = Math.floor(sec / 60); if (min < 60) return `${min}m ago`; const hr = Math.floor(min / 60); if (hr < 24) return `${hr}h ago`; return `${Math.floor(hr / 24)}d ago`; } function WorkspaceSaveDialog({ open, onClose, onSave, }: { open: boolean; onClose: () => void; onSave: (name: string, color: string) => Promise; }) { const { t } = useTranslation(); const [name, setName] = useState(""); const [color, setColor] = useState(FOLDER_COLORS[0]); const [saving, setSaving] = useState(false); useEffect(() => { if (!open) return; setName(""); setColor(FOLDER_COLORS[0]); }, [open]); async function handleSave() { if (!name.trim()) { toast.error(t("newUi.sidebar.workspaces.nameRequired")); return; } setSaving(true); try { await onSave(name.trim(), color); onClose(); } finally { setSaving(false); } } return ( !next && onClose()}> {t("newUi.sidebar.workspaces.saveCurrentTitle")} {t("newUi.sidebar.workspaces.saveCurrentDescription")}
setName(e.target.value)} placeholder={t("newUi.sidebar.workspaces.namePlaceholder")} autoFocus onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} />
{FOLDER_COLORS.map((c) => (
); } function WorkspaceRenameDialog({ workspace, onClose, onSaved, }: { workspace: Workspace | null; onClose: () => void; onSaved: () => void; }) { const { t } = useTranslation(); const [name, setName] = useState(""); const [color, setColor] = useState(FOLDER_COLORS[0]); const [saving, setSaving] = useState(false); useEffect(() => { if (!workspace) return; setName(workspace.name); setColor(workspace.color ?? FOLDER_COLORS[0]); }, [workspace]); async function handleSave() { if (!workspace) return; if (!name.trim()) { toast.error(t("newUi.sidebar.workspaces.nameRequired")); return; } setSaving(true); try { await renameWorkspace(workspace.id, { name: name.trim(), color }); toast.success(t("newUi.sidebar.workspaces.workspaceUpdated")); onSaved(); onClose(); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.workspaces.updateFailed")); } finally { setSaving(false); } } return ( !next && onClose()}> {t("newUi.sidebar.workspaces.rename")}
setName(e.target.value)} autoFocus onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} />
{FOLDER_COLORS.map((c) => (
); } function WorkspaceRow({ workspace, isLastSession, onApply, onUpdateWithCurrent, onRename, onDuplicate, onDelete, onSetDefault, }: { workspace: Workspace; isLastSession: boolean; onApply: () => void; onUpdateWithCurrent: () => void; onRename: () => void; onDuplicate: () => void; onDelete: () => void; onSetDefault: () => void; }) { const { t } = useTranslation(); return (
{isLastSession ? t("newUi.sidebar.workspaces.lastSession") : workspace.name} {workspace.isDefault && ( )} {t("newUi.sidebar.workspaces.tabCount", { count: workspace.tabCount, })}
{isLastSession ? t("newUi.sidebar.workspaces.lastSessionDescription") : workspace.lastUsedAt ? t("newUi.sidebar.workspaces.lastUsed", { time: timeAgo(workspace.lastUsedAt), }) : t("newUi.sidebar.workspaces.neverUsed")} {!isLastSession && (
)}
); } export function WorkspacesPanel({ active, currentPayload, onApplyWorkspace, }: { active?: boolean; currentPayload: () => WorkspacePayload; onApplyWorkspace: (workspace: Workspace) => void; }) { const { t } = useTranslation(); const [workspaces, setWorkspaces] = useState([]); const [loading, setLoading] = useState(true); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const [renameTarget, setRenameTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [applyTarget, setApplyTarget] = useState(null); const hasLoadedRef = useRef(false); const loadWorkspaces = useCallback(async () => { try { const data = await listWorkspaces(); setWorkspaces(data); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.workspaces.loadFailed")); } }, [t]); useEffect(() => { if (!active || hasLoadedRef.current) return; hasLoadedRef.current = true; setLoading(true); loadWorkspaces().finally(() => setLoading(false)); }, [active, loadWorkspaces]); async function handleSaveNew(name: string, color: string) { try { await createWorkspace({ name, color, payload: currentPayload() }); toast.success(t("newUi.sidebar.workspaces.workspaceCreated")); loadWorkspaces(); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.workspaces.saveFailed")); throw error; } } async function handleUpdateWithCurrent(workspace: Workspace) { try { await updateWorkspaceContent(workspace.id, currentPayload()); toast.success(t("newUi.sidebar.workspaces.workspaceUpdated")); loadWorkspaces(); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.workspaces.updateFailed")); } } async function handleDuplicate(workspace: Workspace) { try { await duplicateWorkspace( workspace.id, t("newUi.sidebar.workspaces.duplicateNameSuffix", { name: workspace.name, }), ); toast.success(t("newUi.sidebar.workspaces.workspaceDuplicated")); loadWorkspaces(); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.workspaces.duplicateFailed")); } } async function handleToggleDefault(workspace: Workspace) { try { if (workspace.isDefault) { await unsetDefaultWorkspace(workspace.id); } else { await setDefaultWorkspace(workspace.id); } loadWorkspaces(); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.workspaces.updateFailed")); } } async function handleDelete() { if (!deleteTarget) return; try { await deleteWorkspace(deleteTarget.id); toast.success(t("newUi.sidebar.workspaces.workspaceDeleted")); setDeleteTarget(null); loadWorkspaces(); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.workspaces.deleteFailed")); } } function handleApplyClick(workspace: Workspace) { setApplyTarget(workspace); } function confirmApply() { if (!applyTarget) return; onApplyWorkspace(applyTarget); setApplyTarget(null); } const manualWorkspaces = workspaces.filter((w) => w.kind === "manual"); const lastSession = workspaces.find((w) => w.kind === "last_session"); return (
{t("newUi.sidebar.workspaces.title")} {t("hosts.docsLink")}
{loading ? (
) : manualWorkspaces.length === 0 && !lastSession ? (
{t("newUi.sidebar.workspaces.noWorkspaces")}
) : (
{lastSession && ( handleApplyClick(lastSession)} onUpdateWithCurrent={() => {}} onRename={() => {}} onDuplicate={() => {}} onDelete={() => {}} onSetDefault={() => {}} /> )} {manualWorkspaces.map((workspace) => ( handleApplyClick(workspace)} onUpdateWithCurrent={() => { if ( window.confirm( t("newUi.sidebar.workspaces.updateWithCurrentConfirm", { name: workspace.name, }), ) ) { handleUpdateWithCurrent(workspace); } }} onRename={() => setRenameTarget(workspace)} onDuplicate={() => handleDuplicate(workspace)} onDelete={() => setDeleteTarget(workspace)} onSetDefault={() => handleToggleDefault(workspace)} /> ))}
)}
setSaveDialogOpen(false)} onSave={handleSaveNew} /> setRenameTarget(null)} onSaved={loadWorkspaces} /> !next && setDeleteTarget(null)} > {t("newUi.sidebar.workspaces.deleteWorkspaceTitle")} {t("newUi.sidebar.workspaces.deleteWorkspaceDescription", { name: deleteTarget?.name, })} !next && setApplyTarget(null)} > {t("newUi.sidebar.workspaces.applyConfirmTitle")} {t("newUi.sidebar.workspaces.applyConfirmDescription", { name: applyTarget?.name, })}
); }