From 1c9f65d22ea5f8673589512a0083f3146750e1d2 Mon Sep 17 00:00:00 2001 From: SXKDZ Date: Mon, 10 Aug 2026 17:07:42 -0400 Subject: [PATCH 01/14] fix: restore complete history selection --- app/components/FeedWorkspace.tsx | 89 +++++++++++++++++++--- tests/schemas/feed-history-loading.test.ts | 19 ++++- 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/app/components/FeedWorkspace.tsx b/app/components/FeedWorkspace.tsx index e3a795d..7f5d79f 100644 --- a/app/components/FeedWorkspace.tsx +++ b/app/components/FeedWorkspace.tsx @@ -713,11 +713,22 @@ function interactionResponseText(interaction: FeedInteraction): str .join("\n\n"); } +const FEED_HISTORY_REQUEST_PREVIEW_LENGTH = 800; +const FEED_HISTORY_RESPONSE_PREVIEW_LENGTH = 2_000; +const FEED_HISTORY_JUMP_DURATION_MS = 260; + +function cappedHistoryPreview(content: string, limit: number): string { + const trimmed = content.trim(); + if (trimmed.length <= limit) return trimmed; + return `${trimmed.slice(0, limit).trimEnd()}…`; +} + /** * A focused chooser for creating a new feed from past requests. The complete, - * lightweight request index stays available on the left while only its active - * request/response card is mounted on the right. Responses remain capped until - * expanded so a long history never becomes a second full conversation DOM. + * lightweight request index stays available on the left while the complete + * chronological request/response list remains visible on the right. Collapsed + * previews use bounded strings and CSS rendering containment so a long history + * does not become a second full conversation DOM. */ function FeedHistorySelectionModal({ feedName, @@ -742,6 +753,9 @@ function FeedHistorySelectionModal({ const dialogRef = useRef(null); const searchRef = useRef(null); const indexButtonRefs = useRef(new Map()); + const interactionsPaneRef = useRef(null); + const interactionCardRefs = useRef(new Map()); + const historyScrollFrameRef = useRef(null); const closeRef = useRef(onClose); const creatingRef = useRef(creating); @@ -764,13 +778,31 @@ function FeedHistorySelectionModal({ }, [query, records]); const visibleIds = visible.map(({ interaction }) => interaction.id); const displayedActiveId = visibleIds.includes(activeInteractionId) ? activeInteractionId : (visibleIds[0] ?? ""); - const activeRecord = visible.find(({ interaction }) => interaction.id === displayedActiveId); const allVisibleSelected = Boolean(visibleIds.length) && visibleIds.every((id) => selected.has(id)); useEffect(() => { indexButtonRefs.current.get(displayedActiveId)?.scrollIntoView({ block: "nearest", inline: "nearest" }); }, [displayedActiveId]); + useEffect(() => { + const pane = interactionsPaneRef.current; + if (!pane) return; + const cancelAnimatedScroll = () => { + if (historyScrollFrameRef.current === null) return; + cancelAnimationFrame(historyScrollFrameRef.current); + historyScrollFrameRef.current = null; + }; + pane.addEventListener("wheel", cancelAnimatedScroll, { passive: true }); + pane.addEventListener("touchstart", cancelAnimatedScroll, { passive: true }); + pane.addEventListener("pointerdown", cancelAnimatedScroll); + return () => { + cancelAnimatedScroll(); + pane.removeEventListener("wheel", cancelAnimatedScroll); + pane.removeEventListener("touchstart", cancelAnimatedScroll); + pane.removeEventListener("pointerdown", cancelAnimatedScroll); + }; + }, []); + useEffect(() => { const page = document.querySelector(".feed-page"); const pageWasInert = page?.inert ?? false; @@ -815,6 +847,33 @@ function FeedHistorySelectionModal({ function jumpWithinModal(id: string) { setActiveInteractionId(id); + const pane = interactionsPaneRef.current; + const card = interactionCardRefs.current.get(id); + if (!pane || !card) return; + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const paneTop = pane.getBoundingClientRect().top; + const cardTop = card.getBoundingClientRect().top; + const targetTop = Math.min( + pane.scrollHeight - pane.clientHeight, + Math.max(0, pane.scrollTop + cardTop - paneTop), + ); + if (historyScrollFrameRef.current !== null) cancelAnimationFrame(historyScrollFrameRef.current); + if (reduceMotion) { + pane.scrollTop = targetTop; + historyScrollFrameRef.current = null; + return; + } + const startTop = pane.scrollTop; + const distance = targetTop - startTop; + let startTime: number | null = null; + const animate = (time: number) => { + startTime ??= time; + const progress = Math.min(1, (time - startTime) / FEED_HISTORY_JUMP_DURATION_MS); + const eased = 1 - ((1 - progress) ** 3); + pane.scrollTop = startTop + (distance * eased); + historyScrollFrameRef.current = progress < 1 ? requestAnimationFrame(animate) : null; + }; + historyScrollFrameRef.current = requestAnimationFrame(animate); } function toggleExpanded(id: string) { @@ -903,18 +962,28 @@ function FeedHistorySelectionModal({ -
+
{loading ? (
Loading conversation history…
- ) : activeRecord ? (() => { - const { interaction, number, response } = activeRecord; + ) : visible.length ? visible.map(({ interaction, number, response }) => { const isExpanded = expanded.has(interaction.id); const isRequestExpanded = expandedRequests.has(interaction.id); const isSelected = selected.has(interaction.id); const requestCanExpand = interaction.userText.length > 180 || interaction.userText.split("\n").length > 3; + const requestText = interaction.userText.trim() || "Request with attachments"; + const requestPreview = isRequestExpanded + ? requestText + : cappedHistoryPreview(requestText, FEED_HISTORY_REQUEST_PREVIEW_LENGTH); + const responsePreview = isExpanded + ? response + : cappedHistoryPreview(response, FEED_HISTORY_RESPONSE_PREVIEW_LENGTH); return (
{ + if (node) interactionCardRefs.current.set(interaction.id, node); + else interactionCardRefs.current.delete(interaction.id); + }} className={`feed-history-interaction ${isSelected ? "is-selected" : ""}`} >
You -

{interaction.userText.trim() || "Request with attachments"}

+

{requestPreview}

Agent response -

{response || "No agent response before the next request."}

+

{responsePreview || "No agent response before the next request."}

@@ -946,7 +1015,7 @@ function FeedHistorySelectionModal({
); - })() : ( + }) : (
No requests or responses match “{query}”.
)}
diff --git a/tests/schemas/feed-history-loading.test.ts b/tests/schemas/feed-history-loading.test.ts index 0956223..65e0c92 100644 --- a/tests/schemas/feed-history-loading.test.ts +++ b/tests/schemas/feed-history-loading.test.ts @@ -2,10 +2,11 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; -test("long feeds hydrate once, render a bounded tail, and defer hidden tool bodies", async () => { - const [route, feed] = await Promise.all([ +test("long feeds hydrate once, render bounded work, and retain the full history chooser", async () => { + const [route, feed, historyStyles] = await Promise.all([ readFile(new URL("../../app/api/feed/snippets/[id]/events/route.ts", import.meta.url), "utf8"), readFile(new URL("../../app/components/FeedWorkspace.tsx", import.meta.url), "utf8"), + readFile(new URL("../../app/styles/feed-history.css", import.meta.url), "utf8"), ]); assert.match(route, /controller\.enqueue\(frame\("snapshot", \{/); @@ -26,7 +27,17 @@ test("long feeds hydrate once, render a bounded tail, and defer hidden tool bodi assert.match(feed, /const FeedToolCall = memo/); assert.match(feed, /const FeedToolGroup = memo/); assert.match(feed, /\{expanded \? \([\s\S]*renderToolContent\(operation\.result/s); - assert.match(feed, /activeRecord \? \(\(\) =>/); - assert.doesNotMatch(feed, /visible\.length \? visible\.map\(\(\{ interaction, number, response \}\)/); + assert.match(feed, /visible\.length \? visible\.map\(\(\{ interaction, number, response \}\)/); + assert.doesNotMatch(feed, /const activeRecord = visible\.find/); + assert.match(feed, /cappedHistoryPreview\(response, FEED_HISTORY_RESPONSE_PREVIEW_LENGTH\)/); + assert.match(feed, /window\.matchMedia\("\(prefers-reduced-motion: reduce\)"\)/); + assert.match(feed, /const FEED_HISTORY_JUMP_DURATION_MS = 260/); + assert.match(feed, /\(time - startTime\) \/ FEED_HISTORY_JUMP_DURATION_MS/); + assert.match(feed, /historyScrollFrameRef\.current = requestAnimationFrame\(animate\)/); + assert.match(feed, /pane\.addEventListener\("wheel", cancelAnimatedScroll/); + assert.match(feed, /const paneTop = pane\.getBoundingClientRect\(\)\.top/); + assert.match(feed, /Math\.max\(0, pane\.scrollTop \+ cardTop - paneTop\)/); + assert.doesNotMatch(feed, /interactionCardRefs\.current\.get\(id\)\?\.scrollIntoView/); + assert.doesNotMatch(historyStyles, /content-visibility:\s*auto/); assert.match(feed, /\{loading \? \(\s*
Date: Mon, 10 Aug 2026 17:14:29 -0400 Subject: [PATCH 02/14] fix: consume history selection requests --- app/components/FeedWorkspace.tsx | 29 ++++++++++++++-------- tests/schemas/feed-history-loading.test.ts | 6 +++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/components/FeedWorkspace.tsx b/app/components/FeedWorkspace.tsx index 7f5d79f..02682cc 100644 --- a/app/components/FeedWorkspace.tsx +++ b/app/components/FeedWorkspace.tsx @@ -1047,14 +1047,14 @@ function FeedHistorySelectionModal({ * in), shows proposals to approve/reject, and offers a reply box. Mounted with a * `key` of the snippet id so switching selection resets its state cleanly. */ -function FeedDetail({ snippet, library, models, defaultModelLabel, defaultEffort, selectHistoryInitially, historySelectionReturnFocus, onBack, onChanged, onCreated }: { +function FeedDetail({ snippet, library, models, defaultModelLabel, defaultEffort, historySelectionRequest, onHistorySelectionClosed, onBack, onChanged, onCreated }: { snippet: FeedSnippet; library: LibraryPaper[]; models: FeedModelOption[]; defaultModelLabel: string; defaultEffort: EffortSetting; - selectHistoryInitially: boolean; - historySelectionReturnFocus: HTMLButtonElement | null; + historySelectionRequest: { nonce: number; returnFocus: HTMLButtonElement | null } | null; + onHistorySelectionClosed: () => void; onBack: () => void; onChanged: () => void; onCreated: (id: string) => void; @@ -1071,7 +1071,7 @@ function FeedDetail({ snippet, library, models, defaultModelLabel, defaultEffort const [workingDirectory, setWorkingDirectory] = useState(null); const [openingWorkingDirectory, setOpeningWorkingDirectory] = useState(false); const [streamNonce, setStreamNonce] = useState(0); - const [selectingHistory, setSelectingHistory] = useState(selectHistoryInitially); + const [selectingHistory, setSelectingHistory] = useState(false); const [selectedInteractions, setSelectedInteractions] = useState>(() => new Set()); const [includeToolDetails, setIncludeToolDetails] = useState(false); const [creatingFromHistory, setCreatingFromHistory] = useState(false); @@ -1098,6 +1098,11 @@ function FeedDetail({ snippet, library, models, defaultModelLabel, defaultEffort const replayingHistoryRef = useRef(true); const userScrollIntentRef = useRef(false); const userScrollIntentTimerRef = useRef | null>(null); + const historySelectionRequestNonce = historySelectionRequest?.nonce ?? null; + + useEffect(() => { + if (historySelectionRequestNonce !== null) setSelectingHistory(true); + }, [historySelectionRequestNonce]); const scrollToBottom = useCallback(() => { const body = bodyRef.current; @@ -1426,6 +1431,7 @@ function FeedDetail({ snippet, library, models, defaultModelLabel, defaultEffort setSelectingHistory(false); setSelectedInteractions(new Set()); setIncludeToolDetails(false); + onHistorySelectionClosed(); } function showEarlierInteractions() { @@ -1487,6 +1493,7 @@ function FeedDetail({ snippet, library, models, defaultModelLabel, defaultEffort return; } const payload = await response.json() as { id: string }; + onHistorySelectionClosed(); onCreated(payload.id); } catch (fetchError) { setError(fetchError instanceof Error ? fetchError.message : "The new feed could not be created."); @@ -1803,7 +1810,7 @@ function FeedDetail({ snippet, library, models, defaultModelLabel, defaultEffort creating={creatingFromHistory} loading={!historyReady} error={error} - returnFocus={historySelectionReturnFocus} + returnFocus={historySelectionRequest?.returnFocus ?? null} onToggle={toggleInteraction} onSetVisible={setVisibleInteractions} onIncludeToolDetails={setIncludeToolDetails} @@ -2302,7 +2309,7 @@ export default function FeedWorkspace() {