import type { EnvironmentId, MessageId, OrchestrationCheckpointSummary, OrchestrationEvent, OrchestrationMessage, OrchestrationProposedPlan, OrchestrationReadModel, OrchestrationSession, OrchestrationSessionStatus, OrchestrationThread, OrchestrationThreadActivity, ProjectId, ProviderKind, ScopedProjectRef, ScopedThreadRef, ThreadId, TurnId,} from "@t3tools/contracts";import { resolveModelSlugForProvider } from "@t3tools/shared/model";import { create } from "zustand";import { derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan,} from "./session-logic";import { type ChatMessage, type Project, type ProposedPlan, type SidebarThreadSummary, type Thread, type ThreadSession, type ThreadShell, type ThreadTurnState, type TurnDiffSummary,} from "./types";import { resolveEnvironmentHttpUrl } from "./environments/runtime";import { sanitizeThreadErrorMessage } from "./rpc/transportError"; export interface EnvironmentState { projectIds: ProjectId[]; projectById: Record<ProjectId, Project>; threadIds: ThreadId[]; threadIdsByProjectId: Record<ProjectId, ThreadId[]>; threadShellById: Record<ThreadId, ThreadShell>; threadSessionById: Record<ThreadId, ThreadSession | null>; threadTurnStateById: Record<ThreadId, ThreadTurnState>; messageIdsByThreadId: Record<ThreadId, MessageId[]>; messageByThreadId: Record<ThreadId, Record<MessageId, ChatMessage>>; activityIdsByThreadId: Record<ThreadId, string[]>; activityByThreadId: Record<ThreadId, Record<string, OrchestrationThreadActivity>>; proposedPlanIdsByThreadId: Record<ThreadId, string[]>; proposedPlanByThreadId: Record<ThreadId, Record<string, ProposedPlan>>; turnDiffIdsByThreadId: Record<ThreadId, TurnId[]>; turnDiffSummaryByThreadId: Record<ThreadId, Record<TurnId, TurnDiffSummary>>; sidebarThreadSummaryById: Record<ThreadId, SidebarThreadSummary>; bootstrapComplete: boolean;} export interface AppState { activeEnvironmentId: EnvironmentId | null; environmentStateById: Record<string, EnvironmentState>;} const initialEnvironmentState: EnvironmentState = { projectIds: [], projectById: {}, threadIds: [], threadIdsByProjectId: {}, threadShellById: {}, threadSessionById: {}, threadTurnStateById: {}, messageIdsByThreadId: {}, messageByThreadId: {}, activityIdsByThreadId: {}, activityByThreadId: {}, proposedPlanIdsByThreadId: {}, proposedPlanByThreadId: {}, turnDiffIdsByThreadId: {}, turnDiffSummaryByThreadId: {}, sidebarThreadSummaryById: {}, bootstrapComplete: false,}; const initialState: AppState = { activeEnvironmentId: null, environmentStateById: {},}; const MAX_THREAD_MESSAGES = 2_000;const MAX_THREAD_CHECKPOINTS = 500;const MAX_THREAD_PROPOSED_PLANS = 200;const MAX_THREAD_ACTIVITIES = 500;const EMPTY_THREAD_IDS: ThreadId[] = [];const EMPTY_MESSAGE_IDS: MessageId[] = [];const EMPTY_ACTIVITY_IDS: string[] = [];const EMPTY_PROPOSED_PLAN_IDS: string[] = [];const EMPTY_TURN_IDS: TurnId[] = [];const EMPTY_MESSAGES: ChatMessage[] = [];const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = [];const EMPTY_PROPOSED_PLANS: ProposedPlan[] = [];const EMPTY_TURN_DIFF_SUMMARIES: TurnDiffSummary[] = [];const EMPTY_MESSAGE_MAP: Record<MessageId, ChatMessage> = {};const EMPTY_ACTIVITY_MAP: Record<string, OrchestrationThreadActivity> = {};const EMPTY_PROPOSED_PLAN_MAP: Record<string, ProposedPlan> = {};const EMPTY_TURN_DIFF_MAP: Record<TurnId, TurnDiffSummary> = {};const EMPTY_THREAD_TURN_STATE: ThreadTurnState = Object.freeze({ latestTurn: null }); function arraysEqual<T>(left: readonly T[], right: readonly T[]): boolean { return left.length === right.length && left.every((value, index) => value === right[index]);} function normalizeModelSelection<T extends { provider: "codex" | "claudeAgent"; model: string }>( selection: T,): T { return { ...selection, model: resolveModelSlugForProvider(selection.provider, selection.model), };} function mapProjectScripts(scripts: ReadonlyArray<Project["scripts"][number]>): Project["scripts"] { return scripts.map((script) => ({ ...script }));} function mapSession(session: OrchestrationSession): ThreadSession { return { provider: toLegacyProvider(session.providerName), status: toLegacySessionStatus(session.status), orchestrationStatus: session.status, activeTurnId: session.activeTurnId ?? undefined, createdAt: session.updatedAt, updatedAt: session.updatedAt, ...(session.lastError ? { lastError: session.lastError } : {}), };} function mapMessage(environmentId: EnvironmentId, message: OrchestrationMessage): ChatMessage { const attachments = message.attachments?.map((attachment) => ({ type: "image" as const, id: attachment.id, name: attachment.name, mimeType: attachment.mimeType, sizeBytes: attachment.sizeBytes, previewUrl: resolveEnvironmentHttpUrl({ environmentId, pathname: attachmentPreviewRoutePath(attachment.id), }), })); return { id: message.id, role: message.role, text: message.text, turnId: message.turnId, createdAt: message.createdAt, streaming: message.streaming, ...(message.streaming ? {} : { completedAt: message.updatedAt }), ...(attachments && attachments.length > 0 ? { attachments } : {}), };} function mapProposedPlan(proposedPlan: OrchestrationProposedPlan): ProposedPlan { return { id: proposedPlan.id, turnId: proposedPlan.turnId, planMarkdown: proposedPlan.planMarkdown, implementedAt: proposedPlan.implementedAt, implementationThreadId: proposedPlan.implementationThreadId, createdAt: proposedPlan.createdAt, updatedAt: proposedPlan.updatedAt, };} function mapTurnDiffSummary(checkpoint: OrchestrationCheckpointSummary): TurnDiffSummary { return { turnId: checkpoint.turnId, completedAt: checkpoint.completedAt, status: checkpoint.status, assistantMessageId: checkpoint.assistantMessageId ?? undefined, checkpointTurnCount: checkpoint.checkpointTurnCount, checkpointRef: checkpoint.checkpointRef, files: checkpoint.files.map((file) => ({ ...file })), };} function mapProject( project: OrchestrationReadModel["projects"][number], environmentId: EnvironmentId,): Project { return { id: project.id, environmentId, name: project.title, cwd: project.workspaceRoot, repositoryIdentity: project.repositoryIdentity ?? null, defaultModelSelection: project.defaultModelSelection ? normalizeModelSelection(project.defaultModelSelection) : null, createdAt: project.createdAt, updatedAt: project.updatedAt, scripts: mapProjectScripts(project.scripts), };} function mapThread(thread: OrchestrationThread, environmentId: EnvironmentId): Thread { return { id: thread.id, environmentId, codexThreadId: null, projectId: thread.projectId, title: thread.title, modelSelection: normalizeModelSelection(thread.modelSelection), runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, session: thread.session ? mapSession(thread.session) : null, messages: thread.messages.map((message) => mapMessage(environmentId, message)), proposedPlans: thread.proposedPlans.map(mapProposedPlan), error: sanitizeThreadErrorMessage(thread.session?.lastError), createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, latestTurn: thread.latestTurn, pendingSourceProposedPlan: thread.latestTurn?.sourceProposedPlan, branch: thread.branch, worktreePath: thread.worktreePath, turnDiffSummaries: thread.checkpoints.map(mapTurnDiffSummary), activities: thread.activities.map((activity) => ({ ...activity })), };} function toThreadShell(thread: Thread): ThreadShell { return { id: thread.id, environmentId: thread.environmentId, codexThreadId: thread.codexThreadId, projectId: thread.projectId, title: thread.title, modelSelection: thread.modelSelection, runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, error: thread.error, createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, branch: thread.branch, worktreePath: thread.worktreePath, };} function toThreadTurnState(thread: Thread): ThreadTurnState { return { latestTurn: thread.latestTurn, ...(thread.pendingSourceProposedPlan ? { pendingSourceProposedPlan: thread.pendingSourceProposedPlan } : {}), };} function getLatestUserMessageAt(messages: ReadonlyArray<ChatMessage>): string | null { let latestUserMessageAt: string | null = null; for (const message of messages) { if (message.role !== "user") { continue; } if (latestUserMessageAt === null || message.createdAt > latestUserMessageAt) { latestUserMessageAt = message.createdAt; } } return latestUserMessageAt;} function buildSidebarThreadSummary(thread: Thread): SidebarThreadSummary { return { id: thread.id, environmentId: thread.environmentId, projectId: thread.projectId, title: thread.title, interactionMode: thread.interactionMode, session: thread.session, createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, latestTurn: thread.latestTurn, branch: thread.branch, worktreePath: thread.worktreePath, latestUserMessageAt: getLatestUserMessageAt(thread.messages), hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, hasActionableProposedPlan: hasActionableProposedPlan( findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), ), };} function sidebarThreadSummariesEqual( left: SidebarThreadSummary | undefined, right: SidebarThreadSummary,): boolean { return ( left !== undefined && left.id === right.id && left.projectId === right.projectId && left.title === right.title && left.interactionMode === right.interactionMode && left.session === right.session && left.createdAt === right.createdAt && left.archivedAt === right.archivedAt && left.updatedAt === right.updatedAt && left.latestTurn === right.latestTurn && left.branch === right.branch && left.worktreePath === right.worktreePath && left.latestUserMessageAt === right.latestUserMessageAt && left.hasPendingApprovals === right.hasPendingApprovals && left.hasPendingUserInput === right.hasPendingUserInput && left.hasActionableProposedPlan === right.hasActionableProposedPlan );} function threadShellsEqual(left: ThreadShell | undefined, right: ThreadShell): boolean { return ( left !== undefined && left.id === right.id && left.environmentId === right.environmentId && left.codexThreadId === right.codexThreadId && left.projectId === right.projectId && left.title === right.title && left.modelSelection === right.modelSelection && left.runtimeMode === right.runtimeMode && left.interactionMode === right.interactionMode && left.error === right.error && left.createdAt === right.createdAt && left.archivedAt === right.archivedAt && left.updatedAt === right.updatedAt && left.branch === right.branch && left.worktreePath === right.worktreePath );} function threadTurnStatesEqual(left: ThreadTurnState | undefined, right: ThreadTurnState): boolean { return ( left !== undefined && left.latestTurn === right.latestTurn && left.pendingSourceProposedPlan === right.pendingSourceProposedPlan );} function appendId<T extends string>(ids: readonly T[], id: T): T[] { return ids.includes(id) ? [...ids] : [...ids, id];} function removeId<T extends string>(ids: readonly T[], id: T): T[] { return ids.filter((value) => value !== id);} function buildMessageSlice(thread: Thread): { ids: MessageId[]; byId: Record<MessageId, ChatMessage>;} { return { ids: thread.messages.map((message) => message.id), byId: Object.fromEntries( thread.messages.map((message) => [message.id, message] as const), ) as Record<MessageId, ChatMessage>, };} function buildActivitySlice(thread: Thread): { ids: string[]; byId: Record<string, OrchestrationThreadActivity>;} { return { ids: thread.activities.map((activity) => activity.id), byId: Object.fromEntries( thread.activities.map((activity) => [activity.id, activity] as const), ) as Record<string, OrchestrationThreadActivity>, };} function buildProposedPlanSlice(thread: Thread): { ids: string[]; byId: Record<string, ProposedPlan>;} { return { ids: thread.proposedPlans.map((plan) => plan.id), byId: Object.fromEntries( thread.proposedPlans.map((plan) => [plan.id, plan] as const), ) as Record<string, ProposedPlan>, };} function buildTurnDiffSlice(thread: Thread): { ids: TurnId[]; byId: Record<TurnId, TurnDiffSummary>;} { return { ids: thread.turnDiffSummaries.map((summary) => summary.turnId), byId: Object.fromEntries( thread.turnDiffSummaries.map((summary) => [summary.turnId, summary] as const), ) as Record<TurnId, TurnDiffSummary>, };} function selectThreadMessages(state: EnvironmentState, threadId: ThreadId): ChatMessage[] { const ids = state.messageIdsByThreadId[threadId] ?? EMPTY_MESSAGE_IDS; const byId = state.messageByThreadId[threadId] ?? EMPTY_MESSAGE_MAP; if (ids.length === 0) { return EMPTY_MESSAGES; } return ids.flatMap((id) => { const message = byId[id]; return message ? [message] : []; });} function selectThreadActivities( state: EnvironmentState, threadId: ThreadId,): OrchestrationThreadActivity[] { const ids = state.activityIdsByThreadId[threadId] ?? EMPTY_ACTIVITY_IDS; const byId = state.activityByThreadId[threadId] ?? EMPTY_ACTIVITY_MAP; if (ids.length === 0) { return EMPTY_ACTIVITIES; } return ids.flatMap((id) => { const activity = byId[id]; return activity ? [activity] : []; });} function selectThreadProposedPlans(state: EnvironmentState, threadId: ThreadId): ProposedPlan[] { const ids = state.proposedPlanIdsByThreadId[threadId] ?? EMPTY_PROPOSED_PLAN_IDS; const byId = state.proposedPlanByThreadId[threadId] ?? EMPTY_PROPOSED_PLAN_MAP; if (ids.length === 0) { return EMPTY_PROPOSED_PLANS; } return ids.flatMap((id) => { const plan = byId[id]; return plan ? [plan] : []; });} function selectThreadTurnDiffSummaries( state: EnvironmentState, threadId: ThreadId,): TurnDiffSummary[] { const ids = state.turnDiffIdsByThreadId[threadId] ?? EMPTY_TURN_IDS; const byId = state.turnDiffSummaryByThreadId[threadId] ?? EMPTY_TURN_DIFF_MAP; if (ids.length === 0) { return EMPTY_TURN_DIFF_SUMMARIES; } return ids.flatMap((id) => { const summary = byId[id]; return summary ? [summary] : []; });} function getThread(state: EnvironmentState, threadId: ThreadId): Thread | undefined { const shell = state.threadShellById[threadId]; if (!shell) { return undefined; } const turnState = state.threadTurnStateById[threadId] ?? EMPTY_THREAD_TURN_STATE; return { ...shell, session: state.threadSessionById[threadId] ?? null, latestTurn: turnState.latestTurn, pendingSourceProposedPlan: turnState.pendingSourceProposedPlan, messages: selectThreadMessages(state, threadId), activities: selectThreadActivities(state, threadId), proposedPlans: selectThreadProposedPlans(state, threadId), turnDiffSummaries: selectThreadTurnDiffSummaries(state, threadId), };} function getProjects(state: EnvironmentState): Project[] { return state.projectIds.flatMap((projectId) => { const project = state.projectById[projectId]; return project ? [project] : []; });} function getThreads(state: EnvironmentState): Thread[] { return state.threadIds.flatMap((threadId) => { const thread = getThread(state, threadId); return thread ? [thread] : []; });} function writeThreadState( state: EnvironmentState, nextThread: Thread, previousThread?: Thread,): EnvironmentState { const nextShell = toThreadShell(nextThread); const nextTurnState = toThreadTurnState(nextThread); const previousShell = state.threadShellById[nextThread.id]; const previousTurnState = state.threadTurnStateById[nextThread.id];Lines 1–500 of 1790. Load more
import type { EnvironmentId, MessageId, OrchestrationCheckpointSummary, OrchestrationEvent, OrchestrationMessage, OrchestrationProposedPlan, OrchestrationReadModel, OrchestrationSession, OrchestrationSessionStatus, OrchestrationThread, OrchestrationThreadActivity, ProjectId, ProviderKind, ScopedProjectRef, ScopedThreadRef, ThreadId, TurnId, } from "@t3tools/contracts"; import { resolveModelSlugForProvider } from "@t3tools/shared/model"; import { create } from "zustand"; import { derivePendingApprovals, derivePendingUserInputs, findLatestProposedPlan, hasActionableProposedPlan, } from "./session-logic"; import { type ChatMessage, type Project, type ProposedPlan, type SidebarThreadSummary, type Thread, type ThreadSession, type ThreadShell, type ThreadTurnState, type TurnDiffSummary, } from "./types"; import { resolveEnvironmentHttpUrl } from "./environments/runtime"; import { sanitizeThreadErrorMessage } from "./rpc/transportError"; export interface EnvironmentState { projectIds: ProjectId[]; projectById: Record<ProjectId, Project>; threadIds: ThreadId[]; threadIdsByProjectId: Record<ProjectId, ThreadId[]>; threadShellById: Record<ThreadId, ThreadShell>; threadSessionById: Record<ThreadId, ThreadSession | null>; threadTurnStateById: Record<ThreadId, ThreadTurnState>; messageIdsByThreadId: Record<ThreadId, MessageId[]>; messageByThreadId: Record<ThreadId, Record<MessageId, ChatMessage>>; activityIdsByThreadId: Record<ThreadId, string[]>; activityByThreadId: Record<ThreadId, Record<string, OrchestrationThreadActivity>>; proposedPlanIdsByThreadId: Record<ThreadId, string[]>; proposedPlanByThreadId: Record<ThreadId, Record<string, ProposedPlan>>; turnDiffIdsByThreadId: Record<ThreadId, TurnId[]>; turnDiffSummaryByThreadId: Record<ThreadId, Record<TurnId, TurnDiffSummary>>; sidebarThreadSummaryById: Record<ThreadId, SidebarThreadSummary>; bootstrapComplete: boolean; } export interface AppState { activeEnvironmentId: EnvironmentId | null; environmentStateById: Record<string, EnvironmentState>; } const initialEnvironmentState: EnvironmentState = { projectIds: [], projectById: {}, threadIds: [], threadIdsByProjectId: {}, threadShellById: {}, threadSessionById: {}, threadTurnStateById: {}, messageIdsByThreadId: {}, messageByThreadId: {}, activityIdsByThreadId: {}, activityByThreadId: {}, proposedPlanIdsByThreadId: {}, proposedPlanByThreadId: {}, turnDiffIdsByThreadId: {}, turnDiffSummaryByThreadId: {}, sidebarThreadSummaryById: {}, bootstrapComplete: false, }; const initialState: AppState = { activeEnvironmentId: null, environmentStateById: {}, }; const MAX_THREAD_MESSAGES = 2_000; const MAX_THREAD_CHECKPOINTS = 500; const MAX_THREAD_PROPOSED_PLANS = 200; const MAX_THREAD_ACTIVITIES = 500; const EMPTY_THREAD_IDS: ThreadId[] = []; const EMPTY_MESSAGE_IDS: MessageId[] = []; const EMPTY_ACTIVITY_IDS: string[] = []; const EMPTY_PROPOSED_PLAN_IDS: string[] = []; const EMPTY_TURN_IDS: TurnId[] = []; const EMPTY_MESSAGES: ChatMessage[] = []; const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = []; const EMPTY_PROPOSED_PLANS: ProposedPlan[] = []; const EMPTY_TURN_DIFF_SUMMARIES: TurnDiffSummary[] = []; const EMPTY_MESSAGE_MAP: Record<MessageId, ChatMessage> = {}; const EMPTY_ACTIVITY_MAP: Record<string, OrchestrationThreadActivity> = {}; const EMPTY_PROPOSED_PLAN_MAP: Record<string, ProposedPlan> = {}; const EMPTY_TURN_DIFF_MAP: Record<TurnId, TurnDiffSummary> = {}; const EMPTY_THREAD_TURN_STATE: ThreadTurnState = Object.freeze({ latestTurn: null }); function arraysEqual<T>(left: readonly T[], right: readonly T[]): boolean { return left.length === right.length && left.every((value, index) => value === right[index]); } function normalizeModelSelection<T extends { provider: "codex" | "claudeAgent"; model: string }>( selection: T, ): T { return { ...selection, model: resolveModelSlugForProvider(selection.provider, selection.model), }; } function mapProjectScripts(scripts: ReadonlyArray<Project["scripts"][number]>): Project["scripts"] { return scripts.map((script) => ({ ...script })); } function mapSession(session: OrchestrationSession): ThreadSession { return { provider: toLegacyProvider(session.providerName), status: toLegacySessionStatus(session.status), orchestrationStatus: session.status, activeTurnId: session.activeTurnId ?? undefined, createdAt: session.updatedAt, updatedAt: session.updatedAt, ...(session.lastError ? { lastError: session.lastError } : {}), }; } function mapMessage(environmentId: EnvironmentId, message: OrchestrationMessage): ChatMessage { const attachments = message.attachments?.map((attachment) => ({ type: "image" as const, id: attachment.id, name: attachment.name, mimeType: attachment.mimeType, sizeBytes: attachment.sizeBytes, previewUrl: resolveEnvironmentHttpUrl({ environmentId, pathname: attachmentPreviewRoutePath(attachment.id), }), })); return { id: message.id, role: message.role, text: message.text, turnId: message.turnId, createdAt: message.createdAt, streaming: message.streaming, ...(message.streaming ? {} : { completedAt: message.updatedAt }), ...(attachments && attachments.length > 0 ? { attachments } : {}), }; } function mapProposedPlan(proposedPlan: OrchestrationProposedPlan): ProposedPlan { return { id: proposedPlan.id, turnId: proposedPlan.turnId, planMarkdown: proposedPlan.planMarkdown, implementedAt: proposedPlan.implementedAt, implementationThreadId: proposedPlan.implementationThreadId, createdAt: proposedPlan.createdAt, updatedAt: proposedPlan.updatedAt, }; } function mapTurnDiffSummary(checkpoint: OrchestrationCheckpointSummary): TurnDiffSummary { return { turnId: checkpoint.turnId, completedAt: checkpoint.completedAt, status: checkpoint.status, assistantMessageId: checkpoint.assistantMessageId ?? undefined, checkpointTurnCount: checkpoint.checkpointTurnCount, checkpointRef: checkpoint.checkpointRef, files: checkpoint.files.map((file) => ({ ...file })), }; } function mapProject( project: OrchestrationReadModel["projects"][number], environmentId: EnvironmentId, ): Project { return { id: project.id, environmentId, name: project.title, cwd: project.workspaceRoot, repositoryIdentity: project.repositoryIdentity ?? null, defaultModelSelection: project.defaultModelSelection ? normalizeModelSelection(project.defaultModelSelection) : null, createdAt: project.createdAt, updatedAt: project.updatedAt, scripts: mapProjectScripts(project.scripts), }; } function mapThread(thread: OrchestrationThread, environmentId: EnvironmentId): Thread { return { id: thread.id, environmentId, codexThreadId: null, projectId: thread.projectId, title: thread.title, modelSelection: normalizeModelSelection(thread.modelSelection), runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, session: thread.session ? mapSession(thread.session) : null, messages: thread.messages.map((message) => mapMessage(environmentId, message)), proposedPlans: thread.proposedPlans.map(mapProposedPlan), error: sanitizeThreadErrorMessage(thread.session?.lastError), createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, latestTurn: thread.latestTurn, pendingSourceProposedPlan: thread.latestTurn?.sourceProposedPlan, branch: thread.branch, worktreePath: thread.worktreePath, turnDiffSummaries: thread.checkpoints.map(mapTurnDiffSummary), activities: thread.activities.map((activity) => ({ ...activity })), }; } function toThreadShell(thread: Thread): ThreadShell { return { id: thread.id, environmentId: thread.environmentId, codexThreadId: thread.codexThreadId, projectId: thread.projectId, title: thread.title, modelSelection: thread.modelSelection, runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, error: thread.error, createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, branch: thread.branch, worktreePath: thread.worktreePath, }; } function toThreadTurnState(thread: Thread): ThreadTurnState { return { latestTurn: thread.latestTurn, ...(thread.pendingSourceProposedPlan ? { pendingSourceProposedPlan: thread.pendingSourceProposedPlan } : {}), }; } function getLatestUserMessageAt(messages: ReadonlyArray<ChatMessage>): string | null { let latestUserMessageAt: string | null = null; for (const message of messages) { if (message.role !== "user") { continue; } if (latestUserMessageAt === null || message.createdAt > latestUserMessageAt) { latestUserMessageAt = message.createdAt; } } return latestUserMessageAt; } function buildSidebarThreadSummary(thread: Thread): SidebarThreadSummary { return { id: thread.id, environmentId: thread.environmentId, projectId: thread.projectId, title: thread.title, interactionMode: thread.interactionMode, session: thread.session, createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, latestTurn: thread.latestTurn, branch: thread.branch, worktreePath: thread.worktreePath, latestUserMessageAt: getLatestUserMessageAt(thread.messages), hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, hasActionableProposedPlan: hasActionableProposedPlan( findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), ), }; } function sidebarThreadSummariesEqual( left: SidebarThreadSummary | undefined, right: SidebarThreadSummary, ): boolean { return ( left !== undefined && left.id === right.id && left.projectId === right.projectId && left.title === right.title && left.interactionMode === right.interactionMode && left.session === right.session && left.createdAt === right.createdAt && left.archivedAt === right.archivedAt && left.updatedAt === right.updatedAt && left.latestTurn === right.latestTurn && left.branch === right.branch && left.worktreePath === right.worktreePath && left.latestUserMessageAt === right.latestUserMessageAt && left.hasPendingApprovals === right.hasPendingApprovals && left.hasPendingUserInput === right.hasPendingUserInput && left.hasActionableProposedPlan === right.hasActionableProposedPlan ); } function threadShellsEqual(left: ThreadShell | undefined, right: ThreadShell): boolean { return ( left !== undefined && left.id === right.id && left.environmentId === right.environmentId && left.codexThreadId === right.codexThreadId && left.projectId === right.projectId && left.title === right.title && left.modelSelection === right.modelSelection && left.runtimeMode === right.runtimeMode && left.interactionMode === right.interactionMode && left.error === right.error && left.createdAt === right.createdAt && left.archivedAt === right.archivedAt && left.updatedAt === right.updatedAt && left.branch === right.branch && left.worktreePath === right.worktreePath ); } function threadTurnStatesEqual(left: ThreadTurnState | undefined, right: ThreadTurnState): boolean { return ( left !== undefined && left.latestTurn === right.latestTurn && left.pendingSourceProposedPlan === right.pendingSourceProposedPlan ); } function appendId<T extends string>(ids: readonly T[], id: T): T[] { return ids.includes(id) ? [...ids] : [...ids, id]; } function removeId<T extends string>(ids: readonly T[], id: T): T[] { return ids.filter((value) => value !== id); } function buildMessageSlice(thread: Thread): { ids: MessageId[]; byId: Record<MessageId, ChatMessage>; } { return { ids: thread.messages.map((message) => message.id), byId: Object.fromEntries( thread.messages.map((message) => [message.id, message] as const), ) as Record<MessageId, ChatMessage>, }; } function buildActivitySlice(thread: Thread): { ids: string[]; byId: Record<string, OrchestrationThreadActivity>; } { return { ids: thread.activities.map((activity) => activity.id), byId: Object.fromEntries( thread.activities.map((activity) => [activity.id, activity] as const), ) as Record<string, OrchestrationThreadActivity>, }; } function buildProposedPlanSlice(thread: Thread): { ids: string[]; byId: Record<string, ProposedPlan>; } { return { ids: thread.proposedPlans.map((plan) => plan.id), byId: Object.fromEntries( thread.proposedPlans.map((plan) => [plan.id, plan] as const), ) as Record<string, ProposedPlan>, }; } function buildTurnDiffSlice(thread: Thread): { ids: TurnId[]; byId: Record<TurnId, TurnDiffSummary>; } { return { ids: thread.turnDiffSummaries.map((summary) => summary.turnId), byId: Object.fromEntries( thread.turnDiffSummaries.map((summary) => [summary.turnId, summary] as const), ) as Record<TurnId, TurnDiffSummary>, }; } function selectThreadMessages(state: EnvironmentState, threadId: ThreadId): ChatMessage[] { const ids = state.messageIdsByThreadId[threadId] ?? EMPTY_MESSAGE_IDS; const byId = state.messageByThreadId[threadId] ?? EMPTY_MESSAGE_MAP; if (ids.length === 0) { return EMPTY_MESSAGES; } return ids.flatMap((id) => { const message = byId[id]; return message ? [message] : []; }); } function selectThreadActivities( state: EnvironmentState, threadId: ThreadId, ): OrchestrationThreadActivity[] { const ids = state.activityIdsByThreadId[threadId] ?? EMPTY_ACTIVITY_IDS; const byId = state.activityByThreadId[threadId] ?? EMPTY_ACTIVITY_MAP; if (ids.length === 0) { return EMPTY_ACTIVITIES; } return ids.flatMap((id) => { const activity = byId[id]; return activity ? [activity] : []; }); } function selectThreadProposedPlans(state: EnvironmentState, threadId: ThreadId): ProposedPlan[] { const ids = state.proposedPlanIdsByThreadId[threadId] ?? EMPTY_PROPOSED_PLAN_IDS; const byId = state.proposedPlanByThreadId[threadId] ?? EMPTY_PROPOSED_PLAN_MAP; if (ids.length === 0) { return EMPTY_PROPOSED_PLANS; } return ids.flatMap((id) => { const plan = byId[id]; return plan ? [plan] : []; }); } function selectThreadTurnDiffSummaries( state: EnvironmentState, threadId: ThreadId, ): TurnDiffSummary[] { const ids = state.turnDiffIdsByThreadId[threadId] ?? EMPTY_TURN_IDS; const byId = state.turnDiffSummaryByThreadId[threadId] ?? EMPTY_TURN_DIFF_MAP; if (ids.length === 0) { return EMPTY_TURN_DIFF_SUMMARIES; } return ids.flatMap((id) => { const summary = byId[id]; return summary ? [summary] : []; }); } function getThread(state: EnvironmentState, threadId: ThreadId): Thread | undefined { const shell = state.threadShellById[threadId]; if (!shell) { return undefined; } const turnState = state.threadTurnStateById[threadId] ?? EMPTY_THREAD_TURN_STATE; return { ...shell, session: state.threadSessionById[threadId] ?? null, latestTurn: turnState.latestTurn, pendingSourceProposedPlan: turnState.pendingSourceProposedPlan, messages: selectThreadMessages(state, threadId), activities: selectThreadActivities(state, threadId), proposedPlans: selectThreadProposedPlans(state, threadId), turnDiffSummaries: selectThreadTurnDiffSummaries(state, threadId), }; } function getProjects(state: EnvironmentState): Project[] { return state.projectIds.flatMap((projectId) => { const project = state.projectById[projectId]; return project ? [project] : []; }); } function getThreads(state: EnvironmentState): Thread[] { return state.threadIds.flatMap((threadId) => { const thread = getThread(state, threadId); return thread ? [thread] : []; }); } function writeThreadState( state: EnvironmentState, nextThread: Thread, previousThread?: Thread, ): EnvironmentState { const nextShell = toThreadShell(nextThread); const nextTurnState = toThreadTurnState(nextThread); const previousShell = state.threadShellById[nextThread.id]; const previousTurnState = state.threadTurnStateById[nextThread.id];