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
@@ -4,7 +4,7 @@ import type { WorkspaceStructureProject } from "@/projects/workspace-structure";
import {
appendMissingOrderKeys,
applyStoredOrdering,
buildSidebarStatusWorkspacePlacements,
buildSidebarWorkspaceEntries,
buildSidebarWorkspacePlacementModel,
buildSidebarProjectsFromStructure,
computeSidebarOrderUpdates,
@@ -226,7 +226,7 @@ describe("shared sidebar workspace model", () => {
}),
],
});
const statusRows = buildSidebarStatusWorkspacePlacements({
const workspaceEntries = buildSidebarWorkspaceEntries({
placements: model.workspaces,
sessions: [
{
@@ -290,14 +290,66 @@ describe("shared sidebar workspace model", () => {
],
}),
]);
expect(statusRows.map((entry) => [entry.workspaceKey, entry.statusBucket, entry.name])).toEqual(
[
["host-a:main", "done", "main"],
["host-b:feature", "running", "feature/status-flow"],
],
);
expect(
Array.from(workspaceEntries.values()).map((entry) => [
entry.workspaceKey,
entry.statusBucket,
entry.name,
]),
).toEqual([
["host-a:main", "done", "main"],
["host-b:feature", "running", "feature/status-flow"],
]);
expect(model.projectNamesByKey).toEqual(new Map([["getpaseo/paseo", "getpaseo/paseo"]]));
});
it("preserves unchanged row identities when another workspace updates", () => {
const model = buildSidebarWorkspacePlacementModel({
projects: [project({ projectKey: "project", workspaceKeys: ["srv:one", "srv:two"] })],
});
const one = workspace({
id: "one",
name: "one",
projectId: "project",
projectDisplayName: "project",
});
const two = workspace({
id: "two",
name: "two",
projectId: "project",
projectDisplayName: "project",
});
const previousEntries = buildSidebarWorkspaceEntries({
placements: model.workspaces,
sessions: [
{
serverId: "srv",
workspaceAgentActivity: new Map(),
workspaces: new Map([
["one", one],
["two", two],
]),
},
],
});
const nextEntries = buildSidebarWorkspaceEntries({
placements: model.workspaces,
sessions: [
{
serverId: "srv",
workspaceAgentActivity: new Map(),
workspaces: new Map([
["one", one],
["two", { ...two, status: "running" }],
]),
},
],
previousEntries,
});
expect(nextEntries.get("srv:one")).toBe(previousEntries.get("srv:one"));
expect(nextEntries.get("srv:two")).not.toBe(previousEntries.get("srv:two"));
});
});
describe("shouldShowSidebarHostLabels", () => {