import { useId, useMemo, useRef, useState } from 'react' import type { Session } from '@/types/api' import type { ApiClient } from '@/api/client' import { isTelegramApp } from '@/hooks/useTelegram' import { useSessionActions } from '@/hooks/mutations/useSessionActions' import { SessionActionMenu } from '@/components/SessionActionMenu' import { SessionExportDialog } from '@/components/SessionExportDialog' import { RenameSessionDialog } from '@/components/RenameSessionDialog' import { ConfirmDialog } from '@/components/ui/ConfirmDialog' import { formatReopenError } from '@/lib/reopenError' import { formatCodexReasoningLabel, shouldShowCodexReasoningLabel } from '@/lib/codexStatusLabels' import { getSessionModelLabel } from '@/lib/sessionModelLabel' import { useTranslation } from '@/lib/use-translation' import { AgentFlavorIcon } from '@/components/AgentFlavorIcon' import { isFastServiceTier } from '@/components/AssistantChat/codexFastMode' import { getSessionTitle } from '@/lib/sessionTitle' function FilesIcon(props: { className?: string }) { return ( ) } function OutlineIcon(props: { className?: string }) { return ( ) } function headerToggleClass(active: boolean): string { return `flex h-8 w-8 items-center justify-center rounded-full transition-colors ${ active ? 'bg-[var(--app-button)] text-[var(--app-button-text)] hover:opacity-90' : 'text-[var(--app-hint)] hover:bg-[var(--app-secondary-bg)] hover:text-[var(--app-fg)]' }` } function MoreVerticalIcon(props: { className?: string }) { return ( ) } export function SessionHeader(props: { session: Session onBack: () => void onToggleFiles?: () => void filesActive?: boolean onToggleOutline?: () => void outlineActive?: boolean api: ApiClient | null onSessionDeleted?: () => void onSessionReopened?: (newSessionId: string) => void }) { const { t } = useTranslation() const { session, api, onSessionDeleted, onSessionReopened } = props const title = useMemo(() => getSessionTitle(session), [session]) const worktreeBranch = session.metadata?.worktree?.branch const modelLabel = getSessionModelLabel(session) const agentFlavor = session.metadata?.flavor ?? null const reasoningLabel = shouldShowCodexReasoningLabel(agentFlavor) ? formatCodexReasoningLabel(session.modelReasoningEffort) : null // Match expected Fast badge semantics (#1004): only explicit service tier, no effort/model heuristics. const showFastBadge = agentFlavor === 'codex' && isFastServiceTier(session.serviceTier) const [menuOpen, setMenuOpen] = useState(false) const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 }) const menuId = useId() const menuAnchorRef = useRef(null) const [renameOpen, setRenameOpen] = useState(false) const [exportOpen, setExportOpen] = useState(false) const [archiveOpen, setArchiveOpen] = useState(false) const [deleteOpen, setDeleteOpen] = useState(false) const { archiveSession, reopenSession, renameSession, deleteSession, isPending } = useSessionActions( api, session.id, session.metadata?.flavor ?? null ) const [reopenError, setReopenError] = useState(null) const handleDelete = async () => { await deleteSession() onSessionDeleted?.() } const handleReopen = async () => { setReopenError(null) try { const result = await reopenSession() if (result.sessionId && result.sessionId !== session.id) { onSessionReopened?.(result.sessionId) } } catch (error) { setReopenError(formatReopenError(error)) } } const handleMenuToggle = () => { if (!menuOpen && menuAnchorRef.current) { const rect = menuAnchorRef.current.getBoundingClientRect() setMenuAnchorPoint({ x: rect.right, y: rect.bottom }) } setMenuOpen((open) => !open) } // In Telegram, don't render header (Telegram provides its own) if (isTelegramApp()) { return null } return ( <>
{/* Back button */} {/* Session info - two lines: title and path */}
{title}
{session.metadata?.flavor?.trim() || 'unknown'} {modelLabel ? ( {t(modelLabel.key)}: {modelLabel.value} ) : null} {reasoningLabel ? ( {reasoningLabel} ) : null} {showFastBadge ? ( fast ) : null} {worktreeBranch ? ( {t('session.item.worktree')}: {worktreeBranch} ) : null}
{props.onToggleFiles ? ( ) : null} {props.onToggleOutline ? ( ) : null}
setMenuOpen(false)} sessionActive={session.active} onRename={() => setRenameOpen(true)} onExport={() => setExportOpen(true)} onArchive={() => setArchiveOpen(true)} onReopen={handleReopen} onDelete={() => setDeleteOpen(true)} anchorPoint={menuAnchorPoint} menuId={menuId} /> {reopenError ? ( setReopenError(null)} title={t('dialog.reopen.errorTitle')} description={reopenError} confirmLabel={t('dialog.reopen.dismiss')} confirmingLabel={t('dialog.reopen.dismiss')} onConfirm={async () => setReopenError(null)} isPending={false} /> ) : null} setRenameOpen(false)} currentName={title} onRename={renameSession} isPending={isPending} /> setExportOpen(false)} session={session} api={api} /> setArchiveOpen(false)} title={t('dialog.archive.title')} description={t('dialog.archive.description', { name: title })} confirmLabel={t('dialog.archive.confirm')} confirmingLabel={t('dialog.archive.confirming')} onConfirm={archiveSession} isPending={isPending} destructive /> setDeleteOpen(false)} title={t('dialog.delete.title')} description={t('dialog.delete.description', { name: title })} confirmLabel={t('dialog.delete.confirm')} confirmingLabel={t('dialog.delete.confirming')} onConfirm={handleDelete} isPending={isPending} destructive /> ) }