904 lines · 24.5 KB · TypeScript
RawDownload
import type { NativeApi, ProviderEvent, ProviderKind, ProviderSession } from "@t3tools/contracts";import type { ChatMessage, SessionPhase } from "./types";import { createWsNativeApi } from "./wsNativeApi"; export const PROVIDER_OPTIONS: Array<{  value: ProviderKind;  label: string;  available: boolean;}> = [  { value: "codex", label: "Codex", available: true },  { value: "claudeCode", label: "Claude Code (soon)", available: false },]; let cachedApi: NativeApi | undefined; export function readNativeApi(): NativeApi | undefined {  if (typeof window === "undefined") return undefined;  if (cachedApi) return cachedApi;   // Prefer Electron preload bridge if available  if (window.nativeApi) {    cachedApi = window.nativeApi;    return cachedApi;  }   // Fall back to WebSocket transport  cachedApi = createWsNativeApi();  return cachedApi;} export function asObject(value: unknown): Record<string, unknown> | undefined {  if (!value || typeof value !== "object") return undefined;  return value as Record<string, unknown>;} export function asString(value: unknown): string | undefined {  return typeof value === "string" ? value : undefined;} export function formatTimestamp(isoDate: string): string {  return new Intl.DateTimeFormat(undefined, {    hour: "numeric",    minute: "2-digit",    second: "2-digit",  }).format(new Date(isoDate));} export function formatDuration(durationMs: number): string {  if (!Number.isFinite(durationMs) || durationMs < 0) return "0ms";  if (durationMs < 1_000) return `${Math.max(1, Math.round(durationMs))}ms`;  if (durationMs < 10_000) return `${(durationMs / 1_000).toFixed(1)}s`;  if (durationMs < 60_000) return `${Math.round(durationMs / 1_000)}s`;   const minutes = Math.floor(durationMs / 60_000);  const seconds = Math.round((durationMs % 60_000) / 1_000);  if (seconds === 0) return `${minutes}m`;  if (seconds === 60) return `${minutes + 1}m`;  return `${minutes}m ${seconds}s`;} export function formatElapsed(startIso: string, endIso: string | undefined): string | null {  if (!endIso) return null;   const startedAt = Date.parse(startIso);  const endedAt = Date.parse(endIso);  if (Number.isNaN(startedAt) || Number.isNaN(endedAt) || endedAt < startedAt) {    return null;  }   return formatDuration(endedAt - startedAt);} export interface WorkLogEntry {  id: string;  createdAt: string;  label: string;  detail?: string;  tone: "thinking" | "tool" | "info" | "error";} export interface PendingApproval {  requestId: string;  requestKind: "command" | "file-change";  createdAt: string;  detail?: string;} export type TimelineEntry =  | {      id: string;      kind: "message";      createdAt: string;      message: ChatMessage;    }  | {      id: string;      kind: "work";      createdAt: string;      entry: WorkLogEntry;    }; export interface TurnDiffFileChange {  path: string;  kind?: string;  diff?: string;} export interface TurnDiffSummary {  turnId: string;  completedAt: string;  status?: string;  files: TurnDiffFileChange[];  unifiedDiff?: string;  assistantMessageId?: string;} function normalizeDetail(value: string | undefined): string | undefined {  if (!value) return undefined;  const trimmed = value.trim();  return trimmed.length > 0 ? trimmed : undefined;} function asArray(value: unknown): unknown[] | undefined {  return Array.isArray(value) ? value : undefined;} function approvalDetail(event: ProviderEvent): string | undefined {  const payload = asObject(event.payload);  const command = asString(payload?.command);  if (command) return command;  return asString(payload?.reason);} export function derivePendingApprovals(events: ProviderEvent[]): PendingApproval[] {  const pending = new Map<string, PendingApproval>();  const ordered = [...events].toReversed();   for (const event of ordered) {    if (      event.method === "session/closed" ||      event.method === "session/exited" ||      event.method === "turn/completed"    ) {      pending.clear();      continue;    }     const requestId = event.requestId ?? asString(asObject(event.payload)?.requestId);    if (!requestId) continue;     if (      event.kind === "request" &&      (event.requestKind === "command" || event.requestKind === "file-change")    ) {      const detail = approvalDetail(event);      pending.set(requestId, {        requestId,        requestKind: event.requestKind,        createdAt: event.createdAt,        ...(detail ? { detail } : {}),      });      continue;    }     if (event.method === "item/requestApproval/decision") {      pending.delete(requestId);    }  }   return Array.from(pending.values());} function normalizeItemType(raw: string | undefined): string {  if (!raw) return "item";  return raw    .replace(/([a-z0-9])([A-Z])/g, "$1 $2")    .replace(/[._/-]/g, " ")    .replace(/\s+/g, " ")    .trim()    .toLowerCase();} function shouldDropItemType(type: string): boolean {  if (type.includes("preamble") || type.includes("reasoning") || type.includes("thought")) {    return true;  }   return type === "work" || type.startsWith("work ");} function shouldShowItemLifecycle(type: string): boolean {  return type.includes("tool") || type.includes("command") || type.includes("file change");} function shouldDropMethod(method: string): boolean {  return /(^|\/)(preamble|work|reasoning|thought)(\/|$)/i.test(method);} function itemTypeMeta(type: string): {  label: string;  tone: WorkLogEntry["tone"];} {  if (type.includes("command")) {    return { label: "Command run", tone: "tool" };  }  if (type.includes("file change")) {    return { label: "File change", tone: "tool" };  }  if (type.includes("tool")) {    return { label: "Tool call", tone: "tool" };  }  return { label: "Work item", tone: "info" };} interface ItemLifecycleCandidate {  id: string;  itemId?: string;  createdAt: string;  label: string;  detail?: string;  tone: WorkLogEntry["tone"];  phase: "started" | "completed";} function extractDetail(  payload: Record<string, unknown> | undefined,  item: Record<string, unknown> | undefined,): string | undefined {  const candidates = [    asString(item?.command),    asString(item?.tool),    asString(item?.name),    asString(item?.title),    asString(item?.summary),    asString(item?.text),    asString(item?.prompt),    asString(payload?.message),    asString(payload?.prompt),    asString(payload?.command),  ];   for (const candidate of candidates) {    const detail = normalizeDetail(candidate);    if (detail) return detail;  }  return undefined;} function lifecycleCandidateFromItemEvent(event: ProviderEvent): ItemLifecycleCandidate | null {  const payload = asObject(event.payload);  const item = asObject(payload?.item);  const normalizedType = normalizeItemType(asString(item?.type));  if (shouldDropItemType(normalizedType) || shouldDropMethod(event.method)) {    return null;  }  if (!shouldShowItemLifecycle(normalizedType)) {    return null;  }   const meta = itemTypeMeta(normalizedType);  const isStarted = event.method === "item/started";  const isCompleted = event.method === "item/completed";  if (!isStarted && !isCompleted) {    return null;  }   const detail = extractDetail(payload, item);  const itemId = event.itemId ?? asString(item?.id);  return {    id: event.id,    ...(itemId ? { itemId } : {}),    createdAt: event.createdAt,    label: meta.label,    ...(detail ? { detail } : {}),    tone: meta.tone,    phase: isCompleted ? "completed" : "started",  };} function entryFromRequest(event: ProviderEvent): WorkLogEntry | null {  if (event.kind !== "request") return null;  if (shouldDropMethod(event.method)) return null;   if (event.method.includes("commandExecution")) {    return {      id: event.id,      createdAt: event.createdAt,      label: "Command approval requested",      tone: "tool",    };  }   if (event.method.includes("fileChange")) {    return {      id: event.id,      createdAt: event.createdAt,      label: "File-change approval requested",      tone: "tool",    };  }   if (event.method.includes("requestUserInput")) {    return {      id: event.id,      createdAt: event.createdAt,      label: "Tool requested user input",      tone: "tool",    };  }   return {    id: event.id,    createdAt: event.createdAt,    label: `Request: ${event.method}`,    tone: "info",  };} function entryFromNotification(event: ProviderEvent): WorkLogEntry | null {  if (event.kind !== "notification") return null;  if (shouldDropMethod(event.method)) return null;  if (event.method === "item/agentMessage/delta") return null;  if (event.method === "turn/started") {    return null;  }  if (event.method === "thread/started") return null;   if (event.method === "turn/completed") {    const payload = asObject(event.payload);    const turn = asObject(payload?.turn);    const status = asString(turn?.status);    if (status !== "failed") {      return null;    }    const turnError = asObject(turn?.error);    const turnErrorMessage = asString(turnError?.message);    const turnErrorDetail = normalizeDetail(turnErrorMessage);     return {      id: event.id,      createdAt: event.createdAt,      label: "Turn failed",      ...(turnErrorDetail ? { detail: turnErrorDetail } : {}),      tone: "error",    };  }   if (event.method.startsWith("item/")) return null;   return null;} function entryFromError(event: ProviderEvent): WorkLogEntry | null {  if (event.kind !== "error") return null;  if (shouldDropMethod(event.method)) return null;  const detail = normalizeDetail(event.message);   return {    id: event.id,    createdAt: event.createdAt,    label: "Runtime error",    ...(detail ? { detail } : {}),    tone: "error",  };} function eventTurnId(event: ProviderEvent): string | undefined {  const payload = asObject(event.payload);  const turn = asObject(payload?.turn);  return event.turnId ?? asString(turn?.id);} export function deriveWorkLogEntries(  events: ProviderEvent[],  turnId: string | undefined,): WorkLogEntry[] {  const ordered = [...events].toReversed();  const entries: WorkLogEntry[] = [];  const turnStartedAtIso = turnId    ? ordered.find((event) => {        if (event.method !== "turn/started") return false;        return eventTurnId(event) === turnId;      })?.createdAt    : undefined;  const turnStartedAt = turnStartedAtIso ? Date.parse(turnStartedAtIso) : Number.NaN;   const shouldIncludeEvent = (event: ProviderEvent): boolean => {    if (!turnId) return true;    const scopedTurnId = eventTurnId(event);    if (scopedTurnId && scopedTurnId !== turnId) {      return false;    }     if (!scopedTurnId && !Number.isNaN(turnStartedAt)) {      const eventAt = Date.parse(event.createdAt);      if (!Number.isNaN(eventAt) && eventAt < turnStartedAt) {        return false;      }    }     return true;  };   const completedLifecycleItemIds = new Set<string>();  for (const event of ordered) {    if (!shouldIncludeEvent(event)) continue;    const candidate = lifecycleCandidateFromItemEvent(event);    if (candidate?.phase === "completed" && candidate.itemId) {      completedLifecycleItemIds.add(candidate.itemId);    }  }   for (const event of ordered) {    if (!shouldIncludeEvent(event)) continue;     const lifecycleCandidate = lifecycleCandidateFromItemEvent(event);    if (lifecycleCandidate) {      if (        lifecycleCandidate.phase === "started" &&        lifecycleCandidate.itemId &&        completedLifecycleItemIds.has(lifecycleCandidate.itemId)      ) {        continue;      }      if (lifecycleCandidate.label === "Tool call" && !lifecycleCandidate.detail) {        continue;      }       entries.push({        id: lifecycleCandidate.id,        createdAt: lifecycleCandidate.createdAt,        label: lifecycleCandidate.label,        ...(lifecycleCandidate.detail ? { detail: lifecycleCandidate.detail } : {}),        tone: lifecycleCandidate.tone,      });      continue;    }     const fromRequest = entryFromRequest(event);    if (fromRequest) {      entries.push(fromRequest);      continue;    }     const fromNotification = entryFromNotification(event);    if (fromNotification) {      entries.push(fromNotification);      continue;    }     const fromError = entryFromError(event);    if (fromError) {      entries.push(fromError);    }  }   return entries;} interface MutableTurnDiffFileChange {  path: string;  kind: string | undefined;  diff: string | undefined;} interface MutableTurnDiffSummary {  turnId: string;  completedAt: string | undefined;  status: string | undefined;  unifiedDiff: string | undefined;  assistantMessageId: string | undefined;  filesByPath: Map<string, MutableTurnDiffFileChange>;} function parsePathFromDiff(diff: string): string | undefined {  const normalized = diff.replace(/\r\n/g, "\n");  const gitHeader = normalized.match(/^diff --git a\/(.+) b\/(.+)$/m);  if (gitHeader && gitHeader[2]) return gitHeader[2];  const bPath = normalized.match(/^\+\+\+ b\/(.+)$/m);  if (bPath && bPath[1]) return bPath[1];  const direct = normalized.match(/^\+\+\+ (.+)$/m);  if (!direct || !direct[1] || direct[1] === "/dev/null") {    return undefined;  }  return direct[1];} function splitUnifiedDiffByFile(diff: string): Map<string, string> {  const normalized = diff.replace(/\r\n/g, "\n");  const byPath = new Map<string, string>();  const headerMatches = [...normalized.matchAll(/^diff --git a\/(.+) b\/(.+)$/gm)];   if (headerMatches.length === 0) {    const path = parsePathFromDiff(normalized);    if (path) {      byPath.set(path, normalized.trim());    }    return byPath;  } 

Lines 1–500 of 904.