Keep older chat history and image previews stable (#2490)
* fix(app): keep timeline history and previews stable Treat persisted timeline replicas as display-only so authoritative pagination always comes from the daemon. Require renewed user intent between older-history pages unless the viewport genuinely remains at the history edge. Model assistant image acquisition as loading, loaded, or failed so recreating a preview URL cannot be rendered as an error. * fix(app): keep file image previews current * fix(app): stabilize history pagination edges * fix(app): recover transient timeline loads * fix(app): retain assistant image previews * Stabilize history pagination lifecycle * Stabilize history settlement and image previews * Retry file images after reconnect * Keep active previews and pagination requests alive * Replace image hook tests with typed ports * Integrate timeline hydration with submission authority * Harden pre-hydration timeline reconciliation * Make rewind E2E use real scroll intent * Preserve live timeline state through hydration * Keep mounted image attachments retained * Protect preview persistence and lifecycle hydration * Preserve idless live assistant continuations * Preserve timeline rows across delayed hydration * test(app): cover real assistant image files * Preserve assistant tool ordering during hydration
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
import {
|
||||
normalizeEmptyProjectDescriptor,
|
||||
normalizeWorkspaceDescriptor,
|
||||
selectAgentTimelineState,
|
||||
useSessionStore,
|
||||
type Agent,
|
||||
type SessionReplica,
|
||||
@@ -37,14 +38,6 @@ const StoredAgentSchema = z.object({
|
||||
const StoredTimelineSchema = z.object({
|
||||
agentId: z.string(),
|
||||
items: z.unknown(),
|
||||
cursor: z
|
||||
.object({
|
||||
epoch: z.string(),
|
||||
startSeq: z.number().int().nonnegative(),
|
||||
endSeq: z.number().int().nonnegative(),
|
||||
})
|
||||
.nullable(),
|
||||
hasOlder: z.boolean(),
|
||||
});
|
||||
|
||||
const StoredHostSchema = z.object({
|
||||
@@ -141,8 +134,6 @@ function deserializeTimeline(stored: StoredHost["timeline"]): SessionReplica["ti
|
||||
return {
|
||||
agentId: stored.agentId,
|
||||
items: decoded,
|
||||
cursor: stored.cursor,
|
||||
hasOlder: stored.hasOlder,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -376,24 +367,24 @@ export class ReplicaCache {
|
||||
focusedAgentId ? session.messageSubmissions.get(focusedAgentId) : undefined,
|
||||
),
|
||||
);
|
||||
const items = focusedAgentId
|
||||
? session.agentStreamTail
|
||||
.get(focusedAgentId)
|
||||
?.filter(
|
||||
const timelineState = focusedAgentId
|
||||
? selectAgentTimelineState(session, focusedAgentId)
|
||||
: { status: "cold" as const };
|
||||
const items =
|
||||
timelineState.status === "cold"
|
||||
? undefined
|
||||
: timelineState.items.filter(
|
||||
(item) =>
|
||||
item.kind !== "user_message" ||
|
||||
item.messageId !== undefined ||
|
||||
!item.clientMessageId ||
|
||||
!localSubmissionIds.has(item.clientMessageId),
|
||||
)
|
||||
: undefined;
|
||||
);
|
||||
const timeline =
|
||||
focusedAgent && items
|
||||
? {
|
||||
agentId: focusedAgent.id,
|
||||
items: encodeDates(items.slice(-MAX_TIMELINE_ITEMS)),
|
||||
cursor: session.agentTimelineCursor.get(focusedAgent.id) ?? null,
|
||||
hasOlder: session.agentTimelineHasOlder.get(focusedAgent.id) ?? false,
|
||||
}
|
||||
: null;
|
||||
const stored: StoredHost = {
|
||||
|
||||
Reference in New Issue
Block a user