import { useCallback, useEffect } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { PanelRight, MessageSquare, Terminal, Bot } from 'lucide-react'; import type { Chat, WorkspacePane } from '@/api/types'; import { useWorkspacePanes, MAX_PANES } from '@/hooks/useWorkspacePanes'; import { useSessionChats } from '@/hooks/useSessionChats'; import { useViewport } from '@/hooks/useViewport'; import { ChatPane } from '@/components/panes/ChatPane'; import { ChatTabBar } from '@/components/ChatTabBar'; import { SessionLandingPage } from '@/components/SessionLandingPage'; import { SwipeablePaneTab } from '@/components/SwipeablePaneTab'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { cn } from '@/lib/utils'; interface Props { sessionId: string; projectId: string; } export function Workspace({ sessionId, projectId }: Props) { const { panes, activePaneIdx, setActivePaneIdx, activePaneIdxRef, openChatInPane, switchTab, removeTab, closeOtherTabs, closeTabsToRight, closeAllTabs, showLandingPage, addSplitPane, removePane, removeChatFromPanes, initializeFirstChatIfEmpty, handlePaneDragStart, handlePaneDragOver, handlePaneDragLeave, handlePaneDrop, handlePaneDragEnd, dragOverIdx, draggingIdxRef, } = useWorkspacePanes(sessionId); // Thin wrapper so useSessionChats can route open_chat_in_active_pane events // without knowing about pane indexing. const openChatInActivePane = useCallback( (chatId: string) => openChatInPane(activePaneIdxRef.current, chatId), [openChatInPane, activePaneIdxRef], ); const { chats, createChat, archiveChat, unarchiveChat, deleteChat, renameChat, handleLandingSend, } = useSessionChats(sessionId, { removeChatFromPanes, openChatInPane, openChatInActivePane, initializeFirstChatIfEmpty, }); const { isMobile } = useViewport(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); const location = useLocation(); // URL -> state (mobile only). Handles deep-link arrival and Back button // history pops. On a bare URL (no ?pane), reset to first pane so Back // from a ?pane URL returns the user to a sensible default. useEffect(() => { if (!isMobile || panes.length === 0) return; const paneId = searchParams.get('pane'); if (!paneId) { if (activePaneIdx !== 0) setActivePaneIdx(0); return; } const idx = panes.findIndex((p) => p.id === paneId); if (idx >= 0 && idx !== activePaneIdx) setActivePaneIdx(idx); }, [isMobile, searchParams, panes, activePaneIdx, setActivePaneIdx]); // Switch active pane and push URL (mobile only). User-initiated only; // never called from URL-sync effect. const switchActivePane = useCallback( (idx: number) => { setActivePaneIdx(idx); if (isMobile) { const pane = panes[idx]; if (!pane) return; const params = new URLSearchParams(location.search); params.set('pane', pane.id); navigate(`${location.pathname}?${params.toString()}`); } }, [setActivePaneIdx, isMobile, panes, navigate, location.pathname, location.search], ); function chatsForPane(pane: WorkspacePane): Chat[] { return pane.chatIds .map((id) => chats.find((c) => c.id === id)) .filter((c): c is Chat => c !== undefined); } function paneLabel(pane: WorkspacePane): string { const activeChatId = pane.chatId; if (activeChatId) { const chat = chats.find((c) => c.id === activeChatId); if (chat) return chat.name ?? 'New chat'; } if (pane.kind === 'chat') return 'Chat'; if (pane.kind === 'terminal') return 'Terminal'; if (pane.kind === 'agent') return 'Agent'; return 'Empty'; } return (
addSplitPane('chat')}> Chat addSplitPane('terminal')}> Terminal addSplitPane('agent')}> Agent
{isMobile && panes.length > 1 && (
{panes.map((pane, idx) => ( switchActivePane(idx)} onClose={() => removePane(idx)} canClose={panes.length > 1} /> ))}
)}
{panes.map((pane, idx) => { const visible = !isMobile || idx === activePaneIdx; if (!visible) return null; return (
setActivePaneIdx(idx)} onDragOver={!isMobile && panes.length > 1 ? handlePaneDragOver(idx) : undefined} onDragLeave={!isMobile && panes.length > 1 ? handlePaneDragLeave : undefined} onDrop={!isMobile && panes.length > 1 ? handlePaneDrop(idx) : undefined} >
1} onDragStart={!isMobile && panes.length > 1 ? handlePaneDragStart(idx) : undefined} onDragEnd={!isMobile && panes.length > 1 ? handlePaneDragEnd : undefined} > switchTab(idx, tabIdx)} onRemoveTab={(chatId) => removeTab(idx, chatId)} onCloseOthers={(chatId) => closeOtherTabs(idx, chatId)} onCloseToRight={(chatId) => closeTabsToRight(idx, chatId)} onCloseAll={() => closeAllTabs(idx)} onNewChat={() => void createChat(idx)} onShowHistory={() => showLandingPage(idx)} onRename={renameChat} onRemovePane={panes.length > 1 ? () => removePane(idx) : undefined} />
{pane.kind === 'chat' && pane.chatId ? ( ) : ( openChatInPane(idx, chatId)} onSend={(content) => void handleLandingSend(idx, content)} onReopenChat={async (chatId) => { await unarchiveChat(chatId); openChatInPane(idx, chatId); }} onArchiveChat={archiveChat} onRenameChat={renameChat} onDeleteChat={deleteChat} /> )}
); })}
); }