717 lines · 23.2 KB · TypeScript
RawDownload
import * as Equal from "effect/Equal";import {  formatDuration,  workEntryIndicatesToolNeutralStatus,  workLogEntryIsToolLike,  type TimelineEntry,  type TurnPlanEntry,  type WorkLogEntry,} from "../../session-logic";import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types";import { type MessageId, type OrchestrationLatestTurn, type TurnId } from "@t3tools/contracts"; export const MAX_VISIBLE_WORK_LOG_ENTRIES = 1;export const TIMELINE_MINIMAP_ITEM_SPACING = 8;export const TIMELINE_MINIMAP_MIN_ITEMS = 2;export const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)";export const TIMELINE_CONTENT_MAX_WIDTH = 768;export const TIMELINE_MINIMAP_PERSISTENT_GUTTER = 48; export interface TimelineEndState {  readonly isAtEnd?: boolean;  readonly contentLength?: number;  readonly scroll?: number;  readonly scrollLength?: number;} /** * Follow re-arm band above the hard bottom. Strict on purpose: LegendList's * isNearEnd fires within half a viewport, which re-armed live-follow while the * user was reading history and yanked them back down on the next stream chunk. * A small pixel band (instead of the 1px isAtEnd epsilon alone) keeps re-arming * reliable while streaming content is still growing under the viewport. */export const TIMELINE_FOLLOW_REARM_THRESHOLD_PX = 40; export function resolveTimelineIsAtEnd(  state: TimelineEndState | undefined,  endInset = 0,): boolean | undefined {  if (!state) {    return undefined;  }  if (state.isAtEnd) {    return true;  }  const { contentLength, scroll, scrollLength } = state;  if (contentLength === undefined || scroll === undefined || scrollLength === undefined) {    return state.isAtEnd;  }  // contentLength includes the end inset (composer overlay), so subtract it to  // measure the distance to the real content bottom.  return contentLength - scroll - scrollLength - endInset <= TIMELINE_FOLLOW_REARM_THRESHOLD_PX;} export function shouldPreserveAssistantLineBreaks(text: string): boolean {  return /^★ Insight(?:\s|─)/mu.test(text);} export function resolveTimelineMinimapHeightStyle(itemCount: number): string {  const naturalHeight = Math.max(1, (itemCount - 1) * TIMELINE_MINIMAP_ITEM_SPACING);  return `min(${naturalHeight}px, ${TIMELINE_MINIMAP_MAX_HEIGHT_CSS})`;} export function resolveTimelineMinimapTopPercent(index: number, itemCount: number): number {  if (itemCount <= 1) {    return 0;  }  return (Math.max(0, Math.min(index, itemCount - 1)) / (itemCount - 1)) * 100;} export function resolveTimelineMinimapIndexFromPointer(input: {  readonly itemCount: number;  readonly railTop: number;  readonly railHeight: number;  readonly pointerY: number;}): number | null {  if (input.itemCount <= 0 || input.railHeight <= 0) {    return null;  }  if (input.itemCount === 1) {    return 0;  }   const progress = Math.max(0, Math.min(1, (input.pointerY - input.railTop) / input.railHeight));  return Math.max(0, Math.min(input.itemCount - 1, Math.round(progress * (input.itemCount - 1))));} export function resolveTimelineMinimapHasPersistentGutter(viewportWidth: number): boolean {  if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) {    return false;  }   const contentWidth = Math.min(viewportWidth, TIMELINE_CONTENT_MAX_WIDTH);  const sideGutter = Math.max(0, (viewportWidth - contentWidth) / 2);  return sideGutter >= TIMELINE_MINIMAP_PERSISTENT_GUTTER;} export const TIMELINE_MINIMAP_HIT_STRIP_LEFT = 12;export const TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH = 40;export const TIMELINE_MINIMAP_EXPANDED_HIT_STRIP_WIDTH = "22rem"; /** * The minimap overlays the viewport's left edge while the content column is * centered, so the side gutter between them shrinks under browser zoom or a * narrow pane. A fixed-width hover strip would then sit on top of the message * text and swallow its pointer events. Cap the strip's width so it never * extends past the gutter into the content column; 0 disables the strip. */export function resolveTimelineMinimapHitStripWidth(viewportWidth: number): number {  if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) {    return 0;  }   const contentWidth = Math.min(viewportWidth, TIMELINE_CONTENT_MAX_WIDTH);  const sideGutter = Math.max(0, (viewportWidth - contentWidth) / 2);  return Math.max(    0,    Math.min(      TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH,      Math.floor(sideGutter) - TIMELINE_MINIMAP_HIT_STRIP_LEFT,    ),  );} /** * Once the preview is open, keep the full preview and the space leading to it * interactive. The collapsed strip remains gutter-capped so it cannot block * selecting message text. */export function resolveTimelineMinimapInteractiveWidth(  collapsedWidth: number,  expanded: boolean,): number | string {  return expanded ? TIMELINE_MINIMAP_EXPANDED_HIT_STRIP_WIDTH : collapsedWidth;} function computeElapsedMs(startIso: string, endIso: string): number | null {  const start = Date.parse(startIso);  const end = Date.parse(endIso);  if (!Number.isFinite(start) || !Number.isFinite(end)) return null;  return Math.max(0, end - start);} function maxIsoTimestamp(a: string | null, b: string | null): string | null {  if (a === null) return b;  if (b === null) return a;  const aMs = Date.parse(a);  const bMs = Date.parse(b);  if (!Number.isFinite(aMs)) return b;  if (!Number.isFinite(bMs)) return a;  return bMs > aMs ? b : a;} export interface TimelineDurationMessage {  id: string;  role: "user" | "assistant" | "system";  createdAt: string;  updatedAt: string;  streaming: boolean;} export type TimelineLatestTurn = Pick<  OrchestrationLatestTurn,  "turnId" | "state" | "startedAt" | "completedAt">; export type MessagesTimelineRow =  | {      kind: "work";      id: string;      createdAt: string;      groupedEntries: WorkLogEntry[];    }  | {      kind: "work-toggle";      id: string;      createdAt: string;      groupId: string;      hiddenCount: number;      expanded: boolean;      onlyToolEntries: boolean;    }  | {      kind: "turn-fold";      id: string;      createdAt: string;      turnId: TurnId;      label: string;      expanded: boolean;    }  | {      kind: "message";      id: string;      createdAt: string;      message: ChatMessage;      durationStart: string;      showAssistantMeta: boolean;      showAssistantCopyButton: boolean;      assistantCopyStreaming: boolean;      assistantTurnDiffSummary?: TurnDiffSummary | undefined;      revertTurnCount?: number | undefined;    }  | {      kind: "proposed-plan";      id: string;      createdAt: string;      proposedPlan: ProposedPlan;    }  | {      kind: "turn-plan";      id: string;      createdAt: string;      turnPlan: TurnPlanEntry;    }  | { kind: "working"; id: string; createdAt: string | null }; export interface StableMessagesTimelineRowsState {  byId: Map<string, MessagesTimelineRow>;  result: MessagesTimelineRow[];} export function computeMessageDurationStart(  messages: ReadonlyArray<TimelineDurationMessage>,): Map<string, string> {  const result = new Map<string, string>();  let lastBoundary: string | null = null;   for (const message of messages) {    if (message.role === "user") {      lastBoundary = message.createdAt;    }    result.set(message.id, lastBoundary ?? message.createdAt);    if (message.role === "assistant" && !message.streaming) {      lastBoundary = message.updatedAt;    }  }   return result;} export function normalizeCompactToolLabel(value: string): string {  return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();} export function resolveAssistantMessageCopyState({  text,  showCopyButton,  streaming,}: {  text: string | null;  showCopyButton: boolean;  streaming: boolean;}) {  const hasText = text !== null && text.trim().length > 0;  return {    text: hasText ? text : null,    visible: showCopyButton && hasText && !streaming,  };} function deriveTerminalAssistantMessageIds(timelineEntries: ReadonlyArray<TimelineEntry>) {  const lastAssistantMessageIdByResponseKey = new Map<string, string>();  let nullTurnResponseIndex = 0;   for (const timelineEntry of timelineEntries) {    if (timelineEntry.kind !== "message") {      continue;    }    const { message } = timelineEntry;    if (message.role === "user") {      nullTurnResponseIndex += 1;      continue;    }    if (message.role !== "assistant") {      continue;    }     const responseKey = message.turnId      ? `turn:${message.turnId}`      : `unkeyed:${nullTurnResponseIndex}`;    lastAssistantMessageIdByResponseKey.set(responseKey, message.id);  }   return new Set(lastAssistantMessageIdByResponseKey.values());} interface TurnFold {  turnId: TurnId;  anchorEntryId: string;  createdAt: string;  hiddenEntryIds: ReadonlySet<string>;  label: string;} /** * The session's running turn is authoritative when latestTurn briefly lags or * regresses behind it. Otherwise, the latest turn counts as unsettled while it * is still running (or has not recorded a completion). This is deliberately * keyed on turn lifecycle rather than transient working state: right after the * user sends a message, the previous turn is still the "active" one until the * server creates the new turn, and folding must not flicker through that window. */function deriveUnsettledTurnId(  latestTurn: TimelineLatestTurn | null,  runningTurnId: TurnId | null,): TurnId | null {  if (runningTurnId !== null) {    return runningTurnId;  }  if (!latestTurn) {    return null;  }  const isSettled = latestTurn.completedAt !== null && latestTurn.state !== "running";  return isSettled ? null : latestTurn.turnId;} /** * Settled turns fold their commentary and tool activity behind a * "Worked for ..." row anchored at the turn's first foldable entry; the * terminal assistant message stays visible below the fold. */function deriveTurnFolds(input: {  timelineEntries: ReadonlyArray<TimelineEntry>;  terminalAssistantMessageIds: ReadonlySet<string>;  latestTurn: TimelineLatestTurn | null;  unsettledTurnId: TurnId | null;}): ReadonlyMap<string, TurnFold> {  interface TurnGroup {    entries: Array<TimelineEntry>;    terminalEntry: Extract<TimelineEntry, { kind: "message" }> | null;    hasStreamingMessage: boolean;    /**     * The user message that kicked the turn off. Entry timestamps alone     * undercount the duration (the first entry appears only once the     * provider starts producing output), and a turn cut short by a steer may     * hold a single instantaneous commentary message.     */    startBoundary: string | null;  }  const groupsByTurnId = new Map<TurnId, TurnGroup>();   let pendingUserBoundary: string | null = null;  for (const entry of input.timelineEntries) {    if (entry.kind === "message" && entry.message.role === "user") {      pendingUserBoundary = entry.message.createdAt;      continue;    }    const turnId =      entry.kind === "message" && entry.message.role === "assistant"        ? (entry.message.turnId ?? null)        : entry.kind === "work"          ? (entry.entry.turnId ?? null)          : null;    if (!turnId) {      continue;    }    let group = groupsByTurnId.get(turnId);    if (!group) {      group = {        entries: [],        terminalEntry: null,        hasStreamingMessage: false,        // Each user boundary starts at most one turn; a second turn after the        // same user message (e.g. a steer-superseded continuation) falls back        // to its own first entry.        startBoundary: pendingUserBoundary,      };      pendingUserBoundary = null;      groupsByTurnId.set(turnId, group);    }    group.entries.push(entry);    if (entry.kind === "message") {      if (input.terminalAssistantMessageIds.has(entry.message.id)) {        group.terminalEntry = entry;      }      if (entry.message.streaming) {        group.hasStreamingMessage = true;      }    }  }   const foldsByAnchorEntryId = new Map<string, TurnFold>();  for (const [turnId, group] of groupsByTurnId) {    if (turnId === input.unsettledTurnId) {      continue;    }    if (group.hasStreamingMessage) {      continue;    }    const hiddenEntryIds = new Set<string>();    for (const entry of group.entries) {      if (entry.id === group.terminalEntry?.id) {        continue;      }      // Agent-spawn CTA rows never fold: workflows outlive their launching      // turn (dynamic spawns, background execution), and folding the CTA      // when the turn settles makes a still-running fleet invisible.      if (entry.kind === "work" && entry.entry.agentSpawn !== undefined) {        continue;      }      hiddenEntryIds.add(entry.id);    }    if (hiddenEntryIds.size === 0) {      continue;    }     const firstEntry = group.entries[0];    const lastEntry = group.entries.at(-1);    if (!firstEntry || !lastEntry) {      continue;    }     const isLatestInterruptedTurn =      input.latestTurn?.turnId === turnId && input.latestTurn.state === "interrupted";    // A turn cut short by a steer leaves trailing work entries behind its    // terminal message — take whichever ended last.    const lastEntryEnd =      lastEntry.kind === "message" ? lastEntry.message.updatedAt : lastEntry.createdAt;    const elapsedMs =      input.latestTurn?.turnId === turnId &&      input.latestTurn.startedAt &&      input.latestTurn.completedAt        ? computeElapsedMs(input.latestTurn.startedAt, input.latestTurn.completedAt)        : computeElapsedMs(            group.startBoundary ?? firstEntry.createdAt,            maxIsoTimestamp(group.terminalEntry?.message.updatedAt ?? null, lastEntryEnd) ??              lastEntryEnd,          );    const duration = elapsedMs !== null ? formatDuration(elapsedMs) : null;    const label = isLatestInterruptedTurn      ? duration        ? `You stopped after ${duration}`        : "You stopped this response"      : duration        ? `Worked for ${duration}`        : "Worked";     foldsByAnchorEntryId.set(firstEntry.id, {      turnId,      anchorEntryId: firstEntry.id,      createdAt: firstEntry.createdAt,      hiddenEntryIds,      label,    });  }  return foldsByAnchorEntryId;} export function deriveMessagesTimelineRows(input: {  timelineEntries: ReadonlyArray<TimelineEntry>;  latestTurn?: TimelineLatestTurn | null;  runningTurnId?: TurnId | null;  expandedTurnIds?: ReadonlySet<TurnId>;  expandedWorkGroupIds?: ReadonlySet<string>;  isWorking: boolean;  activeTurnStartedAt: string | null;  turnDiffSummaryByAssistantMessageId: ReadonlyMap<MessageId, TurnDiffSummary>;  revertTurnCountByUserMessageId: ReadonlyMap<MessageId, number>;}): MessagesTimelineRow[] {  const nextRows: MessagesTimelineRow[] = [];  const durationStartByMessageId = computeMessageDurationStart(    input.timelineEntries.flatMap((entry) => (entry.kind === "message" ? [entry.message] : [])),  );  const terminalAssistantMessageIds = deriveTerminalAssistantMessageIds(input.timelineEntries);  const unsettledTurnId = deriveUnsettledTurnId(    input.latestTurn ?? null,    input.runningTurnId ?? null,  );  const foldsByAnchorEntryId = deriveTurnFolds({    timelineEntries: input.timelineEntries,    terminalAssistantMessageIds,    latestTurn: input.latestTurn ?? null,    unsettledTurnId,  });  const collapsedEntryIds = new Set<string>();  for (const fold of foldsByAnchorEntryId.values()) {    if (!input.expandedTurnIds?.has(fold.turnId)) {      for (const entryId of fold.hiddenEntryIds) {        collapsedEntryIds.add(entryId);      }    }  }   for (let index = 0; index < input.timelineEntries.length; index += 1) {    const timelineEntry = input.timelineEntries[index];    if (!timelineEntry) {      continue;    }     const turnFold = foldsByAnchorEntryId.get(timelineEntry.id);    if (turnFold) {      nextRows.push({        kind: "turn-fold",        id: `turn-fold:${turnFold.turnId}`,        createdAt: turnFold.createdAt,        turnId: turnFold.turnId,        label: turnFold.label,        expanded: input.expandedTurnIds?.has(turnFold.turnId) ?? false,      });    }

Lines 1–500 of 717.