import { CheckpointRef, EnvironmentId, MessageId, TurnId } from "@t3tools/contracts";import { codexFeedbackMessage } from "@t3tools/client-runtime/state/threads";import { createRef, type ReactNode, type Ref } from "react";import { renderToStaticMarkup } from "react-dom/server";import { beforeAll, describe, expect, it, vi } from "vite-plus/test";import type { LegendListRef } from "@legendapp/list/react"; vi.mock("@legendapp/list/react", async () => { const legendListTestId = "legend-list"; const LegendList = (props: { data: Array<{ id: string }>; keyExtractor: (item: { id: string }) => string; renderItem: (args: { item: { id: string } }) => ReactNode; ListHeaderComponent?: ReactNode; ListFooterComponent?: ReactNode; anchoredEndSpace?: { anchorIndex: number; anchorMaxSize?: number; anchorOffset?: number; onReady?: (info: { anchorIndex: number }) => void; }; contentInsetEndAdjustment?: number; className?: string; maintainScrollAtEnd?: | boolean | { animated?: boolean; on?: { dataChange?: boolean; itemLayout?: boolean; layout?: boolean; }; }; maintainVisibleContentPosition?: | boolean | { data?: boolean; size?: boolean; shouldRestorePosition?: (item: { id: string }) => boolean; }; ref?: Ref<LegendListRef>; }) => { if (props.anchoredEndSpace) { props.anchoredEndSpace.onReady?.({ anchorIndex: props.anchoredEndSpace.anchorIndex }); } return ( <div data-testid={legendListTestId} data-anchor-index={props.anchoredEndSpace?.anchorIndex} data-anchor-max-size={props.anchoredEndSpace?.anchorMaxSize} data-anchor-offset={props.anchoredEndSpace?.anchorOffset} data-anchor-on-ready={Boolean(props.anchoredEndSpace?.onReady)} data-content-inset-end={props.contentInsetEndAdjustment} data-class-name={props.className} data-maintain-scroll-at-end={props.maintainScrollAtEnd ? "enabled" : undefined} data-maintain-scroll-at-end-animated={ typeof props.maintainScrollAtEnd === "object" ? props.maintainScrollAtEnd.animated : undefined } data-maintain-scroll-at-end-data-change={ typeof props.maintainScrollAtEnd === "object" ? props.maintainScrollAtEnd.on?.dataChange : undefined } data-maintain-scroll-at-end-item-layout={ typeof props.maintainScrollAtEnd === "object" ? props.maintainScrollAtEnd.on?.itemLayout : undefined } data-maintain-scroll-at-end-layout={ typeof props.maintainScrollAtEnd === "object" ? props.maintainScrollAtEnd.on?.layout : undefined } data-maintain-visible-content-position={ typeof props.maintainVisibleContentPosition === "object" ? "object" : props.maintainVisibleContentPosition } data-maintain-visible-content-position-data={ typeof props.maintainVisibleContentPosition === "object" ? props.maintainVisibleContentPosition.data : undefined } data-maintain-visible-content-position-size={ typeof props.maintainVisibleContentPosition === "object" ? props.maintainVisibleContentPosition.size : undefined } data-maintain-visible-content-position-restore={ typeof props.maintainVisibleContentPosition === "object" ? Boolean(props.maintainVisibleContentPosition.shouldRestorePosition) : undefined } > {props.ListHeaderComponent} {props.data.map((item) => ( <div key={props.keyExtractor(item)}>{props.renderItem({ item })}</div> ))} {props.ListFooterComponent} </div> ); }; return { LegendList };}); function MockFileDiff(props: { fileDiff: { name?: string | null; prevName?: string | null }; renderCustomHeader?: (fileDiff: { name?: string | null; prevName?: string | null; }) => React.ReactNode;}) { return ( <div data-testid="file-diff"> {props.renderCustomHeader?.(props.fileDiff)} {props.fileDiff.name ?? props.fileDiff.prevName ?? "diff"} </div> );} vi.mock("@pierre/diffs/react", () => { return { FileDiff: MockFileDiff };}); function matchMedia() { return { matches: false, addEventListener: () => {}, removeEventListener: () => {}, };} let MessagesTimeline: typeof import("./MessagesTimeline").MessagesTimeline; beforeAll(async () => { const classList = { add: () => {}, remove: () => {}, toggle: () => {}, contains: () => false, }; vi.stubGlobal("localStorage", { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {}, }); vi.stubGlobal("window", { matchMedia, addEventListener: () => {}, removeEventListener: () => {}, requestAnimationFrame: (callback: FrameRequestCallback) => { callback(0); return 0; }, cancelAnimationFrame: () => {}, desktopBridge: undefined, }); vi.stubGlobal("document", { documentElement: { classList, offsetHeight: 0, }, }); ({ MessagesTimeline } = await import("./MessagesTimeline"));}, 30_000); const ACTIVE_THREAD_ENVIRONMENT_ID = EnvironmentId.make("environment-local");const MESSAGE_CREATED_AT = "2026-03-17T19:12:28.000Z"; function buildProps() { return { isWorking: false, activeTurnStartedAt: null, listRef: createRef<LegendListRef | null>(), latestTurn: null, runningTurnId: null, turnDiffSummaryByAssistantMessageId: new Map(), routeThreadKey: "environment-local:thread-1", onOpenTurnDiff: () => {}, revertTurnCountByUserMessageId: new Map(), onRevertUserMessage: () => {}, isRevertingCheckpoint: false, openingVideoAttachmentId: null, onImageExpand: () => {}, activeThreadEnvironmentId: ACTIVE_THREAD_ENVIRONMENT_ID, markdownCwd: undefined, resolvedTheme: "light" as const, timestampFormat: "locale" as const, workspaceRoot: undefined, anchorMessageId: null, onAnchorReady: () => {}, contentInsetEndAdjustment: 0, liveFollowEnabled: true, onIsAtEndChange: () => {}, onManualNavigation: () => {}, };} function buildLongUserMessageText(tail = "deep hidden detail only after expand") { return Array.from({ length: 9 }, (_, index) => index === 8 ? tail : `Line ${index + 1}: ${"verbose prompt content ".repeat(8).trim()}`, ).join("\n");} function buildUserTimelineEntry(text: string) { return { id: "entry-1", kind: "message" as const, createdAt: MESSAGE_CREATED_AT, message: { id: MessageId.make("message-1"), role: "user" as const, text, turnId: null, createdAt: MESSAGE_CREATED_AT, updatedAt: MESSAGE_CREATED_AT, streaming: false, }, };} function buildAssistantTimelineEntry(text: string) { const entry = buildUserTimelineEntry(text); return { ...entry, message: { ...entry.message, role: "assistant" as const, }, };} describe("MessagesTimeline", () => { it("renders a feedback command and its pending response as normal thread messages", () => { const submission = { id: MessageId.make("feedback-command"), command: "/feedback The agent stopped early.", createdAt: MESSAGE_CREATED_AT, status: "uploading" as const, }; const messages = [ codexFeedbackMessage(submission), codexFeedbackMessage(submission, "assistant"), ]; const markup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} timelineEntries={messages.map((message) => ({ id: message.id, kind: "message" as const, createdAt: message.createdAt, message, }))} />, ); expect(markup).toContain("/feedback The agent stopped early."); expect(markup).toContain("Sending feedback to OpenAI..."); }); it("renders the returned Codex thread ID in the feedback response", () => { const submission = { id: MessageId.make("feedback-command"), command: "/feedback The agent stopped early.", createdAt: MESSAGE_CREATED_AT, status: "sent" as const, feedbackId: "codex-thread-1", }; const messages = [ codexFeedbackMessage(submission), codexFeedbackMessage(submission, "assistant"), ]; const markup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} timelineEntries={messages.map((message) => ({ id: message.id, kind: "message" as const, createdAt: message.createdAt, message, }))} />, ); expect(markup).toContain("Feedback sent to OpenAI."); expect(markup).toContain("codex-thread-1"); }); it("renders the worked-for row at assistant response text size", () => { const turnId = TurnId.make("turn-with-fold"); const assistantEntry = buildAssistantTimelineEntry("Done."); const markup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} latestTurn={{ turnId, state: "completed", startedAt: "2026-03-17T19:12:20.000Z", completedAt: "2026-03-17T19:12:28.000Z", }} timelineEntries={[ { id: "work-entry-with-fold", kind: "work", createdAt: "2026-03-17T19:12:22.000Z", entry: { id: "work-with-fold", createdAt: "2026-03-17T19:12:22.000Z", turnId, label: "Ran command", tone: "tool", toolLifecycleStatus: "completed", }, }, { ...assistantEntry, message: { ...assistantEntry.message, turnId }, }, ]} />, ); expect(markup).toContain("Worked for 8.0s"); expect(markup).toContain("px-1 text-sm leading-relaxed text-muted-foreground"); }); it("uses the larger leading inset only when the top fade is enabled", () => { const timelineEntries = [buildUserTimelineEntry("Hello")]; const compactMarkup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} timelineEntries={timelineEntries} />, ); const fadedMarkup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} timelineEntries={timelineEntries} topFadeEnabled />, ); expect(compactMarkup).toContain('class="h-3 sm:h-4"'); expect(compactMarkup).not.toContain("topbar-scroll-fade"); expect(fadedMarkup).toContain('class="h-[var(--workspace-titlebar-scroll-fade-height)]"'); expect(fadedMarkup).toContain("topbar-scroll-fade"); }); it("keeps assistant changed-files headers sticky below the thread header", () => { const assistantMessageId = MessageId.make("message-assistant-with-files"); const turnId = TurnId.make("turn-with-files"); const markup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} latestTurn={{ turnId, state: "completed", startedAt: MESSAGE_CREATED_AT, completedAt: MESSAGE_CREATED_AT, }} timelineEntries={[ { id: "entry-assistant-with-files", kind: "message", createdAt: MESSAGE_CREATED_AT, message: { id: assistantMessageId, role: "assistant", text: "Updated the fixture.", turnId, createdAt: MESSAGE_CREATED_AT, updatedAt: MESSAGE_CREATED_AT, streaming: false, }, }, ]} turnDiffSummaryByAssistantMessageId={ new Map([ [ assistantMessageId, { turnId, checkpointTurnCount: 1, checkpointRef: CheckpointRef.make("checkpoint-with-files"), status: "ready", files: [{ path: "README.md", kind: "modified", additions: 2, deletions: 1 }], assistantMessageId, completedAt: MESSAGE_CREATED_AT, }, ], ]) } />, ); expect(markup).toContain("sticky top-2 z-10"); expect(markup).not.toContain("self-start"); expect(markup).toContain("whitespace-nowrap"); expect(markup).toContain("!size-[22px]"); expect(markup).toContain("size-3"); expect(markup).toContain('aria-label="Collapse all folders"'); expect(markup).toContain('aria-label="Open diff"'); expect(markup).toContain("1 changed file"); }); it("treats only the strict list end as the live edge", async () => { const { resolveTimelineIsAtEnd, resolveTimelineMinimapHasPersistentGutter, resolveTimelineMinimapHeightStyle, resolveTimelineMinimapHitStripWidth, resolveTimelineMinimapIndexFromPointer, resolveTimelineMinimapInteractiveWidth, resolveTimelineMinimapTopPercent, } = await import("./MessagesTimeline.logic"); expect(resolveTimelineIsAtEnd({ isAtEnd: true })).toBe(true); expect(resolveTimelineIsAtEnd(undefined)).toBeUndefined(); // Within the pixel band above the content bottom counts as the end... expect( resolveTimelineIsAtEnd({ isAtEnd: false, contentLength: 2000, scroll: 1170, scrollLength: 800, }), ).toBe(true); // ...but half a viewport up (LegendList's isNearEnd territory) does not. expect( resolveTimelineIsAtEnd({ isAtEnd: false, contentLength: 2000, scroll: 900, scrollLength: 800, }), ).toBe(false); // The composer inset is part of contentLength and must not count as // distance-to-end. expect( resolveTimelineIsAtEnd( { isAtEnd: false, contentLength: 2100, scroll: 1170, scrollLength: 800 }, 100, ), ).toBe(true); // Geometry missing (older state shape): fall back to the strict flag. expect(resolveTimelineIsAtEnd({ isAtEnd: false })).toBe(false); expect(resolveTimelineMinimapHeightStyle(5)).toBe("min(32px, calc(100vh - 18rem))"); expect(resolveTimelineMinimapTopPercent(2, 5)).toBe(50); expect( resolveTimelineMinimapIndexFromPointer({ itemCount: 101, railTop: 100, railHeight: 500, pointerY: 350, }), ).toBe(50); expect( resolveTimelineMinimapIndexFromPointer({ itemCount: 101, railTop: 100, railHeight: 500, pointerY: 999, }), ).toBe(100); expect(resolveTimelineMinimapHasPersistentGutter(832)).toBe(false); expect(resolveTimelineMinimapHasPersistentGutter(863)).toBe(false); expect(resolveTimelineMinimapHasPersistentGutter(864)).toBe(true); // No usable gutter (zoomed in / narrow pane): the strip must go inert // instead of overlaying the centered content column. expect(resolveTimelineMinimapHitStripWidth(768)).toBe(0); expect(resolveTimelineMinimapHitStripWidth(792)).toBe(0); // Partial gutter: strip shrinks to what fits between the viewport edge // and the content column. expect(resolveTimelineMinimapHitStripWidth(820)).toBe(14); // Full gutter: unchanged 40px-wide strip. expect(resolveTimelineMinimapHitStripWidth(872)).toBe(40); expect(resolveTimelineMinimapHitStripWidth(1400)).toBe(40); expect(resolveTimelineMinimapHitStripWidth(0)).toBe(0); expect(resolveTimelineMinimapHitStripWidth(Number.NaN)).toBe(0); // The collapsed target stays narrow, but an open preview keeps its full // 20rem width plus the 2rem offset from the minimap rail interactive. expect(resolveTimelineMinimapInteractiveWidth(0, false)).toBe(0); expect(resolveTimelineMinimapInteractiveWidth(14, false)).toBe(14); expect(resolveTimelineMinimapInteractiveWidth(40, false)).toBe(40); expect(resolveTimelineMinimapInteractiveWidth(0, true)).toBe("22rem"); expect(resolveTimelineMinimapInteractiveWidth(14, true)).toBe("22rem"); expect(resolveTimelineMinimapInteractiveWidth(40, true)).toBe("22rem"); }); it("anchors the first user message using its measured height", () => { const onAnchorReady = vi.fn(); const firstEntry = { ...buildUserTimelineEntry("First prompt."), message: { ...buildUserTimelineEntry("First prompt.").message, attachments: [Lines 1–500 of 1500. Load more
import { CheckpointRef, EnvironmentId, MessageId, TurnId } from "@t3tools/contracts"; import { codexFeedbackMessage } from "@t3tools/client-runtime/state/threads"; import { createRef, type ReactNode, type Ref } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { beforeAll, describe, expect, it, vi } from "vite-plus/test"; import type { LegendListRef } from "@legendapp/list/react"; vi.mock("@legendapp/list/react", async () => { const legendListTestId = "legend-list"; const LegendList = (props: { data: Array<{ id: string }>; keyExtractor: (item: { id: string }) => string; renderItem: (args: { item: { id: string } }) => ReactNode; ListHeaderComponent?: ReactNode; ListFooterComponent?: ReactNode; anchoredEndSpace?: { anchorIndex: number; anchorMaxSize?: number; anchorOffset?: number; onReady?: (info: { anchorIndex: number }) => void; }; contentInsetEndAdjustment?: number; className?: string; maintainScrollAtEnd?: | boolean | { animated?: boolean; on?: { dataChange?: boolean; itemLayout?: boolean; layout?: boolean; }; }; maintainVisibleContentPosition?: | boolean | { data?: boolean; size?: boolean; shouldRestorePosition?: (item: { id: string }) => boolean; }; ref?: Ref<LegendListRef>; }) => { if (props.anchoredEndSpace) { props.anchoredEndSpace.onReady?.({ anchorIndex: props.anchoredEndSpace.anchorIndex }); } return ( <div data-testid={legendListTestId} data-anchor-index={props.anchoredEndSpace?.anchorIndex} data-anchor-max-size={props.anchoredEndSpace?.anchorMaxSize} data-anchor-offset={props.anchoredEndSpace?.anchorOffset} data-anchor-on-ready={Boolean(props.anchoredEndSpace?.onReady)} data-content-inset-end={props.contentInsetEndAdjustment} data-class-name={props.className} data-maintain-scroll-at-end={props.maintainScrollAtEnd ? "enabled" : undefined} data-maintain-scroll-at-end-animated={ typeof props.maintainScrollAtEnd === "object" ? props.maintainScrollAtEnd.animated : undefined } data-maintain-scroll-at-end-data-change={ typeof props.maintainScrollAtEnd === "object" ? props.maintainScrollAtEnd.on?.dataChange : undefined } data-maintain-scroll-at-end-item-layout={ typeof props.maintainScrollAtEnd === "object" ? props.maintainScrollAtEnd.on?.itemLayout : undefined } data-maintain-scroll-at-end-layout={ typeof props.maintainScrollAtEnd === "object" ? props.maintainScrollAtEnd.on?.layout : undefined } data-maintain-visible-content-position={ typeof props.maintainVisibleContentPosition === "object" ? "object" : props.maintainVisibleContentPosition } data-maintain-visible-content-position-data={ typeof props.maintainVisibleContentPosition === "object" ? props.maintainVisibleContentPosition.data : undefined } data-maintain-visible-content-position-size={ typeof props.maintainVisibleContentPosition === "object" ? props.maintainVisibleContentPosition.size : undefined } data-maintain-visible-content-position-restore={ typeof props.maintainVisibleContentPosition === "object" ? Boolean(props.maintainVisibleContentPosition.shouldRestorePosition) : undefined } > {props.ListHeaderComponent} {props.data.map((item) => ( <div key={props.keyExtractor(item)}>{props.renderItem({ item })}</div> ))} {props.ListFooterComponent} </div> ); }; return { LegendList }; }); function MockFileDiff(props: { fileDiff: { name?: string | null; prevName?: string | null }; renderCustomHeader?: (fileDiff: { name?: string | null; prevName?: string | null; }) => React.ReactNode; }) { return ( <div data-testid="file-diff"> {props.renderCustomHeader?.(props.fileDiff)} {props.fileDiff.name ?? props.fileDiff.prevName ?? "diff"} </div> ); } vi.mock("@pierre/diffs/react", () => { return { FileDiff: MockFileDiff }; }); function matchMedia() { return { matches: false, addEventListener: () => {}, removeEventListener: () => {}, }; } let MessagesTimeline: typeof import("./MessagesTimeline").MessagesTimeline; beforeAll(async () => { const classList = { add: () => {}, remove: () => {}, toggle: () => {}, contains: () => false, }; vi.stubGlobal("localStorage", { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {}, }); vi.stubGlobal("window", { matchMedia, addEventListener: () => {}, removeEventListener: () => {}, requestAnimationFrame: (callback: FrameRequestCallback) => { callback(0); return 0; }, cancelAnimationFrame: () => {}, desktopBridge: undefined, }); vi.stubGlobal("document", { documentElement: { classList, offsetHeight: 0, }, }); ({ MessagesTimeline } = await import("./MessagesTimeline")); }, 30_000); const ACTIVE_THREAD_ENVIRONMENT_ID = EnvironmentId.make("environment-local"); const MESSAGE_CREATED_AT = "2026-03-17T19:12:28.000Z"; function buildProps() { return { isWorking: false, activeTurnStartedAt: null, listRef: createRef<LegendListRef | null>(), latestTurn: null, runningTurnId: null, turnDiffSummaryByAssistantMessageId: new Map(), routeThreadKey: "environment-local:thread-1", onOpenTurnDiff: () => {}, revertTurnCountByUserMessageId: new Map(), onRevertUserMessage: () => {}, isRevertingCheckpoint: false, openingVideoAttachmentId: null, onImageExpand: () => {}, activeThreadEnvironmentId: ACTIVE_THREAD_ENVIRONMENT_ID, markdownCwd: undefined, resolvedTheme: "light" as const, timestampFormat: "locale" as const, workspaceRoot: undefined, anchorMessageId: null, onAnchorReady: () => {}, contentInsetEndAdjustment: 0, liveFollowEnabled: true, onIsAtEndChange: () => {}, onManualNavigation: () => {}, }; } function buildLongUserMessageText(tail = "deep hidden detail only after expand") { return Array.from({ length: 9 }, (_, index) => index === 8 ? tail : `Line ${index + 1}: ${"verbose prompt content ".repeat(8).trim()}`, ).join("\n"); } function buildUserTimelineEntry(text: string) { return { id: "entry-1", kind: "message" as const, createdAt: MESSAGE_CREATED_AT, message: { id: MessageId.make("message-1"), role: "user" as const, text, turnId: null, createdAt: MESSAGE_CREATED_AT, updatedAt: MESSAGE_CREATED_AT, streaming: false, }, }; } function buildAssistantTimelineEntry(text: string) { const entry = buildUserTimelineEntry(text); return { ...entry, message: { ...entry.message, role: "assistant" as const, }, }; } describe("MessagesTimeline", () => { it("renders a feedback command and its pending response as normal thread messages", () => { const submission = { id: MessageId.make("feedback-command"), command: "/feedback The agent stopped early.", createdAt: MESSAGE_CREATED_AT, status: "uploading" as const, }; const messages = [ codexFeedbackMessage(submission), codexFeedbackMessage(submission, "assistant"), ]; const markup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} timelineEntries={messages.map((message) => ({ id: message.id, kind: "message" as const, createdAt: message.createdAt, message, }))} />, ); expect(markup).toContain("/feedback The agent stopped early."); expect(markup).toContain("Sending feedback to OpenAI..."); }); it("renders the returned Codex thread ID in the feedback response", () => { const submission = { id: MessageId.make("feedback-command"), command: "/feedback The agent stopped early.", createdAt: MESSAGE_CREATED_AT, status: "sent" as const, feedbackId: "codex-thread-1", }; const messages = [ codexFeedbackMessage(submission), codexFeedbackMessage(submission, "assistant"), ]; const markup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} timelineEntries={messages.map((message) => ({ id: message.id, kind: "message" as const, createdAt: message.createdAt, message, }))} />, ); expect(markup).toContain("Feedback sent to OpenAI."); expect(markup).toContain("codex-thread-1"); }); it("renders the worked-for row at assistant response text size", () => { const turnId = TurnId.make("turn-with-fold"); const assistantEntry = buildAssistantTimelineEntry("Done."); const markup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} latestTurn={{ turnId, state: "completed", startedAt: "2026-03-17T19:12:20.000Z", completedAt: "2026-03-17T19:12:28.000Z", }} timelineEntries={[ { id: "work-entry-with-fold", kind: "work", createdAt: "2026-03-17T19:12:22.000Z", entry: { id: "work-with-fold", createdAt: "2026-03-17T19:12:22.000Z", turnId, label: "Ran command", tone: "tool", toolLifecycleStatus: "completed", }, }, { ...assistantEntry, message: { ...assistantEntry.message, turnId }, }, ]} />, ); expect(markup).toContain("Worked for 8.0s"); expect(markup).toContain("px-1 text-sm leading-relaxed text-muted-foreground"); }); it("uses the larger leading inset only when the top fade is enabled", () => { const timelineEntries = [buildUserTimelineEntry("Hello")]; const compactMarkup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} timelineEntries={timelineEntries} />, ); const fadedMarkup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} timelineEntries={timelineEntries} topFadeEnabled />, ); expect(compactMarkup).toContain('class="h-3 sm:h-4"'); expect(compactMarkup).not.toContain("topbar-scroll-fade"); expect(fadedMarkup).toContain('class="h-[var(--workspace-titlebar-scroll-fade-height)]"'); expect(fadedMarkup).toContain("topbar-scroll-fade"); }); it("keeps assistant changed-files headers sticky below the thread header", () => { const assistantMessageId = MessageId.make("message-assistant-with-files"); const turnId = TurnId.make("turn-with-files"); const markup = renderToStaticMarkup( <MessagesTimeline {...buildProps()} latestTurn={{ turnId, state: "completed", startedAt: MESSAGE_CREATED_AT, completedAt: MESSAGE_CREATED_AT, }} timelineEntries={[ { id: "entry-assistant-with-files", kind: "message", createdAt: MESSAGE_CREATED_AT, message: { id: assistantMessageId, role: "assistant", text: "Updated the fixture.", turnId, createdAt: MESSAGE_CREATED_AT, updatedAt: MESSAGE_CREATED_AT, streaming: false, }, }, ]} turnDiffSummaryByAssistantMessageId={ new Map([ [ assistantMessageId, { turnId, checkpointTurnCount: 1, checkpointRef: CheckpointRef.make("checkpoint-with-files"), status: "ready", files: [{ path: "README.md", kind: "modified", additions: 2, deletions: 1 }], assistantMessageId, completedAt: MESSAGE_CREATED_AT, }, ], ]) } />, ); expect(markup).toContain("sticky top-2 z-10"); expect(markup).not.toContain("self-start"); expect(markup).toContain("whitespace-nowrap"); expect(markup).toContain("!size-[22px]"); expect(markup).toContain("size-3"); expect(markup).toContain('aria-label="Collapse all folders"'); expect(markup).toContain('aria-label="Open diff"'); expect(markup).toContain("1 changed file"); }); it("treats only the strict list end as the live edge", async () => { const { resolveTimelineIsAtEnd, resolveTimelineMinimapHasPersistentGutter, resolveTimelineMinimapHeightStyle, resolveTimelineMinimapHitStripWidth, resolveTimelineMinimapIndexFromPointer, resolveTimelineMinimapInteractiveWidth, resolveTimelineMinimapTopPercent, } = await import("./MessagesTimeline.logic"); expect(resolveTimelineIsAtEnd({ isAtEnd: true })).toBe(true); expect(resolveTimelineIsAtEnd(undefined)).toBeUndefined(); // Within the pixel band above the content bottom counts as the end... expect( resolveTimelineIsAtEnd({ isAtEnd: false, contentLength: 2000, scroll: 1170, scrollLength: 800, }), ).toBe(true); // ...but half a viewport up (LegendList's isNearEnd territory) does not. expect( resolveTimelineIsAtEnd({ isAtEnd: false, contentLength: 2000, scroll: 900, scrollLength: 800, }), ).toBe(false); // The composer inset is part of contentLength and must not count as // distance-to-end. expect( resolveTimelineIsAtEnd( { isAtEnd: false, contentLength: 2100, scroll: 1170, scrollLength: 800 }, 100, ), ).toBe(true); // Geometry missing (older state shape): fall back to the strict flag. expect(resolveTimelineIsAtEnd({ isAtEnd: false })).toBe(false); expect(resolveTimelineMinimapHeightStyle(5)).toBe("min(32px, calc(100vh - 18rem))"); expect(resolveTimelineMinimapTopPercent(2, 5)).toBe(50); expect( resolveTimelineMinimapIndexFromPointer({ itemCount: 101, railTop: 100, railHeight: 500, pointerY: 350, }), ).toBe(50); expect( resolveTimelineMinimapIndexFromPointer({ itemCount: 101, railTop: 100, railHeight: 500, pointerY: 999, }), ).toBe(100); expect(resolveTimelineMinimapHasPersistentGutter(832)).toBe(false); expect(resolveTimelineMinimapHasPersistentGutter(863)).toBe(false); expect(resolveTimelineMinimapHasPersistentGutter(864)).toBe(true); // No usable gutter (zoomed in / narrow pane): the strip must go inert // instead of overlaying the centered content column. expect(resolveTimelineMinimapHitStripWidth(768)).toBe(0); expect(resolveTimelineMinimapHitStripWidth(792)).toBe(0); // Partial gutter: strip shrinks to what fits between the viewport edge // and the content column. expect(resolveTimelineMinimapHitStripWidth(820)).toBe(14); // Full gutter: unchanged 40px-wide strip. expect(resolveTimelineMinimapHitStripWidth(872)).toBe(40); expect(resolveTimelineMinimapHitStripWidth(1400)).toBe(40); expect(resolveTimelineMinimapHitStripWidth(0)).toBe(0); expect(resolveTimelineMinimapHitStripWidth(Number.NaN)).toBe(0); // The collapsed target stays narrow, but an open preview keeps its full // 20rem width plus the 2rem offset from the minimap rail interactive. expect(resolveTimelineMinimapInteractiveWidth(0, false)).toBe(0); expect(resolveTimelineMinimapInteractiveWidth(14, false)).toBe(14); expect(resolveTimelineMinimapInteractiveWidth(40, false)).toBe(40); expect(resolveTimelineMinimapInteractiveWidth(0, true)).toBe("22rem"); expect(resolveTimelineMinimapInteractiveWidth(14, true)).toBe("22rem"); expect(resolveTimelineMinimapInteractiveWidth(40, true)).toBe("22rem"); }); it("anchors the first user message using its measured height", () => { const onAnchorReady = vi.fn(); const firstEntry = { ...buildUserTimelineEntry("First prompt."), message: { ...buildUserTimelineEntry("First prompt.").message, attachments: [