269 lines
9.1 KiB
TypeScript
269 lines
9.1 KiB
TypeScript
import { PageClient } from "@/app/page-client";
|
|
import { StateHydrator } from "@/components/state-hydrator";
|
|
import {
|
|
fetchWorkflowSnapshot,
|
|
fetchUserSettings,
|
|
listWorkflows,
|
|
listRepositories,
|
|
listWorkspaces,
|
|
listTaskSessionMessages,
|
|
listQuickChatSessions,
|
|
listQuickTerminalTabs,
|
|
} from "@/lib/api";
|
|
import { listWorkspaceTaskPRs } from "@/lib/api/domains/github-api";
|
|
import { toQuickChatSessions } from "@/lib/quick-chat/map-sessions";
|
|
import { toQuickTerminalTab } from "@/lib/api/domains/quick-terminal-api";
|
|
import { snapshotToState } from "@/lib/ssr/mapper";
|
|
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
|
|
import { resolveDesiredWorkflowId } from "@/lib/kanban/resolve-workflow";
|
|
import { ACTIVE_WORKSPACE_COOKIE } from "@/lib/routing/route-bootstrap";
|
|
import { resolveActiveId } from "@/lib/ssr/resolve-active-id";
|
|
import { readCookies } from "@/lib/server/cookies";
|
|
import type { AppState } from "@/lib/state/store";
|
|
import type { ListWorkspacesResponse, UserSettingsResponse } from "@/lib/types/http";
|
|
|
|
// Root page loader: keeps the old route shape while SPA boot data owns hydration.
|
|
type PageProps = {
|
|
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
|
};
|
|
|
|
function resolveParam(value: string | string[] | undefined): string | undefined {
|
|
return Array.isArray(value) ? value[0] : value;
|
|
}
|
|
|
|
type WorkspaceItem = ListWorkspacesResponse["workspaces"][number];
|
|
function mapWorkspaceItem(ws: WorkspaceItem) {
|
|
return {
|
|
id: ws.id,
|
|
name: ws.name,
|
|
description: ws.description ?? null,
|
|
owner_id: ws.owner_id,
|
|
default_executor_id: ws.default_executor_id ?? null,
|
|
default_environment_id: ws.default_environment_id ?? null,
|
|
default_agent_profile_id: ws.default_agent_profile_id ?? null,
|
|
default_config_agent_profile_id: ws.default_config_agent_profile_id ?? null,
|
|
office_workflow_id: ws.office_workflow_id ?? null,
|
|
created_at: ws.created_at,
|
|
updated_at: ws.updated_at,
|
|
};
|
|
}
|
|
|
|
function buildUserSettingsState(
|
|
resp: UserSettingsResponse | null,
|
|
workspaceId: string | null,
|
|
): AppState["userSettings"] {
|
|
return { ...mapUserSettingsResponse(resp), workspaceId };
|
|
}
|
|
|
|
function buildBaseState(
|
|
workspaces: ListWorkspacesResponse,
|
|
userSettingsResponse: UserSettingsResponse | null,
|
|
activeWorkspaceId: string | null,
|
|
): Partial<AppState> {
|
|
return {
|
|
workspaces: {
|
|
items: workspaces.workspaces.map(mapWorkspaceItem),
|
|
activeId: activeWorkspaceId,
|
|
},
|
|
userSettings: buildUserSettingsState(userSettingsResponse, activeWorkspaceId),
|
|
};
|
|
}
|
|
|
|
export function resolveActiveKanbanWorkspaceId(
|
|
workspaces: WorkspaceItem[],
|
|
workspaceId: string | undefined,
|
|
cookieWorkspaceId: string | null,
|
|
settingsWorkspaceId: string | null,
|
|
): string | null {
|
|
const kanbanWorkspaces = workspaces.filter((workspace) => !workspace.office_workflow_id);
|
|
return resolveActiveId(kanbanWorkspaces, workspaceId, cookieWorkspaceId, settingsWorkspaceId);
|
|
}
|
|
|
|
async function loadSnapshotState(
|
|
workflowId: string,
|
|
taskId: string | undefined,
|
|
sessionId: string | undefined,
|
|
): Promise<Partial<AppState>> {
|
|
const [snapshot, messagesResponse] = await Promise.all([
|
|
fetchWorkflowSnapshot(workflowId, { cache: "no-store" }),
|
|
taskId && sessionId
|
|
? listTaskSessionMessages(
|
|
sessionId,
|
|
{ limit: 50, sort: "desc" },
|
|
{ cache: "no-store" },
|
|
).catch(() => null)
|
|
: Promise.resolve(null),
|
|
]);
|
|
const state: Partial<AppState> = { ...snapshotToState(snapshot) };
|
|
|
|
if (sessionId && messagesResponse) {
|
|
const messages = [...(messagesResponse.messages ?? [])].reverse();
|
|
state.messages = {
|
|
bySession: { [sessionId]: messages },
|
|
metaBySession: {
|
|
[sessionId]: {
|
|
isLoading: false,
|
|
hasMore: messagesResponse.has_more ?? false,
|
|
oldestCursor: messages[0]?.id ?? null,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
return state;
|
|
}
|
|
|
|
async function loadWorkspaceState({
|
|
activeWorkspaceId,
|
|
initialState,
|
|
settingsWorkflowId,
|
|
workflowIdParam,
|
|
}: {
|
|
activeWorkspaceId: string;
|
|
initialState: Partial<AppState>;
|
|
settingsWorkflowId: string | null;
|
|
workflowIdParam: string | undefined;
|
|
}) {
|
|
// Fire-and-forget: warm the backend PR cache for this workspace.
|
|
// The client will fetch the data after mount via useWorkspacePRs.
|
|
listWorkspaceTaskPRs(activeWorkspaceId, { cache: "no-store" }).catch(() => {});
|
|
|
|
const [workflowList, repositoriesResponse, quickChatResponse, quickTerminalResponse] =
|
|
await Promise.all([
|
|
listWorkflows(activeWorkspaceId, { cache: "no-store", includeHidden: true }),
|
|
listRepositories(activeWorkspaceId, undefined, { cache: "no-store" }).catch(() => ({
|
|
repositories: [],
|
|
})),
|
|
listQuickChatSessions(activeWorkspaceId, { cache: "no-store" }).catch(() => ({
|
|
sessions: [],
|
|
task_sessions: [],
|
|
})),
|
|
listQuickTerminalTabs(activeWorkspaceId, { cache: "no-store" }).catch(() => ({ tabs: [] })),
|
|
]);
|
|
// null preserves the user's explicit "All Workflows" choice.
|
|
const workflowId = resolveDesiredWorkflowId({
|
|
activeWorkflowId: workflowIdParam ?? null,
|
|
settingsWorkflowId,
|
|
workspaceWorkflows: workflowList.workflows,
|
|
});
|
|
const quickChatSessions = toQuickChatSessions(quickChatResponse.sessions);
|
|
const quickTerminalTabs = quickTerminalResponse.tabs.map(toQuickTerminalTab);
|
|
|
|
return {
|
|
workflowId,
|
|
initialState: {
|
|
...initialState,
|
|
userSettings: {
|
|
...(initialState.userSettings as AppState["userSettings"]),
|
|
workflowId,
|
|
},
|
|
workflows: {
|
|
items: workflowList.workflows.map((workflow) => ({
|
|
id: workflow.id,
|
|
workspaceId: workflow.workspace_id,
|
|
name: workflow.name,
|
|
hidden: workflow.hidden,
|
|
})),
|
|
activeId: workflowId,
|
|
},
|
|
repositories: {
|
|
itemsByWorkspaceId: { [activeWorkspaceId]: repositoriesResponse.repositories },
|
|
loadingByWorkspaceId: { [activeWorkspaceId]: false },
|
|
loadedByWorkspaceId: { [activeWorkspaceId]: true },
|
|
},
|
|
quickChat: {
|
|
isOpen: false,
|
|
sessions: quickChatSessions,
|
|
activeSessionId: null,
|
|
terminalTabs: quickTerminalTabs,
|
|
activeKind: "conversation" as const,
|
|
activeTerminalTabId: null,
|
|
lastTerminalTabIdByWorkspace: {},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function renderPageClient(
|
|
initialState: Partial<AppState>,
|
|
workspaceId: string,
|
|
taskId?: string,
|
|
sessionId?: string,
|
|
) {
|
|
return (
|
|
<>
|
|
<StateHydrator initialState={initialState} />
|
|
<PageClient workspaceId={workspaceId} initialTaskId={taskId} initialSessionId={sessionId} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function renderUnresolvedPage(initialState: Partial<AppState>) {
|
|
return (
|
|
<>
|
|
<StateHydrator initialState={initialState} />
|
|
<PageClient />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default async function Page({ searchParams }: PageProps) {
|
|
try {
|
|
const resolvedParams = searchParams ? await searchParams : {};
|
|
const workspaceId = resolveParam(resolvedParams.workspaceId);
|
|
const workflowIdParam = resolveParam(resolvedParams.workflowId);
|
|
const taskId = resolveParam(resolvedParams.taskId);
|
|
const sessionId = resolveParam(resolvedParams.sessionId);
|
|
|
|
const [workspaces, userSettingsResponse, cookieStore] = await Promise.all([
|
|
listWorkspaces({ cache: "no-store" }),
|
|
fetchUserSettings({ cache: "no-store" }).catch(() => null),
|
|
readCookies().catch((error) => {
|
|
console.error("Failed to read cookies on Kanban page:", error);
|
|
return null;
|
|
}),
|
|
]);
|
|
const settingsWorkspaceId = userSettingsResponse?.settings?.workspace_id || null;
|
|
const settingsWorkflowId = userSettingsResponse?.settings?.workflow_filter_id || null;
|
|
// The sidebar picker writes the selected workspace to this cookie so the
|
|
// choice survives a refresh even when userSettings is not updated on select.
|
|
// Kanban home only resolves against kanban workspaces; office workspaces
|
|
// belong under /office.
|
|
const cookieWorkspaceId = cookieStore?.get(ACTIVE_WORKSPACE_COOKIE)?.value ?? null;
|
|
// `readCookies()` is client-only in this code path; during SSR this is empty.
|
|
// Workspace selection still works because spa-routes.tsx re-hydrates from
|
|
// `readActiveWorkspaceCookie()` and the generic resolver on first client render.
|
|
const activeWorkspaceId = resolveActiveKanbanWorkspaceId(
|
|
workspaces.workspaces,
|
|
workspaceId,
|
|
cookieWorkspaceId,
|
|
settingsWorkspaceId,
|
|
);
|
|
|
|
const initialState = buildBaseState(workspaces, userSettingsResponse, activeWorkspaceId);
|
|
|
|
if (!activeWorkspaceId) {
|
|
return renderUnresolvedPage(initialState);
|
|
}
|
|
|
|
const workspaceState = await loadWorkspaceState({
|
|
activeWorkspaceId,
|
|
initialState,
|
|
settingsWorkflowId,
|
|
workflowIdParam,
|
|
});
|
|
|
|
if (!workspaceState.workflowId)
|
|
return renderPageClient(workspaceState.initialState, activeWorkspaceId);
|
|
|
|
const snapshotState = await loadSnapshotState(workspaceState.workflowId, taskId, sessionId);
|
|
return renderPageClient(
|
|
{ ...workspaceState.initialState, ...snapshotState },
|
|
activeWorkspaceId,
|
|
taskId,
|
|
sessionId,
|
|
);
|
|
} catch {
|
|
return <PageClient />;
|
|
}
|
|
}
|