mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 11:42:52 +00:00
* perf(app): bound native workspace mount work Native workspace switches retained inactive React trees and mounted large chat histories in aggressive list batches, making Hermes and Fabric work scale with prior usage. Retain only the active native workspace, reveal a shared turn-aligned history window locally before daemon pagination, preserve desktop retention semantics, and pace native list mounting. * test(app): cover bounded history journeys Exercise local history reveal and hidden outline jumps through the production browser viewport while preserving held-page pagination and scroll anchoring. * fix(app): reveal fetched outline targets A bounded merged jump could load its target before the mounted history window, leaving the pending jump unable to scroll. Reveal fetched hidden targets through the existing history-window capability, then scroll once they mount. * test(app): follow anchored history reveals History-start helpers could stop at a transient edge before local window expansion restored the content anchor. Retry the real wheel or keyboard action after geometry settles until the timeline remains at history start. * test(app): wait for history anchor release
214 lines
6.4 KiB
TypeScript
214 lines
6.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { deriveStreamTurnTiming, type StreamTurnTiming } from "@/timeline/turn-time";
|
|
import type { StreamItem } from "@/types/stream";
|
|
import { findMountedWindowStart, getMountedRecentStreamItems } from "./history-window";
|
|
import { getWebPartialVirtualizationThreshold } from "./web-virtualization";
|
|
import { orderHeadForStreamRenderStrategy, orderTailForStreamRenderStrategy } from "./strategy";
|
|
import { resolveStreamRenderStrategy } from "./strategy-resolver";
|
|
|
|
export interface StreamRenderSegments {
|
|
historyVirtualized: StreamItem[];
|
|
historyMounted: StreamItem[];
|
|
liveHead: StreamItem[];
|
|
}
|
|
|
|
export interface StreamHistoryBoundary {
|
|
hasVirtualizedHistory: boolean;
|
|
hasMountedHistory: boolean;
|
|
hasLiveHead: boolean;
|
|
}
|
|
|
|
export interface StreamRenderAuxiliary {
|
|
pendingPermissions: ReactNode;
|
|
turnFooter: ReactNode;
|
|
}
|
|
|
|
export interface AgentStreamRenderModel {
|
|
history: StreamItem[];
|
|
segments: StreamRenderSegments;
|
|
turnTiming: StreamTurnTiming;
|
|
boundary: StreamHistoryBoundary;
|
|
auxiliary: StreamRenderAuxiliary;
|
|
}
|
|
|
|
export interface BuildAgentStreamRenderModelInput {
|
|
isTurnActive: boolean;
|
|
activeTurnStartedAt: Date | null;
|
|
tail: StreamItem[];
|
|
head: StreamItem[];
|
|
platform: "web" | "native";
|
|
isMobileBreakpoint: boolean;
|
|
historyStart?: number;
|
|
}
|
|
|
|
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
|
const EMPTY_AUXILIARY: StreamRenderAuxiliary = {
|
|
pendingPermissions: null,
|
|
turnFooter: null,
|
|
};
|
|
|
|
const orderedTailCache = new WeakMap<StreamItem[], Map<string, StreamItem[]>>();
|
|
const orderedHeadCache = new WeakMap<StreamItem[], Map<string, StreamItem[]>>();
|
|
const splitHistoryCache = new WeakMap<
|
|
StreamItem[],
|
|
Map<string, Pick<AgentStreamRenderModel, "history" | "segments">>
|
|
>();
|
|
const turnTimingCache = new WeakMap<
|
|
StreamItem[],
|
|
WeakMap<StreamItem[], Map<string, StreamTurnTiming>>
|
|
>();
|
|
|
|
function getOrderedItems(params: {
|
|
cache: WeakMap<StreamItem[], Map<string, StreamItem[]>>;
|
|
source: StreamItem[];
|
|
cacheKey: string;
|
|
order: (items: StreamItem[]) => StreamItem[];
|
|
}): StreamItem[] {
|
|
const { cache, source, cacheKey, order } = params;
|
|
let cachedByKey = cache.get(source);
|
|
if (!cachedByKey) {
|
|
cachedByKey = new Map();
|
|
cache.set(source, cachedByKey);
|
|
}
|
|
const cached = cachedByKey.get(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const ordered = order(source);
|
|
cachedByKey.set(cacheKey, ordered);
|
|
return ordered;
|
|
}
|
|
|
|
function splitOrderedTail(params: {
|
|
orderedTail: StreamItem[];
|
|
platform: "web" | "native";
|
|
isMobileBreakpoint: boolean;
|
|
}): Pick<AgentStreamRenderModel, "history" | "segments"> {
|
|
const { orderedTail, platform, isMobileBreakpoint } = params;
|
|
const shouldSplitHistory =
|
|
platform === "web" &&
|
|
!isMobileBreakpoint &&
|
|
orderedTail.length > getWebPartialVirtualizationThreshold();
|
|
const cacheKey = `${platform}:${isMobileBreakpoint}:${getMountedRecentStreamItems()}:${shouldSplitHistory}`;
|
|
let cachedByKey = splitHistoryCache.get(orderedTail);
|
|
if (!cachedByKey) {
|
|
cachedByKey = new Map();
|
|
splitHistoryCache.set(orderedTail, cachedByKey);
|
|
}
|
|
const cached = cachedByKey.get(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
if (!shouldSplitHistory) {
|
|
const unsplit = {
|
|
history: orderedTail,
|
|
segments: {
|
|
historyVirtualized: EMPTY_STREAM_ITEMS,
|
|
historyMounted: orderedTail,
|
|
liveHead: EMPTY_STREAM_ITEMS,
|
|
},
|
|
} satisfies Pick<AgentStreamRenderModel, "history" | "segments">;
|
|
cachedByKey.set(cacheKey, unsplit);
|
|
return unsplit;
|
|
}
|
|
|
|
const mountedWindowStart = findMountedWindowStart({
|
|
items: orderedTail,
|
|
minMountedCount: getMountedRecentStreamItems(),
|
|
});
|
|
const split = {
|
|
history: orderedTail,
|
|
segments: {
|
|
historyVirtualized: orderedTail.slice(0, mountedWindowStart),
|
|
historyMounted: orderedTail.slice(mountedWindowStart),
|
|
liveHead: EMPTY_STREAM_ITEMS,
|
|
},
|
|
} satisfies Pick<AgentStreamRenderModel, "history" | "segments">;
|
|
cachedByKey.set(cacheKey, split);
|
|
return split;
|
|
}
|
|
|
|
function getTurnTiming(params: {
|
|
isTurnActive: boolean;
|
|
activeTurnStartedAt: Date | null;
|
|
tail: StreamItem[];
|
|
head: StreamItem[];
|
|
}): StreamTurnTiming {
|
|
let cachedByHead = turnTimingCache.get(params.tail);
|
|
if (!cachedByHead) {
|
|
cachedByHead = new WeakMap();
|
|
turnTimingCache.set(params.tail, cachedByHead);
|
|
}
|
|
let cachedByActivity = cachedByHead.get(params.head);
|
|
if (!cachedByActivity) {
|
|
cachedByActivity = new Map();
|
|
cachedByHead.set(params.head, cachedByActivity);
|
|
}
|
|
const activityKey = `${params.isTurnActive}:${params.activeTurnStartedAt?.getTime() ?? "none"}`;
|
|
const cached = cachedByActivity.get(activityKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const timing = deriveStreamTurnTiming(params);
|
|
cachedByActivity.set(activityKey, timing);
|
|
return timing;
|
|
}
|
|
|
|
export function buildAgentStreamRenderModel(
|
|
input: BuildAgentStreamRenderModelInput,
|
|
): AgentStreamRenderModel {
|
|
const strategy = resolveStreamRenderStrategy({
|
|
platform: input.platform === "web" ? "web" : "native",
|
|
isMobileBreakpoint: input.isMobileBreakpoint,
|
|
});
|
|
const orderingCacheKey = `${input.platform}:${input.isMobileBreakpoint}`;
|
|
const renderedTail = input.historyStart ? input.tail.slice(input.historyStart) : input.tail;
|
|
const orderedTail = getOrderedItems({
|
|
cache: orderedTailCache,
|
|
source: renderedTail,
|
|
cacheKey: orderingCacheKey,
|
|
order: (items) =>
|
|
orderTailForStreamRenderStrategy({
|
|
strategy,
|
|
streamItems: items,
|
|
}),
|
|
});
|
|
const orderedHead = getOrderedItems({
|
|
cache: orderedHeadCache,
|
|
source: input.head,
|
|
cacheKey: orderingCacheKey,
|
|
order: (items) =>
|
|
orderHeadForStreamRenderStrategy({
|
|
strategy,
|
|
streamHead: items,
|
|
}),
|
|
});
|
|
const splitHistory = splitOrderedTail({
|
|
orderedTail,
|
|
platform: input.platform,
|
|
isMobileBreakpoint: input.isMobileBreakpoint,
|
|
});
|
|
const turnTiming = getTurnTiming({
|
|
isTurnActive: input.isTurnActive,
|
|
activeTurnStartedAt: input.activeTurnStartedAt,
|
|
tail: renderedTail,
|
|
head: input.head,
|
|
});
|
|
|
|
return {
|
|
history: splitHistory.history,
|
|
segments: {
|
|
...splitHistory.segments,
|
|
liveHead: orderedHead,
|
|
},
|
|
turnTiming,
|
|
boundary: {
|
|
hasVirtualizedHistory: splitHistory.segments.historyVirtualized.length > 0,
|
|
hasMountedHistory: splitHistory.segments.historyMounted.length > 0,
|
|
hasLiveHead: orderedHead.length > 0,
|
|
},
|
|
auxiliary: EMPTY_AUXILIARY,
|
|
};
|
|
}
|