import { useCallback, useEffect, useState } from 'react'; import { Archive, Code, MessageSquare, RotateCcw, Terminal, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { ChatInput } from '@/components/ChatInput'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { api } from '@/api/client'; import type { Chat } from '@/api/types'; import { formatRelative } from '@/lib/format'; interface Props { projectId: string; sessionId: string; agentId?: string | null; onAgentChange?: (agentId: string | null) => void | Promise; onSend: (content: string) => void; // Slash-command (skill) send from the landing page. The parent creates the // chat, assigns it to the pane (so it transitions to ChatPane), and invokes // the skill — same transition the text send uses. See useSessionChats. onSkillInvoke: (skillName: string, userMessage: string | null) => void; createChat: () => Promise<{ id: string }>; // Session history: the session's open chats (live), and callbacks to open one // in THIS pane / restore an archived one. Archived chats are fetched here // (the default open-only list excludes them). chats: Chat[]; onOpenChat: (chatId: string) => void; onUnarchiveChat: (chatId: string) => Promise; onArchiveChat: (chatId: string) => Promise; onDeleteChat: (chatId: string) => Promise; } function byRecent(a: Chat, b: Chat): number { return (b.updated_at ?? '').localeCompare(a.updated_at ?? ''); } // Pick the row icon by the chat's seed name: coder and terminal panes create // placeholder chats named 'BooCoder' / 'Terminal' (see useWorkspacePanes // chatNameForPaneKind + the coder chat-resolve). A name heuristic keeps this // frontend-only — matches ProjectSidebar's isCoderSessionName approach. function iconForChat(name: string | null) { if (name === 'BooCoder') return Code; if (name === 'Terminal') return Terminal; return MessageSquare; } export function SessionLandingPage({ projectId, sessionId, agentId, onAgentChange, onSend, onSkillInvoke, createChat, chats, onOpenChat, onUnarchiveChat, onArchiveChat, onDeleteChat, }: Props) { const [chatId, setChatId] = useState(null); const [archived, setArchived] = useState([]); // Plain Cancel/Confirm delete (no type-to-confirm), mirroring ProjectSidebar. const [deleteConfirm, setDeleteConfirm] = useState<{ id: string; name: string | null } | null>(null); // Archived chats aren't in the default (open-only) list, so fetch them. One // shot on session change — the history view is transient (pick a chat and // it's gone), so slight staleness is fine; reopening the pane refetches. useEffect(() => { let cancelled = false; api.chats .listForSession(sessionId, { status: 'archived' }) .then((list) => { if (!cancelled) setArchived(list); }) .catch(() => {}); return () => { cancelled = true; }; }, [sessionId]); const ensureChat = useCallback(async (): Promise => { if (chatId) return chatId; try { const chat = await createChat(); setChatId(chat.id); return chat.id; } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to create chat'); throw err; } }, [chatId, createChat]); const handleSend = useCallback(async (content: string) => { const text = content.trim(); if (!text) return; try { await ensureChat(); onSend(text); } catch { // Error already surfaced via toast. } }, [ensureChat, onSend]); // Route to the parent, which creates the chat, assigns it to the pane (so the // pane transitions to ChatPane and subscribes to the stream), then invokes the // skill — mirroring the text-send transition. Doing the skill invoke locally // (without the pane assignment) left the landing pane stuck/blank. const handleSlashCommand = useCallback((skillName: string, userMessage: string) => { onSkillInvoke(skillName, userMessage.length > 0 ? userMessage : null); }, [onSkillInvoke]); const restoreAndOpen = useCallback(async (id: string) => { try { await onUnarchiveChat(id); onOpenChat(id); } catch { // onUnarchiveChat surfaces its own toast. } }, [onUnarchiveChat, onOpenChat]); const openChats = [...chats.filter((c) => c.status === 'open')].sort(byRecent); const openIds = new Set(openChats.map((c) => c.id)); const archivedChats = archived.filter((c) => !openIds.has(c.id)).sort(byRecent); const isEmpty = openChats.length === 0 && archivedChats.length === 0; return (
{isEmpty ? (

No conversations yet. Send a message to start.

) : ( <> {openChats.length > 0 && ( <>

Conversations

{openChats.map((c) => { const Icon = iconForChat(c.name); return (
); })}
)} {archivedChats.length > 0 && ( <>

Archived

{archivedChats.map((c) => (
))}
)} )}
{ if (!open) setDeleteConfirm(null); }} > Delete chat? Permanently deletes "{deleteConfirm?.name ?? 'New chat'}" and all its messages. This cannot be undone.
); }