import * as Equal from "effect/Equal";import { shallow } from "zustand/vanilla/shallow";import { renderCodexDirectivesForCopy } from "@t3tools/client-runtime/codex-markdown-directives";import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label";import { liveActivityToolStatus, normalizeCompactToolLabel, omitSupersededLifecycleMarkers, resolveWorkEntryToolPresentation, summarizeToolGroup, toolGroupAction, toolGroupSummaryKind, type ToolGroupSummaryKind,} from "@t3tools/client-runtime/work-log/presentation";export { normalizeCompactToolLabel, toolGroupAction,} from "@t3tools/client-runtime/work-log/presentation";import { formatDuration, inferCheckpointTurnCountByTurnId, isStreamingMessageTextUpdate, workEntryDisplayIndicatesToolFailure, workEntryIndicatesToolSuccess, workEntryIndicatesToolNeutralStatus, workLogEntryIsToolLike, type TimelineEntry, type WorkLogEntry,} from "../../session-logic";import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types";import { type MessageId, type OrchestrationLatestTurn, type TurnId } from "@t3tools/contracts";import { formatWorkspaceRelativePath } from "../../filePathDisplay"; const TIMELINE_MINIMAP_ITEM_SPACING = 8;export const TIMELINE_MINIMAP_MIN_ITEMS = 2;const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)";const TIMELINE_CONTENT_MAX_WIDTH = 768;const TIMELINE_MINIMAP_PERSISTENT_GUTTER = 48; function singleToolCallLabel(entry: WorkLogEntry): string { const toolPresentation = resolveWorkEntryToolPresentation(entry, "completed"); if (toolPresentation) return toolPresentation.displayName; const command = entry.command?.trim(); if (command) return command; const heading = normalizeCompactToolLabel(entry.toolTitle || entry.label); return `${heading.charAt(0).toUpperCase()}${heading.slice(1)}`;} export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string | undefined) { const toolPresentation = resolveWorkEntryToolPresentation(entry); if (toolPresentation) return toolPresentation.displayName; if (entry.command) return entry.command; if (entry.detail) return entry.detail; const [firstPath] = entry.changedFiles ?? []; if (firstPath) { const path = formatWorkspaceRelativePath(firstPath, workspaceRoot); return entry.changedFiles!.length === 1 ? path : `${path} +${entry.changedFiles!.length - 1} more`; } const heading = normalizeCompactToolLabel(entry.toolTitle || entry.label); return `${heading.charAt(0).toUpperCase()}${heading.slice(1)}`;} export function liveWorkEntryLabel( entry: WorkLogEntry, workspaceRoot: string | undefined, active: boolean,) { const status = liveActivityToolStatus(entry.toolLifecycleStatus, active); const toolPresentation = resolveWorkEntryToolPresentation({ ...entry, toolLifecycleStatus: status, }); if (toolPresentation) return toolPresentation.displayName; const command = entry.command?.trim(); if (command) { const verb = status === "inProgress" ? "Running" : status === "failed" ? "Failed" : status === "declined" ? "Declined" : status === "stopped" ? "Stopped" : "Ran"; return `${verb} ${commandProgramName(command) ?? "command"}`; } return workEntryDisplayLabel(entry, workspaceRoot);} export function workEntryIsVisibleInGroup( entry: WorkLogEntry, expandedToolGroupEntry = false,): boolean { return ( (expandedToolGroupEntry && (entry.toolLifecycleStatus === "inProgress" || entry.sourceActivityKind === "task.progress")) || !workEntryIndicatesToolNeutralStatus(entry) );} export interface WorkGroupScrollAnchor { readonly entryId: string; readonly offset: number;} /** Restore a visible tool, including a position partway through its expanded output. */export function resolveWorkGroupScrollIndex( entries: ReadonlyArray<{ readonly id: string }>, anchor: WorkGroupScrollAnchor | undefined,): { index: number; viewOffset: number } | undefined { if (!anchor) return undefined; const index = entries.findIndex((entry) => entry.id === anchor.entryId); return index < 0 ? undefined : { index, viewOffset: -anchor.offset };} /** Only newly appended calls may follow the end, never status or output updates. */export function shouldFollowWorkGroupAppend( previous: ReadonlyArray<{ readonly id: string }>, entries: ReadonlyArray<{ readonly id: string }>, distanceFromEnd: number,): boolean { return ( previous.length > 0 && entries.length > previous.length && distanceFromEnd <= 1 && previous.every((entry, index) => entry.id === entries[index]?.id) );} 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. */const TIMELINE_FOLLOW_REARM_THRESHOLD_PX = 40; export function resolveTimelineIsAtEnd(state: TimelineEndState | undefined): boolean | undefined { if (!state) { return undefined; } const { contentLength, scroll, scrollLength } = state; if (contentLength === undefined || scroll === undefined || scrollLength === undefined) { return state.isAtEnd; } // contentLength includes the composer inset spacer, but the composer hides // the same amount of viewport, so the inset cancels: plain // contentLength - scroll - scrollLength is the gap between the last real row // and the visible edge above the composer. LegendList's own isAtEnd subtracts // the inset and is true anywhere in the bottom composer-height band, so it is // only a fallback here, never a short-circuit. return contentLength - scroll - scrollLength <= 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 resolveTimelineMinimapCurrentIndex(input: { readonly scrollTop: number; readonly scrollBottom: number; readonly itemBounds: ReadonlyArray<{ readonly top: number | null; readonly height: number | null; }>;}): number | null { let precedingIndex: number | null = null; for (const [index, item] of input.itemBounds.entries()) { if (item.top === null) { continue; } const inView = item.top < input.scrollBottom && item.top + Math.max(1, item.height ?? 1) > input.scrollTop; if (inView) { // The first visible marker is the turn at the reader's current position. return index; } if (item.top <= input.scrollTop) { precedingIndex = index; } } return precedingIndex;} 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;} const TIMELINE_MINIMAP_HIT_STRIP_LEFT = 12;const TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH = 40;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">; const LIVE_ACTIVITY_ROW_ID = "live-activity-row"; export type MessagesTimelineRow = | { kind: "work"; id: string; createdAt: string; groupedEntries: WorkLogEntry[]; isExpandedToolGroup: boolean; displayLabel?: string; } | { kind: "work-live"; id: string; createdAt: string; entry: WorkLogEntry; groupedEntries: WorkLogEntry[]; groupId: string; expanded: boolean; active: boolean; } | { kind: "work-toggle"; id: string; createdAt: string; turnId?: TurnId | null; groupId: string; hiddenCount: number; expanded: boolean; summary: string; summaryKind: ToolGroupSummaryKind; toolSurface?: WorkLogEntry["toolSurface"]; toolIcon?: WorkLogEntry["toolIcon"]; summaryToolIcon?: "browser" | "device" | "t3-code" | "pull-request"; hasFailure: boolean; } | { kind: "turn-fold"; id: string; createdAt: string; turnId: TurnId; label: string; expanded: boolean; } | { kind: "context-compaction"; id: string; createdAt: string; label: string; } | { kind: "message"; id: string; createdAt: string; message: ChatMessage; durationStart: string; showAssistantMeta: boolean; showAssistantCopyButton: boolean; assistantCopyStreaming: boolean; assistantTurnDiffSummary?: TurnDiffSummary | undefined; revertTurnCount?: number | undefined; } | { kind: "assistant-meta"; id: string; createdAt: string; message: ChatMessage; showAssistantCopyButton: boolean; assistantCopyStreaming: boolean; } | { kind: "proposed-plan"; id: string; createdAt: string; proposedPlan: ProposedPlan; } | { kind: "working"; id: string; createdAt: string | null; } | { kind: "thinking"; 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;} function workGroupIdentity(timelineEntryId: string, entry: WorkLogEntry): string { return entry.toolCallId ? `tool:${entry.turnId ?? "no-turn"}:${entry.toolCallId}` : timelineEntryId;} function workGroupId(timelineEntryId: string, entry: WorkLogEntry): string { return `work-group:${workGroupIdentity(timelineEntryId, entry)}`;} function expandedWorkGroupRow( groupId: string, createdAt: string, groupedEntries: WorkLogEntry[],): Extract<MessagesTimelineRow, { kind: "work" }> { return { kind: "work", id: `${groupId}:details`, createdAt, groupedEntries, isExpandedToolGroup: true, };} export function resolveAssistantMessageCopyState({ text, showCopyButton, streaming,}: { text: string | null; showCopyButton: boolean; streaming: boolean;}) { const hasText = text !== null && text.trim().length > 0; const visible = showCopyButton && hasText && !streaming; return { text: hasText ? (visible ? renderCodexDirectivesForCopy(text) : text) : null, visible, };} 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. */Lines 1–500 of 1418. Load more
import * as Equal from "effect/Equal"; import { shallow } from "zustand/vanilla/shallow"; import { renderCodexDirectivesForCopy } from "@t3tools/client-runtime/codex-markdown-directives"; import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label"; import { liveActivityToolStatus, normalizeCompactToolLabel, omitSupersededLifecycleMarkers, resolveWorkEntryToolPresentation, summarizeToolGroup, toolGroupAction, toolGroupSummaryKind, type ToolGroupSummaryKind, } from "@t3tools/client-runtime/work-log/presentation"; export { normalizeCompactToolLabel, toolGroupAction, } from "@t3tools/client-runtime/work-log/presentation"; import { formatDuration, inferCheckpointTurnCountByTurnId, isStreamingMessageTextUpdate, workEntryDisplayIndicatesToolFailure, workEntryIndicatesToolSuccess, workEntryIndicatesToolNeutralStatus, workLogEntryIsToolLike, type TimelineEntry, type WorkLogEntry, } from "../../session-logic"; import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types"; import { type MessageId, type OrchestrationLatestTurn, type TurnId } from "@t3tools/contracts"; import { formatWorkspaceRelativePath } from "../../filePathDisplay"; const TIMELINE_MINIMAP_ITEM_SPACING = 8; export const TIMELINE_MINIMAP_MIN_ITEMS = 2; const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)"; const TIMELINE_CONTENT_MAX_WIDTH = 768; const TIMELINE_MINIMAP_PERSISTENT_GUTTER = 48; function singleToolCallLabel(entry: WorkLogEntry): string { const toolPresentation = resolveWorkEntryToolPresentation(entry, "completed"); if (toolPresentation) return toolPresentation.displayName; const command = entry.command?.trim(); if (command) return command; const heading = normalizeCompactToolLabel(entry.toolTitle || entry.label); return `${heading.charAt(0).toUpperCase()}${heading.slice(1)}`; } export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string | undefined) { const toolPresentation = resolveWorkEntryToolPresentation(entry); if (toolPresentation) return toolPresentation.displayName; if (entry.command) return entry.command; if (entry.detail) return entry.detail; const [firstPath] = entry.changedFiles ?? []; if (firstPath) { const path = formatWorkspaceRelativePath(firstPath, workspaceRoot); return entry.changedFiles!.length === 1 ? path : `${path} +${entry.changedFiles!.length - 1} more`; } const heading = normalizeCompactToolLabel(entry.toolTitle || entry.label); return `${heading.charAt(0).toUpperCase()}${heading.slice(1)}`; } export function liveWorkEntryLabel( entry: WorkLogEntry, workspaceRoot: string | undefined, active: boolean, ) { const status = liveActivityToolStatus(entry.toolLifecycleStatus, active); const toolPresentation = resolveWorkEntryToolPresentation({ ...entry, toolLifecycleStatus: status, }); if (toolPresentation) return toolPresentation.displayName; const command = entry.command?.trim(); if (command) { const verb = status === "inProgress" ? "Running" : status === "failed" ? "Failed" : status === "declined" ? "Declined" : status === "stopped" ? "Stopped" : "Ran"; return `${verb} ${commandProgramName(command) ?? "command"}`; } return workEntryDisplayLabel(entry, workspaceRoot); } export function workEntryIsVisibleInGroup( entry: WorkLogEntry, expandedToolGroupEntry = false, ): boolean { return ( (expandedToolGroupEntry && (entry.toolLifecycleStatus === "inProgress" || entry.sourceActivityKind === "task.progress")) || !workEntryIndicatesToolNeutralStatus(entry) ); } export interface WorkGroupScrollAnchor { readonly entryId: string; readonly offset: number; } /** Restore a visible tool, including a position partway through its expanded output. */ export function resolveWorkGroupScrollIndex( entries: ReadonlyArray<{ readonly id: string }>, anchor: WorkGroupScrollAnchor | undefined, ): { index: number; viewOffset: number } | undefined { if (!anchor) return undefined; const index = entries.findIndex((entry) => entry.id === anchor.entryId); return index < 0 ? undefined : { index, viewOffset: -anchor.offset }; } /** Only newly appended calls may follow the end, never status or output updates. */ export function shouldFollowWorkGroupAppend( previous: ReadonlyArray<{ readonly id: string }>, entries: ReadonlyArray<{ readonly id: string }>, distanceFromEnd: number, ): boolean { return ( previous.length > 0 && entries.length > previous.length && distanceFromEnd <= 1 && previous.every((entry, index) => entry.id === entries[index]?.id) ); } 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. */ const TIMELINE_FOLLOW_REARM_THRESHOLD_PX = 40; export function resolveTimelineIsAtEnd(state: TimelineEndState | undefined): boolean | undefined { if (!state) { return undefined; } const { contentLength, scroll, scrollLength } = state; if (contentLength === undefined || scroll === undefined || scrollLength === undefined) { return state.isAtEnd; } // contentLength includes the composer inset spacer, but the composer hides // the same amount of viewport, so the inset cancels: plain // contentLength - scroll - scrollLength is the gap between the last real row // and the visible edge above the composer. LegendList's own isAtEnd subtracts // the inset and is true anywhere in the bottom composer-height band, so it is // only a fallback here, never a short-circuit. return contentLength - scroll - scrollLength <= 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 resolveTimelineMinimapCurrentIndex(input: { readonly scrollTop: number; readonly scrollBottom: number; readonly itemBounds: ReadonlyArray<{ readonly top: number | null; readonly height: number | null; }>; }): number | null { let precedingIndex: number | null = null; for (const [index, item] of input.itemBounds.entries()) { if (item.top === null) { continue; } const inView = item.top < input.scrollBottom && item.top + Math.max(1, item.height ?? 1) > input.scrollTop; if (inView) { // The first visible marker is the turn at the reader's current position. return index; } if (item.top <= input.scrollTop) { precedingIndex = index; } } return precedingIndex; } 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; } const TIMELINE_MINIMAP_HIT_STRIP_LEFT = 12; const TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH = 40; 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" >; const LIVE_ACTIVITY_ROW_ID = "live-activity-row"; export type MessagesTimelineRow = | { kind: "work"; id: string; createdAt: string; groupedEntries: WorkLogEntry[]; isExpandedToolGroup: boolean; displayLabel?: string; } | { kind: "work-live"; id: string; createdAt: string; entry: WorkLogEntry; groupedEntries: WorkLogEntry[]; groupId: string; expanded: boolean; active: boolean; } | { kind: "work-toggle"; id: string; createdAt: string; turnId?: TurnId | null; groupId: string; hiddenCount: number; expanded: boolean; summary: string; summaryKind: ToolGroupSummaryKind; toolSurface?: WorkLogEntry["toolSurface"]; toolIcon?: WorkLogEntry["toolIcon"]; summaryToolIcon?: "browser" | "device" | "t3-code" | "pull-request"; hasFailure: boolean; } | { kind: "turn-fold"; id: string; createdAt: string; turnId: TurnId; label: string; expanded: boolean; } | { kind: "context-compaction"; id: string; createdAt: string; label: string; } | { kind: "message"; id: string; createdAt: string; message: ChatMessage; durationStart: string; showAssistantMeta: boolean; showAssistantCopyButton: boolean; assistantCopyStreaming: boolean; assistantTurnDiffSummary?: TurnDiffSummary | undefined; revertTurnCount?: number | undefined; } | { kind: "assistant-meta"; id: string; createdAt: string; message: ChatMessage; showAssistantCopyButton: boolean; assistantCopyStreaming: boolean; } | { kind: "proposed-plan"; id: string; createdAt: string; proposedPlan: ProposedPlan; } | { kind: "working"; id: string; createdAt: string | null; } | { kind: "thinking"; 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; } function workGroupIdentity(timelineEntryId: string, entry: WorkLogEntry): string { return entry.toolCallId ? `tool:${entry.turnId ?? "no-turn"}:${entry.toolCallId}` : timelineEntryId; } function workGroupId(timelineEntryId: string, entry: WorkLogEntry): string { return `work-group:${workGroupIdentity(timelineEntryId, entry)}`; } function expandedWorkGroupRow( groupId: string, createdAt: string, groupedEntries: WorkLogEntry[], ): Extract<MessagesTimelineRow, { kind: "work" }> { return { kind: "work", id: `${groupId}:details`, createdAt, groupedEntries, isExpandedToolGroup: true, }; } export function resolveAssistantMessageCopyState({ text, showCopyButton, streaming, }: { text: string | null; showCopyButton: boolean; streaming: boolean; }) { const hasText = text !== null && text.trim().length > 0; const visible = showCopyButton && hasText && !streaming; return { text: hasText ? (visible ? renderCodexDirectivesForCopy(text) : text) : null, visible, }; } 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. */