import { useKeyboard, useTerminalDimensions } from "@opentui/react";
import { useCallback, useMemo, useState } from "react";
import type { LogSession } from "@/lib/logger";
import { formatProjectNameForDisplay } from "@/lib/tui/sessions/session-display";
import { CenteredModal } from "@/lib/tui/shared/CenteredModal";
import { TUI_COLORS } from "@/lib/tui/shared/colors";
import { KeyboardShortcutsModal } from "@/lib/tui/shared/KeyboardShortcutsModal";
import { ShortcutHint } from "@/lib/tui/shared/ShortcutHint";
import { SessionDetailPane } from "./SessionListDetailPane";
import {
buildSessionOverlayOptions,
resolveSessionOverlayKeyAction,
} from "./session-overlay-utils";
import { useSessionOverlayState } from "./use-session-overlay-state";
const NARROW_WIDTH_THRESHOLD = 110;
interface SessionOverlayProps {
onClose: () => void;
}
function sessionLabel(session: LogSession): string {
return session.name.replace(/\.jsonl$/, "");
}
function SessionHelpModal({ onClose }: { onClose: () => void }) {
useKeyboard((key) => {
if (key.name === "escape" || key.name === "q" || key.name === "?" || key.name === "h") {
onClose();
}
});
return ;
}
const SESSION_OVERLAY_SHORTCUTS = [
{ keys: "[Tab ←/→]", label: "Switch pane focus" },
{ keys: "[↑/↓ j/k]", label: "Navigate / Scroll" },
{ keys: "[d]", label: "Delete selected log" },
{ keys: "[h/?]", label: "Toggle help" },
] as const;
interface SessionDeleteModalProps {
sessionName: string;
error: string | null;
isDeleting: boolean;
}
function SessionDeleteModal({ sessionName, error, isDeleting }: SessionDeleteModalProps) {
return (
{sessionName}
This cannot be undone.
[y]
Delete
[n/Esc]
Cancel
{isDeleting && Deleting...}
{error && {error}}
);
}
type OverlayPane = "list" | "detail";
export function SessionOverlay({ onClose }: SessionOverlayProps) {
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
const {
sessions,
selectedPath,
selectedStats,
isLoading,
sessionsError,
statsLoading,
statsError,
isDeleting,
deleteError,
setSelectedPath,
clearDeleteError,
deleteSelectedSession,
} = useSessionOverlayState();
const [showHelp, setShowHelp] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [focusedPane, setFocusedPane] = useState("list");
const cycleFocus = useCallback(() => {
setFocusedPane((prev) => (prev === "list" ? "detail" : "list"));
}, []);
const { selectOptions, sessionSlots } = useMemo(
() => buildSessionOverlayOptions(sessions),
[sessions]
);
const selectedSession = selectedPath
? (sessions.find((s) => s.path === selectedPath) ?? null)
: null;
const sessionTitle = selectedSession
? `${formatProjectNameForDisplay(selectedSession.projectName)} Logs`
: "Logs";
const confirmDeleteSelectedSession = useCallback(async () => {
const result = await deleteSelectedSession();
if (result.deleted) {
setShowDeleteConfirm(false);
}
}, [deleteSelectedSession]);
const closeDeleteConfirm = useCallback(() => {
if (isDeleting) {
return;
}
setShowDeleteConfirm(false);
clearDeleteError();
}, [clearDeleteError, isDeleting]);
const isOverlayBlocked = showHelp || showDeleteConfirm;
const isNarrow = terminalWidth < NARROW_WIDTH_THRESHOLD;
// Overhead: outer padding (2) + panel border (2) + panel padding (2) + status bar (2) = 8.
// In narrow mode the tab strip (1) and its gap (1) add two more rows.
const selectHeight = Math.max(3, terminalHeight - (isNarrow ? 10 : 8));
useKeyboard((key) => {
const action = resolveSessionOverlayKeyAction({
keyName: key.name,
showHelp,
showDeleteConfirm,
hasSelectedSession: Boolean(selectedSession),
isNarrow,
focusedPane,
});
if (action === "close-delete-confirm") {
closeDeleteConfirm();
return;
}
if (action === "confirm-delete") {
void confirmDeleteSelectedSession();
return;
}
if (action === "toggle-help") {
setShowHelp((prev) => !prev);
return;
}
if (action === "close-help") {
setShowHelp(false);
return;
}
if (action === "open-delete-confirm") {
clearDeleteError();
setShowDeleteConfirm(true);
return;
}
if (action === "cycle-focus") {
cycleFocus();
return;
}
if (action === "focus-detail") {
setFocusedPane("detail");
return;
}
if (action === "close-overlay") {
onClose();
return;
}
});
const listBorderColor =
focusedPane === "list" ? TUI_COLORS.ui.borderFocused : TUI_COLORS.ui.border;
const detailBorderColor =
focusedPane === "detail" ? TUI_COLORS.ui.borderFocused : TUI_COLORS.ui.border;
const listPanel = (
{isLoading ? (
Loading...
) : sessionsError ? (
{sessionsError}
) : selectOptions.length === 0 ? (
No sessions found
) : (
);
const detailPanel = (
{statsLoading ? (
Loading...
) : statsError ? (
{statsError}
) : !selectedStats ? (
Select a session to view details
) : (
)}
);
return (
{isNarrow ? (
{focusedPane === "list" ? "▸ Logs" : " Logs"}
{focusedPane === "detail" ? "▸ Detail" : " Detail"}
{focusedPane === "list" ? listPanel : detailPanel}
) : (
{listPanel}
{detailPanel}
)}
{isNarrow && }
Focus: {focusedPane === "list" ? "List" : "Detail"}
{showHelp && setShowHelp(false)} />}
{showDeleteConfirm && (
)}
);
}