3551 lines · 120.1 KB · TypeScript
RawDownload
import { GitPullRequestIcon } from "lucide-react";import {  type AssistantCitation,  type EnvironmentId,  type MessageId,  type ScopedThreadRef,  type ServerProviderSkill,  type ToolActivityIcon,  type TurnId,} from "@t3tools/contracts";import { parseScopedThreadKey } from "@t3tools/client-runtime/environment";import type { CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates";import {  resolveWorkEntryToolPresentation,  resolveViewedImageAsset,  workEntryViewedImagePath,} from "@t3tools/client-runtime/work-log/presentation";import { resolveWorkGroupScrollAnchor } from "@t3tools/client-runtime/work-log/scroll-anchor";import type { AgentPanelModel } from "@t3tools/client-runtime/state/subagentRuntime";import {  emptyAgentPanelModel,  formatSubagentTokenCount,} from "@t3tools/client-runtime/state/subagentRuntime"; const EMPTY_AGENT_PANEL_MODEL = emptyAgentPanelModel();const NOOP_OPEN_AGENTS = () => {};const NOOP_USE_ARTIFACT_TEMPLATE = () => {};const NOOP_OPEN_ATTACHMENT = (_attachment: ChatFileAttachment) => {};import { resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList";import { toolActivityFaviconUrl } from "@t3tools/shared/favicon";import { formatDuration } from "@t3tools/shared/orchestrationTiming";import { getProjectFaviconCacheKey } from "@t3tools/shared/projectFavicon";import { observeVisibleAnimation } from "../../lib/visibleAnimation";import {  createContext,  Fragment,  memo,  use,  useCallback,  useEffect,  useId,  useLayoutEffect,  useMemo,  useRef,  useState,  type KeyboardEvent,  type MouseEvent,  type ReactNode,} from "react";import {  LegendList,  type LegendListRef,  type MaintainScrollAtEndOptions,} from "@legendapp/list/react";import { FileDiff } from "@pierre/diffs/react";import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider";import {  createMessageAttachmentPreviewProjector,  deriveTimelineEntries,  selectMessageImageResources,  workEntryDisplayIndicatesToolFailure,  workEntrySignalsSevereFailure,  workLogEntryIsToolLike,} from "../../session-logic";import {  type ChatMessage,  type ChatFileAttachment,  type ChatImageAttachment,  isBrowserPreviewAttachment,  isFileAttachment,  isImageAttachment,  isVideoAttachment,  type TurnDiffSummary,} from "../../types";import {  getRenderablePatch,  resolveDiffThemeName,  resolveFileDiffPath,} from "../../lib/diffRendering";import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown";import { T3Wordmark } from "../T3Wordmark";import {  BotIcon,  BrainIcon,  CheckIcon,  ChevronDownIcon,  ChevronRightIcon,  ChevronUpIcon,  CircleAlertIcon,  DownloadIcon,  EyeIcon,  GlobeIcon,  HammerIcon,  MessageCircleIcon,  Minimize2Icon,  MousePointerClickIcon,  PaintbrushIcon,  SearchIcon,  SmartphoneIcon,  SquarePenIcon,  TerminalIcon,  Undo2Icon,  WrenchIcon,  XIcon,  ZapIcon,} from "lucide-react";import { Button } from "../ui/button";import { useAssetUrlRefresh, useAssetUrls, useAssetUrlState } from "../../assets/assetUrls";import { MediaVideoPlayer } from "../media/MediaVideoPlayer";import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area";import {  buildAttachmentVideoAsset,  buildExpandedImagePreview,  ExpandedImagePreview,} from "./ExpandedImagePreview";import {  SNAP_SHOT_ATTACHMENT_FRAME_CLASS,  SnapShotAttachmentDetails,} from "./SnapShotAttachmentDetails";import { ProposedPlanCard } from "./ProposedPlanCard";import { ChangedFilesCard } from "./ChangedFilesTree";import {  CHAT_TIMELINE_ANCHOR_OFFSET,  timelineContentOverflowsViewport,} from "./timelineScrollAnchoring";import { MessageCopyButton } from "./MessageCopyButton";import { PierreEntryIcon } from "./PierreEntryIcon";import { AssistantSelectionToolbar } from "./AssistantSelectionToolbar";import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection";import {  AssistantCitationSource,  type AssistantCitationRequest,  type AssistantCitationTarget,} from "./AssistantCitationSource";import { useAssistantCitationTarget, type CitationHistoryPage } from "./useAssistantCitationTarget";import {  computeStableMessagesTimelineRows,  deriveMessagesTimelineRowsWithState,  type MessagesTimelineRowsProjection,  liveWorkEntryLabel,  resolveAssistantMessageCopyState,  resolveTimelineIsAtEnd,  resolveTimelineMinimapHasPersistentGutter,  resolveTimelineMinimapCurrentIndex,  resolveTimelineMinimapHeightStyle,  resolveTimelineMinimapHitStripWidth,  resolveTimelineMinimapIndexFromPointer,  resolveTimelineMinimapInteractiveWidth,  resolveTimelineMinimapTopPercent,  resolveWorkGroupScrollIndex,  shouldFollowWorkGroupAppend,  shouldPreserveAssistantLineBreaks,  toolGroupAction,  workEntryDisplayLabel,  workEntryIsVisibleInGroup,  type StableMessagesTimelineRowsState,  type MessagesTimelineRow,  TIMELINE_MINIMAP_MIN_ITEMS,  type TimelineLatestTurn,  type WorkGroupScrollAnchor,} from "./MessagesTimeline.logic";import { TerminalContextInlineChip } from "./TerminalContextInlineChip";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import {  deriveDisplayedUserMessageState,  type ParsedTerminalContextEntry,} from "~/lib/terminalContext";import {  extractTrailingElementContexts,  type ParsedElementContextEntry,} from "~/lib/elementContext";import {  extractTrailingPreviewAnnotation,  type ParsedPreviewAnnotation,} from "~/lib/previewAnnotation";import { cn } from "~/lib/utils";import { useUiStateStore } from "~/uiStateStore";import { type TimestampFormat } from "@t3tools/contracts/settings";import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";import {  buildInlineTerminalContextText,  formatInlineTerminalContextLabel,  textContainsInlineTerminalContextLabels,} from "./userMessageTerminalContexts";import { deriveAgentSpawnSummary } from "./agentSpawnSummary";import { SkillInlineText } from "./SkillInlineText";import { formatWorkspaceRelativePath } from "../../filePathDisplay";import {  buildReviewCommentRenderablePatch,  formatReviewCommentFence,  parseReviewCommentMessageSegments,  type ReviewCommentContext,} from "../../reviewCommentContext"; // ---------------------------------------------------------------------------// Context — shared state consumed by every row component via Context.// Propagates through LegendList's memo boundaries for shared callbacks and// non-row-scoped state. `nowIso` is intentionally excluded — self-ticking// components (WorkingTimer, LiveElapsed) handle it.// --------------------------------------------------------------------------- interface TimelineRowSharedState {  citationRequest: AssistantCitationTarget | null;  listRef: React.RefObject<LegendListRef | null>;  timestampFormat: TimestampFormat;  routeThreadKey: string;  threadRef: ScopedThreadRef | null;  markdownCwd: string | undefined;  resolvedTheme: "light" | "dark";  workspaceRoot: string | undefined;  skills: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;  activeThreadEnvironmentId: EnvironmentId;  onRevertToTurnCount: (targetTurnCount: number) => void;  onUseArtifactTemplate: (template: CodexArtifactTemplate) => void;  onImageExpand: (preview: ExpandedImagePreview) => void;  onFileOpen: (attachment: ChatFileAttachment) => void;  onFileDownload: (attachment: ChatFileAttachment) => void;  onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void;  onToggleTurnFold: (turnId: TurnId) => void;  onToggleWorkGroup: (groupId: string, anchorKey: string) => void;  onToggleWorkEntry: (anchorKey: string, collapsed: boolean) => void;  workGroupViewState: WorkGroupViewState;  agentPanelModel: AgentPanelModel;  onOpenAgents: () => void;} interface TimelineRowActivityState {  isWorking: boolean;  isPreparingWorktree: boolean;  isCompacting: boolean;  isRevertingCheckpoint: boolean;  latestTurnId: TurnId | null;} const TimelineRowCtx = createContext<TimelineRowSharedState>(null!);const TimelineRowActivityCtx = createContext<TimelineRowActivityState>(null!); interface WorkGroupViewState {  scrollPositions: Map<string, WorkGroupScrollAnchor>;  expandedEntries: Set<string>;} const WorkGroupViewCtx = createContext<{  state: WorkGroupViewState;  onToggleEntry: (collapsed: boolean) => void;} | null>(null);const TIMELINE_LIST_HEADER = <div className="h-3 sm:h-4" />;const TIMELINE_LIST_FADE_HEADER = (  <div className="h-[var(--workspace-titlebar-scroll-fade-height)]" />); // Header row shown when older turns exist beyond the loaded window. Plain// button, no spinner animation; the label change is the loading indicator.function TimelineLoadEarlierHeader({  loading,  onLoadEarlier,  fade,}: {  loading: boolean;  onLoadEarlier: () => void;  fade: boolean;}) {  return (    <div className={fade ? "pt-[var(--workspace-titlebar-scroll-fade-height)]" : "pt-3 sm:pt-4"}>      <div className="mx-auto w-full max-w-3xl pb-2">        <button          type="button"          onClick={onLoadEarlier}          disabled={loading}          className="w-full py-1.5 text-xs text-muted-foreground/60 hover:text-foreground disabled:cursor-default"        >          {loading ? "Loading earlier turns…" : "Load earlier turns"}        </button>      </div>    </div>  );}function TimelineListFooter({ composerInset }: { readonly composerInset: number }) {  return (    <div aria-hidden>      <div style={{ height: composerInset }} />      <div className="h-3 sm:h-4" />    </div>  );}const EMPTY_TIMELINE_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];const TIMELINE_MAINTAIN_SCROLL_AT_END = {  animated: false,  on: {    dataChange: true,    // Composer inset changes must not move already-visible messages. New    // rows and row growth still keep live-follow pinned through the other    // triggers below.    footerLayout: false,    itemLayout: true,    layout: true,  },} as const satisfies MaintainScrollAtEndOptions; // ---------------------------------------------------------------------------// Props (public API)// --------------------------------------------------------------------------- interface MessagesTimelineProps {  citationRequest?: AssistantCitationRequest | null;  citationHistoryLoading?: boolean;  onCiteAssistantText?: (    citation: AssistantCitation,    sourceAnchor: AssistantCitationSourceAnchor,  ) => boolean;  agentPanelModel?: AgentPanelModel;  onOpenAgents?: () => void;  isWorking: boolean;  isPreparingWorktree?: boolean;  isCompacting?: boolean;  activeTurnStartedAt: string | null;  listRef: React.RefObject<LegendListRef | null>;  timelineEntries: ReturnType<typeof deriveTimelineEntries>;  latestTurn: TimelineLatestTurn | null;  runningTurnId: TurnId | null;  turnDiffSummaries: ReadonlyArray<TurnDiffSummary>;  routeThreadKey: string;  onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void;  supportsConversationRollback: boolean;  onRevertToTurnCount: (targetTurnCount: number) => void;  onUseArtifactTemplate?: (template: CodexArtifactTemplate) => void;  isRevertingCheckpoint: boolean;  onImageExpand: (preview: ExpandedImagePreview) => void;  onFileOpen?: (attachment: ChatFileAttachment) => void;  onFileDownload?: (attachment: ChatFileAttachment) => void;  activeThreadEnvironmentId: EnvironmentId;  markdownCwd: string | undefined;  resolvedTheme: "light" | "dark";  timestampFormat: TimestampFormat;  workspaceRoot: string | undefined;  skills?: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;  anchorMessageId: MessageId | null;  onAnchorReady: (messageId: MessageId, anchorIndex: number) => void;  contentInsetEndAdjustment: number;  /**   * Whether the timeline should keep pinning to the live edge as content   * grows. Off while the user is reading history; LegendList's own   * maintainScrollAtEnd would otherwise re-pin regardless of ChatView's   * scroll-mode refs whenever the user drifts near the bottom.   */  liveFollowEnabled: boolean;  onIsAtEndChange: (isAtEnd: boolean) => void;  /**   * Whether the real rows extend past the viewport above the composer.   * Reported after scrolls, row size changes, and viewport resizes.   */  onContentOverflowChange?: (overflows: boolean) => void;  onToolOutputCollapsedAtEnd?: () => void;  onManualNavigation: () => void;  hideEmptyPlaceholder?: boolean;  topFadeEnabled?: boolean;  /** Non-null when older turns exist beyond the loaded window. */  loadEarlier?: CitationHistoryPage | null;} // ---------------------------------------------------------------------------// MessagesTimeline — list owner// --------------------------------------------------------------------------- export const MessagesTimeline = memo(function MessagesTimeline({  citationRequest = null,  citationHistoryLoading = false,  onCiteAssistantText,  isWorking,  isPreparingWorktree = false,  isCompacting = false,  activeTurnStartedAt,  agentPanelModel = EMPTY_AGENT_PANEL_MODEL,  onOpenAgents = NOOP_OPEN_AGENTS,  listRef,  timelineEntries,  latestTurn,  runningTurnId,  turnDiffSummaries,  routeThreadKey,  onOpenTurnDiff,  supportsConversationRollback,  onRevertToTurnCount,  onUseArtifactTemplate = NOOP_USE_ARTIFACT_TEMPLATE,  isRevertingCheckpoint,  onImageExpand,  onFileOpen = NOOP_OPEN_ATTACHMENT,  onFileDownload = NOOP_OPEN_ATTACHMENT,  activeThreadEnvironmentId,  markdownCwd,  resolvedTheme,  timestampFormat,  workspaceRoot,  skills = EMPTY_TIMELINE_SKILLS,  anchorMessageId,  onAnchorReady,  contentInsetEndAdjustment,  liveFollowEnabled,  onIsAtEndChange,  onContentOverflowChange,  onToolOutputCollapsedAtEnd,  onManualNavigation,  hideEmptyPlaceholder = false,  topFadeEnabled = false,  loadEarlier = null,}: MessagesTimelineProps) {  const [expandedTurnIds, setExpandedTurnIds] = useState<ReadonlySet<TurnId>>(new Set());  const citationThreadRef = useMemo(() => parseScopedThreadKey(routeThreadKey), [routeThreadKey]);  const expandCitedTurn = useCallback((turnId: TurnId) => {    setExpandedTurnIds((current) =>      current.has(turnId) ? current : new Set([...current, turnId]),    );  }, []);  const [expandedWorkGroupIds, setExpandedWorkGroupIds] = useState<ReadonlySet<string>>(new Set());  // Scroll/disclosure state outlives virtualized rows, but never the current thread.  const workGroupViewState = useMemo<WorkGroupViewState>(    () => ({ scrollPositions: new Map(), expandedEntries: new Set() }),    [routeThreadKey],  );  const [disclosureToggleSettling, setDisclosureToggleSettling] = useState(false);  const [minimapStripMap] = useState(() => new Map<string, HTMLSpanElement>());  const disclosureAnchorKeyRef = useRef<string | null>(null);  const disclosureSettleFrameRef = useRef<number | null>(null);  const disclosureSettleSecondFrameRef = useRef<number | null>(null);  useEffect(() => {    return () => {      if (disclosureSettleFrameRef.current !== null) {        cancelAnimationFrame(disclosureSettleFrameRef.current);      }      if (disclosureSettleSecondFrameRef.current !== null) {        cancelAnimationFrame(disclosureSettleSecondFrameRef.current);      }    };  }, []);   const suspendEndScrollMaintenanceForDisclosure = useCallback(    (anchorKey: string, collapsed = false) => {      disclosureAnchorKeyRef.current = anchorKey;      setDisclosureToggleSettling(true);      if (disclosureSettleFrameRef.current !== null) {        cancelAnimationFrame(disclosureSettleFrameRef.current);      }      if (disclosureSettleSecondFrameRef.current !== null) {        cancelAnimationFrame(disclosureSettleSecondFrameRef.current);      }      disclosureSettleFrameRef.current = requestAnimationFrame(() => {        disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => {          disclosureAnchorKeyRef.current = null;          setDisclosureToggleSettling(false);          disclosureSettleFrameRef.current = null;          disclosureSettleSecondFrameRef.current = null;          // Wait for row measurement and the disclosure click's blur check.          // Closing output can reveal the end without a scroll event.          if (collapsed && resolveTimelineIsAtEnd(listRef.current?.getState()) === true) {            onToolOutputCollapsedAtEnd?.();          }        });      });    },    [listRef, onToolOutputCollapsedAtEnd],  );   const shouldRestoreVisibleContentPosition = useCallback((row: MessagesTimelineRow) => {    const disclosureAnchorKey = disclosureAnchorKeyRef.current;    return disclosureAnchorKey === null || row.id === disclosureAnchorKey;  }, []);   const maintainVisibleContentPosition = useMemo(    () => ({      data: true,      size: true,      shouldRestorePosition: shouldRestoreVisibleContentPosition,    }),    [shouldRestoreVisibleContentPosition],  );   const onToggleTurnFold = useCallback(    (turnId: TurnId) => {      suspendEndScrollMaintenanceForDisclosure(`turn-fold:${turnId}`);      setExpandedTurnIds((existing) => {        const next = new Set(existing);        if (next.has(turnId)) {          next.delete(turnId);        } else {          next.add(turnId);        }        return next;      });    },    [suspendEndScrollMaintenanceForDisclosure],  );  const onToggleWorkGroup = useCallback(    (groupId: string, anchorKey: string) => {      suspendEndScrollMaintenanceForDisclosure(anchorKey, expandedWorkGroupIds.has(groupId));      setExpandedWorkGroupIds((existing) => {        const next = new Set(existing);        if (next.has(groupId)) {          next.delete(groupId);        } else {

Lines 1–500 of 3551.