"use client"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { SessionMobileTopBar } from "./session-mobile-top-bar"; import { SessionMobileBottomNav } from "./session-mobile-bottom-nav"; import { SessionTaskSwitcherSheet } from "./session-task-switcher-sheet"; import { MobileFileViewerPanel } from "./mobile-file-viewer-panel"; import { TaskChatPanel } from "../task-chat-panel"; import { TaskPlanPanel } from "../task-plan-panel"; import { MobileChangesPanel } from "./mobile-changes-panel"; import { ReviewDialog } from "@/components/review/review-dialog"; import { useReviewDialog } from "../use-review-dialog"; import { TaskFilesPanel } from "../task-files-panel"; import { PassthroughToolbar } from "../passthrough-toolbar"; import { MobileTerminalKeybar, KEYBAR_HEIGHT_PX } from "./mobile-terminal-keybar"; import { MobileTerminalPane } from "./mobile-terminal-pane"; import { MobileSessionsPicker } from "./mobile-sessions-section"; import { SessionPanelContent } from "@kandev/ui/pannel-session"; import { useSessionLayoutState } from "@/hooks/use-session-layout-state"; import { useVisualViewportOffset } from "@/hooks/use-visual-viewport-offset"; import { useToast } from "@/components/toast-provider"; import { fetchAndOpenFile } from "../file-browser-hooks"; import type { MobileSessionPanel } from "@/lib/state/slices/ui/types"; import type { OpenFileTab } from "@/lib/types/backend"; const TOP_NAV_HEIGHT = "3.5rem"; const BOTTOM_NAV_HEIGHT = "3.25rem"; type SessionMobileLayoutProps = { workspaceId: string | null; workflowId: string | null; sessionId?: string | null; baseBranch?: string; worktreeBranch?: string | null; taskTitle?: string; isRemoteExecutor?: boolean; remoteExecutorType?: string | null; remoteExecutorName?: string | null; remoteState?: string | null; remoteCreatedAt?: string | null; remoteCheckedAt?: string | null; remoteStatusError?: string | null; }; function MobileChatPanelContent({ activeTaskId, isPassthroughMode, effectiveSessionId, onOpenFile, }: { activeTaskId: string | null; isPassthroughMode: boolean; effectiveSessionId: string | null; onOpenFile: (path: string) => void; }) { if (!activeTaskId) { return (
No task selected
); } return (
{isPassthroughMode ? (
) : ( )}
); } type MobilePanelAreaProps = { currentMobilePanel: MobileSessionPanel; activeTaskId: string | null; isPassthroughMode: boolean; effectiveSessionId: string | null; selectedFile: OpenFileTab | null; selectedDiff: { path: string; content?: string } | null; handleOpenFileFromChat: (path: string) => void; handleClearSelectedDiff: () => void; handleOpenFile: (file: OpenFileTab) => void; handlePanelChangeAndClearSheet: (panel: MobileSessionPanel) => void; topNavHeight: string; bottomNavHeight: string; }; function MobilePanelArea({ currentMobilePanel, activeTaskId, isPassthroughMode, effectiveSessionId, selectedFile, selectedDiff, handleOpenFileFromChat, handleClearSelectedDiff, handleOpenFile, handlePanelChangeAndClearSheet, topNavHeight, bottomNavHeight, }: MobilePanelAreaProps) { const { keyboardOpen, bottomOffset } = useVisualViewportOffset(); // Keep terminal content's visible bottom glued to the keybar top. When the // keyboard is up, the content area already pads for the bottom nav (which // is now under the keyboard), so we subtract it back out and add the // keyboard height instead. const terminalPaddingBottom = keyboardOpen ? `calc(${bottomOffset + KEYBAR_HEIGHT_PX}px - ${bottomNavHeight} - env(safe-area-inset-bottom, 0px))` : `${KEYBAR_HEIGHT_PX}px`; return (
{currentMobilePanel === "chat" && (
)} {currentMobilePanel === "plan" && (
)} {currentMobilePanel === "changes" && (
)} {currentMobilePanel === "files" && (
{selectedFile ? ( handlePanelChangeAndClearSheet("files")} /> ) : ( )}
)} {currentMobilePanel === "terminal" && (
)}
); } type MobileTopBarStickyProps = { activeTaskId: string | null; workspaceId: string | null; taskTitle?: string; effectiveSessionId: string | null; baseBranch?: string; worktreeBranch?: string | null; onMenuClick: () => void; showApproveButton: boolean; onApprove: () => void; isRemoteExecutor?: boolean; remoteExecutorType?: string | null; remoteExecutorName?: string | null; remoteState?: string | null; remoteCreatedAt?: string | null; remoteCheckedAt?: string | null; remoteStatusError?: string | null; }; function MobileTopBarSticky(props: MobileTopBarStickyProps) { return (
); } function MobileReviewDialogMount({ sessionId, review, }: { sessionId: string | null; review: ReturnType; }) { if (!sessionId) return null; return ( ); } export function useMobilePanelHandlers({ effectiveSessionId, handlePanelChange, }: { effectiveSessionId: string | null; handlePanelChange: (panel: MobileSessionPanel) => void; }) { const { toast } = useToast(); const [selectedFile, setSelectedFile] = useState(null); const [trackedSessionId, setTrackedSessionId] = useState(effectiveSessionId); const latestRequestIdRef = useRef(0); // Reset viewer when switching sessions — adjust state during render per // https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes if (trackedSessionId !== effectiveSessionId) { setTrackedSessionId(effectiveSessionId); setSelectedFile(null); } useEffect(() => { latestRequestIdRef.current += 1; }, [effectiveSessionId]); const handleOpenFileFromChat = useCallback( (path: string) => { if (!effectiveSessionId) return; const requestId = (latestRequestIdRef.current += 1); void fetchAndOpenFile( effectiveSessionId, path, (file) => { if (requestId !== latestRequestIdRef.current) return; setSelectedFile(file); handlePanelChange("files"); }, toast, ); }, [effectiveSessionId, handlePanelChange, toast], ); const handleOpenFile = useCallback( (file: OpenFileTab) => { latestRequestIdRef.current += 1; setSelectedFile(file); handlePanelChange("files"); }, [handlePanelChange], ); const handlePanelChangeAndClearSheet = useCallback( (panel: MobileSessionPanel) => { latestRequestIdRef.current += 1; setSelectedFile(null); handlePanelChange(panel); }, [handlePanelChange], ); return { selectedFile, handleOpenFileFromChat, handleOpenFile, handlePanelChangeAndClearSheet, }; } export const SessionMobileLayout = memo(function SessionMobileLayout({ workspaceId, workflowId, sessionId, baseBranch, worktreeBranch, taskTitle, isRemoteExecutor, remoteExecutorType, remoteExecutorName, remoteState, remoteCreatedAt, remoteCheckedAt, remoteStatusError, }: SessionMobileLayoutProps) { const { activeTaskId, effectiveSessionId, isPassthroughMode, selectedDiff, handleClearSelectedDiff, totalChangesCount, hasUnseenPlanUpdate, showApproveButton, handleApprove, currentMobilePanel, handlePanelChange, isTaskSwitcherOpen, handleMenuClick, setMobileSessionTaskSwitcherOpen, } = useSessionLayoutState({ sessionId }); const { selectedFile, handleOpenFileFromChat, handleOpenFile, handlePanelChangeAndClearSheet } = useMobilePanelHandlers({ effectiveSessionId, handlePanelChange }); const review = useReviewDialog(effectiveSessionId); return (
{/* Fixed Bottom Navigation */} {/* Task Switcher Sheet */}
); });