fix(app): initialize reconnect policy from current app state

Initialize visibility before host bootstrap so a background mount cannot leave reconnect enabled. Seed sidebar fixtures through the workspace owner so cache hydration preserves their rows.
This commit is contained in:
Mohamed Boudra
2026-09-05 14:18:36 +02:00
parent 54d94c3715
commit 4a14d1bacc
6 changed files with 67 additions and 11 deletions
+2 -4
View File
@@ -69,6 +69,7 @@ import {
resolveStartupNavigationReady,
shouldRunStartupGiveUpTimer,
startHostRuntimeBootstrap,
bindHostRuntimeAppState,
type StartupBlocker,
} from "@/navigation/host-runtime-bootstrap";
import { registerWorkspaceRouteNavigationRef } from "@/navigation/workspace-route-navigation";
@@ -374,10 +375,7 @@ async function shouldStartBuiltInDaemon(): Promise<boolean> {
function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
useEffect(() => {
const store = getHostRuntimeStore();
const subscription = AppState.addEventListener("change", (nextState) => {
store.setAppVisible(nextState === "active");
});
return () => subscription.remove();
return bindHostRuntimeAppState(store, AppState);
}, []);
useEffect(() => {
@@ -38,7 +38,7 @@ import {
} from "@/runtime/host-runtime";
import type { HostProfile } from "@/types/host-connection";
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
import { seedSessionWorkspaces } from "@/test/seed-session";
import { seedRuntimeWorkspaces } from "@/test/seed-session";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import { useWorkspaceFields } from "@/stores/session-store-hooks";
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
@@ -167,7 +167,7 @@ function initializeSidebarState(workspaces: WorkspaceDescriptor[]): void {
act(() => {
setHostProfiles([makeHost()]);
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
seedSessionWorkspaces(SERVER_ID, new Map(workspaces.map((entry) => [entry.id, entry])));
seedRuntimeWorkspaces(SERVER_ID, new Map(workspaces.map((entry) => [entry.id, entry])));
useSessionStore.getState().setHasHydratedWorkspaces(SERVER_ID, true);
useSidebarOrderStore.setState({
projectOrder: ["project-a", "project-b"],
@@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getHostRuntimeStore } from "@/runtime/host-runtime";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
import { seedSessionWorkspaces } from "@/test/seed-session";
import { seedRuntimeWorkspaces } from "@/test/seed-session";
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import { useSidebarViewStore } from "@/stores/sidebar-view-store";
@@ -96,7 +96,7 @@ describe("WorkspaceShortcutTargetsSubscriber", () => {
act(() => {
setHostProfiles([hostProfile()]);
useSessionStore.getState().initializeSession("srv", null as unknown as DaemonClient);
seedSessionWorkspaces(
seedRuntimeWorkspaces(
"srv",
new Map([
["ws-1", workspaceDescriptor({ id: "ws-1", name: "Workspace 1" })],
@@ -142,7 +142,7 @@ describe("WorkspaceShortcutTargetsSubscriber", () => {
it("publishes status-mode shortcut targets in visual status order", async () => {
act(() => {
useSidebarViewStore.getState().setGroupMode("status");
seedSessionWorkspaces(
seedRuntimeWorkspaces(
"srv",
new Map([
[
@@ -214,11 +214,11 @@ describe("WorkspaceShortcutTargetsSubscriber", () => {
setHostProfiles([hostProfile("host-a"), hostProfile("host-b")]);
useSessionStore.getState().initializeSession("host-a", null as unknown as DaemonClient);
useSessionStore.getState().initializeSession("host-b", null as unknown as DaemonClient);
seedSessionWorkspaces(
seedRuntimeWorkspaces(
"host-a",
new Map([["a-1", workspaceDescriptor({ id: "a-1", name: "Host A" })]]),
);
seedSessionWorkspaces(
seedRuntimeWorkspaces(
"host-b",
new Map([["b-1", workspaceDescriptor({ id: "b-1", name: "Host B" })]]),
);
@@ -6,6 +6,7 @@ import {
resolveStartupRoute,
shouldRunStartupGiveUpTimer,
startHostRuntimeBootstrap,
bindHostRuntimeAppState,
} from "./host-runtime-bootstrap";
import type { DaemonStartResult, StartDaemonIfEnabledInput } from "@/runtime/daemon-start-service";
@@ -393,3 +394,34 @@ describe("resolveHostIndexRoute", () => {
).toEqual("/open-project");
});
});
describe("host runtime app lifecycle", () => {
it.each(["inactive", "background"] as const)(
"starts with reconnect disabled when mounted %s",
(currentState) => {
const visibility: boolean[] = [];
let listener: ((state: "active" | "background") => void) | undefined;
let subscribed = true;
const dispose = bindHostRuntimeAppState(
{ setAppVisible: (visible) => visibility.push(visible) },
{
currentState,
addEventListener: (_event, handler) => {
listener = handler;
return {
remove: () => {
subscribed = false;
},
};
},
},
);
expect(visibility).toEqual([false]);
listener?.("active");
listener?.("background");
expect(visibility).toEqual([false, true, false]);
dispose();
expect(subscribed).toBe(false);
},
);
});
@@ -1,3 +1,4 @@
import type { AppState } from "react-native";
import type { ActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import type {
DaemonStartCondition,
@@ -250,3 +251,14 @@ export function resolveStartupRoute(input: ResolveStartupRouteInput): StartupRou
return resolveReadyIndexStartupRoute(input);
}
export function bindHostRuntimeAppState(
store: { setAppVisible: (visible: boolean) => void },
appState: Pick<typeof AppState, "currentState" | "addEventListener">,
): () => void {
const subscription = appState.addEventListener("change", (state) => {
store.setAppVisible(state === "active");
});
store.setAppVisible(appState.currentState === "active");
return () => subscription.remove();
}
+14
View File
@@ -56,3 +56,17 @@ export function seedSessionHosts(serverIds: readonly string[]): void {
})),
);
}
export function seedRuntimeWorkspaces(
serverId: string,
workspaces: Map<string, WorkspaceDescriptor>,
): void {
const runtime = getHostRuntimeStore();
for (const id of useSessionStore.getState().sessions[serverId]?.workspaces.keys() ?? []) {
if (!workspaces.has(id)) runtime.removeWorkspaceSnapshot(serverId, id);
}
for (const workspace of workspaces.values()) {
runtime.acceptProjectSnapshot(serverId, projectFromWorkspace(workspace));
}
runtime.acceptWorkspaceSnapshots(serverId, [...workspaces.values()]);
}