mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Reshape stream head/tail spacing
This commit is contained in:
@@ -123,6 +123,43 @@ function findLayoutItem(layout: StreamLayout, id: string): StreamLayoutItem {
|
||||
}
|
||||
|
||||
describe("layoutStream", () => {
|
||||
it.each(["web", "android"] as const)(
|
||||
"keeps split assistant block spacing identical to unsplit history on %s",
|
||||
(platform) => {
|
||||
const firstBlock = assistantMessage("turn:block:0", 2, { groupId: "turn", index: 0 });
|
||||
const secondBlock = assistantMessage("turn:block:1", 3, { groupId: "turn", index: 1 });
|
||||
const thirdBlock = assistantMessage("turn:block:2", 4, { groupId: "turn", index: 2 });
|
||||
const splitLayout = layoutFor({
|
||||
platform,
|
||||
agentStatus: "running",
|
||||
tail: [userMessage("u1", 1), firstBlock],
|
||||
head: [secondBlock, thirdBlock],
|
||||
timingIds: [firstBlock.id, secondBlock.id, thirdBlock.id],
|
||||
});
|
||||
const unsplitLayout = layoutFor({
|
||||
platform,
|
||||
agentStatus: "running",
|
||||
tail: [userMessage("u1", 1), firstBlock, secondBlock, thirdBlock],
|
||||
timingIds: [firstBlock.id, secondBlock.id, thirdBlock.id],
|
||||
});
|
||||
|
||||
expect(findLayoutItem(splitLayout, firstBlock.id).belowItem?.id).toBe(secondBlock.id);
|
||||
expect(findLayoutItem(splitLayout, secondBlock.id).aboveItem?.id).toBe(firstBlock.id);
|
||||
expect(findLayoutItem(splitLayout, firstBlock.id).assistantSpacing).toBe(
|
||||
findLayoutItem(unsplitLayout, firstBlock.id).assistantSpacing,
|
||||
);
|
||||
expect(findLayoutItem(splitLayout, secondBlock.id).assistantSpacing).toBe(
|
||||
findLayoutItem(unsplitLayout, secondBlock.id).assistantSpacing,
|
||||
);
|
||||
expect(findLayoutItem(splitLayout, firstBlock.id).gapBelow).toBe(
|
||||
findLayoutItem(unsplitLayout, firstBlock.id).gapBelow,
|
||||
);
|
||||
expect(findLayoutItem(splitLayout, secondBlock.id).gapBelow).toBe(
|
||||
findLayoutItem(unsplitLayout, secondBlock.id).gapBelow,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not duplicate footers when a native assistant turn spans history and live head", () => {
|
||||
const historyBlock = assistantMessage("turn:block:0", 2, { groupId: "turn", index: 0 });
|
||||
const headBlock = assistantMessage("turn:head", 3, { groupId: "turn", index: 1 });
|
||||
@@ -195,6 +232,39 @@ describe("layoutStream", () => {
|
||||
expect(findLayoutItem(layout, headBlock.id).assistantSpacing).toBe("compactTop");
|
||||
});
|
||||
|
||||
it.each(["web", "android"] as const)(
|
||||
"keeps split tool sequencing and gapBelow identical to unsplit history on %s",
|
||||
(platform) => {
|
||||
const shell = toolCall("tool-1", 2);
|
||||
const thinking = thought("thought-1", 3);
|
||||
const assistant = assistantMessage("a1", 4);
|
||||
const splitLayout = layoutFor({
|
||||
platform,
|
||||
tail: [userMessage("u1", 1), shell],
|
||||
head: [thinking, assistant],
|
||||
});
|
||||
const unsplitLayout = layoutFor({
|
||||
platform,
|
||||
tail: [userMessage("u1", 1), shell, thinking, assistant],
|
||||
});
|
||||
|
||||
expect(findLayoutItem(splitLayout, shell.id).belowItem?.id).toBe(thinking.id);
|
||||
expect(findLayoutItem(splitLayout, thinking.id).aboveItem?.id).toBe(shell.id);
|
||||
expect(findLayoutItem(splitLayout, shell.id).toolSequence).toBe(
|
||||
findLayoutItem(unsplitLayout, shell.id).toolSequence,
|
||||
);
|
||||
expect(findLayoutItem(splitLayout, thinking.id).toolSequence).toBe(
|
||||
findLayoutItem(unsplitLayout, thinking.id).toolSequence,
|
||||
);
|
||||
expect(findLayoutItem(splitLayout, shell.id).gapBelow).toBe(
|
||||
findLayoutItem(unsplitLayout, shell.id).gapBelow,
|
||||
);
|
||||
expect(findLayoutItem(splitLayout, thinking.id).gapBelow).toBe(
|
||||
findLayoutItem(unsplitLayout, thinking.id).gapBelow,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("computes tool sequence position from strategy-aware neighbors", () => {
|
||||
const shell = toolCall("tool-1", 2);
|
||||
const thinking = thought("thought-1", 3);
|
||||
|
||||
@@ -32,7 +32,6 @@ export interface StreamLayout {
|
||||
history: StreamLayoutItem[];
|
||||
liveHead: StreamLayoutItem[];
|
||||
auxiliaryTurnFooter: TurnFooterHost | null;
|
||||
historyToHeadGap: number;
|
||||
}
|
||||
|
||||
export interface StreamLayoutInput {
|
||||
@@ -243,6 +242,5 @@ export function layoutStream(input: StreamLayoutInput): StreamLayout {
|
||||
history,
|
||||
liveHead,
|
||||
auxiliaryTurnFooter,
|
||||
historyToHeadGap: getGapBetweenStreamItems(historyBoundaryItem, liveHeadBoundaryItem),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ export interface StreamHistoryBoundary {
|
||||
hasVirtualizedHistory: boolean;
|
||||
hasMountedHistory: boolean;
|
||||
hasLiveHead: boolean;
|
||||
historyToHeadGap: number;
|
||||
}
|
||||
|
||||
export interface StreamRenderAuxiliary {
|
||||
@@ -205,7 +204,6 @@ export function buildAgentStreamRenderModel(
|
||||
hasVirtualizedHistory: splitHistory.segments.historyVirtualized.length > 0,
|
||||
hasMountedHistory: splitHistory.segments.historyMounted.length > 0,
|
||||
hasLiveHead: orderedHead.length > 0,
|
||||
historyToHeadGap: 0,
|
||||
},
|
||||
auxiliary: EMPTY_AUXILIARY,
|
||||
};
|
||||
|
||||
@@ -125,7 +125,6 @@ describe("createWebStreamStrategy", () => {
|
||||
hasVirtualizedHistory: true,
|
||||
hasMountedHistory: false,
|
||||
hasLiveHead: false,
|
||||
historyToHeadGap: 0,
|
||||
},
|
||||
renderers: createRenderers(rowRenderCount),
|
||||
listEmptyComponent: null,
|
||||
@@ -171,7 +170,6 @@ describe("createWebStreamStrategy", () => {
|
||||
hasVirtualizedHistory: false,
|
||||
hasMountedHistory: true,
|
||||
hasLiveHead: false,
|
||||
historyToHeadGap: 0,
|
||||
},
|
||||
renderers: createRenderers(vi.fn()),
|
||||
listEmptyComponent: null,
|
||||
|
||||
@@ -584,9 +584,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
</div>
|
||||
) : null}
|
||||
{mountedHistoryRows}
|
||||
{boundary.hasMountedHistory && boundary.hasLiveHead && boundary.historyToHeadGap > 0 ? (
|
||||
<HistoryToHeadSpacer height={boundary.historyToHeadGap} />
|
||||
) : null}
|
||||
{liveHeadRows}
|
||||
{liveAuxiliary}
|
||||
{shouldRenderEmpty ? listEmptyComponent : null}
|
||||
@@ -634,12 +631,3 @@ export function createWebStreamStrategy(input: CreateWebStreamStrategyInput): St
|
||||
getBottomOffset: (metrics) => Math.max(0, metrics.contentHeight - metrics.viewportHeight),
|
||||
});
|
||||
}
|
||||
|
||||
interface HistoryToHeadSpacerProps {
|
||||
height: number;
|
||||
}
|
||||
|
||||
function HistoryToHeadSpacer({ height }: HistoryToHeadSpacerProps) {
|
||||
const spacerStyle = useMemo(() => ({ height, width: "100%" as const }), [height]);
|
||||
return <div style={spacerStyle} />;
|
||||
}
|
||||
|
||||
@@ -613,16 +613,13 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
const renderModel = useMemo<AgentStreamRenderModel>(() => {
|
||||
return {
|
||||
...baseRenderModel,
|
||||
boundary: {
|
||||
...baseRenderModel.boundary,
|
||||
historyToHeadGap: streamLayout.historyToHeadGap,
|
||||
},
|
||||
boundary: baseRenderModel.boundary,
|
||||
auxiliary: {
|
||||
pendingPermissions: pendingPermissionsNode,
|
||||
turnFooter: turnFooterNode,
|
||||
},
|
||||
};
|
||||
}, [baseRenderModel, pendingPermissionsNode, streamLayout.historyToHeadGap, turnFooterNode]);
|
||||
}, [baseRenderModel, pendingPermissionsNode, turnFooterNode]);
|
||||
|
||||
const emptyStateStyle = useMemo(() => [stylesheet.emptyState, stylesheet.contentWrapper], []);
|
||||
const listEmptyComponent = useMemo(
|
||||
|
||||
Reference in New Issue
Block a user