import { type AssetResource, ProviderDriverKind, type OrchestrationV2ExecutionNode, type OrchestrationV2PlanArtifact, type OrchestrationV2ProjectedTurnItem, type OrchestrationV2RunAttempt, type OrchestrationV2ThreadProjection, type OrchestrationV2TurnItem, type PlanId, type RunId, type ThreadId, type ToolActivitySurface, type ToolActivityIcon, type ToolActivitySource, } from "@t3tools/contracts"; import { extractToolActivityPresentation } from "@t3tools/client-runtime/work-log/tool-presentation"; import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints"; import type { ThreadPendingApproval, ThreadPendingUserInput, } from "@t3tools/client-runtime/state/thread-requests"; import type { ThreadRunSummary, ThreadRuntimeSummary } from "@t3tools/client-runtime/state/shell"; import { turnItemIsWorkspacePreparation } from "@t3tools/client-runtime/state/turn-item-presentation"; import { isImageAttachment, type ChatAttachment, type ChatMessage, type ProposedPlan, type SessionPhase, type TurnDiffSummary, } from "./types"; import * as DateTime from "effect/DateTime"; import * as Equal from "effect/Equal"; import { shallow } from "zustand/vanilla/shallow"; export { formatDuration, formatElapsed } from "@t3tools/shared/orchestrationTiming"; export type ProviderPickerKind = ProviderDriverKind; export const PROVIDER_OPTIONS: Array<{ value: ProviderPickerKind; label: string; available: boolean; pickerSidebarBadge?: "new" | "soon"; }> = [ { value: ProviderDriverKind.make("codex"), label: "Codex", available: true }, { value: ProviderDriverKind.make("claudeAgent"), label: "Claude", available: true }, { value: ProviderDriverKind.make("opencode"), label: "OpenCode", available: true, pickerSidebarBadge: "new", }, { value: ProviderDriverKind.make("cursor"), label: "Cursor", available: true, pickerSidebarBadge: "new", }, { value: ProviderDriverKind.make("grok"), label: "Grok", available: true }, { value: ProviderDriverKind.make("antigravity"), label: "Antigravity", available: true, pickerSidebarBadge: "new", }, ]; export type WorkLogToolLifecycleStatus = | "idle" | "inProgress" | "completed" | "failed" | "declined" | "stopped"; export interface WorkLogEntry { readonly id: string; readonly createdAt: string; readonly runId?: RunId | null; readonly label: string; readonly detail?: string; readonly command?: string; readonly rawCommand?: string; readonly changedFiles?: ReadonlyArray; readonly tone: "thinking" | "tool" | "info" | "error"; readonly toolTitle?: string; readonly toolCallId?: string; readonly viewedImagePath?: string; readonly toolSurface?: ToolActivitySurface; readonly toolIcon?: ToolActivityIcon; readonly toolSource?: ToolActivitySource; readonly sourceActivityKind?: string; readonly taskId?: string; readonly agentRole?: string; readonly toolData?: unknown; readonly requestKind?: string; readonly itemType?: OrchestrationV2TurnItem["type"]; readonly toolLifecycleStatus?: WorkLogToolLifecycleStatus; readonly structuredPayload?: OrchestrationV2TurnItem; readonly sourceItemType?: OrchestrationV2TurnItem["type"]; readonly projectedItem?: OrchestrationV2ProjectedTurnItem; } export type PendingApproval = ThreadPendingApproval; export type PendingUserInput = ThreadPendingUserInput; export interface ActivePlanState { readonly createdAt: string; readonly runId: RunId | null; readonly explanation?: string | null; readonly steps: Array<{ readonly step: string; readonly status: "pending" | "inProgress" | "completed"; }>; } export interface LatestProposedPlanState { readonly id: PlanId; readonly createdAt: string; readonly updatedAt: string; readonly runId: RunId | null; readonly planMarkdown: string; readonly status: OrchestrationV2PlanArtifact["status"]; } export type TimelineAttempt = Pick< OrchestrationV2RunAttempt, "id" | "runId" | "attemptOrdinal" | "rootNodeId" | "status" >; export type TimelineEntry = ( | { readonly id: string; readonly kind: "message"; readonly createdAt: string; readonly message: ChatMessage; readonly projectedItem?: OrchestrationV2ProjectedTurnItem; } | { readonly id: string; readonly kind: "proposed-plan"; readonly createdAt: string; readonly proposedPlan: ProposedPlan; } | { readonly id: string; readonly kind: "work"; readonly createdAt: string; readonly entry: WorkLogEntry; } | { readonly id: string; readonly kind: "event"; readonly createdAt: string; readonly projectedItem: OrchestrationV2ProjectedTurnItem; } ) & { /** V2 identity resolved from the item's execution node, when locally available. */ readonly attempt?: TimelineAttempt; }; export function workLogEntryIsToolLike(entry: WorkLogEntry): boolean { return ( entry.tone === "tool" || entry.tone === "thinking" || entry.tone === "error" || entry.command !== undefined || entry.requestKind !== undefined ); } /** Heuristic: providers often emit successful item status while error text lives in `detail` / `command`. */ function toolDetailTextLooksLikeFailure(text: string): boolean { const t = text.toLowerCase(); if (t.includes("file not found")) { return true; } if (t.includes("no files found")) { return true; } if ( t.includes("enoent") || t.includes("no such file or directory") || t.includes("no such file") ) { return true; } if (t.includes("cannot find path") && t.includes("because it does not exist")) { return true; } if (t.includes("commandnotfoundexception")) { return true; } if (t.includes("is not recognized as the name of a cmdlet")) { return true; } if (t.includes("is not recognized") && t.includes("the term '")) { return true; } if (t.includes("a parameter cannot be found that matches parameter name")) { return true; } if (t.includes("command not found")) { return true; } if (//i.test(text)) { return true; } if (/exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text)) { return true; } if (/exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text)) { return true; } return false; } function workEntryIndicatesToolFailureFromOutput( entry: WorkLogEntry, includeCommand: boolean, ): boolean { if (entry.tone === "error") { return true; } const ls = entry.toolLifecycleStatus; if (ls === "failed" || ls === "declined") { return true; } if (!workLogEntryIsToolLike(entry)) { return false; } const parts: string[] = []; if (entry.detail) { parts.push(entry.detail); } if (includeCommand && entry.command) { parts.push(entry.command); } const blob = parts.join("\n"); if (blob.length === 0) { return false; } return toolDetailTextLooksLikeFailure(blob); } /** True when a tool failed, including providers that put error output in `command`. */ export function workEntryIndicatesToolFailure(entry: WorkLogEntry): boolean { return workEntryIndicatesToolFailureFromOutput(entry, true); } /** True when the rendered result indicates failure. The command itself is user intent, not output. */ export function workEntryDisplayIndicatesToolFailure(entry: WorkLogEntry): boolean { return workEntryIndicatesToolFailureFromOutput(entry, false); } /** Severe failures keep the red treatment ordinary tool failures lost: provider * runtime errors mean the turn or a core side effect broke, not that a * command exited nonzero. */ export function workEntrySignalsSevereFailure(entry: WorkLogEntry): boolean { return entry.itemType === "error"; } export function workEntryIndicatesToolSuccess(entry: WorkLogEntry): boolean { if ( !workLogEntryIsToolLike(entry) || workEntryIndicatesToolFailure(entry) || entry.tone === "thinking" ) { return false; } const status = entry.toolLifecycleStatus; return ( status !== "failed" && status !== "declined" && status !== "inProgress" && status !== "stopped" && status !== "idle" ); } export function workEntryIndicatesToolNeutralStatus(entry: WorkLogEntry): boolean { return ( workLogEntryIsToolLike(entry) && !workEntryIndicatesToolFailure(entry) && !workEntryIndicatesToolSuccess(entry) ); } export function isLatestRunSettled( latestRun: Pick | null, runtime: Pick | null, ): boolean { if (latestRun === null) return false; if ( latestRun.status === "preparing" || latestRun.status === "queued" || latestRun.status === "starting" || latestRun.status === "running" || latestRun.status === "waiting" ) return false; return runtime?.activeRunId !== latestRun.runId; } export function deriveActiveWorkStartedAt( latestRun: Pick | null, runtime: Pick | null, sendStartedAt: string | null, ): string | null { if (runtime?.activeRunId !== null && runtime?.activeRunId !== undefined) { return latestRun?.runId === runtime.activeRunId ? (latestRun.startedAt ?? sendStartedAt) : sendStartedAt; } return isLatestRunSettled(latestRun, runtime) ? sendStartedAt : (latestRun?.startedAt ?? sendStartedAt); } export function derivePendingApprovals( approvals: ReadonlyArray, ): ThreadPendingApproval[] { return [...approvals].toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)); } export function derivePendingUserInputs( inputs: ReadonlyArray, ): ThreadPendingUserInput[] { return [...inputs].toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)); } export function deriveActivePlanState( projection: OrchestrationV2ThreadProjection | null, latestRunId: RunId | undefined, ): ActivePlanState | null { if (projection === null) return null; const plans = projection.plans.filter((plan) => plan.kind === "todo_list"); const plan = [...plans].toReversed().find((candidate) => candidate.runId === latestRunId) ?? plans.at(-1) ?? null; if (plan === null || plan.steps.length === 0) return null; return { createdAt: planItemTime(projection, plan.id), runId: plan.runId, explanation: plan.explanation ?? null, steps: plan.steps.map(({ text, status }) => ({ step: text, status: status === "running" ? "inProgress" : status, })), }; } function planItemTime(projection: OrchestrationV2ThreadProjection, planId: PlanId): string { const item = projection.turnItems.findLast( (candidate) => (candidate.type === "proposed_plan" || candidate.type === "todo_list") && candidate.planId === planId, ); return DateTime.formatIso(item?.updatedAt ?? projection.updatedAt); } function toLatestProposedPlanState( projection: OrchestrationV2ThreadProjection, plan: Extract, ): LatestProposedPlanState { const updatedAt = planItemTime(projection, plan.id); return { id: plan.id, createdAt: updatedAt, updatedAt, runId: plan.runId, planMarkdown: plan.markdown, status: plan.status, }; } export function findLatestProposedPlan( projection: OrchestrationV2ThreadProjection | null, latestRunId: RunId | string | null | undefined, ): LatestProposedPlanState | null { if (projection === null) return null; const plans = projection.plans.filter((plan) => plan.kind === "proposed_plan"); const candidates = latestRunId ? plans.filter((plan) => plan.runId === latestRunId) : plans; const plan = [...(candidates.length > 0 ? candidates : plans)] .toSorted( (left, right) => planItemTime(projection, left.id).localeCompare(planItemTime(projection, right.id)) || left.id.localeCompare(right.id), ) .at(-1); return plan === undefined ? null : toLatestProposedPlanState(projection, plan); } export function findSidebarProposedPlan(input: { readonly threads: ReadonlyArray<{ readonly id: ThreadId; readonly projection: OrchestrationV2ThreadProjection; }>; readonly latestRun: Pick | null; readonly latestRunSettled: boolean; readonly threadId: ThreadId | string | null | undefined; }): LatestProposedPlanState | null { if (!input.latestRunSettled && input.latestRun?.sourcePlanRef !== undefined) { const source = input.latestRun.sourcePlanRef; const sourceProjection = input.threads.find( (thread) => thread.id === source.threadId, )?.projection; const plan = sourceProjection?.plans.find( (candidate) => candidate.kind === "proposed_plan" && candidate.id === source.planId, ); if (sourceProjection !== undefined && plan?.kind === "proposed_plan") { return toLatestProposedPlanState(sourceProjection, plan); } } const activeProjection = input.threads.find((thread) => thread.id === input.threadId)?.projection; return findLatestProposedPlan(activeProjection ?? null, input.latestRun?.runId); } export function hasActionableProposedPlan(plan: LatestProposedPlanState | null): boolean { return plan?.status === "active"; } const STANDALONE_V2_ITEM_TYPES = new Set([ "approval_request", "fork", "handoff", "run_interrupt_request", "run_interrupt_result", "subagent", "thread_created", "user_input_request", ]); const PERSISTENT_RESOURCE_V2_ITEM_TYPES = new Set([ "fork", "subagent", "thread_created", ]); export function timelineEntryIsPersistentResourceCard(entry: TimelineEntry): boolean { return ( entry.kind === "event" && PERSISTENT_RESOURCE_V2_ITEM_TYPES.has(entry.projectedItem.item.type) ); } function projectedItemCreatedAt(row: OrchestrationV2ProjectedTurnItem): string { return DateTime.formatIso(row.item.startedAt ?? row.item.updatedAt); } function projectedWorkEntryStatus( item: OrchestrationV2TurnItem, ): NonNullable { switch (item.status) { case "pending": case "running": case "waiting": return "inProgress"; case "completed": return "completed"; case "idle": return "idle"; case "failed": return "failed"; case "cancelled": case "interrupted": return "stopped"; } } function projectedWorkEntryTone(item: OrchestrationV2TurnItem): WorkLogEntry["tone"] { if (item.type === "error") return "info"; if (item.type === "reasoning") return "thinking"; switch (item.type) { case "command_execution": case "file_change": case "file_search": case "web_search": case "dynamic_tool": case "subagent": return "tool"; default: return "info"; } } export function providerErrorPresentation( item: Extract, ): { readonly label: string; readonly detail: string } { if (item.retry === undefined) { return { label: item.title?.trim() || "Provider error", detail: item.failure.message, }; } const progress = item.retry.maxAttempts === null ? `${item.retry.attempt}` : `${item.retry.attempt}/${item.retry.maxAttempts}`; const label = item.status === "running" ? `Retrying provider (${progress})` : item.status === "completed" ? `Provider recovered (${progress} retries)` : item.status === "failed" ? `Provider error after ${progress} retries` : `Provider retry stopped (${progress})`; const retryDelay = item.status === "running" && item.retry.retryDelayMs !== null && item.retry.retryDelayMs > 0 ? item.retry.retryDelayMs < 1_000 ? ` Retrying in ${item.retry.retryDelayMs}ms.` : ` Retrying in ${(item.retry.retryDelayMs / 1_000).toFixed(1).replace(/\.0$/u, "")}s.` : ""; return { label, detail: `${item.failure.message}${retryDelay}`, }; } function projectedWorkEntry(row: OrchestrationV2ProjectedTurnItem): WorkLogEntry { const { item } = row; const title = item.title?.trim() || null; const common = { id: item.id, createdAt: projectedItemCreatedAt(row), runId: item.runId, tone: projectedWorkEntryTone(item), itemType: item.type, toolLifecycleStatus: projectedWorkEntryStatus(item), structuredPayload: item, projectedItem: row, ...extractToolActivityPresentation(item), } as const; switch (item.type) { case "compaction": return { ...common, label: item.status === "running" ? "Compacting context" : "Context compacted", sourceActivityKind: "context-compaction", ...(item.summary ? { detail: item.summary } : {}), }; case "reasoning": return { ...common, label: title ?? "Thinking", ...(item.text ? { detail: item.text } : {}), }; case "command_execution": return { ...common, label: title ?? "Ran command", command: item.input, rawCommand: item.input, ...(item.output ? { detail: item.output } : {}), toolTitle: title ?? "Command", toolData: item, }; case "file_change": { const detail = item.diffStr ?? item.newStr; return { ...common, label: title ?? `Changed ${item.fileName}`, changedFiles: [item.fileName], ...(detail ? { detail } : {}), toolTitle: title ?? "File change", toolData: item, }; } case "file_search": return { ...common, label: title ?? "Searched files", ...(item.pattern ? { detail: item.pattern } : {}), toolTitle: title ?? "File search", toolData: item, }; case "web_search": return { ...common, label: title ?? "Searched the web", ...(item.patterns?.length ? { detail: item.patterns.join(", ") } : {}), toolTitle: title ?? "Web search", toolData: item, }; case "checkpoint": return { ...common, label: title ?? "Checkpoint captured", changedFiles: item.files.map((file) => file.path), toolData: item, }; case "system_notice": return { ...common, label: item.message, sourceActivityKind: "runtime.warning", }; case "error": { const presentation = providerErrorPresentation(item); return { ...common, ...presentation, ...(item.retry === undefined ? { sourceActivityKind: "runtime.error" } : {}), toolData: item, }; } case "dynamic_tool": return { ...common, label: title ?? item.toolName ?? "Tool call", toolTitle: title ?? item.toolName ?? "Tool", toolData: { input: item.input, output: item.output }, }; default: return { ...common, label: title ?? item.type.replaceAll("_", " "), toolData: item, }; } } /** * Builds the web timeline in the exact order committed by `visibleTurnItems`. * Committed rows are presented directly from their projected item. Queued * input is absent by construction until dispatch creates its user turn item. * Persistent client-owned messages are inserted by timestamp without sorting * the canonical sequence. True optimistic sends remain appended afterward. */ export interface TimelineEntriesInput { readonly visibleTurnItems: ReadonlyArray; readonly optimisticMessages: ReadonlyArray; readonly anchoredMessages?: ReadonlyArray; readonly attachmentUrlById?: ReadonlyMap; readonly attempts?: ReadonlyArray; readonly nodes?: ReadonlyArray; readonly plans?: ReadonlyArray; } export interface TimelineEntriesProjection { readonly input: TimelineEntriesInput; readonly entries: TimelineEntry[]; } export function deriveTimelineEntriesFromVisibleTurnItems( input: TimelineEntriesInput, ): TimelineEntry[] { const committedMessageIds = new Set(); const entries: TimelineEntry[] = []; const attemptByRootNodeId = new Map( (input.attempts ?? []).map((attempt) => [attempt.rootNodeId, attempt] as const), ); const nodeById = new Map((input.nodes ?? []).map((node) => [node.id, node] as const)); const planById = new Map((input.plans ?? []).map((plan) => [plan.id, plan] as const)); const resolveAttempt = (item: OrchestrationV2TurnItem): TimelineAttempt | undefined => { if (item.nodeId === null || item.runId === null) return undefined; let nodeId: OrchestrationV2ExecutionNode["id"] | null = item.nodeId; const visited = new Set(); while (nodeId !== null && !visited.has(nodeId)) { visited.add(nodeId); const directAttempt = attemptByRootNodeId.get(nodeId); if (directAttempt?.runId === item.runId) return directAttempt; const node = nodeById.get(nodeId); if (node === undefined) return undefined; const rootAttempt = attemptByRootNodeId.get(node.rootNodeId); if (rootAttempt?.runId === item.runId) return rootAttempt; nodeId = node.parentNodeId; } return undefined; }; for (const row of input.visibleTurnItems) { const { item } = row; if (turnItemIsWorkspacePreparation(item)) continue; // Task progress belongs in the composer, not between conversation entries. if (item.type === "todo_list") continue; const createdAt = projectedItemCreatedAt(row); const attempt = resolveAttempt(item); const attemptMetadata = attempt === undefined ? {} : { attempt }; if (item.type === "user_message" || item.type === "assistant_message") { const message: ChatMessage = { id: item.messageId, role: item.type === "user_message" ? "user" : "assistant", text: item.text, ...((item.attachments?.length ?? 0) > 0 ? { attachments: (item.attachments ?? []).map((attachment) => { const previewUrl = input.attachmentUrlById?.get(attachment.id); return previewUrl ? { ...attachment, previewUrl } : attachment; }), } : {}), runId: item.runId, streaming: item.type === "assistant_message" && item.streaming, ...(item.type === "user_message" ? { createdBy: item.createdBy, creationSource: item.creationSource } : {}), createdAt, updatedAt: DateTime.formatIso(item.updatedAt), ...(item.type === "user_message" ? { inputIntent: item.inputIntent } : {}), }; committedMessageIds.add(message.id); entries.push({ id: message.id, kind: "message", createdAt, message, projectedItem: row, ...attemptMetadata, }); continue; } if (item.type === "proposed_plan") { const plan = planById.get(item.planId); const proposedPlan = { id: item.planId, runId: item.runId, planMarkdown: item.markdown, status: plan?.kind === "proposed_plan" ? plan.status : ("active" as const), createdAt, updatedAt: DateTime.formatIso(item.updatedAt), }; entries.push({ id: item.id, kind: "proposed-plan", createdAt, proposedPlan, ...attemptMetadata, }); continue; } if (STANDALONE_V2_ITEM_TYPES.has(item.type)) { entries.push({ id: item.id, kind: "event", createdAt, projectedItem: row, ...attemptMetadata, }); continue; } entries.push({ id: item.id, kind: "work", createdAt, entry: projectedWorkEntry(row), ...attemptMetadata, }); } const retainedMessageIds = new Set(committedMessageIds); for (const message of input.anchoredMessages ?? []) { if (retainedMessageIds.has(message.id)) continue; retainedMessageIds.add(message.id); const entry: TimelineEntry = { id: message.id, kind: "message", createdAt: message.createdAt, message, }; const insertionIndex = entries.findIndex( (candidate) => candidate.createdAt > message.createdAt, ); if (insertionIndex === -1) { entries.push(entry); } else { entries.splice(insertionIndex, 0, entry); } } for (const message of input.optimisticMessages) { if (message.inputIntent !== "queued_turn" && !retainedMessageIds.has(message.id)) { retainedMessageIds.add(message.id); entries.push({ id: message.id, kind: "message", createdAt: message.createdAt, message, }); } } return entries; } type AttachmentResource = Extract; const EMPTY_IMAGE_RESOURCES = Object.freeze>([]); /** A mounted row requests its stored images. Local previews keep their existing URLs. */ export function selectMessageImageResources( attachments: ChatMessage["attachments"], ): ReadonlyArray { const attachmentIds = new Set(); for (const attachment of attachments ?? []) { if (!isImageAttachment(attachment)) continue; const previewUrl = attachment.previewUrl; if (previewUrl?.startsWith("blob:") || previewUrl?.startsWith("data:")) continue; attachmentIds.add(attachment.id); } return attachmentIds.size === 0 ? EMPTY_IMAGE_RESOURCES : Array.from(attachmentIds, (attachmentId) => ({ _tag: "attachment", attachmentId })); } /** Handoffs need server URLs even while their message rows are unmounted. */ export function selectHandoffImageResources( messages: ReadonlyArray> | undefined, handoffs: Readonly>>, ): ReadonlyArray { if (Object.keys(handoffs).length === 0) return EMPTY_IMAGE_RESOURCES; const attachmentIds = new Set(); for (const message of messages ?? []) { if (message.role !== "user" || !handoffs[message.id]?.length) continue; for (const attachment of message.attachments ?? []) { if (isImageAttachment(attachment)) attachmentIds.add(attachment.id); } } return attachmentIds.size === 0 ? EMPTY_IMAGE_RESOURCES : Array.from(attachmentIds, (attachmentId) => ({ _tag: "attachment", attachmentId })); } /** Own one mapper per preview stage. Immutable messages retain unchanged preview objects. */ export function createMessageAttachmentPreviewProjector() { const attachmentsBySource = new WeakMap< ReadonlyArray, ReadonlyArray >(); const messagesBySource = new WeakMap(); return ( message: ChatMessage, previewUrlFor: (attachment: ChatAttachment) => string | undefined, ): ChatMessage => { const source = message.attachments; if (!source || source.length === 0) return message; const previous = attachmentsBySource.get(source) ?? source; let changed: ChatAttachment[] | undefined; let hasOverrides = false; for (const [index, attachment] of source.entries()) { const previewUrl = previewUrlFor(attachment); const sourceUrl = "previewUrl" in attachment ? attachment.previewUrl : undefined; const previousAttachment = previous[index]!; const previousUrl = "previewUrl" in previousAttachment ? previousAttachment.previewUrl : undefined; const next = !previewUrl || previewUrl === sourceUrl ? attachment : previewUrl === previousUrl ? previousAttachment : { ...attachment, previewUrl }; hasOverrides ||= next !== attachment; if (next !== previousAttachment) { changed ??= previous.slice(); changed[index] = next; } } const attachments = hasOverrides ? (changed ?? previous) : source; attachmentsBySource.set(source, attachments); if (attachments === source) { messagesBySource.delete(message); return message; } const previousMessage = messagesBySource.get(message); if (previousMessage?.attachments === attachments) return previousMessage; const result = { ...message, attachments }; messagesBySource.set(message, result); return result; }; } /** Text and update time do not change a streaming assistant message's row structure. */ export function isStreamingMessageTextUpdate(previous: ChatMessage, next: ChatMessage): boolean { if ( previous.role !== "assistant" || next.role !== "assistant" || !previous.streaming || !next.streaming ) { return false; } const { text: _previousText, updatedAt: _previousUpdatedAt, ...previousMetadata } = previous; const { text: _nextText, updatedAt: _nextUpdatedAt, ...nextMetadata } = next; return shallow(previousMetadata, nextMetadata); } /** Keep provenance and execution metadata in the rebuild boundary, including inspector data. */ export function isStreamingTurnItemTextUpdate( previous: OrchestrationV2ProjectedTurnItem, next: OrchestrationV2ProjectedTurnItem, ): boolean { const { item: previousItem, ...previousSource } = previous; const { item: nextItem, ...nextSource } = next; if ( previousItem.type !== "assistant_message" || nextItem.type !== "assistant_message" || !previousItem.streaming || !nextItem.streaming || !shallow(previousSource, nextSource) || projectedItemCreatedAt(previous) !== projectedItemCreatedAt(next) ) { return false; } const { text: _previousText, updatedAt: _previousUpdatedAt, ...previousMetadata } = previousItem; const { text: _nextText, updatedAt: _nextUpdatedAt, ...nextMetadata } = nextItem; // Wire decoding can recreate timestamps and attachments on each update. // Compare only the changed item's metadata, never the entire transcript. return Equal.equals(previousMetadata, nextMetadata); } function reuseTimelineEntries( input: TimelineEntriesInput, previous: TimelineEntriesProjection, ): TimelineEntry[] | null { const before = previous.input; if ( input.visibleTurnItems.length < before.visibleTurnItems.length || !shallow(input.optimisticMessages, before.optimisticMessages) || !shallow(input.anchoredMessages, before.anchoredMessages) || !shallow(input.attachmentUrlById, before.attachmentUrlById) || !shallow(input.attempts, before.attempts) || !shallow(input.nodes, before.nodes) || !shallow(input.plans, before.plans) ) { return null; } const appended = input.visibleTurnItems.length > before.visibleTurnItems.length; // Anchored and optimistic messages need to be interleaved/deduplicated when // committed items arrive. Keep the full projection for that transition. if ( appended && (input.optimisticMessages.length > 0 || (input.anchoredMessages?.length ?? 0) > 0) ) { return null; } const replacements = new Map< OrchestrationV2ProjectedTurnItem, OrchestrationV2ProjectedTurnItem >(); for (const [index, previousItem] of before.visibleTurnItems.entries()) { const item = input.visibleTurnItems[index]!; if (item === previousItem) continue; if (!isStreamingTurnItemTextUpdate(previousItem, item)) return null; replacements.set(previousItem, item); } if (replacements.size === 0 && !appended) return previous.entries; const entries = previous.entries.map((entry): TimelineEntry => { const row = entry.kind === "message" && entry.projectedItem !== undefined ? replacements.get(entry.projectedItem) : undefined; if (entry.kind !== "message" || row?.item.type !== "assistant_message") return entry; return { ...entry, projectedItem: row, message: { ...entry.message, text: row.item.text, updatedAt: DateTime.formatIso(row.item.updatedAt), }, }; }); if (appended) { entries.push( ...deriveTimelineEntriesFromVisibleTurnItems({ ...input, visibleTurnItems: input.visibleTurnItems.slice(before.visibleTurnItems.length), }), ); } return entries; } /** Reuse immutable entries during streaming without reordering the canonical v2 sequence. */ export function deriveTimelineEntriesFromVisibleTurnItemsWithState( input: TimelineEntriesInput, previous: TimelineEntriesProjection | null = null, ): TimelineEntriesProjection { const reused = previous === null ? null : reuseTimelineEntries(input, previous); if (reused !== null) return { input, entries: reused }; const entries = deriveTimelineEntriesFromVisibleTurnItems(input); if (previous === null || !shallow(input.attachmentUrlById, previous.input.attachmentUrlById)) { return { input, entries }; } // Tool output and lifecycle changes rebuild grouping, but unchanged message // objects and previews still let memoized history rows stay mounted. const previousMessages = new Map( previous.entries.flatMap((entry) => entry.kind === "message" ? [[entry.id, entry] as const] : [], ), ); return { input, entries: entries.map((entry) => { if (entry.kind !== "message" || entry.projectedItem === undefined) return entry; const before = previousMessages.get(entry.id); if (before?.projectedItem !== entry.projectedItem) return entry; return before.attempt === entry.attempt ? before : { ...entry, message: before.message }; }), }; } export function inferCheckpointTurnCountByRunId( summaries: ReadonlyArray, ): Record { return Object.fromEntries( summaries.flatMap((summary) => summary.runId === null ? [] : [[summary.runId, summary.checkpointTurnCount] as const], ), ); } export function deriveRevertTurnCountByUserMessageId(input: { readonly timelineEntries: ReadonlyArray; readonly checkpoints: ReadonlyArray; }): Map { const readyCheckpointByRunId = new Map(); for (const checkpoint of input.checkpoints) { if (checkpoint.status === "ready") { readyCheckpointByRunId.set(checkpoint.runId, checkpoint); } } const byUserMessageId = new Map(); for (const entry of input.timelineEntries) { if (entry.kind !== "message" || entry.message.role !== "user") continue; if (entry.message.inputIntent !== "turn_start" && entry.message.inputIntent !== "queued_turn") { continue; } if (entry.message.runId === null) continue; const checkpoint = readyCheckpointByRunId.get(entry.message.runId); if (checkpoint === undefined) continue; byUserMessageId.set(entry.message.id, Math.max(0, checkpoint.checkpointTurnCount - 1)); } return byUserMessageId; } export function derivePhase(runtime: ThreadRuntimeSummary | null): SessionPhase { if (runtime === null) return "disconnected"; if ( runtime.status === "preparing" || runtime.status === "starting" || runtime.status === "queued" ) return "connecting"; if (runtime.status === "running" || runtime.status === "waiting") return "running"; return "ready"; } export type { TurnDiffSummary };