import { html, useEffect, useRef } from '../lib/preact.js'; import { getStatusMeta } from '../utils/status.js'; import { formatDuration, getContextUsageSummary } from '../utils/formatting.js'; import { ChatMessages } from './ChatMessages.js'; import { QuestionBlock } from './QuestionBlock.js'; import { SimpleInput } from './SimpleInput.js'; import { AgentActivityIndicator } from './AgentActivityIndicator.js'; export function SessionCard({ session, onClick, conversation, onFetchConversation, onRespond, onDismiss, enlarged = false, autocompleteConfig = null, isNewlySpawned = false }) { const hasQuestions = session.pending_questions && session.pending_questions.length > 0; const statusMeta = getStatusMeta(session.status); const agent = session.agent === 'codex' ? 'codex' : 'claude'; const agentHeaderClass = agent === 'codex' ? 'agent-header-codex' : 'agent-header-claude'; const contextUsage = getContextUsageSummary(session.context_usage); // Fetch conversation when card mounts useEffect(() => { if (!conversation && onFetchConversation) { onFetchConversation(session.session_id, session.project_dir, agent); } }, [session.session_id, session.project_dir, agent, conversation, onFetchConversation]); const chatPaneRef = useRef(null); const stickyToBottomRef = useRef(true); // Start in "sticky" mode const scrollUpAccumulatorRef = useRef(0); // Track cumulative scroll-up distance const prevConversationLenRef = useRef(0); // Track user intent via wheel events (only fires from actual user scrolling) useEffect(() => { const el = chatPaneRef.current; if (!el) return; const handleWheel = (e) => { // User scrolling up - accumulate distance before disabling sticky if (e.deltaY < 0) { scrollUpAccumulatorRef.current += Math.abs(e.deltaY); // Only disable sticky mode after scrolling up ~50px (meaningful intent) if (scrollUpAccumulatorRef.current > 50) { stickyToBottomRef.current = false; } } // User scrolling down - reset accumulator and check if near bottom if (e.deltaY > 0) { scrollUpAccumulatorRef.current = 0; const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; if (distanceFromBottom < 100) { stickyToBottomRef.current = true; } } }; el.addEventListener('wheel', handleWheel, { passive: true }); return () => el.removeEventListener('wheel', handleWheel); }, []); // Auto-scroll when conversation changes useEffect(() => { const el = chatPaneRef.current; if (!el || !conversation) return; const prevLen = prevConversationLenRef.current; const currLen = conversation.length; const hasNewMessages = currLen > prevLen; const isFirstLoad = prevLen === 0 && currLen > 0; // Check if user just submitted (always scroll for their own messages) const lastMsg = conversation[currLen - 1]; const userJustSubmitted = hasNewMessages && lastMsg?.role === 'user'; prevConversationLenRef.current = currLen; // Auto-scroll if in sticky mode, first load, or user just submitted if (isFirstLoad || userJustSubmitted || (hasNewMessages && stickyToBottomRef.current)) { requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; }); } }, [conversation]); const handleDismissClick = (e) => { e.stopPropagation(); if (onDismiss) onDismiss(session.session_id); }; // Container classes differ based on enlarged mode const spawnClass = isNewlySpawned ? ' session-card-spawned' : ''; const containerClasses = enlarged ? 'glass-panel flex w-full max-w-[90vw] max-h-[90vh] flex-col overflow-hidden rounded-2xl border border-selection/80' : 'glass-panel flex h-[850px] max-h-[850px] w-[600px] cursor-pointer flex-col overflow-hidden rounded-xl border border-selection/70 transition-[border-color,box-shadow] duration-200 hover:border-starting/35 hover:shadow-panel' + spawnClass; return html`
onClick && onClick(session)} >
${session.project || session.name || 'Session'}
${statusMeta.label} ${agent} ${session.project_dir && html` ${session.project_dir.split('/').slice(-2).join('/')} `}
${contextUsage && html`
${contextUsage.headline} ${contextUsage.detail} ${contextUsage.trail && html`${contextUsage.trail}`}
`}
${formatDuration(session.started_at)} ${session.status === 'done' && html` `}
<${ChatMessages} messages=${conversation || []} status=${session.status} limit=${enlarged ? null : 20} />
<${AgentActivityIndicator} session=${session} />
${hasQuestions ? html` <${QuestionBlock} questions=${session.pending_questions} sessionId=${session.session_id} status=${session.status} onRespond=${onRespond} /> ` : html` <${SimpleInput} sessionId=${session.session_id} status=${session.status} onRespond=${onRespond} autocompleteConfig=${autocompleteConfig} /> `}
`; }