'use client'; import { useEffect, useMemo, useState } from 'react'; import { TaskTopBar } from '@/components/task/task-top-bar'; import { TaskLayout } from '@/components/task/task-layout'; import { DebugOverlay } from '@/components/debug-overlay'; import type { Repository, RepositoryScript, Task } from '@/lib/types/http'; import type { Terminal } from '@/hooks/domains/session/use-terminals'; import type { KanbanState } from '@/lib/state/slices'; import { DEBUG_UI } from '@/lib/config'; import { TooltipProvider } from '@kandev/ui/tooltip'; import { useRepositories } from '@/hooks/domains/workspace/use-repositories'; import { useSessionAgent } from '@/hooks/domains/session/use-session-agent'; import { useSessionResumption } from '@/hooks/domains/session/use-session-resumption'; import { useSessionAgentctl } from '@/hooks/domains/session/use-session-agentctl'; import { useAppStore } from '@/components/state-provider'; import { fetchTask } from '@/lib/api'; import { useTasks } from '@/hooks/use-tasks'; import { useResponsiveBreakpoint } from '@/hooks/use-responsive-breakpoint'; import type { Layout } from 'react-resizable-panels'; type TaskPageContentProps = { task: Task | null; sessionId?: string | null; initialRepositories?: Repository[]; initialScripts?: RepositoryScript[]; initialTerminals?: Terminal[]; defaultLayouts?: Record; }; /** * TaskPageContent is the main content component for the task/session page. * It handles task state, agent management, and renders the task layout. * * This is a shared component used by the session page (/s/[sessionId]). */ export function TaskPageContent({ task: initialTask, sessionId = null, initialRepositories = [], initialScripts = [], initialTerminals, defaultLayouts = {}, }: TaskPageContentProps) { const [taskDetails, setTaskDetails] = useState(initialTask); const [isMounted, setIsMounted] = useState(false); const [showDebugOverlay, setShowDebugOverlay] = useState(false); const { isMobile } = useResponsiveBreakpoint(); const activeTaskId = useAppStore((state) => state.tasks.activeTaskId); const activeSessionId = useAppStore((state) => state.tasks.activeSessionId); const setActiveSession = useAppStore((state) => state.setActiveSession); const setActiveTask = useAppStore((state) => state.setActiveTask); const effectiveTaskId = activeTaskId ?? initialTask?.id ?? null; const kanbanTask = useAppStore((state) => effectiveTaskId ? state.kanban.tasks.find((item: KanbanState['tasks'][number]) => item.id === effectiveTaskId) ?? null : null ); const task = useMemo(() => { // Only use taskDetails if it matches the current task ID (avoid mixing old/new data) const matchingTaskDetails = taskDetails?.id === effectiveTaskId ? taskDetails : null; const matchingInitialTask = initialTask?.id === effectiveTaskId ? initialTask : null; const baseTask = matchingTaskDetails ?? matchingInitialTask; if (!baseTask && !kanbanTask) return null; // If we have full task details, merge with kanban updates if (baseTask) { if (!kanbanTask) return baseTask; return { ...baseTask, title: kanbanTask.title ?? baseTask.title, description: kanbanTask.description ?? baseTask.description, workflow_step_id: (kanbanTask.workflowStepId as string | undefined) ?? baseTask.workflow_step_id, position: kanbanTask.position ?? baseTask.position, state: (kanbanTask.state as Task['state'] | undefined) ?? baseTask.state, repositories: baseTask.repositories, }; } // Fallback: construct minimal task from kanban data while full details load // This allows immediate UI response when switching tasks if (kanbanTask) { // Reuse workspace/board from previous task context (same board) const prevWorkspaceId = taskDetails?.workspace_id ?? initialTask?.workspace_id; const prevBoardId = taskDetails?.board_id ?? initialTask?.board_id; return { id: kanbanTask.id, title: kanbanTask.title, description: kanbanTask.description ?? '', workflow_step_id: kanbanTask.workflowStepId, position: kanbanTask.position, state: kanbanTask.state ?? 'CREATED', workspace_id: prevWorkspaceId ?? '', board_id: prevBoardId ?? '', priority: 0, repositories: [], created_at: '', updated_at: kanbanTask.updatedAt ?? '', } as Task; } return null; }, [taskDetails, initialTask, kanbanTask, effectiveTaskId]); useTasks(task?.board_id ?? null); const connectionStatus = useAppStore((state) => state.connection.status); // Regular task agent hook for non-resumed sessions const { isAgentRunning, isAgentLoading, worktreePath: agentWorktreePath, worktreeBranch: agentWorktreeBranch, taskSessionId, taskSessionState: agentSessionState, handleStartAgent, handleStopAgent, } = useSessionAgent(task); const initialSessionId = sessionId ?? taskSessionId ?? null; const effectiveSessionId = activeSessionId ?? initialSessionId; const storeSessionState = useAppStore((state) => effectiveSessionId ? state.taskSessions.items[effectiveSessionId]?.state ?? null : null ); const agentctlStatus = useSessionAgentctl(effectiveSessionId); const previewOpen = useAppStore((state) => effectiveSessionId ? state.previewPanel.openBySessionId[effectiveSessionId] ?? false : false ); const previewStage = useAppStore((state) => effectiveSessionId ? state.previewPanel.stageBySessionId[effectiveSessionId] ?? 'closed' : 'closed' ); const previewUrl = useAppStore((state) => effectiveSessionId ? state.previewPanel.urlBySessionId[effectiveSessionId] ?? '' : '' ); const devProcessId = useAppStore((state) => effectiveSessionId ? state.processes.devProcessBySessionId[effectiveSessionId] : undefined ); const devProcessStatus = useAppStore((state) => devProcessId ? state.processes.processesById[devProcessId]?.status ?? null : null ); // Session resumption hook - handles auto-resume on page reload const { resumptionState, error: resumptionError, worktreePath: resumedWorktreePath, worktreeBranch: resumedWorktreeBranch, } = useSessionResumption(task?.id ?? null, effectiveSessionId); // Merge state from resumption and regular agent hooks const isResuming = resumptionState === 'checking' || resumptionState === 'resuming'; const isResumed = resumptionState === 'resumed' || resumptionState === 'running'; // Both hooks read taskSessionState from the store, so prefer resumedSessionState as it's always // updated when we have a sessionId from URL. For worktree, use resumed values when resuming/resumed. const taskSessionState = storeSessionState ?? agentSessionState; const worktreePath = effectiveSessionId ? (resumedWorktreePath ?? agentWorktreePath) : agentWorktreePath; const worktreeBranch = effectiveSessionId ? (resumedWorktreeBranch ?? agentWorktreeBranch) : agentWorktreeBranch; // Derive working state for debug overlay const isAgentWorking = taskSessionState !== null ? taskSessionState === 'STARTING' || taskSessionState === 'RUNNING' : isAgentRunning && (task?.state === 'IN_PROGRESS' || task?.state === 'SCHEDULING'); // Messages are loaded inside TaskChatPanel for the active session. useEffect(() => { queueMicrotask(() => setIsMounted(true)); }, []); useEffect(() => { if (!initialTask?.id) return; // Sync SSR data to store on mount or when SSR props change. // Don't include activeTaskId in deps - we only want to sync FROM SSR, not react to store changes. if (initialSessionId) { setActiveSession(initialTask.id, initialSessionId); } else { setActiveTask(initialTask.id); } }, [initialTask?.id, initialSessionId, setActiveSession, setActiveTask]); useEffect(() => { if (!activeTaskId) return; if (taskDetails?.id === activeTaskId) return; // Skip fetch if we already have task in kanban state (same board) and we have essential data // The kanban state already has title, description, state which covers most UI needs // Only fetch for additional details like repositories, metadata, etc. if (kanbanTask && taskDetails?.workspace_id && taskDetails?.board_id) { // We have kanban data and can reuse workspace/board from current context // Fetch in background without blocking (low priority) fetchTask(activeTaskId, { cache: 'no-store' }) .then((response) => setTaskDetails(response)) .catch((error) => console.error('[TaskPageContent] Failed to load task details:', error)); return; } // Full fetch needed for first load or cross-board navigation fetchTask(activeTaskId, { cache: 'no-store' }) .then((response) => setTaskDetails(response)) .catch((error) => console.error('[TaskPageContent] Failed to load task details:', error)); }, [activeTaskId, taskDetails?.id, taskDetails?.workspace_id, taskDetails?.board_id, kanbanTask]); const { repositories } = useRepositories(task?.workspace_id ?? null, Boolean(task?.workspace_id)); const effectiveRepositories = repositories.length ? repositories : initialRepositories; const repository = useMemo( () => effectiveRepositories.find((item: Repository) => item.id === task?.repositories?.[0]?.repository_id) ?? null, [effectiveRepositories, task?.repositories] ); if (!isMounted) { return
; } return (
{DEBUG_UI && showDebugOverlay && ( )} {/* TaskTopBar is hidden on mobile - mobile layout has its own top bar */} {!isMobile && ( setShowDebugOverlay((prev) => !prev)} /> )}
); }