4282ba262f
The desktop task top bar carried a small cloud icon (RemoteCloudTooltip) in the breadcrumb cluster while the right-side Executor popover showed the same kind of info — two surfaces for one concept. Remove the cloud icon from the desktop top bar; kanban cards, task list rows, and the mobile top bar keep theirs since they have no popover. The popover button now picks its icon by executor type (cloud for sprites, container for docker, server for local PC) using a shared getExecutorStatusIcon helper. While the prepare progress is preparing the icon swaps to a spinner so the user can see at a glance that work is in flight. The popover content gains a Preparing-environment section that streams "Step N of M: <step>", an amber inline note when the missing-sandbox fallback is in progress, an Environment-ready row with duration on success, and a failed-step summary on failure. Polling promotes to the active 3s cadence while preparing so the icon flips to ready without the user opening the popover.
589 lines
21 KiB
TypeScript
589 lines
21 KiB
TypeScript
"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 { useTaskFocus } from "@/hooks/domains/session/use-task-focus";
|
|
import { useAppStore } from "@/components/state-provider";
|
|
import { useEnsureTaskSession } from "@/hooks/domains/session/use-ensure-task-session";
|
|
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";
|
|
import { TaskArchivedProvider } from "./task-archived-context";
|
|
import { SessionCommands } from "@/components/session-commands";
|
|
import { VcsDialogsProvider } from "@/components/vcs/vcs-dialogs";
|
|
import {
|
|
buildDebugEntries,
|
|
deriveIsAgentWorking,
|
|
buildArchivedValue,
|
|
resolveTaskProps,
|
|
} from "@/components/task/task-page-content-helpers";
|
|
|
|
type TaskPageContentProps = {
|
|
task: Task | null;
|
|
taskId?: string | null;
|
|
sessionId?: string | null;
|
|
initialRepositories?: Repository[];
|
|
initialScripts?: RepositoryScript[];
|
|
initialTerminals?: Terminal[];
|
|
defaultLayouts?: Record<string, Layout>;
|
|
initialLayout?: string | null;
|
|
};
|
|
|
|
function resolveEffectiveTask(
|
|
taskDetails: Task | null,
|
|
initialTask: Task | null,
|
|
kanbanTask: KanbanState["tasks"][number] | null,
|
|
effectiveTaskId: string | null,
|
|
): Task | null {
|
|
const matchingTaskDetails = taskDetails?.id === effectiveTaskId ? taskDetails : null;
|
|
const matchingInitialTask = initialTask?.id === effectiveTaskId ? initialTask : null;
|
|
const baseTask = matchingTaskDetails ?? matchingInitialTask;
|
|
|
|
if (!baseTask && !kanbanTask) return null;
|
|
if (baseTask) return mergeBaseWithKanban(baseTask, kanbanTask);
|
|
if (kanbanTask) return buildTaskFromKanban(kanbanTask, taskDetails, initialTask);
|
|
return null;
|
|
}
|
|
|
|
function mergeBaseWithKanban(
|
|
baseTask: Task,
|
|
kanbanTask: KanbanState["tasks"][number] | null,
|
|
): Task {
|
|
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,
|
|
};
|
|
}
|
|
|
|
function buildTaskFromKanban(
|
|
kanbanTask: KanbanState["tasks"][number],
|
|
taskDetails: Task | null,
|
|
initialTask: Task | null,
|
|
): Task {
|
|
const prevWorkspaceId = taskDetails?.workspace_id ?? initialTask?.workspace_id;
|
|
const prevBoardId = taskDetails?.workflow_id ?? initialTask?.workflow_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 ?? "",
|
|
workflow_id: prevBoardId ?? "",
|
|
priority: 0,
|
|
repositories: [],
|
|
created_at: "",
|
|
updated_at: kanbanTask.updatedAt ?? "",
|
|
} as Task;
|
|
}
|
|
|
|
function useWorkflowStepsMapped() {
|
|
const kanbanSteps = useAppStore((state) => state.kanban.steps);
|
|
return useMemo(
|
|
() =>
|
|
kanbanSteps.map((s) => ({
|
|
id: s.id,
|
|
name: s.title,
|
|
color: s.color,
|
|
position: s.position,
|
|
events: s.events,
|
|
allow_manual_move: s.allow_manual_move,
|
|
prompt: s.prompt,
|
|
is_start_step: s.is_start_step,
|
|
agent_profile_id: s.agent_profile_id,
|
|
})),
|
|
[kanbanSteps],
|
|
);
|
|
}
|
|
|
|
function useSessionPanelState(effectiveSessionId: string | null | undefined) {
|
|
const storeSessionState = useAppStore((state) =>
|
|
effectiveSessionId ? (state.taskSessions.items[effectiveSessionId]?.state ?? null) : null,
|
|
);
|
|
const isSessionPassthrough = useAppStore((state) =>
|
|
effectiveSessionId
|
|
? state.taskSessions.items[effectiveSessionId]?.is_passthrough === true
|
|
: false,
|
|
);
|
|
// Use the task-level workflow step for the top-bar stepper. Individual sessions
|
|
// may lag behind (e.g. a completed session stays at its old step), but the
|
|
// task's step reflects the current workflow position and stays stable across
|
|
// tab switches within the same task.
|
|
const sessionWorkflowStepId = useAppStore((state) => {
|
|
const taskId = state.tasks.activeTaskId;
|
|
if (!taskId) return null;
|
|
const task = state.kanban.tasks.find((t: { id: string }) => t.id === taskId);
|
|
return (task?.workflowStepId as string) ?? null;
|
|
});
|
|
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,
|
|
);
|
|
return {
|
|
storeSessionState,
|
|
isSessionPassthrough,
|
|
sessionWorkflowStepId,
|
|
previewOpen,
|
|
previewStage,
|
|
previewUrl,
|
|
devProcessId,
|
|
devProcessStatus,
|
|
};
|
|
}
|
|
|
|
function useMergedAgentState(
|
|
agent: ReturnType<typeof useSessionAgent>,
|
|
resumption: ReturnType<typeof useSessionResumption>,
|
|
sessionPanel: ReturnType<typeof useSessionPanelState>,
|
|
effectiveSessionId: string | null | undefined,
|
|
task: Task | null,
|
|
) {
|
|
const isResuming =
|
|
resumption.resumptionState === "checking" || resumption.resumptionState === "resuming";
|
|
const isResumed =
|
|
resumption.resumptionState === "resumed" || resumption.resumptionState === "running";
|
|
const taskSessionState = sessionPanel.storeSessionState ?? agent.taskSessionState;
|
|
const worktreePath = effectiveSessionId
|
|
? (resumption.worktreePath ?? agent.worktreePath)
|
|
: agent.worktreePath;
|
|
const worktreeBranch = effectiveSessionId
|
|
? (resumption.worktreeBranch ?? agent.worktreeBranch)
|
|
: agent.worktreeBranch;
|
|
const isAgentWorking = deriveIsAgentWorking(
|
|
taskSessionState,
|
|
agent.isAgentRunning,
|
|
task?.state ?? null,
|
|
);
|
|
return { isResuming, isResumed, taskSessionState, worktreePath, worktreeBranch, isAgentWorking };
|
|
}
|
|
|
|
type TaskPageInnerProps = {
|
|
task: Task | null;
|
|
effectiveSessionId: string | null;
|
|
repository: Repository | null;
|
|
agent: ReturnType<typeof useSessionAgent>;
|
|
merged: ReturnType<typeof useMergedAgentState>;
|
|
resumption: ReturnType<typeof useSessionResumption>;
|
|
sessionPanel: ReturnType<typeof useSessionPanelState>;
|
|
agentctlStatus: ReturnType<typeof useSessionAgentctl>;
|
|
connectionStatus: string;
|
|
workflowSteps: ReturnType<typeof useWorkflowStepsMapped>;
|
|
archivedValue: ReturnType<typeof buildArchivedValue>;
|
|
isMobile: boolean;
|
|
showDebugOverlay: boolean;
|
|
onToggleDebugOverlay: () => void;
|
|
initialScripts: RepositoryScript[];
|
|
initialTerminals?: Terminal[];
|
|
defaultLayouts: Record<string, Layout>;
|
|
initialLayout?: string | null;
|
|
};
|
|
|
|
type RemoteExecutorStatus = {
|
|
is_remote_executor?: boolean;
|
|
executor_type?: string | null;
|
|
executor_name?: string | null;
|
|
remote_name?: string | null;
|
|
remote_state?: string | null;
|
|
remote_created_at?: string | null;
|
|
remote_checked_at?: string | null;
|
|
remote_status_error?: string | null;
|
|
};
|
|
|
|
function toNullable(value: string | null | undefined): string | null {
|
|
return value ?? null;
|
|
}
|
|
|
|
function resolveRemoteExecutor(status?: RemoteExecutorStatus | null) {
|
|
const remoteExecutorName = status?.remote_name ?? status?.executor_name ?? null;
|
|
return {
|
|
isRemoteExecutor: status?.is_remote_executor ?? false,
|
|
remoteExecutorType: toNullable(status?.executor_type),
|
|
remoteExecutorName,
|
|
remoteState: toNullable(status?.remote_state),
|
|
remoteCreatedAt: toNullable(status?.remote_created_at),
|
|
remoteCheckedAt: toNullable(status?.remote_checked_at),
|
|
remoteStatusError: toNullable(status?.remote_status_error),
|
|
};
|
|
}
|
|
|
|
/** Pick the best step ID. Both session-level (from session.state_changed WS event) and
|
|
* task-level (from task.updated WS event + kanban snapshot) carry the same value, but
|
|
* session.state_changed is delivered directly while task.updated goes through the hub's
|
|
* broadcast channel, so the session value arrives first and is less likely to be stale. */
|
|
function resolveCurrentStepId(
|
|
sessionStepId: string | null,
|
|
taskStepId: string | null,
|
|
): string | null {
|
|
return sessionStepId || taskStepId || null;
|
|
}
|
|
|
|
function buildTaskTopBarProps(params: {
|
|
taskProps: ReturnType<typeof resolveTaskProps>;
|
|
agent: ReturnType<typeof useSessionAgent>;
|
|
merged: ReturnType<typeof useMergedAgentState>;
|
|
workflowSteps: ReturnType<typeof useWorkflowStepsMapped>;
|
|
showDebugOverlay: boolean;
|
|
onToggleDebugOverlay: () => void;
|
|
effectiveSessionId: string | null;
|
|
remote: ReturnType<typeof resolveRemoteExecutor>;
|
|
sessionWorkflowStepId: string | null;
|
|
agentctlReady: boolean;
|
|
}) {
|
|
const { taskProps, agent, merged, workflowSteps, showDebugOverlay, onToggleDebugOverlay } =
|
|
params;
|
|
return {
|
|
taskId: taskProps.taskId,
|
|
activeSessionId: params.effectiveSessionId,
|
|
taskTitle: taskProps.taskTitle,
|
|
onStartAgent: agent.handleStartAgent,
|
|
onStopAgent: agent.handleStopAgent,
|
|
isAgentRunning: agent.isAgentRunning || merged.isResumed,
|
|
isAgentLoading: agent.isAgentLoading || merged.isResuming,
|
|
showDebugOverlay,
|
|
onToggleDebugOverlay,
|
|
workflowSteps,
|
|
currentStepId: resolveCurrentStepId(params.sessionWorkflowStepId, taskProps.workflowStepId),
|
|
workflowId: taskProps.workflowId,
|
|
workspaceId: taskProps.workspaceId,
|
|
isArchived: taskProps.isArchived,
|
|
isRemoteExecutor: params.remote.isRemoteExecutor,
|
|
isAgentctlReady: params.agentctlReady,
|
|
remoteExecutorType: params.remote.remoteExecutorType,
|
|
};
|
|
}
|
|
|
|
function buildTaskLayoutProps(params: {
|
|
taskProps: ReturnType<typeof resolveTaskProps>;
|
|
repository: Repository | null;
|
|
effectiveSessionId: string | null;
|
|
initialScripts: RepositoryScript[];
|
|
initialTerminals?: Terminal[];
|
|
defaultLayouts: Record<string, Layout>;
|
|
merged: ReturnType<typeof useMergedAgentState>;
|
|
remote: ReturnType<typeof resolveRemoteExecutor>;
|
|
initialLayout?: string | null;
|
|
}) {
|
|
const { taskProps, repository, effectiveSessionId, initialScripts, initialTerminals } = params;
|
|
return {
|
|
workspaceId: taskProps.workspaceId,
|
|
workflowId: taskProps.workflowId,
|
|
sessionId: effectiveSessionId,
|
|
repository: repository ?? null,
|
|
initialScripts,
|
|
initialTerminals,
|
|
defaultLayouts: params.defaultLayouts,
|
|
initialLayout: params.initialLayout,
|
|
taskTitle: taskProps.taskTitle,
|
|
baseBranch: taskProps.baseBranch,
|
|
worktreeBranch: params.merged.worktreeBranch,
|
|
isRemoteExecutor: params.remote.isRemoteExecutor,
|
|
remoteExecutorType: params.remote.remoteExecutorType,
|
|
remoteExecutorName: params.remote.remoteExecutorName,
|
|
remoteState: params.remote.remoteState,
|
|
remoteCreatedAt: params.remote.remoteCreatedAt,
|
|
remoteCheckedAt: params.remote.remoteCheckedAt,
|
|
remoteStatusError: params.remote.remoteStatusError,
|
|
};
|
|
}
|
|
|
|
function maybeBuildDebugEntries(params: {
|
|
isVisible: boolean;
|
|
connectionStatus: string;
|
|
task: Task | null;
|
|
effectiveSessionId: string | null | undefined;
|
|
merged: ReturnType<typeof useMergedAgentState>;
|
|
resumption: ReturnType<typeof useSessionResumption>;
|
|
sessionPanel: ReturnType<typeof useSessionPanelState>;
|
|
agentctlStatus: ReturnType<typeof useSessionAgentctl>;
|
|
}) {
|
|
if (!params.isVisible) return null;
|
|
return buildDebugEntries({
|
|
connectionStatus: params.connectionStatus,
|
|
task: params.task,
|
|
effectiveSessionId: params.effectiveSessionId,
|
|
taskSessionState: params.merged.taskSessionState,
|
|
isAgentWorking: params.merged.isAgentWorking,
|
|
resumptionState: params.resumption.resumptionState,
|
|
resumptionError: params.resumption.error,
|
|
agentctlStatus: params.agentctlStatus,
|
|
previewOpen: params.sessionPanel.previewOpen,
|
|
previewStage: params.sessionPanel.previewStage,
|
|
previewUrl: params.sessionPanel.previewUrl,
|
|
devProcessId: params.sessionPanel.devProcessId,
|
|
devProcessStatus: params.sessionPanel.devProcessStatus,
|
|
});
|
|
}
|
|
|
|
function TaskPageInner({
|
|
task,
|
|
effectiveSessionId,
|
|
repository,
|
|
agent,
|
|
merged,
|
|
resumption,
|
|
sessionPanel,
|
|
agentctlStatus,
|
|
connectionStatus,
|
|
workflowSteps,
|
|
archivedValue,
|
|
isMobile,
|
|
showDebugOverlay,
|
|
onToggleDebugOverlay,
|
|
initialScripts,
|
|
initialTerminals,
|
|
defaultLayouts,
|
|
initialLayout,
|
|
}: TaskPageInnerProps) {
|
|
const taskProps = resolveTaskProps(task, repository);
|
|
const remote = resolveRemoteExecutor(resumption.sessionStatus as RemoteExecutorStatus | null);
|
|
const debugEntries = maybeBuildDebugEntries({
|
|
isVisible: DEBUG_UI && showDebugOverlay,
|
|
connectionStatus,
|
|
task,
|
|
effectiveSessionId,
|
|
merged,
|
|
resumption,
|
|
sessionPanel,
|
|
agentctlStatus,
|
|
});
|
|
const topBarProps = buildTaskTopBarProps({
|
|
taskProps,
|
|
agent,
|
|
merged,
|
|
workflowSteps,
|
|
showDebugOverlay,
|
|
onToggleDebugOverlay,
|
|
effectiveSessionId,
|
|
remote,
|
|
sessionWorkflowStepId: sessionPanel.sessionWorkflowStepId,
|
|
agentctlReady: agentctlStatus.isReady,
|
|
});
|
|
const layoutProps = buildTaskLayoutProps({
|
|
taskProps,
|
|
repository,
|
|
effectiveSessionId,
|
|
initialScripts,
|
|
initialTerminals,
|
|
defaultLayouts,
|
|
merged,
|
|
remote,
|
|
initialLayout,
|
|
});
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<VcsDialogsProvider
|
|
sessionId={effectiveSessionId}
|
|
baseBranch={taskProps.baseBranch}
|
|
taskTitle={taskProps.taskTitle}
|
|
displayBranch={merged.worktreeBranch}
|
|
>
|
|
<div className="h-screen w-full flex flex-col bg-background overflow-hidden">
|
|
<SessionCommands
|
|
sessionId={effectiveSessionId}
|
|
baseBranch={taskProps.baseBranch}
|
|
isAgentRunning={merged.isAgentWorking}
|
|
hasWorktree={Boolean(merged.worktreeBranch)}
|
|
isPassthrough={sessionPanel.isSessionPassthrough}
|
|
/>
|
|
{debugEntries && <DebugOverlay title="Task Debug" entries={debugEntries} />}
|
|
{!isMobile && <TaskTopBar {...topBarProps} />}
|
|
<TaskArchivedProvider value={archivedValue}>
|
|
<TaskLayout {...layoutProps} />
|
|
</TaskArchivedProvider>
|
|
</div>
|
|
</VcsDialogsProvider>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
function syncActiveTaskSession(params: {
|
|
initialTaskId: string | undefined;
|
|
fallbackTaskId: string | null | undefined;
|
|
initialSessionId: string | null;
|
|
setActiveSession: (taskId: string, sessionId: string) => void;
|
|
setActiveTask: (taskId: string) => void;
|
|
}) {
|
|
const taskId = params.initialTaskId ?? params.fallbackTaskId;
|
|
if (!taskId) return;
|
|
if (params.initialSessionId) params.setActiveSession(taskId, params.initialSessionId);
|
|
else params.setActiveTask(taskId);
|
|
}
|
|
|
|
function useTaskDetails(activeTaskId: string | null, initialTask: Task | null) {
|
|
const [taskDetails, setTaskDetails] = useState<Task | null>(initialTask);
|
|
const kanbanTask = useAppStore((state) =>
|
|
activeTaskId
|
|
? (state.kanban.tasks.find(
|
|
(item: KanbanState["tasks"][number]) => item.id === activeTaskId,
|
|
) ?? null)
|
|
: null,
|
|
);
|
|
const effectiveTaskId = activeTaskId ?? initialTask?.id ?? null;
|
|
const task = useMemo(
|
|
() => resolveEffectiveTask(taskDetails, initialTask, kanbanTask, effectiveTaskId),
|
|
[taskDetails, initialTask, kanbanTask, effectiveTaskId],
|
|
);
|
|
useTasks(task?.workflow_id ?? null);
|
|
|
|
useEffect(() => {
|
|
if (!activeTaskId || taskDetails?.id === activeTaskId) return;
|
|
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?.workflow_id,
|
|
kanbanTask,
|
|
setTaskDetails,
|
|
]);
|
|
|
|
return { task, kanbanTask };
|
|
}
|
|
|
|
function useTaskPageData(
|
|
initialTask: Task | null,
|
|
fallbackTaskId: string | null | undefined,
|
|
sessionId: string | null,
|
|
initialRepositories: Repository[],
|
|
) {
|
|
const activeTaskId = useAppStore((state) => state.tasks.activeTaskId);
|
|
const setActiveSession = useAppStore((state) => state.setActiveSession);
|
|
const setActiveTask = useAppStore((state) => state.setActiveTask);
|
|
|
|
// Validate that activeSessionId belongs to activeTaskId to prevent showing
|
|
// messages from an unrelated session when navigating to a task without sessions.
|
|
const validatedActiveSessionId = useAppStore((state) => {
|
|
const sid = state.tasks.activeSessionId;
|
|
if (!sid || !activeTaskId) return null;
|
|
const session = state.taskSessions.items[sid];
|
|
return session?.task_id === activeTaskId ? sid : null;
|
|
});
|
|
|
|
const { task } = useTaskDetails(activeTaskId, initialTask);
|
|
|
|
const agent = useSessionAgent(task);
|
|
useEnsureTaskSession(task);
|
|
const initialSessionId = sessionId ?? agent.taskSessionId ?? null;
|
|
const effectiveSessionId = validatedActiveSessionId ?? initialSessionId;
|
|
|
|
useEffect(() => {
|
|
syncActiveTaskSession({
|
|
initialTaskId: initialTask?.id,
|
|
fallbackTaskId,
|
|
initialSessionId,
|
|
setActiveSession,
|
|
setActiveTask,
|
|
});
|
|
}, [initialTask?.id, fallbackTaskId, initialSessionId, setActiveSession, setActiveTask]);
|
|
|
|
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],
|
|
);
|
|
|
|
return { task, agent, effectiveSessionId, repository };
|
|
}
|
|
|
|
export function TaskPageContent({
|
|
task: initialTask,
|
|
taskId: initialTaskId = null,
|
|
sessionId = null,
|
|
initialRepositories = [],
|
|
initialScripts = [],
|
|
initialTerminals,
|
|
defaultLayouts = {},
|
|
initialLayout,
|
|
}: TaskPageContentProps) {
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
const [showDebugOverlay, setShowDebugOverlay] = useState(false);
|
|
const { isMobile } = useResponsiveBreakpoint();
|
|
const connectionStatus = useAppStore((state) => state.connection.status);
|
|
|
|
const { task, agent, effectiveSessionId, repository } = useTaskPageData(
|
|
initialTask,
|
|
initialTaskId,
|
|
sessionId,
|
|
initialRepositories,
|
|
);
|
|
|
|
const workflowSteps = useWorkflowStepsMapped();
|
|
const sessionPanel = useSessionPanelState(effectiveSessionId);
|
|
const agentctlStatus = useSessionAgentctl(effectiveSessionId);
|
|
const resumption = useSessionResumption(task?.id ?? null, effectiveSessionId);
|
|
const merged = useMergedAgentState(agent, resumption, sessionPanel, effectiveSessionId, task);
|
|
const archivedValue = useMemo(() => buildArchivedValue(task, repository), [task, repository]);
|
|
// Mark this session as actively focused so the backend lifts polling to fast.
|
|
// Sidebar cards subscribe but never focus, so they stay on the cheap slow tier.
|
|
useTaskFocus(effectiveSessionId);
|
|
|
|
useEffect(() => {
|
|
queueMicrotask(() => setIsMounted(true));
|
|
}, []);
|
|
|
|
if (!isMounted) return <div className="h-screen w-full bg-background" />;
|
|
|
|
return (
|
|
<TaskPageInner
|
|
task={task}
|
|
effectiveSessionId={effectiveSessionId ?? null}
|
|
repository={repository}
|
|
agent={agent}
|
|
merged={merged}
|
|
resumption={resumption}
|
|
sessionPanel={sessionPanel}
|
|
agentctlStatus={agentctlStatus}
|
|
connectionStatus={connectionStatus}
|
|
workflowSteps={workflowSteps}
|
|
archivedValue={archivedValue}
|
|
isMobile={isMobile}
|
|
showDebugOverlay={showDebugOverlay}
|
|
onToggleDebugOverlay={() => setShowDebugOverlay((prev) => !prev)}
|
|
initialScripts={initialScripts}
|
|
initialTerminals={initialTerminals}
|
|
defaultLayouts={defaultLayouts}
|
|
initialLayout={initialLayout}
|
|
/>
|
|
);
|
|
}
|