Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { isToolCapsule } from '../../tool-cards/toolCardMetadata';
import { getConcurrentCapsuleRows, type ConcurrentCapsuleRow } from '../../tool-cards/toolCapsuleLayout';
import { useFlowChatContext, useFlowChatVolatileContext } from './FlowChatContext';
import { FileEditGroupView } from './FileEditGroupView';
import { buildFlowGroupRenderSegments, estimateFlowGroupItemsHeight } from './flowGroupRenderSegments';
import { buildFlowGroupRenderSegments, estimateFlowGroupItemsHeight, type FlowGroupRenderSegment } from './flowGroupRenderSegments';
import './ExploreRegion.scss';
import { useFlowChatReaderValue } from '../../timeline/readerState';

Expand Down Expand Up @@ -110,7 +110,12 @@ export const FlowGroupRenderer: React.FC<FlowGroupRendererProps> = React.memo(({
const groupRevealGate = useMemo(() => ({ ...revealGate, report }), [revealGate, report]);
const allItems = useMemo(() => recordedItems.filter(isFlowItemVisible), [recordedItems]);
const parallelRows = useMemo(() => getConcurrentCapsuleRows(allItems), [allItems]);
const renderSegments = useMemo(() => buildFlowGroupRenderSegments(allItems), [allItems]);
const prevSegmentsRef = useRef<readonly FlowGroupRenderSegment[]>([]);
const renderSegments = useMemo(() => {
const next = buildFlowGroupRenderSegments(allItems, prevSegmentsRef.current);
prevSegmentsRef.current = next;
return next;
}, [allItems]);
const toolItemCount = useMemo(() => allItems.filter(item => item.type === 'tool').length, [allItems]);
const filePath = category === 'file-edit' ? fileEditTarget(allItems.find(item => item.type === 'tool')) : undefined;
const fileStatus = useMemo(() => fileEditGroupStatus(category === 'file-edit' ? allItems : []), [category, allItems]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ it('keeps thinking beside its continuation and existing segment identities stabl
]);
});

it('keeps native AgentWait segment boundaries stable as calls complete', () => {
const items = Array.from({ length: 20 }, (_, index) => read(index));
items[15] = { ...items[15], toolName: 'AgentWait', status: 'running' };
items[16] = { ...items[16], toolName: 'AgentWait', status: 'running' };
const before = buildFlowGroupRenderSegments(items);
const after = buildFlowGroupRenderSegments(items.map(item => ({ ...item, status: 'completed' })));
expect(before[0].items.at(-1)?.id).toBe('call-15');
expect(after.map(segment => segment.key)).toEqual(before.map(segment => segment.key));
it('reuses closed segment object references when appending new items', () => {
const initialItems: FlowItem[] = Array.from({ length: 32 }, (_, index) => read(index));
const firstPass = buildFlowGroupRenderSegments(initialItems);
expect(firstPass.length).toBe(2);

const updatedItems = [...initialItems, read(32), read(33)];
const secondPass = buildFlowGroupRenderSegments(updatedItems, firstPass);
expect(secondPass[0]).toBe(firstPass[0]);
expect(secondPass[1]).toBe(firstPass[1]);
expect(secondPass.length).toBe(3);
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,75 @@ export function estimateFlowGroupItemsHeight(items: readonly FlowItem[]): number
}, 0);
}

/** Stable append boundaries keep reasoning beside its successor and capsule runs together. */
export function buildFlowGroupRenderSegments(items: readonly FlowItem[]): FlowGroupRenderSegment[] {
/**
* Stable append boundaries keep reasoning beside its successor and capsule runs together.
* When `previous` segments are provided, settled prefix segments are reused verbatim by reference,
* and the growing trailing segment is incrementally updated, enabling high-frequency streaming reuse.
*/
export function buildFlowGroupRenderSegments(
items: readonly FlowItem[],
previous?: readonly FlowGroupRenderSegment[],
): FlowGroupRenderSegment[] {
const segments: FlowGroupRenderSegment[] = [];
let start = 0;

const append = (end: number) => {
const members = items.slice(start, end);
segments.push({ key: members[0].id, items: members, startIndex: start,
estimatedHeightPx: estimateFlowGroupItemsHeight(members),
});
const segIdx = segments.length;
const reusable = previous?.[segIdx];

// Case 1: Closed settled segment where member items are completely unchanged by reference
if (
reusable &&
reusable.startIndex === start &&
reusable.key === members[0]?.id &&
reusable.items.length === members.length &&
reusable.items.every((it, idx) => it === members[idx])
) {
segments.push(reusable);
}
// Case 2: Live/growing trailing segment where items share the same key & prefix
else if (
reusable &&
reusable.startIndex === start &&
reusable.key === members[0]?.id &&
(
// Either identical length with replaced streaming tail object
(reusable.items.length === members.length &&
reusable.items.slice(0, -1).every((it, idx) => it === members[idx])) ||
// Or growing by appended items
(reusable.items.length <= members.length &&
reusable.items.every((it, idx) => it === members[idx]))
)
) {
reusable.items = members;
reusable.estimatedHeightPx = estimateFlowGroupItemsHeight(members);
segments.push(reusable);
}
// Case 3: Fresh segment
else {
segments.push({
key: members[0]?.id ?? `seg-${start}`,
items: members,
startIndex: start,
estimatedHeightPx: estimateFlowGroupItemsHeight(members),
});
}
start = end;
};

for (let index = 1; index < items.length; index += 1) {
if (index - start >= SEGMENT_ITEMS && items[index - 1].type !== 'thinking'
&& !(isCapsule(items[index - 1]) && isCapsule(items[index]))) append(index);
if (
index - start >= SEGMENT_ITEMS &&
items[index - 1].type !== 'thinking' &&
!(isCapsule(items[index - 1]) && isCapsule(items[index]))
) {
append(index);
}
}
if (start < items.length) append(items.length);
if (start < items.length) {
append(items.length);
}

return segments;
}
Loading