Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,62 @@ describe("MessagesTimeline attachments", () => {
});

describe("MessagesTimeline history and thread links", () => {
it.each([
["imported runless", null],
["standard", RunId.make("citation-run")],
] as const)("reveals a cited response inside a folded %s turn", async (_label, runId) => {
const quote = "The imported response contains this cited detail.";
const props = {
...baseProps(),
timelineEntries: [
message("Investigate the issue.", { runId }, "citation-prompt"),
message(quote, { role: "assistant", runId }, "citation-source"),
message("Investigation complete.", { role: "assistant", runId }, "citation-answer"),
],
};
await renderElement(<MessagesTimeline {...props} />);
const source = () =>
container.querySelector<HTMLElement>('[data-assistant-citation-source="citation-source"]');
await vi.waitFor(() => expect(text()).toContain("Investigation complete."));
expect(buttons(/^Worked/)).toHaveLength(1);
expect(source()).toBeNull();

await act(async () =>
roots.at(-1)!.render(
<MessagesTimeline
{...props}
citationRequest={{
key: "open-citation",
citation: {
version: 1,
environmentId: ENVIRONMENT_ID,
threadId: ThreadId.make(`thread-${threadCount}`),
messageId: MessageId.make("citation-source"),
text: quote,
start: 0,
end: quote.length,
prefix: "",
suffix: "",
},
}}
/>,
),
);

await vi.waitFor(() => {
expect(source()?.textContent ?? "").toContain(quote);
const highlight = CSS.highlights.get("t3-assistant-citation");
expect(highlight).toBeDefined();
const range = [...highlight!][0] as Range;
expect(range.toString()).toBe(quote);
const viewport = container.querySelector<HTMLElement>("[data-assistant-citation-viewport]")!;
const bounds = range.getBoundingClientRect();
expect(bounds.height).toBeGreaterThan(0);
expect(bounds.top).toBeGreaterThanOrEqual(viewport.getBoundingClientRect().top);
expect(bounds.bottom).toBeLessThanOrEqual(viewport.getBoundingClientRect().bottom);
});
});

it("loads earlier turns from a control ahead of the bounded timeline", async () => {
const onLoadEarlier = vi.fn();
await renderTimeline([message("Recent activity")], {
Expand Down
19 changes: 14 additions & 5 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -800,8 +800,7 @@ const ConversationTimeline = memo(function ConversationTimeline({
});
}, [latestRun, listIdentityKey]);

// Find the fold that hides the match: imported turns have no run id but still
// fold under a synthetic key, so the match's own run id cannot open them.
// Imported turns fold under synthetic keys rather than the entries' own run ids.
const findFoldRunIds = findActive
? timelineTurnFoldRunIdsByEntryId({
timelineEntries,
Expand All @@ -811,9 +810,8 @@ const ConversationTimeline = memo(function ConversationTimeline({
runlessWorkActive,
})
: null;
const activeFindRunId = activeFindMatch
? findFoldRunIds?.get(activeFindMatch.entryId)
: undefined;
const activeFindRunId =
findActive && activeFindMatch ? findFoldRunIds?.get(activeFindMatch.entryId) : undefined;
const [expandedFindRunId, setExpandedFindRunId] = useState<RunId | undefined>();
if (expandedFindRunId !== activeFindRunId) {
setExpandedFindRunId(activeFindRunId);
Expand Down Expand Up @@ -1003,6 +1001,17 @@ const ConversationTimeline = memo(function ConversationTimeline({
viewport: timelineViewportElement,
historyLoading: citationHistoryLoading,
loadEarlier,
getTurnFoldRunId: (entryId) =>
(
findFoldRunIds ??
timelineTurnFoldRunIdsByEntryId({
timelineEntries,
latestRun,
runningRunId,
isWorking,
runlessWorkActive,
})
).get(entryId),
onExpandTurn: expandCitedRun,
onManualNavigation,
});
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/chat/useAssistantCitationTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function useAssistantCitationTarget({
viewport,
historyLoading,
loadEarlier,
getTurnFoldRunId,
onExpandTurn,
onManualNavigation,
}: {
Expand All @@ -30,6 +31,7 @@ export function useAssistantCitationTarget({
viewport: HTMLElement | null;
historyLoading: boolean;
loadEarlier: CitationHistoryPage | null;
getTurnFoldRunId: (entryId: string) => RunId | undefined;
onExpandTurn: (runId: RunId) => void;
onManualNavigation: () => void;
}) {
Expand Down Expand Up @@ -123,12 +125,14 @@ export function useAssistantCitationTarget({
(row) => row.kind === "message" && row.message.id === navigation.target.citation.messageId,
);
if (index < 0) {
if (source.message.runId) onExpandTurn(source.message.runId);
const foldRunId = getTurnFoldRunId(source.id);
if (foldRunId) onExpandTurn(foldRunId);
return;
}
if (listLoaded && listRef.current) setReady(navigation.target);
}, [
entries,
getTurnFoldRunId,
historyLoading,
listLoaded,
listRef,
Expand Down
Loading