import { render } from "preact"; import { useRef, useState } from "preact/hooks"; import type { Message } from "../src/lib/api"; import type { AgentAttachment } from "../src/lib/agent-attachments"; import type { AgentRoomHookResult } from "../src/hooks/use-agent-room"; import { createAgentOptimisticOutbox } from "../src/lib/agent-optimistic-outbox"; import { AgentConversation } from "../src/components/agent/agent-conversation"; import "../src/index.css"; const timestamp = "2026-10-29T12:00:00.000Z"; const scrollAway = new URLSearchParams(location.search).get("scrollAway") === "1"; const detailedReply = Array.from({ length: 12 }, (_, index) => `Synthetic streamed paragraph ${index + 1} exercises a live Agent message update after the user sends a multiline prompt. ${"The current room snapshot keeps enough content to measure the animated row geometry. ".repeat(2)}`, ).join("\n\n"); const growingReply = `${detailedReply}\n\nAn additional streamed paragraph grows the same stable message identity.`; const middleReply = Array.from({ length: 7 }, (_, index) => `Synthetic intermediate response paragraph ${index + 1} keeps the stable reply measurable during streaming. ${"The row is still in its entrance animation. ".repeat(2)}`, ).join("\n\n"); const shorterReply = [ "The synthetic Agent response is being finalized while its entrance animation is still active.", "This update changes the stable message's intrinsic height and exercises the same room update path.", ].join("\n\n"); const finalReply = "Final synthetic Agent reply."; window.addEventListener("error", event => { document.documentElement.dataset.fixtureError = event.message; }); window.addEventListener("unhandledrejection", event => { document.documentElement.dataset.fixtureError = String(event.reason); }); function message(id: string, role: Message["role"], content: string, status = "complete", createdAt = timestamp): Message { return { id, createdAt, role, content, name: null, metadata: { kind: "message", status } }; } function Fixture() { const [revision, setRevision] = useState(0); const [roomState, setRoomState] = useState(() => ({ messages: Array.from({ length: 12 }, (_, index) => { const role: Message["role"] = index % 2 ? "assistant" : "user"; return message(`fixture-history-${index}`, role, `Earlier synthetic thread message ${index + 1}. ${"The history preserves a realistic scrollable Agent transcript. ".repeat(2)}`, "complete", new Date(Date.parse(timestamp) + index * 1000).toISOString()); }), activity: null as null | { state: string; isThinking: boolean; queued: number; backgroundWorkers: number; tools: never[]; queuedMessages: Message[] }, })); const outbox = useRef | null>(null); if (!outbox.current) { outbox.current = createAgentOptimisticOutbox({ storage: { read: () => [], write: () => undefined }, newId: () => "fixture-request", now: () => Date.parse(timestamp) + 100_000, changed: () => setRevision(value => value + 1), submit: (_content, requestId) => new Promise(resolve => { document.documentElement.dataset.agentSubmitCalled = "true"; window.setTimeout(() => { setRoomState(current => ({ ...current, activity: { state: "thinking", isThinking: true, queued: 0, backgroundWorkers: 0, tools: [], queuedMessages: [], } })); resolve({ status: "queued", messageId: `inbound:sybil:${requestId}` }); }, 70); window.setTimeout(() => { setRoomState(current => ({ ...current, messages: [...current.messages, message("fixture-agent-reply", "assistant", detailedReply, "complete", new Date(Date.parse(timestamp) + 200_000).toISOString())], activity: null })); const streamSnapshots = [ [20, growingReply], [45, middleReply], [70, detailedReply], [95, shorterReply], [130, "A concise streamed final answer with one extra line.\n\nThe last snapshot settles the message height."], [165, finalReply], ] as const; for (const [delay, content] of streamSnapshots) window.setTimeout(() => setRoomState(current => ({ ...current, messages: current.messages.map(item => item.id === "fixture-agent-reply" ? { ...item, content } : item), })), delay); }, 450); }), }); } void revision; const queue = outbox.current; const messages = queue.display(roomState.messages); const uploadQueue = { snapshot: () => [], add: () => undefined, remove: () => undefined, ready: () => [], reselect: async () => undefined, retry: () => undefined, clear: () => undefined, }; const room = { messages, activity: roomState.activity, unreadCount: 0, connection: "connected", error: null, isLoading: false, isLoadingOlder: false, olderError: null, hasOlder: false, historyBeforeCursor: null, attachmentCapability: undefined, uploadQueue, uploadAttachment: async () => { throw new Error("No upload in fixture"); }, downloadAttachment: async () => new Blob(), isTyping: roomState.activity !== null, sendMessage: (content: string, attachments?: AgentAttachment[], onPersisted?: () => void) => { document.documentElement.dataset.roomSendCalled = "true"; return queue.send(content, attachments, onPersisted); }, retryMessage: async () => undefined, markRead: () => { const activeEntryAnimation = [...document.querySelectorAll("[data-agent-entry-id]")] .some(entry => entry.getAnimations().some(animation => animation.effect?.target === entry && animation.playState === "running")); if (activeEntryAnimation) document.documentElement.dataset.readDuringEntryAnimation = "true"; document.documentElement.dataset.readCount = String(Number(document.documentElement.dataset.readCount || "0") + 1); }, retry: () => undefined, loadOlder: () => undefined, unreadCountIsPartial: false, } as unknown as AgentRoomHookResult; return undefined} />; } render(, document.getElementById("root")!); const samples: Array> = []; const finished = new Set(); const timelineObserver = new MutationObserver(() => { for (const [id, label] of [["inbound:sybil:fixture-request", "user"], ["fixture-agent-reply", "agent"]]) { const entry = document.querySelector(`[data-agent-entry-id="${id}"]`); if (!entry || entry.dataset.geometryCaptured) continue; const animation = entry.getAnimations().find(candidate => candidate.effect?.target === entry); if (!animation) continue; entry.dataset.geometryCaptured = "true"; sample(`${label}-start`, entry); window.setTimeout(() => sample(`${label}-mid`, entry), 120); window.setTimeout(() => sample(`${label}-late`, entry), 195); window.setTimeout(() => sample(`${label}-pre-end`, entry), 225); animation.finished.then(() => { sample(`${label}-finish`, entry); window.setTimeout(() => { sample(`${label}-stable`, entry); finished.add(label); if (finished.size === 2) { document.getElementById("result")!.textContent = JSON.stringify({ samples, roomSendCalled: document.documentElement.dataset.roomSendCalled === "true", agentSubmitCalled: document.documentElement.dataset.agentSubmitCalled === "true", composerFocused: document.activeElement?.getAttribute("aria-label") === "Message Sybil Agent", readDuringEntryAnimation: document.documentElement.dataset.readDuringEntryAnimation === "true", contentVisibility: getComputedStyle(entry).contentVisibility, scrollAwayState: (window as any).__scrollAwayState || null, }); document.documentElement.dataset.fixtureComplete = "true"; } }, 150); }).catch(() => undefined); } }); function sample(label: string, entry: HTMLElement) { const scroller = document.querySelector(".agent-room__scroll")!; const timeline = document.querySelector(".agent-room__timeline")!; const rect = entry.getBoundingClientRect(); const content = entry.querySelector(".agent-room__message")!; const contentHeight = content.getBoundingClientRect().height; const entryAnimations = entry.getAnimations().filter(animation => animation.effect?.target === entry); const anchorState = (window as any).__geometryAnchor as { id: string; offset: number } | undefined; const anchor = anchorState && [...document.querySelectorAll("[data-agent-entry-id]")] .find(item => item.dataset.agentEntryId === anchorState.id); samples.push({ label, time: Number(performance.now().toFixed(1)), rowHeight: Number(rect.height.toFixed(2)), rowScrollHeight: entry.scrollHeight, contentHeight: Number(contentHeight.toFixed(2)), emptyHeight: Number((rect.height - contentHeight).toFixed(2)), bottomGap: Number((scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight).toFixed(2)), ...(anchor ? { anchorShift: Number((anchor.getBoundingClientRect().top - scroller.getBoundingClientRect().top - anchorState!.offset).toFixed(2)) } : {}), entryAnimationCount: entryAnimations.length, keyframeHeights: entryAnimations.flatMap(animation => animation.effect?.getKeyframes().map(frame => frame.height) || []), computedHeight: getComputedStyle(entry).height, overflow: getComputedStyle(entry).overflow, }); } const waitForTimeline = window.setInterval(() => { const timeline = document.querySelector(".agent-room__timeline"); if (!timeline) return; window.clearInterval(waitForTimeline); timelineObserver.observe(timeline, { childList: true, subtree: true, characterData: true }); window.setTimeout(() => { document.documentElement.dataset.readCount = "0"; const textarea = document.querySelector('textarea[aria-label="Message Sybil Agent"]')!; textarea.value = Array.from({ length: 12 }, (_, index) => `Synthetic sent message line ${index + 1}`).join("\n"); textarea.dispatchEvent(new Event("input", { bubbles: true })); textarea.form!.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); if (scrollAway) window.setTimeout(() => { const scroller = document.querySelector(".agent-room__scroll")!; const requestedTop = Math.max(0, scroller.scrollTop - 250); scroller.scrollTop = requestedTop; scroller.dispatchEvent(new Event("scroll")); (window as any).__scrollAwayState = { requestedTop, actualTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, clientHeight: scroller.clientHeight, bottomGap: scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight }; const top = scroller.getBoundingClientRect().top; const anchor = [...scroller.querySelectorAll("[data-agent-entry-id]")] .find(item => item.getBoundingClientRect().bottom > top + 8); if (anchor) (window as any).__geometryAnchor = { id: anchor.dataset.agentEntryId, offset: anchor.getBoundingClientRect().top - top, }; }, 60); }, 120); }, 10);