Files
kandev/apps/web/lib/ssr/session-page-state.ts
T
2026-05-01 15:18:18 +01:00

386 lines
12 KiB
TypeScript

import {
fetchWorkflowSnapshot,
fetchTask,
fetchUserSettings,
listAgents,
listWorkflows,
listRepositories,
listTaskSessionMessages,
listTaskSessions,
listWorkspaces,
} from "@/lib/api";
import { toAgentProfileOption } from "@/lib/state/slices/settings/types";
import { listSessionTurns } from "@/lib/api/domains/session-api";
import { fetchTerminals } from "@/lib/api/domains/user-shell-api";
import type {
ListMessagesResponse,
Task,
TaskSession,
UserSettingsResponse,
WorkflowSnapshot,
} from "@/lib/types/http";
import type { Terminal } from "@/hooks/domains/session/use-terminals";
import { snapshotToState, taskToState } from "@/lib/ssr/mapper";
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
function buildWorktreeState(allSessions: TaskSession[]) {
const sessionsWithWorktrees = allSessions.filter((s) => s.worktree_id);
return {
worktrees: {
items: Object.fromEntries(
sessionsWithWorktrees.map((s) => [
s.worktree_id,
{
id: s.worktree_id!,
sessionId: s.id,
repositoryId: s.repository_id ?? undefined,
path: s.worktree_path ?? undefined,
branch: s.worktree_branch ?? undefined,
},
]),
),
},
sessionWorktreesBySessionId: {
itemsBySessionId: Object.fromEntries(
sessionsWithWorktrees.map((s) => [s.id, [s.worktree_id!]]),
),
},
};
}
type BuildSessionPageStateParams = {
task: Task;
sessionId: string | null;
snapshot: Awaited<ReturnType<typeof fetchWorkflowSnapshot>>;
agents: Awaited<ReturnType<typeof listAgents>>;
repositories: Awaited<ReturnType<typeof listRepositories>>["repositories"];
allSessions: TaskSession[];
workspaces: Awaited<ReturnType<typeof listWorkspaces>>["workspaces"];
workflows: Awaited<ReturnType<typeof listWorkflows>>["workflows"];
turns: Awaited<ReturnType<typeof listSessionTurns>>["turns"];
userSettingsResponse: UserSettingsResponse | null;
messagesResponse: ListMessagesResponse | null;
};
function buildSessionPageState(p: BuildSessionPageStateParams) {
const { task, sessionId, snapshot, agents, allSessions, messagesResponse } = p;
const messages = messagesResponse?.messages ? [...messagesResponse.messages].reverse() : [];
const taskState = taskToState(task, sessionId, {
items: messages,
hasMore: messagesResponse?.has_more ?? false,
oldestCursor: messages[0]?.id ?? null,
});
return {
...snapshotToState(snapshot),
...taskState,
...buildResourceState(p),
...buildSessionState(p),
...buildWorktreeState(allSessions),
...buildPrepareProgressState(allSessions),
settingsAgents: { items: agents.agents },
settingsData: { agentsLoaded: true, executorsLoaded: false },
userSettings: mapUserSettingsResponse(p.userSettingsResponse),
};
}
function buildResourceState(p: BuildSessionPageStateParams) {
const { task, agents, repositories, workspaces, workflows } = p;
const repositoryId = task.repositories?.[0]?.repository_id;
const repository = repositories.find((r) => r.id === repositoryId);
const scripts = repository?.scripts ?? [];
return {
workspaces: {
items: workspaces.map((w) => ({
id: w.id,
name: w.name,
description: w.description ?? null,
owner_id: w.owner_id,
default_executor_id: w.default_executor_id ?? null,
default_environment_id: w.default_environment_id ?? null,
default_agent_profile_id: w.default_agent_profile_id ?? null,
created_at: w.created_at,
updated_at: w.updated_at,
})),
activeId: task.workspace_id,
},
workflows: {
items: workflows.map((w) => ({
id: w.id,
workspaceId: w.workspace_id,
name: w.name,
hidden: w.hidden,
})),
activeId: task.workflow_id,
},
repositories: {
itemsByWorkspaceId: { [task.workspace_id]: repositories },
loadingByWorkspaceId: { [task.workspace_id]: false },
loadedByWorkspaceId: { [task.workspace_id]: true },
},
repositoryScripts: repositoryId
? {
itemsByRepositoryId: { [repositoryId]: scripts },
loadingByRepositoryId: { [repositoryId]: false },
loadedByRepositoryId: { [repositoryId]: true },
}
: { itemsByRepositoryId: {}, loadingByRepositoryId: {}, loadedByRepositoryId: {} },
agentProfiles: {
items: agents.agents.flatMap((agent) =>
agent.profiles.map((profile) => toAgentProfileOption(agent, profile)),
),
version: 0,
},
};
}
function buildSessionState(p: BuildSessionPageStateParams) {
const { task, sessionId, allSessions, turns } = p;
return {
taskSessions: { items: Object.fromEntries(allSessions.map((s) => [s.id, s])) },
taskSessionsByTask: {
itemsByTaskId: { [task.id]: allSessions },
loadingByTaskId: { [task.id]: false },
loadedByTaskId: { [task.id]: true },
},
turns: sessionId
? {
bySession: { [sessionId]: turns },
activeBySession: {
[sessionId]: turns.filter((t) => !t.completed_at).pop()?.id ?? null,
},
}
: { bySession: {}, activeBySession: {} },
environmentIdBySessionId: Object.fromEntries(
allSessions.filter((s) => s.task_environment_id).map((s) => [s.id, s.task_environment_id!]),
),
};
}
function buildPrepareProgressState(allSessions: TaskSession[]) {
const bySessionId: Record<
string,
{
sessionId: string;
status: string;
steps: Array<{
name: string;
command?: string;
status: string;
output?: string;
error?: string;
warning?: string;
warningDetail?: string;
startedAt?: string;
endedAt?: string;
}>;
errorMessage?: string;
durationMs?: number;
}
> = {};
for (const session of allSessions) {
const pr = session.metadata?.prepare_result as
| {
status?: string;
steps?: Array<{
name: string;
command?: string;
status: string;
output?: string;
error?: string;
warning?: string;
warning_detail?: string;
started_at?: string;
ended_at?: string;
}>;
error_message?: string;
duration_ms?: number;
}
| undefined;
if (!pr) continue;
bySessionId[session.id] = {
sessionId: session.id,
status: pr.status ?? "completed",
steps: (pr.steps ?? []).map((s) => ({
name: s.name,
command: s.command,
status: s.status,
output: s.output,
error: s.error,
warning: s.warning,
warningDetail: s.warning_detail,
startedAt: s.started_at,
endedAt: s.ended_at,
})),
errorMessage: pr.error_message,
durationMs: pr.duration_ms,
};
}
if (Object.keys(bySessionId).length === 0) return {};
return { prepareProgress: { bySessionId } };
}
export type FetchedSessionData = {
task: Task;
sessionId: string | null;
initialState: ReturnType<typeof taskToState>;
initialTerminals: Terminal[];
};
export async function fetchSessionData(sessionId: string): Promise<FetchedSessionData> {
const [allSessionsResponse, task] = await (async () => {
// We need task + sessions; caller provides sessionId
const { fetchTaskSession } = await import("@/lib/api");
const sessionResponse = await fetchTaskSession(sessionId, { cache: "no-store" });
const session = sessionResponse.session;
if (!session?.task_id) throw new Error("No task_id found for session");
const t = await fetchTask(session.task_id, { cache: "no-store" });
const sessResp = await listTaskSessions(session.task_id, { cache: "no-store" });
return [sessResp, t] as const;
})();
return fetchSessionDataFromTask(task, sessionId, allSessionsResponse);
}
export async function fetchSessionDataForTask(taskId: string): Promise<FetchedSessionData> {
const task = await fetchTask(taskId, { cache: "no-store" });
const allSessionsResponse = await listTaskSessions(taskId, { cache: "no-store" });
const sessions = allSessionsResponse.sessions ?? [];
const sessionId = task.primary_session_id ?? sessions[0]?.id;
if (!sessionId) {
// No sessions yet — fetch task/workspace data so the store is seeded and
// the auto-start hook can fire immediately without a client-side crash.
return fetchTaskDataOnly(task, allSessionsResponse);
}
return fetchSessionDataFromTask(task, sessionId, allSessionsResponse);
}
async function fetchTaskDataOnly(
task: Task,
allSessionsResponse: Awaited<ReturnType<typeof listTaskSessions>>,
): Promise<FetchedSessionData> {
const [
snapshot,
agents,
repositoriesResponse,
workspacesResponse,
workflowsResponse,
userSettingsResponse,
] = await Promise.all([
task.workflow_id
? fetchWorkflowSnapshot(task.workflow_id, { cache: "no-store" })
: Promise.resolve({ steps: [], tasks: [] } as unknown as WorkflowSnapshot),
listAgents({ cache: "no-store" }),
listRepositories(task.workspace_id, { includeScripts: true }, { cache: "no-store" }),
listWorkspaces({ cache: "no-store" }).catch(() => ({ workspaces: [] })),
listWorkflows(task.workspace_id, { cache: "no-store", includeHidden: true }).catch(() => ({
workflows: [],
})),
fetchUserSettings({ cache: "no-store" }).catch(() => null),
]);
const allSessions = allSessionsResponse.sessions ?? [];
const repositories = repositoriesResponse.repositories ?? [];
const workspaces = workspacesResponse.workspaces ?? [];
const workflows = workflowsResponse.workflows ?? [];
const initialState = buildSessionPageState({
task,
sessionId: null,
snapshot,
agents,
repositories,
allSessions,
workspaces,
workflows,
turns: [],
userSettingsResponse,
messagesResponse: null,
});
return { task, sessionId: null, initialState, initialTerminals: [] };
}
async function fetchSessionDataFromTask(
task: Task,
sessionId: string,
allSessionsResponse: Awaited<ReturnType<typeof listTaskSessions>>,
): Promise<FetchedSessionData> {
const [
snapshot,
agents,
repositoriesResponse,
workspacesResponse,
workflowsResponse,
turnsResponse,
userSettingsResponse,
terminalsResponse,
messagesResponse,
] = await Promise.all([
task.workflow_id
? fetchWorkflowSnapshot(task.workflow_id, { cache: "no-store" })
: Promise.resolve({ steps: [], tasks: [] } as unknown as WorkflowSnapshot),
listAgents({ cache: "no-store" }),
listRepositories(task.workspace_id, { includeScripts: true }, { cache: "no-store" }),
listWorkspaces({ cache: "no-store" }).catch(() => ({ workspaces: [] })),
listWorkflows(task.workspace_id, { cache: "no-store", includeHidden: true }).catch(() => ({
workflows: [],
})),
listSessionTurns(sessionId, { cache: "no-store" }).catch(() => ({ turns: [], total: 0 })),
fetchUserSettings({ cache: "no-store" }).catch(() => null),
fetchTerminals(sessionId).catch(() => []),
listTaskSessionMessages(sessionId, { limit: 50, sort: "desc" }, { cache: "no-store" }).catch(
() => null as ListMessagesResponse | null,
),
]);
const allSessions = allSessionsResponse.sessions ?? [];
const repositories = repositoriesResponse.repositories ?? [];
const workspaces = workspacesResponse.workspaces ?? [];
const workflows = workflowsResponse.workflows ?? [];
const turns = turnsResponse.turns ?? [];
const initialTerminals: Terminal[] = terminalsResponse.map((t) => ({
id: t.terminal_id,
type: t.initial_command ? ("script" as const) : ("shell" as const),
label: t.label,
closable: t.closable,
}));
const initialState = buildSessionPageState({
task,
sessionId,
snapshot,
agents,
repositories,
allSessions,
workspaces,
workflows,
turns,
userSettingsResponse,
messagesResponse,
});
return { task, sessionId, initialState, initialTerminals };
}
export function extractInitialRepositories(
initialState: FetchedSessionData["initialState"] | null,
task: Task | null,
) {
return initialState?.repositories?.itemsByWorkspaceId?.[task?.workspace_id ?? ""] ?? [];
}
export function extractInitialScripts(
initialState: FetchedSessionData["initialState"] | null,
task: Task | null,
) {
const repoId = task?.repositories?.[0]?.repository_id ?? "";
return initialState?.repositoryScripts?.itemsByRepositoryId?.[repoId] ?? [];
}