1964 lines · 65.2 KB · TypeScript
RawDownload
import type {  EnvironmentId,  MessageId,  OrchestrationCheckpointSummary,  OrchestrationEvent,  OrchestrationLatestTurn,  OrchestrationMessage,  OrchestrationProposedPlan,  OrchestrationReadModel,  OrchestrationShellSnapshot,  OrchestrationShellStreamEvent,  OrchestrationSession,  OrchestrationSessionStatus,  OrchestrationThread,  OrchestrationThreadShell,  OrchestrationThreadActivity,  ProjectId,  ScopedProjectRef,  ScopedThreadRef,} from "@t3tools/contracts";import { ProviderKind } from "@t3tools/contracts";import type { ThreadId, TurnId } from "@t3tools/contracts";import { Schema } from "effect";import { resolveModelSlugForProvider } from "@t3tools/shared/model";import { create } from "zustand";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";import { getThreadFromEnvironmentState } from "./threadDerivation"; export interface EnvironmentState {  projectIds: ProjectId[];  projectById: Record<ProjectId, Project>;   // ---------------------------------------------------------------------------  // Thread bookkeeping — written by BOTH shell stream and detail stream.  // Both streams ensure the thread is registered here; the bookkeeping is  // additive (append-only IDs) so concurrent writes are safe.  // ---------------------------------------------------------------------------  threadIds: ThreadId[];  threadIdsByProjectId: Record<ProjectId, ThreadId[]>;   // ---------------------------------------------------------------------------  // Thread shell / session / turn — written by BOTH shell stream and detail  // stream.  The shell stream is the *authoritative* source (server pre-  // computes these from the projection pipeline), but the detail stream also  // writes them so the active thread has up-to-date state even if the shell  // event hasn't arrived yet.  Structural equality checks in both write  // functions prevent unnecessary React re-renders when both streams deliver  // equivalent data.  // ---------------------------------------------------------------------------  threadShellById: Record<ThreadId, ThreadShell>;  threadSessionById: Record<ThreadId, ThreadSession | null>;  threadTurnStateById: Record<ThreadId, ThreadTurnState>;   // ---------------------------------------------------------------------------  // Thread detail content — written ONLY by the detail stream  // (writeThreadState / syncServerThreadDetail).  The shell stream never  // touches these.  // ---------------------------------------------------------------------------  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>>;   // ---------------------------------------------------------------------------  // Sidebar summary — written ONLY by the shell stream  // (writeThreadShellState / mapThreadShell).  Pre-computed server-side with  // fields like latestUserMessageAt, hasPendingApprovals, etc.  The detail  // stream must NOT write here; the shell stream is the single source of  // truth for sidebar data.  // ---------------------------------------------------------------------------  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[] = []; 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: ProviderKind; 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]    | OrchestrationShellSnapshot["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 mapThreadShell(  thread: OrchestrationThreadShell,  environmentId: EnvironmentId,): {  shell: ThreadShell;  session: ThreadSession | null;  turnState: ThreadTurnState;  summary: SidebarThreadSummary;} {  const shell: ThreadShell = {    id: thread.id,    environmentId,    codexThreadId: null,    projectId: thread.projectId,    title: thread.title,    modelSelection: normalizeModelSelection(thread.modelSelection),    runtimeMode: thread.runtimeMode,    interactionMode: thread.interactionMode,    error: sanitizeThreadErrorMessage(thread.session?.lastError),    createdAt: thread.createdAt,    archivedAt: thread.archivedAt,    updatedAt: thread.updatedAt,    branch: thread.branch,    worktreePath: thread.worktreePath,  };  const session = thread.session ? mapSession(thread.session) : null;  const turnState: ThreadTurnState = {    latestTurn: thread.latestTurn,    pendingSourceProposedPlan: thread.latestTurn?.sourceProposedPlan,  };  const summary: SidebarThreadSummary = {    id: thread.id,    environmentId,    projectId: thread.projectId,    title: thread.title,    interactionMode: thread.interactionMode,    session,    createdAt: thread.createdAt,    archivedAt: thread.archivedAt,    updatedAt: thread.updatedAt,    latestTurn: thread.latestTurn,    branch: thread.branch,    worktreePath: thread.worktreePath,    latestUserMessageAt: thread.latestUserMessageAt,    hasPendingApprovals: thread.hasPendingApprovals,    hasPendingUserInput: thread.hasPendingUserInput,    hasActionableProposedPlan: thread.hasActionableProposedPlan,  };  return {    shell,    session,    turnState,    summary,  };} 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 sourceProposedPlansEqual(  left: OrchestrationLatestTurn["sourceProposedPlan"] | undefined,  right: OrchestrationLatestTurn["sourceProposedPlan"] | undefined,): boolean {  if (left === right) return true;  if (left === undefined || right === undefined) return false;  return left.threadId === right.threadId && left.planId === right.planId;} function latestTurnsEqual(  left: OrchestrationLatestTurn | null | undefined,  right: OrchestrationLatestTurn | null | undefined,): boolean {  if (left === right) return true;  if (left == null || right == null) return false;  return (    left.turnId === right.turnId &&    left.state === right.state &&    left.requestedAt === right.requestedAt &&    left.startedAt === right.startedAt &&    left.completedAt === right.completedAt &&    left.assistantMessageId === right.assistantMessageId &&    sourceProposedPlansEqual(left.sourceProposedPlan, right.sourceProposedPlan)  );} function threadSessionsEqual(  left: ThreadSession | null | undefined,  right: ThreadSession | null | undefined,): boolean {  if (left === right) return true;  if (left == null || right == null) return false;  return (    left.provider === right.provider &&    left.status === right.status &&    left.orchestrationStatus === right.orchestrationStatus &&    left.activeTurnId === right.activeTurnId &&    left.createdAt === right.createdAt &&    left.updatedAt === right.updatedAt &&    left.lastError === right.lastError  );} 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 &&    threadSessionsEqual(left.session, right.session) &&    left.createdAt === right.createdAt &&    left.archivedAt === right.archivedAt &&    left.updatedAt === right.updatedAt &&    latestTurnsEqual(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 &&    latestTurnsEqual(left.latestTurn, right.latestTurn) &&    sourceProposedPlansEqual(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 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 = getThreadFromEnvironmentState(state, threadId);    return thread ? [thread] : [];  });}

Lines 1–500 of 1964.