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
@@ -1,10 +1,7 @@
import { useCallback, useEffect, useMemo } from "react";
import equal from "fast-deep-equal";
import { shallow } from "zustand/shallow";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
import { selectWorkspace, workspaceEqualityFns } from "@/stores/session-store-hooks/selectors";
import { useHostProjects } from "@/projects/host-projects";
import { fetchAllWorkspaceDescriptors } from "@/projects/workspace-fetching";
import { getHostRuntimeStore, useHostRegistryLoaded, useHosts } from "@/runtime/host-runtime";
@@ -14,7 +11,6 @@ import { shouldSuppressWorkspaceForLocalArchive } from "@/contexts/session-works
import {
buildSidebarWorkspacePlacementModel,
computeSidebarOrderUpdates,
createSidebarWorkspaceEntry,
deriveSidebarLoadingState,
type SidebarProjectEntry,
type SidebarWorkspaceEntry,
@@ -26,10 +22,9 @@ export {
applyStoredOrdering,
buildSidebarProjectsFromHostProjects,
buildSidebarProjectsFromStructure,
buildSidebarStatusWorkspacePlacements,
createSidebarWorkspaceEntry,
buildSidebarWorkspacePlacementModel,
computeSidebarOrderUpdates,
createSidebarWorkspaceEntry,
deriveSidebarLoadingState,
shouldShowSidebarHostLabels,
type SidebarLoadingState,
@@ -42,39 +37,6 @@ export {
type SidebarWorkspaceEntry,
} from "./sidebar-workspaces-view-model";
export function useSidebarWorkspaceEntry(
serverId: string | null,
workspaceId: string | null,
): SidebarWorkspaceEntry | null {
// Deep-compare so that adding/removing unrelated pending creates doesn't re-render this row.
const pendingCreateAttempts = useStoreWithEqualityFn(
useCreateFlowStore,
(state) => state.pendingByDraftId,
workspaceEqualityFns.deep,
);
// Single subscription: reads workspace + agents together, computes the full entry, and
// deep-compares the output. Agents-Map identity churn (setAgents replaces the Map on every
// status transition) never causes a React re-render unless the derived entry actually changes.
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
const workspace = selectWorkspace(state, serverId, workspaceId);
if (!workspace) return null;
const workspaceAgentActivity = serverId
? state.sessions[serverId]?.workspaceAgentActivity
: undefined;
return createSidebarWorkspaceEntry({
serverId: serverId ?? "",
workspace,
pendingCreateAttempts,
workspaceAgentActivity,
});
},
equal,
);
}
const EMPTY_ORDER: string[] = [];
const EMPTY_PROJECTS: SidebarProjectEntry[] = [];
const EMPTY_WORKSPACES: SidebarWorkspacePlacement[] = [];