1161 lines · 40.0 KB · TypeScript
RawDownload
import {  type OrchestrationEvent,  type OrchestrationMessage,  type OrchestrationProposedPlan,  type ProjectId,  type ProviderKind,  ThreadId,  type OrchestrationReadModel,  type OrchestrationSession,  type OrchestrationCheckpointSummary,  type OrchestrationThread,  type OrchestrationSessionStatus,} from "@t3tools/contracts";import { resolveModelSlugForProvider } from "@t3tools/shared/model";import { create } from "zustand";import {  findLatestProposedPlan,  hasActionableProposedPlan,  derivePendingApprovals,  derivePendingUserInputs,} from "./session-logic";import { sanitizeThreadErrorMessage } from "./rpc/transportError";import { type ChatMessage, type Project, type SidebarThreadSummary, type Thread } from "./types"; // ── State ──────────────────────────────────────────────────────────── export interface AppState {  projects: Project[];  threads: Thread[];  sidebarThreadsById: Record<string, SidebarThreadSummary>;  threadIdsByProjectId: Record<string, ThreadId[]>;  bootstrapComplete: boolean;} const initialState: AppState = {  projects: [],  threads: [],  sidebarThreadsById: {},  threadIdsByProjectId: {},  bootstrapComplete: false,};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[] = []; // ── Pure helpers ────────────────────────────────────────────────────── function updateThread(  threads: Thread[],  threadId: ThreadId,  updater: (t: Thread) => Thread,): Thread[] {  let changed = false;  const next = threads.map((t) => {    if (t.id !== threadId) return t;    const updated = updater(t);    if (updated !== t) changed = true;    return updated;  });  return changed ? next : threads;} function updateProject(  projects: Project[],  projectId: Project["id"],  updater: (project: Project) => Project,): Project[] {  let changed = false;  const next = projects.map((project) => {    if (project.id !== projectId) {      return project;    }    const updated = updater(project);    if (updated !== project) {      changed = true;    }    return updated;  });  return changed ? next : projects;} 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): Thread["session"] {  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(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: toAttachmentPreviewUrl(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): Thread["proposedPlans"][number] {  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,): Thread["turnDiffSummaries"][number] {  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 mapThread(thread: OrchestrationThread): Thread {  return {    id: thread.id,    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(mapMessage),    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 mapProject(project: OrchestrationReadModel["projects"][number]): Project {  return {    id: project.id,    name: project.title,    cwd: project.workspaceRoot,    defaultModelSelection: project.defaultModelSelection      ? normalizeModelSelection(project.defaultModelSelection)      : null,    createdAt: project.createdAt,    updatedAt: project.updatedAt,    scripts: mapProjectScripts(project.scripts),  };} function getLatestUserMessageAt(  messages: ReadonlyArray<Thread["messages"][number]>,): 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,    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 appendThreadIdByProjectId(  threadIdsByProjectId: Record<string, ThreadId[]>,  projectId: ProjectId,  threadId: ThreadId,): Record<string, ThreadId[]> {  const existingThreadIds = threadIdsByProjectId[projectId] ?? EMPTY_THREAD_IDS;  if (existingThreadIds.includes(threadId)) {    return threadIdsByProjectId;  }  return {    ...threadIdsByProjectId,    [projectId]: [...existingThreadIds, threadId],  };} function removeThreadIdByProjectId(  threadIdsByProjectId: Record<string, ThreadId[]>,  projectId: ProjectId,  threadId: ThreadId,): Record<string, ThreadId[]> {  const existingThreadIds = threadIdsByProjectId[projectId] ?? EMPTY_THREAD_IDS;  if (!existingThreadIds.includes(threadId)) {    return threadIdsByProjectId;  }  const nextThreadIds = existingThreadIds.filter(    (existingThreadId) => existingThreadId !== threadId,  );  if (nextThreadIds.length === existingThreadIds.length) {    return threadIdsByProjectId;  }  if (nextThreadIds.length === 0) {    const nextThreadIdsByProjectId = { ...threadIdsByProjectId };    delete nextThreadIdsByProjectId[projectId];    return nextThreadIdsByProjectId;  }  return {    ...threadIdsByProjectId,    [projectId]: nextThreadIds,  };} function buildThreadIdsByProjectId(threads: ReadonlyArray<Thread>): Record<string, ThreadId[]> {  const threadIdsByProjectId: Record<string, ThreadId[]> = {};  for (const thread of threads) {    const existingThreadIds = threadIdsByProjectId[thread.projectId] ?? EMPTY_THREAD_IDS;    threadIdsByProjectId[thread.projectId] = [...existingThreadIds, thread.id];  }  return threadIdsByProjectId;} function buildSidebarThreadsById(  threads: ReadonlyArray<Thread>,): Record<string, SidebarThreadSummary> {  return Object.fromEntries(    threads.map((thread) => [thread.id, buildSidebarThreadSummary(thread)]),  );} function checkpointStatusToLatestTurnState(status: "ready" | "missing" | "error") {  if (status === "error") {    return "error" as const;  }  if (status === "missing") {    return "interrupted" as const;  }  return "completed" as const;} function compareActivities(  left: Thread["activities"][number],  right: Thread["activities"][number],): number {  if (left.sequence !== undefined && right.sequence !== undefined) {    if (left.sequence !== right.sequence) {      return left.sequence - right.sequence;    }  } else if (left.sequence !== undefined) {    return 1;  } else if (right.sequence !== undefined) {    return -1;  }   return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);} function buildLatestTurn(params: {  previous: Thread["latestTurn"];  turnId: NonNullable<Thread["latestTurn"]>["turnId"];  state: NonNullable<Thread["latestTurn"]>["state"];  requestedAt: string;  startedAt: string | null;  completedAt: string | null;  assistantMessageId: NonNullable<Thread["latestTurn"]>["assistantMessageId"];  sourceProposedPlan?: Thread["pendingSourceProposedPlan"];}): NonNullable<Thread["latestTurn"]> {  const resolvedPlan =    params.previous?.turnId === params.turnId      ? params.previous.sourceProposedPlan      : params.sourceProposedPlan;  return {    turnId: params.turnId,    state: params.state,    requestedAt: params.requestedAt,    startedAt: params.startedAt,    completedAt: params.completedAt,    assistantMessageId: params.assistantMessageId,    ...(resolvedPlan ? { sourceProposedPlan: resolvedPlan } : {}),  };} function rebindTurnDiffSummariesForAssistantMessage(  turnDiffSummaries: ReadonlyArray<Thread["turnDiffSummaries"][number]>,  turnId: Thread["turnDiffSummaries"][number]["turnId"],  assistantMessageId: NonNullable<Thread["latestTurn"]>["assistantMessageId"],): Thread["turnDiffSummaries"] {  let changed = false;  const nextSummaries = turnDiffSummaries.map((summary) => {    if (summary.turnId !== turnId || summary.assistantMessageId === assistantMessageId) {      return summary;    }    changed = true;    return {      ...summary,      assistantMessageId: assistantMessageId ?? undefined,    };  });  return changed ? nextSummaries : [...turnDiffSummaries];} function retainThreadMessagesAfterRevert(  messages: ReadonlyArray<ChatMessage>,  retainedTurnIds: ReadonlySet<string>,  turnCount: number,): ChatMessage[] {  const retainedMessageIds = new Set<string>();  for (const message of messages) {    if (message.role === "system") {      retainedMessageIds.add(message.id);      continue;    }    if (      message.turnId !== undefined &&      message.turnId !== null &&      retainedTurnIds.has(message.turnId)    ) {      retainedMessageIds.add(message.id);    }  }   const retainedUserCount = messages.filter(    (message) => message.role === "user" && retainedMessageIds.has(message.id),  ).length;  const missingUserCount = Math.max(0, turnCount - retainedUserCount);  if (missingUserCount > 0) {    const fallbackUserMessages = messages      .filter(        (message) =>          message.role === "user" &&          !retainedMessageIds.has(message.id) &&          (message.turnId === undefined ||            message.turnId === null ||            retainedTurnIds.has(message.turnId)),      )      .toSorted(        (left, right) =>          left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id),      )      .slice(0, missingUserCount);    for (const message of fallbackUserMessages) {      retainedMessageIds.add(message.id);    }  }   const retainedAssistantCount = messages.filter(    (message) => message.role === "assistant" && retainedMessageIds.has(message.id),  ).length;  const missingAssistantCount = Math.max(0, turnCount - retainedAssistantCount);  if (missingAssistantCount > 0) {    const fallbackAssistantMessages = messages      .filter(        (message) =>          message.role === "assistant" &&          !retainedMessageIds.has(message.id) &&          (message.turnId === undefined ||            message.turnId === null ||            retainedTurnIds.has(message.turnId)),      )      .toSorted(        (left, right) =>          left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id),      )      .slice(0, missingAssistantCount);    for (const message of fallbackAssistantMessages) {      retainedMessageIds.add(message.id);    }  }   return messages.filter((message) => retainedMessageIds.has(message.id));} function retainThreadActivitiesAfterRevert(  activities: ReadonlyArray<Thread["activities"][number]>,  retainedTurnIds: ReadonlySet<string>,): Thread["activities"] {  return activities.filter(    (activity) => activity.turnId === null || retainedTurnIds.has(activity.turnId),  );} function retainThreadProposedPlansAfterRevert(  proposedPlans: ReadonlyArray<Thread["proposedPlans"][number]>,  retainedTurnIds: ReadonlySet<string>,): Thread["proposedPlans"] {  return proposedPlans.filter(    (proposedPlan) => proposedPlan.turnId === null || retainedTurnIds.has(proposedPlan.turnId),  );} function toLegacySessionStatus(  status: OrchestrationSessionStatus,): "connecting" | "ready" | "running" | "error" | "closed" {  switch (status) {    case "starting":      return "connecting";    case "running":      return "running";    case "error":      return "error";    case "ready":    case "interrupted":      return "ready";    case "idle":    case "stopped":      return "closed";  }} function toLegacyProvider(providerName: string | null): ProviderKind {  if (providerName === "codex" || providerName === "claudeAgent") {    return providerName;  }  return "codex";

Lines 1–500 of 1161.