diff --git a/packages/app/src/agent-stream/layout.test.ts b/packages/app/src/agent-stream/layout.test.ts
index cf39b6920..fba61bdba 100644
--- a/packages/app/src/agent-stream/layout.test.ts
+++ b/packages/app/src/agent-stream/layout.test.ts
@@ -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);
diff --git a/packages/app/src/agent-stream/layout.ts b/packages/app/src/agent-stream/layout.ts
index de0fe38f9..1c6e83882 100644
--- a/packages/app/src/agent-stream/layout.ts
+++ b/packages/app/src/agent-stream/layout.ts
@@ -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),
};
}
diff --git a/packages/app/src/agent-stream/model.ts b/packages/app/src/agent-stream/model.ts
index 20f1f787c..fe92e63fc 100644
--- a/packages/app/src/agent-stream/model.ts
+++ b/packages/app/src/agent-stream/model.ts
@@ -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,
};
diff --git a/packages/app/src/agent-stream/strategy-web.test.tsx b/packages/app/src/agent-stream/strategy-web.test.tsx
index b4e14f895..3a689a866 100644
--- a/packages/app/src/agent-stream/strategy-web.test.tsx
+++ b/packages/app/src/agent-stream/strategy-web.test.tsx
@@ -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,
diff --git a/packages/app/src/agent-stream/strategy-web.tsx b/packages/app/src/agent-stream/strategy-web.tsx
index 3053aded0..7f5bea596 100644
--- a/packages/app/src/agent-stream/strategy-web.tsx
+++ b/packages/app/src/agent-stream/strategy-web.tsx
@@ -584,9 +584,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
) : null}
{mountedHistoryRows}
- {boundary.hasMountedHistory && boundary.hasLiveHead && boundary.historyToHeadGap > 0 ? (
-