"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 */}
);
});