1208 lines · 39.2 KB · TypeScript
RawDownload
import {  type Dispatch,  type ReactNode,  createContext,  createElement,  useContext,  useEffect,  useReducer,} from "react"; import { type ProviderEvent, type ProviderSession, type TerminalEvent, normalizeProjectScripts } from "@t3tools/contracts";import { resolveModelSlug } from "./model-logic";import { hydratePersistedState, toPersistedState } from "./persistenceSchema";import {  applyEventToMessages,  asObject,  asString,  deriveTurnDiffFilesFromUnifiedDiff,  deriveTurnDiffSummaries,  inferCheckpointTurnCountByTurnId,  evolveSession,} from "./session-logic";import {  type ChatAttachment,  DEFAULT_THREAD_TERMINAL_ID,  DEFAULT_RUNTIME_MODE,  MAX_THREAD_TERMINAL_COUNT,  type ProjectScript,  type Project,  type RuntimeMode,  type Thread,  type ThreadTerminalGroup,} from "./types"; // ── Actions ────────────────────────────────────────────────────────── type Action =  | { type: "ADD_PROJECT"; project: Project }  | { type: "SET_PROJECT_SCRIPTS"; projectId: string; scripts: ProjectScript[] }  | { type: "SYNC_PROJECTS"; projects: Project[] }  | { type: "TOGGLE_PROJECT"; projectId: string }  | { type: "DELETE_PROJECT"; projectId: string }  | { type: "ADD_THREAD"; thread: Thread }  | { type: "SET_ACTIVE_THREAD"; threadId: string }  | { type: "TOGGLE_THREAD_TERMINAL"; threadId: string }  | { type: "SET_THREAD_TERMINAL_OPEN"; threadId: string; open: boolean }  | { type: "SET_THREAD_TERMINAL_HEIGHT"; threadId: string; height: number }  | { type: "SPLIT_THREAD_TERMINAL"; threadId: string; terminalId: string }  | { type: "NEW_THREAD_TERMINAL"; threadId: string; terminalId: string }  | { type: "SET_THREAD_ACTIVE_TERMINAL"; threadId: string; terminalId: string }  | { type: "CLOSE_THREAD_TERMINAL"; threadId: string; terminalId: string }  | { type: "TOGGLE_DIFF" }  | {      type: "OPEN_DIFF";      threadId: string;      turnId?: string;      filePath?: string;    }  | {      type: "SET_DIFF_TARGET";      threadId: string;      turnId?: string;      filePath?: string;    }  | { type: "CLOSE_DIFF" }  | {      type: "APPLY_EVENT";      event: ProviderEvent;      activeAssistantItemRef: { current: string | null };    }  | { type: "APPLY_TERMINAL_EVENT"; event: TerminalEvent }  | { type: "UPDATE_SESSION"; threadId: string; session: ProviderSession }  | {      type: "PUSH_USER_MESSAGE";      threadId: string;      id: string;      text: string;      attachments?: ChatAttachment[];    }  | { type: "SET_ERROR"; threadId: string; error: string | null }  | { type: "SET_THREAD_TITLE"; threadId: string; title: string }  | { type: "SET_THREAD_MODEL"; threadId: string; model: string }  | {      type: "REVERT_TO_CHECKPOINT";      threadId: string;      sessionId: string;      threadRuntimeId: string;      turnCount: number;      messageCount: number;    }  | {      type: "SET_THREAD_TURN_CHECKPOINT_COUNTS";      threadId: string;      checkpointTurnCountByTurnId: Record<string, number>;    }  | {      type: "SET_THREAD_TURN_CHECKPOINT_DIFFS";      threadId: string;      checkpointDiffByTurnId: Record<string, string>;    }  | {      type: "SET_THREAD_BRANCH";      threadId: string;      branch: string | null;      worktreePath: string | null;    }  | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode }  | { type: "DELETE_THREAD"; threadId: string }; // ── State ──────────────────────────────────────────────────────────── export interface AppState {  projects: Project[];  threads: Thread[];  activeThreadId: string | null;  runtimeMode: RuntimeMode;  diffOpen: boolean;  diffThreadId: string | null;  diffTurnId: string | null;  diffFilePath: string | null;} const PERSISTED_STATE_KEY = "t3code:renderer-state:v7";const LEGACY_PERSISTED_STATE_KEYS = [  "t3code:renderer-state:v6",  "t3code:renderer-state:v5",  "t3code:renderer-state:v4",  "t3code:renderer-state:v3",  "codething:renderer-state:v4",  "codething:renderer-state:v3",  "codething:renderer-state:v2",  "codething:renderer-state:v1",] as const; const initialState: AppState = {  projects: [],  threads: [],  activeThreadId: null,  runtimeMode: DEFAULT_RUNTIME_MODE,  diffOpen: false,  diffThreadId: null,  diffTurnId: null,  diffFilePath: null,}; // ── Helpers ────────────────────────────────────────────────────────── function readPersistedState(): AppState {  if (typeof window === "undefined") return initialState;   try {    const rawCurrent = window.localStorage.getItem(PERSISTED_STATE_KEY);    const legacyValues = LEGACY_PERSISTED_STATE_KEYS.map((key) => window.localStorage.getItem(key));    const rawLegacy = legacyValues.find((value) => value !== null) ?? null;    const raw = rawCurrent ?? rawLegacy;    if (!raw) return initialState;    const rawCodethingV1 = window.localStorage.getItem("codething:renderer-state:v1");    const hydrated = hydratePersistedState(raw, !rawCurrent && raw === rawCodethingV1);    if (!hydrated) return initialState;     const threads = hydrated.threads.map((thread) => normalizeThreadTerminals(thread));    const activeThreadId = threads.some((thread) => thread.id === hydrated.activeThreadId)      ? hydrated.activeThreadId      : (threads[0]?.id ?? null);     return {      ...hydrated,      threads,      activeThreadId,      diffOpen: false,      diffThreadId: null,      diffTurnId: null,      diffFilePath: null,    };  } catch {    return initialState;  }} function persistState(state: AppState): void {  if (typeof window === "undefined") return;   try {    window.localStorage.setItem(PERSISTED_STATE_KEY, JSON.stringify(toPersistedState(state)));    for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) {      window.localStorage.removeItem(legacyKey);    }  } catch {    // Ignore quota/storage errors to avoid breaking chat UX.  }} function updateThread(  threads: Thread[],  threadId: string,  updater: (t: Thread) => Thread,): Thread[] {  return threads.map((t) => (t.id === threadId ? updater(t) : t));} function resetDiffTargetIfMissing(state: AppState, threads: Thread[]) {  if (!state.diffThreadId) {    return {      diffOpen: state.diffOpen,      diffThreadId: state.diffThreadId,      diffTurnId: state.diffTurnId,      diffFilePath: state.diffFilePath,    };  }  const hasThread = threads.some((thread) => thread.id === state.diffThreadId);  if (hasThread) {    return {      diffOpen: state.diffOpen,      diffThreadId: state.diffThreadId,      diffTurnId: state.diffTurnId,      diffFilePath: state.diffFilePath,    };  }  return {    diffOpen: false,    diffThreadId: null,    diffTurnId: null,    diffFilePath: null,  };} function mergeTurnDiffSummaries(  existing: Thread["turnDiffSummaries"],  next: Thread["turnDiffSummaries"],): Thread["turnDiffSummaries"] {  if (next.length === 0) return existing;   const existingByTurnId = new Map(existing.map((summary) => [summary.turnId, summary] as const));  const merged = next.map((summary) => {    const previous = existingByTurnId.get(summary.turnId);    if (!previous) {      return summary;    }     const prefersCheckpointDiff = previous.checkpointDiffLoaded || summary.checkpointDiffLoaded;    const files =      prefersCheckpointDiff ||      (summary.files.length === 0 && previous.files.length > 0 && summary.unifiedDiff === undefined)        ? previous.files        : summary.files;    const unifiedDiff =      prefersCheckpointDiff || summary.unifiedDiff === undefined        ? (previous.unifiedDiff ?? summary.unifiedDiff)        : summary.unifiedDiff;     return {      ...summary,      files,      ...(unifiedDiff !== undefined ? { unifiedDiff } : {}),      ...(summary.assistantMessageId        ? {}        : previous.assistantMessageId          ? { assistantMessageId: previous.assistantMessageId }          : {}),      ...(typeof summary.checkpointTurnCount === "number"        ? {}        : typeof previous.checkpointTurnCount === "number"          ? { checkpointTurnCount: previous.checkpointTurnCount }          : {}),      ...(summary.checkpointDiffLoaded || previous.checkpointDiffLoaded        ? { checkpointDiffLoaded: true }        : {}),    };  });   const mergedTurnIds = new Set(merged.map((summary) => summary.turnId));  for (const summary of existing) {    if (!mergedTurnIds.has(summary.turnId)) {      merged.push(summary);    }  }   const sorted = merged.toSorted((a, b) => {    const aTime = Date.parse(a.completedAt);    const bTime = Date.parse(b.completedAt);    if (Number.isNaN(aTime) || Number.isNaN(bTime)) {      return b.completedAt.localeCompare(a.completedAt);    }    return bTime - aTime;  });   const inferredTurnCountByTurnId = inferCheckpointTurnCountByTurnId(sorted);  return sorted.map((summary) =>    typeof summary.checkpointTurnCount === "number"      ? summary      : Object.assign({}, summary, {          checkpointTurnCount: inferredTurnCountByTurnId[summary.turnId],        }),  );} function normalizeTerminalIds(terminalIds: string[]): string[] {  const ids = terminalIds.map((id) => id.trim()).filter((id) => id.length > 0);  const unique = [...new Set(ids)].slice(0, MAX_THREAD_TERMINAL_COUNT);  if (unique.length > 0) {    return unique;  }  return [DEFAULT_THREAD_TERMINAL_ID];} function normalizeRunningTerminalIds(  runningTerminalIds: string[],  terminalIds: string[],): string[] {  if (runningTerminalIds.length === 0) return [];  const validTerminalIdSet = new Set(terminalIds);  return [...new Set(runningTerminalIds)]    .map((id) => id.trim())    .filter((id) => id.length > 0 && validTerminalIdSet.has(id));}  function normalizeTerminalGroupIds(terminalIds: string[]): string[] {  return [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))];} function fallbackGroupId(terminalId: string): string {  return `group-${terminalId}`;} function assignUniqueGroupId(groupId: string, usedGroupIds: Set<string>): string {  if (!usedGroupIds.has(groupId)) {    usedGroupIds.add(groupId);    return groupId;  }  let suffix = 2;  while (usedGroupIds.has(`${groupId}-${suffix}`)) {    suffix += 1;  }  const uniqueGroupId = `${groupId}-${suffix}`;  usedGroupIds.add(uniqueGroupId);  return uniqueGroupId;} function normalizeTerminalGroups(thread: Thread, terminalIds: string[]): ThreadTerminalGroup[] {  const validTerminalIdSet = new Set(terminalIds);  const assignedTerminalIds = new Set<string>();  const usedGroupIds = new Set<string>();  const groups: ThreadTerminalGroup[] = [];   for (const group of thread.terminalGroups) {    const groupTerminalIds = normalizeTerminalGroupIds(group.terminalIds).filter((terminalId) => {      if (!validTerminalIdSet.has(terminalId)) return false;      if (assignedTerminalIds.has(terminalId)) return false;      return true;    });    if (groupTerminalIds.length === 0) continue;    for (const terminalId of groupTerminalIds) {      assignedTerminalIds.add(terminalId);    }    const baseGroupId =      group.id.trim().length > 0        ? group.id.trim()        : fallbackGroupId(groupTerminalIds[0] ?? DEFAULT_THREAD_TERMINAL_ID);    groups.push({      id: assignUniqueGroupId(baseGroupId, usedGroupIds),      terminalIds: groupTerminalIds,    });  }   for (const terminalId of terminalIds) {    if (assignedTerminalIds.has(terminalId)) continue;    groups.push({      id: assignUniqueGroupId(fallbackGroupId(terminalId), usedGroupIds),      terminalIds: [terminalId],    });  }   if (groups.length > 0) {    return groups;  }   return [    {      id: fallbackGroupId(DEFAULT_THREAD_TERMINAL_ID),      terminalIds: [DEFAULT_THREAD_TERMINAL_ID],    },  ];} function findGroupIndexByTerminalId(  terminalGroups: ThreadTerminalGroup[],  terminalId: string,): number {  return terminalGroups.findIndex((group) => group.terminalIds.includes(terminalId));} function normalizeThreadTerminals(thread: Thread): Thread {  const terminalIds = normalizeTerminalIds(thread.terminalIds);  const activeTerminalId = terminalIds.includes(thread.activeTerminalId)    ? thread.activeTerminalId    : (terminalIds[0] ?? DEFAULT_THREAD_TERMINAL_ID);  const terminalGroups = normalizeTerminalGroups(thread, terminalIds);  const activeGroupIndexFromId = terminalGroups.findIndex(    (group) => group.id === thread.activeTerminalGroupId,  );  const activeGroupIndexFromTerminal = findGroupIndexByTerminalId(terminalGroups, activeTerminalId);  const activeGroupIndex =    activeGroupIndexFromId >= 0      ? activeGroupIndexFromId      : activeGroupIndexFromTerminal >= 0        ? activeGroupIndexFromTerminal        : 0;  const activeTerminalGroupId =    terminalGroups[activeGroupIndex]?.id ??    terminalGroups[0]?.id ??    fallbackGroupId(activeTerminalId);   return {    ...thread,    terminalIds,    runningTerminalIds: normalizeRunningTerminalIds(thread.runningTerminalIds, terminalIds),    activeTerminalId,    terminalGroups,    activeTerminalGroupId,  };} function closeThreadTerminal(thread: Thread, terminalId: string): Thread {  if (!thread.terminalIds.includes(terminalId)) {    return thread;  }   const remainingTerminalIds = thread.terminalIds.filter((id) => id !== terminalId);  if (remainingTerminalIds.length === 0) {    const nextTerminalGroupId = fallbackGroupId(DEFAULT_THREAD_TERMINAL_ID);    return normalizeThreadTerminals({      ...thread,      terminalOpen: false,      terminalIds: [DEFAULT_THREAD_TERMINAL_ID],      runningTerminalIds: [],      activeTerminalId: DEFAULT_THREAD_TERMINAL_ID,      terminalGroups: [        {          id: nextTerminalGroupId,          terminalIds: [DEFAULT_THREAD_TERMINAL_ID],        },      ],      activeTerminalGroupId: nextTerminalGroupId,    });  }   const closedTerminalIndex = thread.terminalIds.indexOf(terminalId);  const closedTerminalGroup = thread.terminalGroups.find((group) =>    group.terminalIds.includes(terminalId),  );  const closedTerminalGroupIndex = closedTerminalGroup    ? closedTerminalGroup.terminalIds.indexOf(terminalId)    : -1;  const remainingTerminalsInClosedGroup = (closedTerminalGroup?.terminalIds ?? []).filter(    (id) => id !== terminalId,  );  const nextActiveTerminalId =    thread.activeTerminalId === terminalId      ? (remainingTerminalsInClosedGroup[          Math.min(closedTerminalGroupIndex, remainingTerminalsInClosedGroup.length - 1)        ] ??        remainingTerminalIds[Math.min(closedTerminalIndex, remainingTerminalIds.length - 1)] ??        remainingTerminalIds[0] ??        DEFAULT_THREAD_TERMINAL_ID)      : thread.activeTerminalId;  const nextTerminalGroups = thread.terminalGroups    .map((group) => ({      ...group,      terminalIds: group.terminalIds.filter((id) => id !== terminalId),    }))    .filter((group) => group.terminalIds.length > 0);   return normalizeThreadTerminals({    ...thread,    terminalIds: remainingTerminalIds,    runningTerminalIds: thread.runningTerminalIds.filter((id) => id !== terminalId),    activeTerminalId: nextActiveTerminalId,    terminalGroups: nextTerminalGroups,  });} function findThreadBySessionId(threads: Thread[], sessionId: string): Thread | undefined {  return threads.find((t) => t.session?.sessionId === sessionId);} function getEventTurnId(event: ProviderEvent): string | undefined {  if (event.turnId) return event.turnId;  const payload = asObject(event.payload);  const turn = asObject(payload?.turn);  return asString(turn?.id);} function getEventThreadId(event: ProviderEvent): string | undefined {  if (event.threadId) return event.threadId;  const payload = asObject(event.payload);  const payloadThread = asObject(payload?.thread);  const payloadMessage = asObject(payload?.msg);  return (    asString(payload?.threadId) ??

Lines 1–500 of 1208.