import { shouldPreserveAssistantLineBreaks } from "@t3tools/shared/markdownPipeline"; import { type RunId } from "@t3tools/contracts"; import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; import { memo, use, useState } from "react"; import { type ChatMessage, type TurnDiffSummary } from "../../types"; import ChatMarkdown from "../ChatMarkdown"; import { GitForkIcon } from "lucide-react"; import { Button } from "../ui/button"; import { ProposedPlanCard } from "./ProposedPlanCard"; import { HtmlRenderFrame } from "./HtmlRenderFrame"; import { McpAppFrame } from "./McpAppFrame"; import { checkpointFileTotals } from "@t3tools/client-runtime/state/thread-checkpoints"; import { ChangedFilesCard } from "./ChangedFilesTree"; import { useFileContextMenuHandler } from "../../fileContextMenu"; import { useAssistantMessageText, useProject, useThreadShell } from "../../state/entities"; import { MessageCopyButton } from "./MessageCopyButton"; import { AssistantCitationSource } from "./AssistantCitationSource"; import { resolveAssistantMessageCopyState } from "./MessagesTimeline.logic"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "~/lib/utils"; import { useUiStateStore } from "~/uiStateStore"; import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat"; import { useV2ItemSupport } from "../../state/v2ItemSupport"; import { TimelineRowCtx, TimelineRowActivityCtx, type TimelineRow } from "./timelineRowContext"; import { MESSAGE_HEADING_LEVEL, MessageAuthorHeading } from "./TimelineRowParts"; export function AssistantTimelineRow({ row }: { row: Extract }) { const ctx = use(TimelineRowCtx); // The timeline holds a streaming message's row while only its text grows. const liveText = useAssistantMessageText( row.message.streaming ? ctx.threadRef : null, row.projectedItem?.item.id, ); const text = liveText ?? row.message.text; const messageText = text || (row.message.streaming ? "" : "(empty response)"); return ( <>
T3 Code
{ctx.onOpenTurnDiff ? ( ) : null} {row.showAssistantMeta ? ( ) : null}
); } function AssistantForkButton({ projectedItem, }: { readonly projectedItem: NonNullable["projectedItem"]>; }) { const ctx = use(TimelineRowCtx); const [busy, setBusy] = useState(false); const support = useV2ItemSupport({ environmentId: ctx.activeThreadEnvironmentId, sourceThreadId: projectedItem.sourceThreadId, sourceItemId: projectedItem.sourceItemId, }); const canFork = canForkProjectedAssistantItem({ projectedItem, capabilities: support.providerSession?.capabilities, }); const onForkFromRun = ctx.onForkFromRun; if (!canFork || onForkFromRun === undefined || projectedItem.item.runId === null) return null; const runId = projectedItem.item.runId; return ( { setBusy(true); void onForkFromRun({ sourceThreadId: projectedItem.sourceThreadId, runId }).finally( () => setBusy(false), ); }} aria-label="Fork from this response" /> } > Fork from this response ); } export function AssistantMetaTimelineRow({ row, }: { row: Extract; }) { return (
); } function AssistantMessageMeta({ className, projectedItem, message, showCopyButton, copyStreaming, alwaysVisible = false, }: { className?: string; projectedItem?: Extract["projectedItem"]; message: ChatMessage; showCopyButton: boolean; copyStreaming: boolean; alwaysVisible?: boolean; }) { const ctx = use(TimelineRowCtx); return (
{projectedItem && projectedItem.item.status !== "completed" ? ( {projectedItem.item.status} ) : null} {projectedItem?.item.type === "assistant_message" ? ( ) : null} {!message.streaming && ( }> {formatDayAwareTimestamp(message.updatedAt, ctx.timestampFormat)} {formatChatTimestampTooltip(message.updatedAt, ctx.timestampFormat)} )}
); } function AssistantCopyButton({ message, showCopyButton, streaming, }: { message: ChatMessage; showCopyButton: boolean; streaming: boolean; }) { const assistantCopyState = resolveAssistantMessageCopyState({ text: message.text ?? null, showCopyButton, streaming, }); if (!assistantCopyState.visible) { return null; } return ; } export function ProposedPlanTimelineRow({ row, }: { row: Extract; }) { const ctx = use(TimelineRowCtx); return (
); } export function HtmlRenderTimelineRow({ row, }: { row: Extract; }) { const ctx = use(TimelineRowCtx); const { onAppFullscreenChange } = ctx; const onFullscreenChange = (fullscreen: boolean) => onAppFullscreenChange(row.id, fullscreen); if (ctx.renderHtmlRender) { return
{ctx.renderHtmlRender(row)}
; } return (
); } export function McpAppTimelineRow({ row }: { row: Extract }) { const ctx = use(TimelineRowCtx); const { awaitingUser } = use(TimelineRowActivityCtx); return (
ctx.onAppFullscreenChange(row.id, fullscreen)} />
); } /** Subscribes directly to the UI state store for expand/collapse state, * so toggling re-renders only this component — not the entire list. */ const AssistantChangedFilesSection = memo(function AssistantChangedFilesSection({ turnSummary, routeThreadKey, resolvedTheme, onOpenTurnDiff, }: { turnSummary: TurnDiffSummary | undefined; routeThreadKey: string; resolvedTheme: "light" | "dark"; displayThreadKey?: string; onOpenTurnDiff: (runId: RunId, filePath?: string) => void; }) { if (!turnSummary) return null; const checkpointFiles = turnSummary.files; if (checkpointFiles.length === 0) return null; return ( ); }); function AssistantChangedFilesSectionInner({ turnSummary, checkpointFiles, routeThreadKey, resolvedTheme, onOpenTurnDiff, }: { turnSummary: TurnDiffSummary; checkpointFiles: TurnDiffSummary["files"]; routeThreadKey: string; resolvedTheme: "light" | "dark"; displayThreadKey?: string; onOpenTurnDiff: (runId: RunId, filePath?: string) => void; }) { const ctx = use(TimelineRowCtx); const persistedExpanded = useUiStateStore( (store) => store.threadChangedFilesExpandedById[routeThreadKey]?.[turnSummary.runId], ); const setExpanded = useUiStateStore((store) => store.setThreadChangedFilesExpanded); const allDirectoriesExpanded = persistedExpanded ?? false; const thread = useThreadShell(ctx.threadRef); const activeProject = useProject( thread && thread.projectId ? { environmentId: thread.environmentId, projectId: thread.projectId } : null, ); const onFileContextMenu = useFileContextMenuHandler(ctx.activeThreadEnvironmentId); return ( setExpanded(routeThreadKey, turnSummary.runId, !allDirectoriesExpanded) } onOpenTurnDiff={onOpenTurnDiff} onFileContextMenu={(filePath, event) => onFileContextMenu( { environmentId: ctx.activeThreadEnvironmentId, filePath, workspaceRoot: ctx.workspaceRoot, repositoryRoot: thread?.worktreePath == null ? activeProject?.repositoryIdentity?.rootPath : undefined, }, event, ) } /> ); }