Files
kandev/apps/web/app/s/[sessionId]/page.tsx
T
2026-02-17 22:59:53 +00:00

259 lines
9.3 KiB
TypeScript

import { StateHydrator } from '@/components/state-hydrator';
import { readLayoutDefaults } from '@/lib/layout/read-layout-defaults';
import {
fetchWorkflowSnapshot,
fetchTaskSession,
fetchTask,
fetchUserSettings,
listAgents,
listAvailableAgents,
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 } from '@/lib/types/http';
import type { Terminal } from '@/hooks/domains/session/use-terminals';
import { snapshotToState, taskToState } from '@/lib/ssr/mapper';
import { TaskPageContent } from '@/components/task/task-page-content';
export default async function SessionPage({
params,
}: {
params: Promise<{ sessionId: string }>;
}) {
let initialState: ReturnType<typeof taskToState> | null = null;
let task: Task | null = null;
let sessionId: string | null = null;
let initialTerminals: Terminal[] = [];
const defaultLayouts = await readLayoutDefaults();
try {
const { sessionId: paramSessionId } = await params;
sessionId = paramSessionId;
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');
}
task = await fetchTask(session.task_id, { cache: 'no-store' });
const [snapshot, agents, repositoriesResponse, allSessionsResponse, availableAgentsResponse, workspacesResponse, workflowsResponse, turnsResponse, userSettingsResponse, terminalsResponse] = await Promise.all([
fetchWorkflowSnapshot(task.workflow_id, { cache: 'no-store' }),
listAgents({ cache: 'no-store' }),
listRepositories(task.workspace_id, { includeScripts: true }, { cache: 'no-store' }),
listTaskSessions(session.task_id, { cache: 'no-store' }),
listAvailableAgents({ cache: 'no-store' }).catch(() => ({ agents: [] })),
listWorkspaces({ cache: 'no-store' }).catch(() => ({ workspaces: [] })),
listWorkflows(task.workspace_id, { cache: 'no-store' }).catch(() => ({ workflows: [] })),
listSessionTurns(sessionId, { cache: 'no-store' }).catch(() => ({ turns: [], total: 0 })),
fetchUserSettings({ cache: 'no-store' }).catch(() => null),
fetchTerminals(sessionId).catch(() => []),
]);
const repositories = repositoriesResponse.repositories ?? [];
const allSessions = allSessionsResponse.sessions ?? [session];
const availableAgents = availableAgentsResponse.agents ?? [];
const workspaces = workspacesResponse.workspaces ?? [];
const workflows = workflowsResponse.workflows ?? [];
const turns = turnsResponse.turns ?? [];
const userSettings = userSettingsResponse?.settings;
// Transform terminals to frontend format
initialTerminals = terminalsResponse.map((t) => ({
id: t.terminal_id,
type: t.initial_command ? 'script' as const : 'shell' as const,
label: t.label,
closable: t.closable,
}));
// Get repository scripts from the repository (already fetched with includeScripts)
const repositoryId = task.repositories?.[0]?.repository_id;
const repository = repositories.find(r => r.id === repositoryId);
const scripts = repository?.scripts ?? [];
let messagesResponse: ListMessagesResponse | null = null;
try {
// Load most recent messages in descending order, then reverse to show oldest-to-newest
messagesResponse = await listTaskSessionMessages(
sessionId,
{ limit: 50, sort: 'desc' },
{ cache: 'no-store' }
);
} catch (error) {
console.warn(
'Could not load session messages for SSR:',
error instanceof Error ? error.message : String(error)
);
}
const messages = messagesResponse?.messages ? [...messagesResponse.messages].reverse() : [];
const taskState = taskToState(task, sessionId, {
items: messages,
hasMore: messagesResponse?.has_more ?? false,
// oldestCursor should be the first (oldest) message ID for lazy loading older messages
oldestCursor: messages[0]?.id ?? null,
});
initialState = {
...snapshotToState(snapshot),
...taskState,
workspaces: {
items: workspaces.map((workspace) => ({
id: workspace.id,
name: workspace.name,
description: workspace.description ?? null,
owner_id: workspace.owner_id,
default_executor_id: workspace.default_executor_id ?? null,
default_environment_id: workspace.default_environment_id ?? null,
default_agent_profile_id: workspace.default_agent_profile_id ?? null,
created_at: workspace.created_at,
updated_at: workspace.updated_at,
})),
activeId: task.workspace_id,
},
workflows: {
items: workflows.map((workflow) => ({
id: workflow.id,
workspaceId: workflow.workspace_id,
name: workflow.name,
})),
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,
},
taskSessions: {
items: Object.fromEntries(allSessions.map((s) => [s.id, s])),
},
taskSessionsByTask: {
itemsByTaskId: {
[task.id]: allSessions,
},
loadingByTaskId: {
[task.id]: false,
},
loadedByTaskId: {
[task.id]: true,
},
},
turns: {
bySession: {
[sessionId]: turns,
},
activeBySession: {
// Find the most recent (last) incomplete turn
[sessionId]: turns.filter((t) => !t.completed_at).pop()?.id ?? null,
},
},
worktrees: {
items: Object.fromEntries(
allSessions
.filter((s) => s.worktree_id)
.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(
allSessions
.filter((s) => s.worktree_id)
.map((s) => [s.id, [s.worktree_id!]])
),
},
settingsAgents: {
items: agents.agents,
},
settingsData: {
agentsLoaded: true,
executorsLoaded: false,
environmentsLoaded: false,
},
availableAgents: {
items: availableAgents,
loaded: true,
loading: false,
},
userSettings: {
workspaceId: userSettings?.workspace_id || null,
workflowId: userSettings?.workflow_filter_id || null,
kanbanViewMode: userSettings?.kanban_view_mode || null,
repositoryIds: Array.from(new Set(userSettings?.repository_ids ?? [])).sort(),
preferredShell: userSettings?.preferred_shell || null,
shellOptions: userSettingsResponse?.shell_options ?? [],
defaultEditorId: userSettings?.default_editor_id || null,
enablePreviewOnClick: userSettings?.enable_preview_on_click ?? false,
chatSubmitKey: userSettings?.chat_submit_key ?? 'cmd_enter',
reviewAutoMarkOnScroll: userSettings?.review_auto_mark_on_scroll ?? true,
lspAutoStartLanguages: userSettings?.lsp_auto_start_languages ?? [],
lspAutoInstallLanguages: userSettings?.lsp_auto_install_languages ?? [],
lspServerConfigs: userSettings?.lsp_server_configs ?? {},
loaded: Boolean(userSettings),
},
};
} catch (error) {
console.warn(
'Could not SSR session (client will load via WebSocket):',
error instanceof Error ? error.message : String(error)
);
initialState = null;
task = null;
sessionId = null;
}
return (
<>
{initialState ? <StateHydrator initialState={initialState} sessionId={sessionId ?? undefined} /> : null}
<TaskPageContent
task={task}
sessionId={sessionId}
initialRepositories={initialState?.repositories?.itemsByWorkspaceId?.[task?.workspace_id ?? ''] ?? []}
initialScripts={initialState?.repositoryScripts?.itemsByRepositoryId?.[task?.repositories?.[0]?.repository_id ?? ''] ?? []}
initialTerminals={initialTerminals}
defaultLayouts={defaultLayouts}
/>
</>
);
}