"use client"; import { useEffect, useRef, useState } from "react"; import { Archive, ArchiveRestore, MoreHorizontal, Pencil, Trash2, UserRound } from "lucide-react"; import { Button } from "@multica/ui/components/ui/button"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, } from "@multica/ui/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@multica/ui/components/ui/alert-dialog"; import { useWorkspacePaths } from "@multica/core/paths"; import { useUpdateChatSession, useDeleteChatSession, useSetChatSessionArchived, } from "@multica/core/chat/mutations"; import { useChatStore } from "@multica/core/chat"; import type { Agent, ChatSession } from "@multica/core/types"; import { ActorAvatar } from "../../common/actor-avatar"; import { useNavigation } from "../../navigation"; import { useT } from "../../i18n"; /** * Per-session header for the conversation pane: agent avatar + editable chat * title + agent subtitle, with a ⋯ menu (rename / view agent profile / delete). * The avatar's hover card is the lightweight "view profile" affordance; the * menu item navigates to the full agent page. */ export function ChatSessionHeader({ session, agent, onArchive, }: { session: ChatSession; agent: Agent | null; // Archiving the open conversation must move the pane off it (advance to the // next chat on desktop, back to the list on mobile), so the parent owns it — // see ChatPage.handleArchive. Falls back to a plain status flip if unwired. onArchive?: (session: ChatSession) => void; }) { const { t } = useT("chat"); const wsPaths = useWorkspacePaths(); const { push } = useNavigation(); const updateSession = useUpdateChatSession(); const deleteSession = useDeleteChatSession(); const setArchived = useSetChatSessionArchived(); const setActiveSession = useChatStore((s) => s.setActiveSession); const isArchived = session.status === "archived"; const [editing, setEditing] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); const [draft, setDraft] = useState(session.title ?? ""); const inputRef = useRef(null); const title = session.title?.trim() || t(($) => $.window.untitled); useEffect(() => { if (editing) { inputRef.current?.focus(); inputRef.current?.select(); } }, [editing]); const startRename = () => { setDraft(session.title ?? ""); setEditing(true); }; const commitRename = () => { setEditing(false); const trimmed = draft.trim(); if (!trimmed || trimmed === session.title) return; updateSession.mutate({ sessionId: session.id, title: trimmed }); }; const viewProfile = () => { if (agent) push(wsPaths.agentDetail(agent.id)); }; const doDelete = () => { setConfirmDelete(false); setActiveSession(null); deleteSession.mutate(session.id); }; const doArchive = () => onArchive ? onArchive(session) : setArchived.mutate({ sessionId: session.id, archived: true }); const doUnarchive = () => setArchived.mutate({ sessionId: session.id, archived: false }); return (
{agent ? ( ) : ( )}
{editing ? ( $.header.rename)} onChange={(e) => setDraft(e.target.value)} onBlur={commitRename} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); commitRename(); } else if (e.key === "Escape") { e.preventDefault(); setEditing(false); } }} className="w-full rounded-sm bg-background px-1 py-0.5 text-sm font-semibold outline-none ring-1 ring-border focus-visible:ring-brand" /> ) : ( )} {agent && (
{agent.name} {agent.description ? ` · ${agent.description}` : ""}
)}
} > {t(($) => $.header.rename)} {agent && ( {t(($) => $.header.view_profile)} )} {isArchived ? ( <> {t(($) => $.header.unarchive)} {/* Hard delete is offered only once a chat is archived. */} setConfirmDelete(true)}> {t(($) => $.header.delete)} ) : ( {t(($) => $.header.archive)} )} {t(($) => $.session_history.delete_dialog.title)} {title} {t(($) => $.session_history.delete_dialog.cancel)} {t(($) => $.session_history.delete_dialog.confirm)}
); }