mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 11:12:53 +00:00
perf(app): avoid stream updates in hidden chats (#7901)
Retained chats already preserve their last visible transcript. Keep their stream-tail selection stable while inactive so incoming chunks do not schedule React work; reveal reads the current model tail.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
|
||||
// The view retains its last visible transcript. Selecting a stable value here
|
||||
// also prevents incoming chunks from scheduling React work in a hidden panel.
|
||||
// Revealing the panel reads the current tail directly from the model.
|
||||
export function useAgentStreamTail(serverId: string, agentId: string | undefined) {
|
||||
const active = useRetainedPanelActive();
|
||||
return useSessionStore((state) =>
|
||||
active && agentId ? state.sessions[serverId]?.agentStreamTail.get(agentId) : undefined,
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import invariant from "tiny-invariant";
|
||||
import { shallow, useShallow } from "zustand/shallow";
|
||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||
import { useAgentStreamTail } from "@/agent-stream/use-agent-stream-tail";
|
||||
import { AgentStreamView, type AgentStreamViewHandle } from "@/agent-stream/view";
|
||||
import { ArchivedAgentCallout } from "@/components/archived-agent-callout";
|
||||
import { ComposerDock } from "@/composer/dock";
|
||||
@@ -1365,9 +1366,7 @@ const AgentStreamSection = memo(function AgentStreamSection({
|
||||
const bottomOverlayControlClearance = hasVisibleComposerTracks
|
||||
? resolveComposerTrackControlClearance(isCompactFormFactor)
|
||||
: 0;
|
||||
const streamItemsRaw = useSessionStore((state) =>
|
||||
agentId ? state.sessions[serverId]?.agentStreamTail?.get(agentId) : undefined,
|
||||
);
|
||||
const streamItemsRaw = useAgentStreamTail(serverId, agentId);
|
||||
const pendingMessageSubmissions = useSessionStore(
|
||||
useShallow((state) =>
|
||||
agentId
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React, {
|
||||
act,
|
||||
Profiler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { useAgentStreamTail } from "@/agent-stream/use-agent-stream-tail";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { RetainedPanelActivity, useRetainedPanelActive } from "@/components/retained-panel";
|
||||
@@ -220,3 +224,69 @@ describe("workspace sheet environment", () => {
|
||||
source.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
for (const [runtime, Chat] of [
|
||||
["native", NativeChat],
|
||||
["web", WebChat],
|
||||
] as const) {
|
||||
it(`${runtime} ignores hidden stream chunks and reads the latest transcript on reveal`, async () => {
|
||||
const serverId = `hidden-stream-${runtime}`;
|
||||
const agentId = "background-agent";
|
||||
const store = useSessionStore.getState();
|
||||
store.initializeSession(serverId, null);
|
||||
const profile = {
|
||||
commits: 0,
|
||||
onRender() {
|
||||
profile.commits++;
|
||||
},
|
||||
};
|
||||
const publish = (text: string) => {
|
||||
const item: StreamItem = {
|
||||
id: "message",
|
||||
kind: "assistant_message",
|
||||
text,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
store.setAgentStreamTail(serverId, new Map([[agentId, [item]]]));
|
||||
};
|
||||
function Content() {
|
||||
const items = useAgentStreamTail(serverId, agentId);
|
||||
const [draft] = useState("unsent draft");
|
||||
return (
|
||||
<span>
|
||||
{draft}:{items?.[0]?.kind === "assistant_message" ? items[0].text : ""}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const show = (active: boolean) =>
|
||||
act(() =>
|
||||
root.render(
|
||||
<Profiler id="workspace" onRender={profile.onRender}>
|
||||
<RetainedPanelActivity active={active}>
|
||||
<Chat>
|
||||
<Content />
|
||||
</Chat>
|
||||
</RetainedPanelActivity>
|
||||
</Profiler>,
|
||||
),
|
||||
);
|
||||
try {
|
||||
publish("first chunk");
|
||||
show(true);
|
||||
expect(container.textContent).toBe("unsent draft:first chunk");
|
||||
show(false);
|
||||
const hiddenCommits = profile.commits;
|
||||
for (let index = 0; index < 10; index++) {
|
||||
await act(async () => publish(`chunk ${index}`));
|
||||
}
|
||||
expect(profile.commits).toBe(hiddenCommits);
|
||||
show(true);
|
||||
expect(container.textContent).toBe("unsent draft:chunk 9");
|
||||
await act(async () => publish("live after reveal"));
|
||||
expect(container.textContent).toBe("unsent draft:live after reveal");
|
||||
} finally {
|
||||
act(() => root.render(null));
|
||||
store.clearSession(serverId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user