817 lines · 26.6 KB · TypeScript
RawDownload
import {  type AssetCreateUrlInput,  type AssetCreateUrlResult,  type ChatFileAttachment,  type EnvironmentId,  isProviderDriverKind,  ProjectId,  type MessageId,  type ModelSelection,  type ProviderInteractionMode,  type ProviderDriverKind,  type ServerProvider,  type ScopedProjectRef,  type ScopedThreadRef,  type ThreadId,  type TurnId,} from "@t3tools/contracts";import { resolveAssetUrl } from "@t3tools/client-runtime/state/assets";import {  squashAtomCommandFailure,  type AtomCommandResult,} from "@t3tools/client-runtime/state/runtime";import { videoMimeType } from "@t3tools/shared/video";import {  appendCodexArtifactTemplateUsePrompt,  codexArtifactTemplateUsePrompt,  type CodexArtifactTemplate,} from "@t3tools/client-runtime/codex-artifact-templates";import {  type ChatMessage,  isImageAttachment,  type SessionPhase,  type Thread,  type ThreadShell,} from "../types";import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore";import * as Schema from "effect/Schema";import { appAtomRegistry } from "../rpc/atomRegistry";import { environmentThreadDetails } from "../state/threads";import {  filterTerminalContextsWithText,  stripInlineTerminalContextPlaceholders,  type TerminalContextDraft,} from "../lib/terminalContext";import type { DraftThreadEnvMode } from "../composerDraftStore";import type { ComposerSubmissionIntent } from "../composer-logic";import type { TimelineEntry } from "../session-logic";import type { DesktopPreviewOverlay } from "../previewStateStore";import type { RightPanelSurface } from "../rightPanelStore"; export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";export const MAX_HIDDEN_MOUNTED_TERMINAL_THREADS = 10;export const MAX_HIDDEN_MOUNTED_PREVIEW_THREADS = 3;export const ENVIRONMENT_RECONNECT_WARNING_GRACE_MS = 2_000; export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String); export function agentControlledBrowserCloseConfirmation(  surfaces: readonly RightPanelSurface[],  desktopByTabId: Readonly<Record<string, Pick<DesktopPreviewOverlay, "controller"> | undefined>>,): string | null {  const activeBrowserCount = surfaces.filter(    (surface) =>      surface.kind === "preview" &&      surface.resourceId !== null &&      desktopByTabId[surface.resourceId]?.controller === "agent",  ).length;  if (activeBrowserCount === 0) return null;  if (activeBrowserCount === 1) {    return [      "Close browser while the agent is using it?",      "The agent is actively controlling this browser. Closing it may interrupt the current browser action.",    ].join("\n");  }  return [    `Close ${activeBrowserCount} browsers while the agent is using them?`,    "The agent is actively controlling these browsers. Closing them may interrupt the current browser actions.",  ].join("\n");} export function shouldOpenProactivePullRequest(  previousTargetKey: string | null | undefined,  targetKey: string | null,): boolean {  return previousTargetKey !== undefined && targetKey !== null && targetKey !== previousTargetKey;} export function shouldOpenProactiveTurnDiff(input: {  previousRunningTurnId: TurnId | null | undefined;  runningTurnId: TurnId | null;  settledTurnId: TurnId | null;  turnCompleted: boolean;}): boolean {  return (    input.previousRunningTurnId !== undefined &&    input.previousRunningTurnId !== null &&    input.runningTurnId === null &&    input.turnCompleted &&    input.settledTurnId === input.previousRunningTurnId  );} export function codexArtifactTemplatePromptToAppend(  currentDraft: string,  template: CodexArtifactTemplate,): string | null {  return appendCodexArtifactTemplateUsePrompt(currentDraft, template) === currentDraft    ? null    : codexArtifactTemplateUsePrompt(template);} export function shouldDockDraftHeroForSubmission(input: {  isDraftHeroState: boolean;  activeThreadKey: string | null;  submissionIntent: ComposerSubmissionIntent;}): boolean {  return (    input.submissionIntent === "foreground" &&    input.isDraftHeroState &&    input.activeThreadKey !== null  );} export function shouldReleaseTimelineAnchorForToolActivity(input: {  anchorMessageId: MessageId | null;  liveFollowEnabled: boolean;  runningTurnId: TurnId | null;  timelineEntries: ReadonlyArray<TimelineEntry>;}): boolean {  if (input.anchorMessageId === null || !input.liveFollowEnabled || input.runningTurnId === null) {    return false;  }   return input.timelineEntries.some((timelineEntry) => {    if (timelineEntry.kind !== "work" || timelineEntry.entry.turnId !== input.runningTurnId) {      return false;    }     const entry = timelineEntry.entry;    return (      entry.tone === "tool" ||      entry.itemType !== undefined ||      entry.requestKind !== undefined ||      (entry.command?.trim().length ?? 0) > 0    );  });} export function toolGroupConsumesUpwardNavigation(target: EventTarget | null): boolean {  const elementTarget = target instanceof Element ? target : null;  const group = elementTarget?.closest<HTMLElement>("[data-tool-group-scroll]");  if (!group) return false;   // A nested result or the group itself can consume an upward scroll.  for (let element = elementTarget; element; element = element.parentElement) {    if (element.scrollTop > 0) {      const overflowY = getComputedStyle(element).overflowY;      if (overflowY === "auto" || overflowY === "scroll") return true;    }    if (element === group) break;  }  return false;} export function resolveDraftHeroState(input: {  isLocalDraftThread: boolean;  hasTimelineEntries: boolean;  isWorking: boolean;  draftHeroDockRequested: boolean;  backgroundSubmissionPending: boolean;}): boolean {  if (input.backgroundSubmissionPending) {    return true;  }  return (    input.isLocalDraftThread &&    !input.hasTimelineEntries &&    !input.isWorking &&    !input.draftHeroDockRequested  );} export function resolveDraftPromotionNavigationTarget(input: {  serverThreadRef: ScopedThreadRef | null;  serverThread: Pick<Thread, "latestTurn" | "session"> | null | undefined;  backgroundSubmissionPending: boolean;}): ScopedThreadRef | null {  if (input.backgroundSubmissionPending) {    return null;  }  const sessionStatus = input.serverThread?.session?.status;  const turnStarted = input.serverThread?.latestTurn?.startedAt != null;  const startupStopped =    sessionStatus === "error" || sessionStatus === "stopped" || sessionStatus === "interrupted";  // Keep local preparation feedback mounted until the server can render the  // running turn or its startup error on the canonical thread route.  return turnStarted || startupStopped ? input.serverThreadRef : null;} export function scheduleEnvironmentReconnectWarning(showWarning: () => void): () => void {  const timeoutId = globalThis.setTimeout(showWarning, ENVIRONMENT_RECONNECT_WARNING_GRACE_MS);  return () => globalThis.clearTimeout(timeoutId);} export function hasEnvironmentReconnectWarningGraceElapsed(  activeEnvironmentId: EnvironmentId | null,  elapsedEnvironmentId: EnvironmentId | null,): boolean {  return activeEnvironmentId !== null && activeEnvironmentId === elapsedEnvironmentId;} export function startNewThreadForProject(  projectRef: ScopedProjectRef | null,  handleNewThread: (projectRef: ScopedProjectRef) => Promise<unknown>,): boolean {  if (projectRef === null) return false;  void handleNewThread(projectRef);   return true;} export function resolveThreadMetadataUpdateForNextTurn(input: {  currentModelSelection: ModelSelection;  nextModelSelection?: ModelSelection;  currentBranch: string | null;  nextBranch?: string;}): {  modelSelection?: ModelSelection;  branch?: string;  worktreePath?: null;} | null {  const nextModelSelection = input.nextModelSelection;  const modelSelectionChanged =    nextModelSelection !== undefined &&    (nextModelSelection.model !== input.currentModelSelection.model ||      nextModelSelection.instanceId !== input.currentModelSelection.instanceId ||      JSON.stringify(nextModelSelection.options ?? null) !==        JSON.stringify(input.currentModelSelection.options ?? null));  const branchChanged = input.nextBranch !== undefined && input.nextBranch !== input.currentBranch;  if (!modelSelectionChanged && !branchChanged) {    return null;  }  return {    ...(modelSelectionChanged ? { modelSelection: nextModelSelection } : {}),    ...(branchChanged ? { branch: input.nextBranch, worktreePath: null } : {}),  };} export function buildLocalDraftThread(  threadId: ThreadId,  draftThread: DraftThreadState,  fallbackModelSelection: ModelSelection,): Thread {  return {    id: threadId,    environmentId: draftThread.environmentId,    projectId: draftThread.projectId,    title: "New thread",    modelSelection: fallbackModelSelection,    runtimeMode: draftThread.runtimeMode,    interactionMode: draftThread.interactionMode,    session: null,    messages: [],    createdAt: draftThread.createdAt,    updatedAt: draftThread.createdAt,    archivedAt: null,    settledOverride: null,    settledAt: null,    deletedAt: null,    latestTurn: null,    branch: draftThread.branch,    worktreePath: draftThread.worktreePath,    checkpoints: [],    activities: [],    proposedPlans: [],  };} export function buildLoadingThreadFromShell(shell: ThreadShell): Thread {  return {    ...shell,    messages: [],    proposedPlans: [],    activities: [],    checkpoints: [],    deletedAt: null,  };} export function shouldWriteThreadErrorToCurrentServerThread(input: {  activeServerThread:    | {        environmentId: EnvironmentId;        id: ThreadId;      }    | null    | undefined;  routeThreadRef: ScopedThreadRef;  targetThreadId: ThreadId;}): boolean {  return Boolean(    input.activeServerThread &&    input.targetThreadId === input.routeThreadRef.threadId &&    input.activeServerThread.environmentId === input.routeThreadRef.environmentId &&    input.activeServerThread.id === input.targetThreadId,  );} export function buildThreadTurnInterruptInput(thread: Pick<Thread, "id" | "session">): {  threadId: ThreadId;  turnId?: TurnId;} {  const runningTurnId = thread.session?.status === "running" ? thread.session.activeTurnId : null;  return {    threadId: thread.id,    ...(runningTurnId !== null ? { turnId: runningTurnId } : {}),  };} export function reconcileMountedTerminalThreadIds(input: {  currentThreadIds: ReadonlyArray<string>;  openThreadIds: ReadonlyArray<string>;  activeThreadId: string | null;  activeThreadTerminalOpen: boolean;  maxHiddenThreadCount?: number;}): string[] {  return reconcileRetainedMountedThreadIds({    currentThreadIds: input.currentThreadIds,    openThreadIds: input.openThreadIds,    activeThreadId: input.activeThreadId,    activeThreadOpen: input.activeThreadTerminalOpen,    maxHiddenThreadCount: input.maxHiddenThreadCount ?? MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,  });} export function reconcileRetainedMountedThreadIds(input: {  currentThreadIds: ReadonlyArray<string>;  openThreadIds: ReadonlyArray<string>;  activeThreadId: string | null;  activeThreadOpen: boolean;  maxHiddenThreadCount: number;  retainInactiveActiveThread?: boolean;}): string[] {  const openThreadIdSet = new Set(input.openThreadIds);  const hiddenThreadIds = input.currentThreadIds.filter(    (threadId) =>      (threadId !== input.activeThreadId || input.retainInactiveActiveThread === true) &&      openThreadIdSet.has(threadId),  );  const maxHiddenThreadCount = Math.max(0, input.maxHiddenThreadCount);  const nextThreadIds =    hiddenThreadIds.length > maxHiddenThreadCount      ? hiddenThreadIds.slice(-maxHiddenThreadCount)      : hiddenThreadIds;   if (    input.activeThreadId &&    input.activeThreadOpen &&    !nextThreadIds.includes(input.activeThreadId)  ) {    nextThreadIds.push(input.activeThreadId);  }   return nextThreadIds;} export function revokeBlobPreviewUrl(previewUrl: string | undefined): void {  if (!previewUrl || typeof URL === "undefined" || !previewUrl.startsWith("blob:")) {    return;  }  URL.revokeObjectURL(previewUrl);} /** Signs an attachment URL without reading its bytes, so video playback can request byte ranges. */export async function resolveFileAttachmentUrl(input: {  attachment: ChatFileAttachment;  environmentId: EnvironmentId;  httpBaseUrl: string;  createAssetUrl: (input: {    environmentId: EnvironmentId;    input: AssetCreateUrlInput;  }) => Promise<AtomCommandResult<AssetCreateUrlResult, unknown>>;}): Promise<string> {  const { attachment } = input;  const result = await input.createAssetUrl({    environmentId: input.environmentId,    input: {      resource: {        _tag: "attachment",        attachmentId: attachment.id,        fileName: attachment.name,        mimeType: videoMimeType(attachment) ?? attachment.mimeType,      },    },  });  if (result._tag === "Failure") throw squashAtomCommandFailure(result);  const url = resolveAssetUrl(input.httpBaseUrl, result.value.relativeUrl);  if (url === null) throw new Error("The environment returned an invalid attachment URL.");  return url;} export function revokeUserMessagePreviewUrls(message: ChatMessage): void {  if (message.role !== "user" || !message.attachments) {    return;  }  for (const attachment of message.attachments) {    if (!isImageAttachment(attachment)) {      continue;    }    revokeBlobPreviewUrl(attachment.previewUrl);  }} export function collectUserMessageBlobPreviewUrls(message: ChatMessage): string[] {  if (message.role !== "user" || !message.attachments) {    return [];  }  const previewUrls: string[] = [];  for (const attachment of message.attachments) {    if (!isImageAttachment(attachment)) continue;    if (!attachment.previewUrl || !attachment.previewUrl.startsWith("blob:")) continue;    previewUrls.push(attachment.previewUrl);  }  return previewUrls;} export interface PullRequestDialogState {  initialReference: string | null;  key: number;} export function readFileAsDataUrl(file: File): Promise<string> {  return new Promise((resolve, reject) => {    const reader = new FileReader();    reader.addEventListener("load", () => {      if (typeof reader.result === "string") {        resolve(reader.result);        return;      }      reject(new Error("Could not read image data."));    });    reader.addEventListener("error", () => {      reject(reader.error ?? new Error("Failed to read image."));    });    reader.readAsDataURL(file);  });} export function resolveSendEnvMode(input: {  requestedEnvMode: DraftThreadEnvMode;  isGitRepo: boolean;}): DraftThreadEnvMode {  return input.isGitRepo ? input.requestedEnvMode : "local";} export function resolveBackgroundDraftWorkspaceOptions(input: {  envMode: DraftThreadEnvMode;  branch: string | null;  startFromOrigin: boolean;}): {  envMode: DraftThreadEnvMode;  branch: string | null;  worktreePath: null;  startFromOrigin: boolean;} {  return {    envMode: input.envMode,    branch: input.branch,    worktreePath: null,    startFromOrigin: input.envMode === "worktree" && input.startFromOrigin,  };} export function cloneComposerImageForRetry(  image: ComposerImageAttachment,): ComposerImageAttachment {  if (typeof URL === "undefined" || !image.previewUrl.startsWith("blob:")) {    return image;  }  try {    return {      ...image,      previewUrl: URL.createObjectURL(image.file),    };  } catch {    return image;  }} export function deriveComposerSendState(options: {  prompt: string;  imageCount: number;  terminalContexts: ReadonlyArray<TerminalContextDraft>;  /**   * Optional element-pick attachment count. Element contexts contribute to   * "sendable content" exactly like images and (text-bearing) terminal   * contexts do: a prompt of just element chips is still a valid send.   */  elementContextCount?: number;}): {

Lines 1–500 of 817.