Prevent Android chats from freezing or going blank (#1989)

* fix(app): stabilize streamed chat rendering

Use deterministic Markdown AST keys and preserve unique row identities when an assistant message resumes after a tool. Keep native gesture and plan-card children correctly keyed to avoid unsupported Fabric reconciliation paths.

* fix(app): stabilize retained native panels

Keep retained workspace roots in a stable native order and make active
panels present in the selection commit. Hidden panels now stop expensive
subscriptions and animations without losing mounted state.

* fix(app): harden retained panel activity
This commit is contained in:
Mohamed Boudra
2026-07-11 15:39:14 +08:00
committed by GitHub
parent 860fcb2e35
commit 28a27b02d3
40 changed files with 1244 additions and 543 deletions
@@ -0,0 +1,54 @@
import { useMemo, useRef } from "react";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { useSessionStore } from "@/stores/session-store";
import {
areSidebarWorkspaceSessionsEqual,
buildSidebarWorkspaceEntries,
selectSidebarWorkspaceSessions,
type SidebarWorkspaceEntry,
type SidebarWorkspacePlacement,
type SidebarWorkspaceSession,
} from "./sidebar-workspaces-view-model";
const EMPTY_ENTRIES = new Map<string, SidebarWorkspaceEntry>();
const EMPTY_SESSIONS: SidebarWorkspaceSession[] = [];
const EMPTY_PENDING_CREATE_ATTEMPTS: Record<string, never> = {};
export function useSidebarWorkspaceEntries(
placements: readonly SidebarWorkspacePlacement[],
enabled = true,
): ReadonlyMap<string, SidebarWorkspaceEntry> {
const serverIds = useMemo(
() => Array.from(new Set(placements.map((placement) => placement.serverId))),
[placements],
);
const sessions = useStoreWithEqualityFn(
useSessionStore,
(state) =>
enabled ? selectSidebarWorkspaceSessions(state.sessions, serverIds) : EMPTY_SESSIONS,
areSidebarWorkspaceSessionsEqual,
);
const pendingCreateAttempts = useCreateFlowStore((state) =>
enabled ? state.pendingByDraftId : EMPTY_PENDING_CREATE_ATTEMPTS,
);
const previousEntriesRef = useRef<ReadonlyMap<string, SidebarWorkspaceEntry>>(EMPTY_ENTRIES);
// Collection ownership is intentional: retained sidebars have one cheap
// subscription to structurally shared indexes, never one session-store
// subscription per mounted row.
return useMemo(() => {
if (!enabled || placements.length === 0 || sessions.length === 0) {
previousEntriesRef.current = EMPTY_ENTRIES;
return EMPTY_ENTRIES;
}
const entries = buildSidebarWorkspaceEntries({
placements,
sessions,
pendingCreateAttempts,
previousEntries: previousEntriesRef.current,
});
previousEntriesRef.current = entries;
return entries;
}, [enabled, pendingCreateAttempts, placements, sessions]);
}