Skip to content
Closed
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
42 changes: 31 additions & 11 deletions packages/react-ui/src/components/AgentInterface/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,20 @@ function groupIntoTurns(messages: Message[]): { messages: Message[]; startIndex:
return groups;
}

/** Visible body of an assistant segment, with inline sentinels stripped. */
function segmentBody(segment: AssistantMessage): string {
return separateContentAndContext(segment.content ?? "").content;
}

/** Latest segment that is OpenUI-lang rather than thinking prose. */
function latestLangSegment(segments: AssistantMessage[]): AssistantMessage | null {
for (let i = segments.length - 1; i >= 0; i--) {
const segment = segments[i]!;
if (hasLangSyntax(segmentBody(segment))) return segment;
}
return null;
}

/**
* A whole turn as one unit. Owned here rather than per-message
* so the tray is a single element keyed by the turn's first segment ({@link Messages})
Expand Down Expand Up @@ -377,20 +391,26 @@ const InterleavedTurn = ({
);
const turnActivities = useToolActivities(turnMessage, allMessages);

const lastContent = separateContentAndContext(last.content ?? "").content;
// Show the last segment as the answer once it looks like Lang, or once the run
// settles.
const answer = !turnLive || hasLangSyntax(lastContent) ? last : null;
// Live: keep the latest Lang program on the renderer even if a newer
// thinking/tool segment has already started — otherwise a mid-turn status
// Card vanishes into the tray as raw source. Settled: last segment wins once
// it has content (markdown answers are not Lang); if the run ended on an
// empty trailing segment, keep the last Lang so a status Card isn't lost.
const lastLang = latestLangSegment(segments);
const lastBody = segmentBody(last);
const answer = turnLive ? lastLang : lastBody ? last : (lastLang ?? last);
const answerMessage = useMemo(() => (answer ? { ...answer, toolCalls: [] } : null), [answer]);
const answerContent = answer ? segmentBody(answer) : "";

// Rows in run order: each non-answer segment's thinking prose, then its tools.
// Lang programs stay off the tray — they are answers, not muted markdown.
const steps = useMemo<TimelineStep[]>(() => {
const byCallId = new Map(turnActivities.map((a) => [a.toolCall.id, a]));
const rows: TimelineStep[] = [];
for (const seg of segments) {
if (seg.id !== answer?.id) {
const prose = separateContentAndContext(seg.content ?? "").content;
if (prose) rows.push({ type: "text", id: seg.id, text: prose });
const prose = segmentBody(seg);
if (prose && !hasLangSyntax(prose)) rows.push({ type: "text", id: seg.id, text: prose });
}
for (const tc of seg.toolCalls ?? []) {
const activity = byCallId.get(tc.id);
Expand All @@ -405,9 +425,9 @@ const InterleavedTurn = ({
const registry = useArtifactRendererRegistry();
const matched = getMatchedRendererActivities(registry, turnActivities);

// Hold the tray open until the answer's first tokens arrive. `answer` is
// `last` or null, so reuse the already-parsed `lastContent`.
const answerStarted = !!answer && lastContent.length > 0;
// Hold the tray open until some Lang (this segment or an earlier status UI)
// has tokens. Using `answerContent` so a previous status Card still counts.
const answerStarted = answerContent.length > 0;

return (
<>
Expand Down Expand Up @@ -470,8 +490,8 @@ const RenderGroup = ({
}) => {
// A group is either a run of assistant/tool messages or one standalone
// non-assistant message (user/system/…). The former is always one interleaved
// turn, which owns the tool-call timeline (it reads `last` for the answer, so
// a length-one assistant group works too); the latter renders on its own.
// turn, which owns the tool-call timeline (a length-one assistant group
// works too); the latter renders on its own.
const assistants = group.filter((m): m is AssistantMessage => m.role === "assistant");
const message = group[0]!;

Expand Down
Loading