chore: commit all local changes

This commit is contained in:
Mohamed Boudra
2026-02-25 13:53:23 +07:00
parent a060ae1256
commit c4523c70b5
18 changed files with 1818 additions and 67 deletions
@@ -0,0 +1,197 @@
import { expect, test, type Page } from "@playwright/test";
const SERVER_ID =
process.env.PLAYWRIGHT_REPRO_SERVER_ID ?? "srv_ETXtcjYRGrCI";
const AGENT_ID =
process.env.PLAYWRIGHT_REPRO_AGENT_ID ??
"3533e6c3-c0b3-4310-b85c-eb07cbb501a0";
const APP_BASE_URL = process.env.PLAYWRIGHT_REPRO_BASE_URL ?? "http://localhost:8081";
const DAEMON_ENDPOINT =
process.env.PLAYWRIGHT_REPRO_DAEMON_ENDPOINT ?? "127.0.0.1:6767";
const SUBMIT_TEXT = process.env.PLAYWRIGHT_REPRO_MESSAGE ?? "hello";
const AGENT_URL = `${APP_BASE_URL}/h/${SERVER_ID}/agent/${AGENT_ID}`;
const NEAR_BOTTOM_THRESHOLD_PX = 64;
type ScrollMetrics = {
offsetY: number;
contentHeight: number;
viewportHeight: number;
distanceFromBottom: number;
};
test.use({ browserName: "firefox" });
function seedDaemonRegistryScript(params: {
serverId: string;
endpoint: string;
nowIso: string;
}) {
const daemon = {
serverId: params.serverId,
label: "localhost",
connections: [
{
id: `direct:${params.endpoint}`,
type: "direct",
endpoint: params.endpoint,
},
],
preferredConnectionId: `direct:${params.endpoint}`,
createdAt: params.nowIso,
updatedAt: params.nowIso,
};
localStorage.setItem("@paseo:e2e", "1");
localStorage.setItem("@paseo:daemon-registry", JSON.stringify([daemon]));
localStorage.setItem(
"@paseo:create-agent-preferences",
JSON.stringify({
serverId: params.serverId,
provider: "codex",
providerPreferences: {
claude: { model: "haiku" },
codex: { model: "gpt-5.1-codex-mini", thinkingOptionId: "low" },
},
})
);
}
async function readScrollMetrics(page: Page): Promise<ScrollMetrics> {
return page.getByTestId("agent-chat-scroll").evaluate((root: Element) => {
const rootElement = root as HTMLElement;
const candidates = [rootElement, ...Array.from(rootElement.querySelectorAll("*"))];
const scrollElement =
candidates.find(
(element) =>
element instanceof HTMLElement &&
element.scrollHeight - element.clientHeight > 1
) ?? rootElement;
const offsetY = Math.max(0, scrollElement.scrollTop);
const contentHeight = Math.max(0, scrollElement.scrollHeight);
const viewportHeight = Math.max(0, scrollElement.clientHeight);
const distanceFromBottom = Math.max(
0,
contentHeight - (offsetY + viewportHeight)
);
return {
offsetY,
contentHeight,
viewportHeight,
distanceFromBottom,
};
});
}
async function scrollUpFromBottom(
page: Page,
pixels: number
): Promise<void> {
await page.getByTestId("agent-chat-scroll").evaluate(
(root: Element, amount: number) => {
const rootElement = root as HTMLElement;
const candidates = [
rootElement,
...Array.from(rootElement.querySelectorAll("*")),
];
const scrollElement =
candidates.find(
(element) =>
element instanceof HTMLElement &&
element.scrollHeight - element.clientHeight > 1
) ?? rootElement;
const bottomOffset = Math.max(
0,
scrollElement.scrollHeight - scrollElement.clientHeight
);
scrollElement.scrollTop = Math.max(0, bottomOffset - amount);
},
pixels
);
}
test("repro: submit while scrolled up should stay anchored to bottom (Firefox)", async ({
page,
}, testInfo) => {
const userAgent = await page.evaluate(() => navigator.userAgent);
expect(userAgent).toContain("Firefox");
await page.addInitScript(seedDaemonRegistryScript, {
serverId: SERVER_ID,
endpoint: DAEMON_ENDPOINT,
nowIso: new Date().toISOString(),
});
await page.goto(AGENT_URL, { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("agent-chat-scroll")).toBeVisible({
timeout: 60_000,
});
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeVisible({
timeout: 60_000,
});
// Require enough history so the repro actually scrolls away from bottom.
await expect
.poll(async () => {
const metrics = await readScrollMetrics(page);
return Math.max(0, metrics.contentHeight - metrics.viewportHeight);
})
.toBeGreaterThan(300);
await scrollUpFromBottom(page, 900);
await page.waitForTimeout(250);
const beforeSubmit = await readScrollMetrics(page);
const beforePath = testInfo.outputPath("before-submit.png");
await page.screenshot({ path: beforePath, fullPage: true });
await testInfo.attach("before-submit", {
path: beforePath,
contentType: "image/png",
});
await page.getByRole("textbox", { name: "Message agent..." }).fill(SUBMIT_TEXT);
await page.getByRole("textbox", { name: "Message agent..." }).press("Enter");
await page.waitForTimeout(1200);
const afterSubmit = await readScrollMetrics(page);
const afterPath = testInfo.outputPath("after-submit.png");
await page.screenshot({ path: afterPath, fullPage: true });
await testInfo.attach("after-submit", {
path: afterPath,
contentType: "image/png",
});
await testInfo.attach("firefox-scroll-metrics", {
body: JSON.stringify(
{
url: AGENT_URL,
submitText: SUBMIT_TEXT,
thresholdPx: NEAR_BOTTOM_THRESHOLD_PX,
beforeScreenshot: beforePath,
afterScreenshot: afterPath,
beforeSubmit,
afterSubmit,
},
null,
2
),
contentType: "application/json",
});
console.log(
`[firefox-scroll-repro] ${JSON.stringify(
{
submitText: SUBMIT_TEXT,
beforeSubmit,
afterSubmit,
},
null,
2
)}`
);
expect(beforeSubmit.distanceFromBottom).toBeGreaterThan(NEAR_BOTTOM_THRESHOLD_PX);
expect(afterSubmit.distanceFromBottom).toBeLessThanOrEqual(
NEAR_BOTTOM_THRESHOLD_PX
);
});
+30
View File
@@ -0,0 +1,30 @@
import { defineConfig, devices } from "@playwright/test";
const baseURL =
process.env.E2E_BASE_URL ??
`http://localhost:${process.env.E2E_METRO_PORT ?? "8081"}`;
export default defineConfig({
testDir: "./e2e",
globalSetup: "./e2e/global-setup.ts",
timeout: 60_000,
expect: {
timeout: 10_000,
},
fullyParallel: false,
workers: 1,
retries: process.env.CI ? 1 : 0,
reporter: [["list"]],
use: {
baseURL,
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{
name: "Desktop Firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
@@ -61,6 +61,8 @@ interface AgentInputAreaProps {
onAddImages?: (addImages: (images: ImageAttachment[]) => void) => void
/** Optional draft context for listing commands before an agent exists. */
commandDraftConfig?: DraftCommandConfig
/** Called when a message is about to be sent (any path: keyboard, dictation, queued). */
onMessageSent?: () => void
}
const EMPTY_ARRAY: readonly QueuedMessage[] = []
@@ -77,6 +79,7 @@ export function AgentInputArea({
autoFocus = false,
onAddImages,
commandDraftConfig,
onMessageSent,
}: AgentInputAreaProps) {
markScrollInvestigationRender(`AgentInputArea:${serverId}:${agentId}`)
const { theme } = useUnistyles()
@@ -169,6 +172,7 @@ export function AgentInputArea({
}, [addImages, onAddImages])
const submitMessage = useCallback(async (text: string, images?: ImageAttachment[]) => {
onMessageSent?.()
if (onSubmitMessageRef.current) {
await onSubmitMessageRef.current({ text, images })
return
@@ -177,7 +181,7 @@ export function AgentInputArea({
throw new Error('Host is not connected')
}
await sendAgentMessageRef.current(agentIdRef.current, text, images)
}, [])
}, [onMessageSent])
useEffect(() => {
agentIdRef.current = agentId
@@ -137,6 +137,82 @@ function scrollAnchorIntoView(params: {
return true;
}
function forceScrollContainerToBottom(
refs: StreamRenderRefs,
fallbackOffset: number
): void {
const resolveNode = (
input: unknown
): HTMLElement | null => {
if (!(input instanceof HTMLElement)) {
return null;
}
if (input.scrollHeight - input.clientHeight > 1) {
return input;
}
let node: HTMLElement | null = input.parentElement;
while (node) {
if (node.scrollHeight - node.clientHeight > 1) {
return node;
}
node = node.parentElement;
}
return null;
};
const scrollViewHandle = refs.scrollViewRef.current as
| {
getNativeScrollRef?: () => unknown;
getScrollableNode?: () => unknown;
getInnerViewNode?: () => unknown;
getNativeRef?: () => unknown;
}
| null;
const anchorHandle = refs.bottomAnchorRef.current as
| ({ getNativeRef?: () => unknown } & object)
| null;
const candidates: unknown[] = [
scrollViewHandle?.getNativeScrollRef?.(),
scrollViewHandle?.getScrollableNode?.(),
scrollViewHandle?.getInnerViewNode?.(),
scrollViewHandle?.getNativeRef?.(),
scrollViewHandle,
typeof anchorHandle?.getNativeRef === "function"
? anchorHandle.getNativeRef()
: anchorHandle,
];
let scrollNode: HTMLElement | null = null;
for (const candidate of candidates) {
scrollNode = resolveNode(candidate);
if (scrollNode) {
break;
}
}
if (!scrollNode && typeof document !== "undefined") {
scrollNode = resolveNode(
document.querySelector("[data-testid='agent-chat-scroll']")
);
}
if (!scrollNode) {
return;
}
const snap = () => {
scrollNode.scrollTop = Math.max(
fallbackOffset,
scrollNode.scrollHeight - scrollNode.clientHeight
);
};
snap();
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(snap);
}
}
function createStreamRenderStrategy(
config: StreamRenderStrategyConfig
): StreamRenderStrategy {
@@ -255,14 +331,20 @@ function createForwardStreamStrategy(): StreamRenderStrategy {
getBottomOffset: (metrics) =>
Math.max(0, metrics.contentHeight - metrics.viewportHeight),
scrollToBottom: ({ refs, metrics, animated }) => {
if (scrollAnchorIntoView({ refs, animated })) {
return;
const bottomOffset = Math.max(
0,
metrics.contentHeight - metrics.viewportHeight
);
const usedAnchor = scrollAnchorIntoView({ refs, animated });
if (!usedAnchor) {
refs.scrollViewRef.current?.scrollToEnd?.({ animated });
}
refs.scrollViewRef.current?.scrollToEnd?.({ animated });
// Always apply deterministic bottom offset to avoid partial anchors.
refs.scrollViewRef.current?.scrollTo?.({
y: Math.max(0, metrics.contentHeight - metrics.viewportHeight),
y: bottomOffset,
animated,
});
forceScrollContainerToBottom(refs, bottomOffset);
},
scrollToOffset: ({ refs, offset, animated }) => {
refs.scrollViewRef.current?.scrollTo({ y: offset, animated });
@@ -1,12 +1,14 @@
import {
Fragment,
createElement,
forwardRef,
isValidElement,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
useCallback,
} from "react";
import type { ComponentType, ReactElement, ReactNode } from "react";
import {
@@ -104,6 +106,10 @@ function renderStreamEdgeComponent(
return createElement(component);
}
export interface AgentStreamViewHandle {
scrollToBottom(): void;
}
export interface AgentStreamViewProps {
agentId: string;
serverId?: string;
@@ -112,13 +118,13 @@ export interface AgentStreamViewProps {
pendingPermissions: Map<string, PendingPermission>;
}
export function AgentStreamView({
export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(function AgentStreamView({
agentId,
serverId,
agent,
streamItems,
pendingPermissions,
}: AgentStreamViewProps) {
}, ref) {
const flatListRef = useRef<FlatList<StreamItem>>(null);
const scrollViewRef = useRef<ScrollView>(null);
const bottomAnchorRef = useRef<View>(null);
@@ -139,6 +145,7 @@ export function AgentStreamView({
const hasScrolledInitially = useRef(false);
const hasAutoScrolledOnce = useRef(false);
const isNearBottomRef = useRef(true);
const pendingAnchorRequestRef = useRef(false);
const pendingAutoScrollFrameRef = useRef<number | null>(null);
const pendingAutoScrollAnimatedRef = useRef(false);
const scrollOffsetYRef = useRef(0);
@@ -182,6 +189,7 @@ export function AgentStreamView({
hasScrolledInitially.current = false;
hasAutoScrolledOnce.current = false;
isNearBottomRef.current = true;
pendingAnchorRequestRef.current = false;
setExpandedInlineToolCallIds(new Set());
}, [agentId]);
@@ -224,14 +232,29 @@ export function AgentStreamView({
]
);
const updateNearBottom = useCallback((value: boolean) => {
if (isNearBottomRef.current === value) return;
isNearBottomRef.current = value;
setIsNearBottom(value);
}, []);
const requestAnchorToBottom = useCallback(() => {
pendingAnchorRequestRef.current = true;
}, []);
const handleScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
const previousOffsetY = scrollOffsetYRef.current;
const previousContentHeight = streamViewportMetricsRef.current.contentHeight;
scrollOffsetYRef.current = contentOffset.y;
streamViewportMetricsRef.current = {
contentHeight: Math.max(0, contentSize.height),
viewportHeight: Math.max(0, layoutMeasurement.height),
};
const offsetDelta = contentOffset.y - previousOffsetY;
const contentHeightDelta =
streamViewportMetricsRef.current.contentHeight - previousContentHeight;
const threshold = Math.max(insets.bottom, 32);
const nearBottom = isNearBottomForStreamRenderStrategy({
strategy: streamRenderStrategy,
@@ -241,9 +264,22 @@ export function AgentStreamView({
viewportHeight: streamViewportMetricsRef.current.viewportHeight,
});
if (isNearBottomRef.current !== nearBottom) {
isNearBottomRef.current = nearBottom;
setIsNearBottom(nearBottom);
const pendingAnchorBefore = pendingAnchorRequestRef.current;
const shouldSuppressFalseNearBottom =
pendingAnchorBefore &&
!nearBottom &&
Math.abs(offsetDelta) <= 1 &&
contentHeightDelta > 0;
if (shouldSuppressFalseNearBottom) {
updateNearBottom(true);
} else {
updateNearBottom(nearBottom);
}
const shouldClearPendingAnchor =
pendingAnchorBefore && !nearBottom && Math.abs(offsetDelta) > 1;
if (shouldClearPendingAnchor) {
pendingAnchorRequestRef.current = false;
}
if (showDesktopWebScrollbar) {
@@ -255,6 +291,7 @@ export function AgentStreamView({
showDesktopWebScrollbar,
streamRenderStrategy,
streamScrollbarMetrics,
updateNearBottom,
]
);
@@ -282,12 +319,17 @@ export function AgentStreamView({
animated,
});
scrollOffsetYRef.current = targetOffset;
isNearBottomRef.current = true;
setIsNearBottom(true);
updateNearBottom(true);
},
[streamRenderRefs, streamRenderStrategy]
[updateNearBottom, streamRenderRefs, streamRenderStrategy]
);
useImperativeHandle(ref, () => ({
scrollToBottom() {
requestAnchorToBottom();
},
}), [requestAnchorToBottom]);
const handleContentSizeChange = useCallback(
(width: number, height: number) => {
const previousMetrics = streamViewportMetricsRef.current;
@@ -310,7 +352,11 @@ export function AgentStreamView({
scrollToBottomInternal({ animated: false });
hasAutoScrolledOnce.current = true;
hasScrolledInitially.current = true;
} else if (wasNearBottom || isNearBottomRef.current) {
} else if (
wasNearBottom ||
isNearBottomRef.current ||
pendingAnchorRequestRef.current
) {
scrollToBottomInternal({ animated: false });
}
}
@@ -376,7 +422,7 @@ export function AgentStreamView({
return () => handle.cancel();
}
if (!isNearBottomRef.current) {
if (!isNearBottomRef.current && !pendingAnchorRequestRef.current) {
return;
}
@@ -393,8 +439,6 @@ export function AgentStreamView({
function scrollToBottom() {
const animated = streamRenderStrategy.shouldAnimateManualScrollToBottom();
scrollToBottomInternal({ animated });
isNearBottomRef.current = true;
setIsNearBottom(true);
}
const flatListData = useMemo(() => {
@@ -982,7 +1026,7 @@ export function AgentStreamView({
</View>
</ToolCallSheetProvider>
);
}
});
function normalizeInlinePath(
rawPath: string,
@@ -153,7 +153,9 @@ export function processTimelineResponse(
// ----------------------------------------------------------------
// Replace path: full hydration from scratch
// ----------------------------------------------------------------
nextTail = hydrateStreamState(toHydratedEvents(timelineUnits));
nextTail = hydrateStreamState(toHydratedEvents(timelineUnits), {
source: "canonical",
});
nextHead = [];
if (payload.startCursor && payload.endCursor) {
@@ -222,7 +224,9 @@ export function processTimelineResponse(
if (acceptedUnits.length > 0) {
nextTail = acceptedUnits.reduce<StreamItem[]>(
(state, { event, timestamp }) =>
reduceStreamUpdate(state, event, timestamp),
reduceStreamUpdate(state, event, timestamp, {
source: "canonical",
}),
currentTail
);
}
@@ -385,6 +389,7 @@ export function processAgentStreamEvent(
head: currentHead,
event,
timestamp,
source: "live",
})
: {
tail: currentTail,
@@ -31,7 +31,7 @@ import {
import { MenuHeader } from "@/components/headers/menu-header";
import { BackHeader } from "@/components/headers/back-header";
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { AgentStreamView } from "@/components/agent-stream-view";
import { AgentStreamView, type AgentStreamViewHandle } from "@/components/agent-stream-view";
import { AgentInputArea } from "@/components/agent-input-area";
import { ExplorerSidebar } from "@/components/explorer-sidebar";
import { FileDropZone } from "@/components/file-drop-zone";
@@ -240,6 +240,7 @@ function AgentScreenContent({
const resolvedAgentId = agentId;
const { isArchivingAgent } = useArchiveAgent();
const streamViewRef = useRef<AgentStreamViewHandle>(null);
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
@@ -1235,6 +1236,7 @@ function AgentScreenContent({
style={[styles.content, animatedKeyboardStyle]}
>
<AgentStreamView
ref={streamViewRef}
agentId={effectiveAgent.id}
serverId={serverId}
agent={effectiveAgent}
@@ -1254,6 +1256,7 @@ function AgentScreenContent({
autoFocus
isSubmitLoading={showPendingCreateSubmitLoading}
onAddImages={handleAddImagesCallback}
onMessageSent={() => streamViewRef.current?.scrollToBottom()}
/>
)}
+89
View File
@@ -522,4 +522,93 @@ describe("stream reducer canonical tool calls", () => {
authoritativeTimestamp.getTime()
);
});
it("keeps canonical assistant/user/assistant order during replay", () => {
const state: StreamItem[] = [
{
kind: "assistant_message",
id: "a1",
text: "Saved that preference. ",
timestamp: new Date("2025-01-01T11:20:00Z"),
},
{
kind: "user_message",
id: "u1",
text: "the other qeustion is i mgiht be thinking that its winner takes it all",
timestamp: new Date("2025-01-01T11:20:01Z"),
},
];
const event: AgentStreamEventPayload = {
type: "timeline",
provider: "claude",
item: {
type: "assistant_message",
text: "Right. And it probably isn't.",
},
};
const next = reduceStreamUpdate(
state,
event,
new Date("2025-01-01T11:20:02Z"),
{ source: "canonical" }
);
assert.deepStrictEqual(next.map((item) => item.kind), [
"assistant_message",
"user_message",
"assistant_message",
]);
assert.strictEqual(
next[0]?.kind === "assistant_message" ? next[0].text : null,
"Saved that preference. "
);
assert.strictEqual(
next[2]?.kind === "assistant_message" ? next[2].text : null,
"Right. And it probably isn't."
);
});
it("keeps live optimistic assistant merge behavior", () => {
const state: StreamItem[] = [
{
kind: "assistant_message",
id: "a1",
text: "Saved that preference. ",
timestamp: new Date("2025-01-01T11:21:00Z"),
},
{
kind: "user_message",
id: "u1",
text: "the other qeustion is i mgiht be thinking that its winner takes it all",
timestamp: new Date("2025-01-01T11:21:01Z"),
},
];
const event: AgentStreamEventPayload = {
type: "timeline",
provider: "claude",
item: {
type: "assistant_message",
text: "Right. And it probably isn't.",
},
};
const next = reduceStreamUpdate(
state,
event,
new Date("2025-01-01T11:21:02Z"),
{ source: "live" }
);
assert.deepStrictEqual(next.map((item) => item.kind), [
"assistant_message",
"user_message",
]);
assert.strictEqual(
next[0]?.kind === "assistant_message" ? next[0].text : null,
"Saved that preference. Right. And it probably isn't."
);
});
});
+25 -8
View File
@@ -160,6 +160,8 @@ export interface TodoListItem {
items: TodoEntry[];
}
export type StreamUpdateSource = "live" | "canonical";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -230,7 +232,8 @@ function appendUserMessage(
function appendAssistantMessage(
state: StreamItem[],
text: string,
timestamp: Date
timestamp: Date,
source: StreamUpdateSource
): StreamItem[] {
const { chunk, hasContent } = normalizeChunk(text);
if (!chunk) {
@@ -250,7 +253,11 @@ function appendAssistantMessage(
// If the last item is a user_message (optimistic append to head during
// interrupt), look one further back for the streaming assistant_message.
const secondLast = state[state.length - 2];
if (last?.kind === "user_message" && secondLast?.kind === "assistant_message") {
if (
source === "live" &&
last?.kind === "user_message" &&
secondLast?.kind === "assistant_message"
) {
const updated: AssistantMessageItem = {
...secondLast,
text: `${secondLast.text}${chunk}`,
@@ -525,8 +532,10 @@ function formatErrorMessage(message: string): string {
export function reduceStreamUpdate(
state: StreamItem[],
event: AgentStreamEventPayload,
timestamp: Date
timestamp: Date,
options?: { source?: StreamUpdateSource }
): StreamItem[] {
const source = options?.source ?? "live";
switch (event.type) {
case "timeline": {
const item = event.item;
@@ -541,7 +550,12 @@ export function reduceStreamUpdate(
);
break;
case "assistant_message":
nextState = appendAssistantMessage(state, item.text, timestamp);
nextState = appendAssistantMessage(
state,
item.text,
timestamp,
source
);
break;
case "reasoning":
return appendThought(state, item.text, timestamp);
@@ -691,11 +705,12 @@ export function reduceStreamUpdate(
* Hydrate stream state from a batch of AgentManager stream events
*/
export function hydrateStreamState(
events: Array<{ event: AgentStreamEventPayload; timestamp: Date }>
events: Array<{ event: AgentStreamEventPayload; timestamp: Date }>,
options?: { source?: StreamUpdateSource }
): StreamItem[] {
const hydrated = events.reduce<StreamItem[]>(
(state, { event, timestamp }) => {
return reduceStreamUpdate(state, event, timestamp);
return reduceStreamUpdate(state, event, timestamp, options);
},
[]
);
@@ -853,8 +868,10 @@ export function applyStreamEvent(params: {
head: StreamItem[];
event: AgentStreamEventPayload;
timestamp: Date;
source?: StreamUpdateSource;
}): ApplyStreamEventResult {
const { tail, head, event, timestamp } = params;
const source = params.source ?? "live";
let nextTail = tail;
let nextHead = head;
let changedTail = false;
@@ -894,7 +911,7 @@ export function applyStreamEvent(params: {
// For streamable kinds, apply to head
if (incomingKind !== null && isStreamableKind(incomingKind)) {
const reduced = reduceStreamUpdate(nextHead, event, timestamp);
const reduced = reduceStreamUpdate(nextHead, event, timestamp, { source });
if (reduced !== nextHead) {
nextHead = reduced;
changedHead = true;
@@ -903,7 +920,7 @@ export function applyStreamEvent(params: {
}
// For non-streamable kinds or non-timeline events, apply to tail
const reduced = reduceStreamUpdate(nextTail, event, timestamp);
const reduced = reduceStreamUpdate(nextTail, event, timestamp, { source });
if (reduced !== nextTail) {
nextTail = reduced;
changedTail = true;