From 620edafed7345e4586e6d6780a17cf8713ad0260 Mon Sep 17 00:00:00 2001
From: Alex Southwell <4596216+saphid@users.noreply.github.com>
Date: Mon, 26 Oct 2026 15:49:44 +1100
Subject: [PATCH 1/2] fix(mobile): release hidden sidebar PR summary demand
---
.../threads/ThreadNavigationSidebar.tsx | 14 +-
apps/mobile/src/state/thread-pr-demand.tsx | 16 +
.../src/state/use-thread-pr.demand.test.tsx | 524 ++++++++++++++++++
apps/mobile/src/state/use-thread-pr.ts | 6 +-
apps/mobile/vite.config.ts | 1 +
5 files changed, 555 insertions(+), 6 deletions(-)
create mode 100644 apps/mobile/src/state/thread-pr-demand.tsx
create mode 100644 apps/mobile/src/state/use-thread-pr.demand.test.tsx
diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
index 1e3e5a03891a4..a5d9ea6c944ae 100644
--- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
+++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
@@ -68,6 +68,7 @@ import {
} from "./thread-list-v2-items";
import { useThreadRowProviderInstanceResolver } from "./thread-provider-instance";
import type { ThreadPrPresentation } from "../../state/thread-pr-presentation";
+import { ThreadPrDemandProvider } from "../../state/thread-pr-demand";
import {
isThreadListV2ListItem,
threadListV2ListItemsAreEqual,
@@ -109,10 +110,15 @@ interface ThreadNavigationSidebarProps {
}
export function ThreadNavigationSidebar(props: ThreadNavigationSidebarProps) {
- if (Platform.OS !== "ios") {
- return ;
- }
- return ;
+ return (
+
+ {Platform.OS !== "ios" ? (
+
+ ) : (
+
+ )}
+
+ );
}
function NativeSidebarContainer(props: ThreadNavigationSidebarProps) {
diff --git a/apps/mobile/src/state/thread-pr-demand.tsx b/apps/mobile/src/state/thread-pr-demand.tsx
new file mode 100644
index 0000000000000..d6e507bd33727
--- /dev/null
+++ b/apps/mobile/src/state/thread-pr-demand.tsx
@@ -0,0 +1,16 @@
+import { createContext, type ReactNode } from "react";
+
+import { useAppActive } from "../lib/appState";
+
+export const ThreadPrDemandContext = createContext(true);
+
+export function ThreadPrDemandProvider(props: {
+ readonly visible: boolean;
+ readonly children: ReactNode;
+}) {
+ const active = useAppActive();
+ // A sidebar can stay visible beside a focused detail route, so route focus cannot gate its reads.
+ return (
+ {props.children}
+ );
+}
diff --git a/apps/mobile/src/state/use-thread-pr.demand.test.tsx b/apps/mobile/src/state/use-thread-pr.demand.test.tsx
new file mode 100644
index 0000000000000..03a1e654ff050
--- /dev/null
+++ b/apps/mobile/src/state/use-thread-pr.demand.test.tsx
@@ -0,0 +1,524 @@
+import { RegistryContext } from "@effect/atom-react";
+import {
+ EnvironmentId,
+ ProjectId,
+ ThreadId,
+ WS_METHODS,
+ type PullRequestRef,
+} from "@t3tools/contracts";
+import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
+import {
+ EnvironmentRegistry,
+ EnvironmentSupervisor,
+ AVAILABLE_CONNECTION_STATE,
+ PrimaryConnectionTarget,
+ type SupervisorConnectionState,
+ type PreparedConnection,
+ type ConnectionCatalogEntry,
+} from "@t3tools/client-runtime/connection";
+import type { RpcSession } from "@t3tools/client-runtime/rpc";
+import * as Effect from "effect/Effect";
+import * as Latch from "effect/Latch";
+import * as Option from "effect/Option";
+import * as Stream from "effect/Stream";
+import * as SubscriptionRef from "effect/SubscriptionRef";
+import { AtomRegistry } from "effect/reactivity";
+import { Activity, act, useState } from "react";
+import { createRoot } from "react-dom/client";
+import { expect, it } from "@effect/vitest";
+import { vi } from "vite-plus/test";
+
+const fixture = vi.hoisted(() => ({
+ registry: undefined as AtomRegistry.AtomRegistry | undefined,
+ environment: undefined as EnvironmentRegistry.EnvironmentRegistry["Service"] | undefined,
+ active: "active",
+ listeners: new Set<() => void>(),
+}));
+
+vi.mock("react-native", () => ({
+ AppState: {
+ get currentState() {
+ return fixture.active;
+ },
+ addEventListener: (_event: string, listener: () => void) => {
+ fixture.listeners.add(listener);
+ return { remove: () => fixture.listeners.delete(listener) };
+ },
+ },
+}));
+vi.mock("../connection/runtime", async () => {
+ const { Atom } = await import("effect/reactivity");
+ const Layer = await import("effect/Layer");
+ const { EnvironmentRegistry } = await import("@t3tools/client-runtime/connection");
+ return {
+ connectionAtomRuntime: Atom.runtime(
+ Layer.suspend(() =>
+ Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, fixture.environment!),
+ ),
+ ),
+ };
+});
+vi.mock("./atom-registry", () => ({
+ get appAtomRegistry() {
+ return fixture.registry!;
+ },
+}));
+vi.mock("./server", async () => {
+ const { Atom } = await import("effect/reactivity");
+ const config = Atom.make({ environment: { capabilities: { threadPullRequests: true } } });
+ return { serverEnvironment: { configValueAtom: () => config } };
+});
+
+import { ThreadPrDemandProvider } from "./thread-pr-demand";
+import { useThreadPr } from "./use-thread-pr";
+import { deriveLayout, deriveWorkspacePaneLayout, isPrimarySidebarPinned } from "../lib/layout";
+
+const environmentId = EnvironmentId.make("sidebar-environment");
+const reference = {
+ projectId: ProjectId.make("project"),
+ repository: "acme/app",
+ number: 12,
+ url: "https://github.com/acme/app/pull/12",
+};
+function thread(overrides: Partial = {}): EnvironmentThreadShell {
+ return {
+ environmentId,
+ id: ThreadId.make("thread"),
+ pullRequests: [],
+ linkedPullRequest: null,
+ branchPullRequest: reference,
+ ...overrides,
+ } as EnvironmentThreadShell;
+}
+
+const setup = Effect.fn("TestThreadPr.setup")(function* () {
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+ vi.useFakeTimers({
+ toFake: ["Date", "setTimeout", "clearTimeout", "setInterval", "clearInterval"],
+ });
+ vi.setSystemTime(0);
+ fixture.active = "active";
+ fixture.listeners.clear();
+ const requests: Array<{ environmentId: EnvironmentId; input: PullRequestRef }> = [];
+ let state = "open";
+ let replyGate: Latch.Latch | null = null;
+ const target = new PrimaryConnectionTarget({
+ environmentId,
+ label: "Sidebar environment",
+ httpBaseUrl: "https://sidebar.test",
+ wsBaseUrl: "wss://sidebar.test",
+ });
+ const supervisor = yield* Effect.gen(function* () {
+ return EnvironmentSupervisor.EnvironmentSupervisor.of({
+ target,
+ state: yield* SubscriptionRef.make({
+ ...AVAILABLE_CONNECTION_STATE,
+ phase: "connected" as const,
+ desired: true,
+ network: "online" as const,
+ attempt: 1,
+ generation: 1,
+ }),
+ session: yield* SubscriptionRef.make(
+ Option.some({
+ client: {
+ [WS_METHODS.pullRequestsSummary]: (input: PullRequestRef) =>
+ Effect.gen(function* () {
+ requests.push({ environmentId: currentEnvironment, input });
+ if (replyGate !== null) yield* replyGate.await;
+ return {
+ ...input,
+ provider: "github",
+ state,
+ title: `Change ${input.number}`,
+ url: `https://github.com/${input.repository}/pull/${input.number}`,
+ baseBranch: "main",
+ headBranch: "feature",
+ isDraft: false,
+ updatedAt: null,
+ };
+ }),
+ [WS_METHODS.pullRequestsSubscribeRefreshes]: () => Stream.never,
+ },
+ ready: Effect.void,
+ } as unknown as RpcSession),
+ ),
+ prepared: yield* SubscriptionRef.make(Option.none()),
+ connect: Effect.void,
+ disconnect: Effect.void,
+ retryNow: Effect.void,
+ });
+ });
+ let currentEnvironment = environmentId;
+ const environment = {
+ entries: yield* SubscriptionRef.make>(
+ new Map(),
+ ),
+ run: (id, effect) => {
+ currentEnvironment = id;
+ return Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor);
+ },
+ followStream: (_id, stream) =>
+ Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
+ stateChanges: () => SubscriptionRef.changes(supervisor.state),
+ } satisfies Pick<
+ EnvironmentRegistry.EnvironmentRegistry["Service"],
+ "entries" | "run" | "followStream" | "stateChanges"
+ >;
+ fixture.environment =
+ environment as unknown as EnvironmentRegistry.EnvironmentRegistry["Service"];
+ const registry = AtomRegistry.make();
+ fixture.registry = registry;
+ const container = document.createElement("div");
+ const root = createRoot(container);
+ let mounts = 0;
+ function Row({ value, name }: { value: EnvironmentThreadShell; name: string }) {
+ const [passive] = useState(() => ++mounts);
+ const pr = useThreadPr(value);
+ return (
+
+ );
+ }
+ return {
+ requests,
+ get mounts() {
+ return mounts;
+ },
+ setState(next: string) {
+ state = next;
+ },
+ holdReplies() {
+ replyGate = Latch.makeUnsafe();
+ },
+ async releaseReplies() {
+ await act(async () => {
+ replyGate?.openUnsafe();
+ replyGate = null;
+ });
+ },
+ badge(name = "sidebar") {
+ return container.querySelector(`output[data-name="${name}"]`)?.textContent;
+ },
+ passive() {
+ return container.querySelector("output")?.getAttribute("data-passive");
+ },
+ async render({
+ visible = true,
+ value = thread(),
+ shared = false,
+ activity = "visible",
+ }: {
+ visible?: boolean;
+ value?: EnvironmentThreadShell;
+ shared?: boolean;
+ activity?: "visible" | "hidden";
+ } = {}) {
+ await act(async () =>
+ root.render(
+
+
+
+
+
+
+
+
+ {shared ?
: null}
+ ,
+ ),
+ );
+ },
+ async advance(ms: number) {
+ await act(async () => vi.advanceTimersByTimeAsync(ms));
+ },
+ async appState(next: string) {
+ await act(async () => {
+ fixture.active = next;
+ for (const listener of fixture.listeners) listener();
+ });
+ },
+ [Symbol.dispose]() {
+ act(() => root.unmount());
+ registry.dispose();
+ expect(fixture.listeners.size).toBe(0);
+ vi.useRealTimers();
+ },
+ };
+});
+
+it.effect.each([
+ {
+ name: "root pins the sidebar despite a closed preference",
+ pathname: "/",
+ preferred: false,
+ inspector: false,
+ visible: true,
+ },
+ {
+ name: "detail route keeps an open split sidebar live",
+ pathname: "/threads/1",
+ preferred: true,
+ inspector: false,
+ visible: true,
+ },
+ {
+ name: "detail route respects a closed sidebar",
+ pathname: "/threads/1",
+ preferred: false,
+ inspector: false,
+ visible: false,
+ },
+ {
+ name: "inspector suppresses a retained sidebar when the split has no room",
+ pathname: "/threads/1",
+ preferred: true,
+ inspector: true,
+ visible: false,
+ },
+])("uses the actual pane visibility: $name", ({ pathname, preferred, inspector, visible }) =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ const layout = deriveLayout({ width: 1024, height: 834 });
+ const pinned = isPrimarySidebarPinned({
+ pathname,
+ nativeWorkspace: false,
+ usesSplitView: layout.usesSplitView,
+ });
+ const panes = deriveWorkspacePaneLayout({
+ layout,
+ viewportWidth: 1024,
+ primarySidebarPreferredVisible: pinned || preferred,
+ auxiliaryPanePreferredVisible: inspector,
+ auxiliaryPaneRole: "inspector",
+ });
+ expect(panes.primarySidebarVisible).toBe(visible);
+ yield* Effect.promise(async () => {
+ await h.render({ visible: panes.primarySidebarVisible });
+ await h.advance(190_000);
+ expect(h.requests).toHaveLength(visible ? 4 : 0);
+ expect(h.badge()).toBe(visible ? "#12 pull request open" : "none");
+ });
+ }),
+);
+
+it.effect(
+ "releases retained hidden sidebar demand and keeps the badge while reveal revalidates",
+ () =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ await h.render();
+ expect(h.badge()).toBe("#12 pull request open");
+ const passive = h.passive();
+ expect(h.requests).toHaveLength(1);
+ await h.render({ visible: false });
+ await h.advance(10_000);
+ const afterGrace = h.requests.length;
+ await h.advance(180_000);
+ expect(h.requests.length - afterGrace).toBe(0);
+ expect(h.passive()).toBe(passive);
+ h.holdReplies();
+ h.setState("closed");
+ await h.render();
+ expect(h.badge()).toBe("#12 pull request open");
+ expect(h.requests.length).toBe(afterGrace + 1);
+ expect(h.mounts).toBe(1);
+ await h.releaseReplies();
+ expect(h.badge()).toBe("#12 pull request closed");
+ });
+ }),
+);
+
+it.effect("does not start a fallback query for a sidebar first mounted hidden", () =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ await h.render({ visible: false });
+ await h.advance(180_000);
+ expect(h.requests).toHaveLength(0);
+ expect(h.badge()).toBe("none");
+ await h.render();
+ expect(h.requests).toHaveLength(1);
+ expect(h.badge()).toBe("#12 pull request open");
+ expect(h.mounts).toBe(1);
+ });
+ }),
+);
+
+it.effect("keeps a shared visible reader polling while its sidebar reader is hidden", () =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ await h.render({ shared: true });
+ expect(h.requests).toHaveLength(1);
+ await h.render({ visible: false, shared: true });
+ await h.advance(180_000);
+ expect(h.requests).toHaveLength(4);
+ expect(h.badge("shared")).toBe("#12 pull request open");
+ h.setState("closed");
+ await h.advance(60_000);
+ expect(h.requests).toHaveLength(5);
+ expect(h.badge("shared")).toBe("#12 pull request closed");
+ await h.render({ shared: true });
+ expect(h.badge()).toBe("#12 pull request closed");
+ });
+ }),
+);
+
+it.effect.each(["inactive", "background"])("releases sidebar demand while the OS is %s", (state) =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ await h.render();
+ await h.appState(state);
+ await h.advance(10_000);
+ const before = h.requests.length;
+ await h.advance(180_000);
+ expect(h.requests.length - before).toBe(0);
+ h.holdReplies();
+ await h.appState("active");
+ expect(h.badge()).toBe("#12 pull request open");
+ expect(h.requests.length - before).toBe(1);
+ expect(h.mounts).toBe(1);
+ });
+ }),
+);
+
+it.effect.each([
+ { field: "environment", changed: { environmentId: EnvironmentId.make("other") } },
+ {
+ field: "project",
+ changed: { branchPullRequest: { ...reference, projectId: ProjectId.make("other") } },
+ },
+ {
+ field: "repository",
+ changed: { branchPullRequest: { ...reference, repository: "acme/other" } },
+ },
+ { field: "PR", changed: { branchPullRequest: { ...reference, number: 13 } } },
+])("does not show an old badge after a hidden $field change", ({ changed }) =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ await h.render();
+ await h.render({ visible: false });
+ await h.advance(10_000);
+ const before = h.requests.length;
+ const value = thread(changed);
+ await h.render({ visible: false, value });
+ expect(h.badge()).toBe("none");
+ await h.advance(180_000);
+ expect(h.requests.length).toBe(before);
+ h.holdReplies();
+ await h.render({ value });
+ expect(h.badge()).toBe("none");
+ expect(h.requests.length).toBe(before + 1);
+ expect(h.requests.at(-1)?.environmentId).toBe(value.environmentId);
+ expect(h.requests.at(-1)?.input).toMatchObject({
+ projectId: value.branchPullRequest!.projectId,
+ repository: value.branchPullRequest!.repository,
+ number: value.branchPullRequest!.number,
+ });
+ await h.releaseReplies();
+ expect(h.badge()).toBe(`#${value.branchPullRequest!.number} pull request open`);
+ expect(h.mounts).toBe(1);
+ });
+ }),
+);
+
+it.effect("clears a removed PR while hidden instead of resurrecting its cached badge", () =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ await h.render();
+ const value = thread({ branchPullRequest: null });
+ await h.render({ visible: false, value });
+ expect(h.badge()).toBe("none");
+ await h.advance(190_000);
+ await h.render({ value });
+ expect(h.requests).toHaveLength(1);
+ expect(h.badge()).toBe("none");
+ });
+ }),
+);
+
+it.effect.each([true, false])(
+ "renders linked presentations with no fallback RPC, snapshot present: %s",
+ (snapshot) =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ const value = thread({
+ pullRequests: [
+ {
+ host: "github.com",
+ repository: "acme/app",
+ number: 14,
+ url: "https://github.com/acme/app/pull/14",
+ source: "manual",
+ linkedAt: "2026-10-26T00:00:00Z",
+ stack: null,
+ snapshot: snapshot
+ ? {
+ title: "Linked",
+ state: "open",
+ isDraft: false,
+ headBranch: "feature",
+ baseBranch: "main",
+ updatedAt: null,
+ syncedAt: "2026-10-26T00:00:00Z",
+ }
+ : null,
+ },
+ ],
+ });
+ await h.render({ value });
+ expect(h.badge()).toBe(`#14 pull request ${snapshot ? "open" : "status pending"}`);
+ await h.render({ value, visible: false });
+ await h.advance(190_000);
+ await h.render({ value });
+ expect(h.requests).toHaveLength(0);
+ expect(h.badge()).toBe(`#14 pull request ${snapshot ? "open" : "status pending"}`);
+ });
+ }),
+);
+
+it.effect("keeps mounted passive state through an Android-style Activity tab switch", () =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ await h.render();
+ const passive = h.passive();
+ await h.render({ activity: "hidden" });
+ await h.advance(10_000);
+ const before = h.requests.length;
+ await h.advance(180_000);
+ expect(h.requests.length - before).toBe(0);
+ h.holdReplies();
+ await h.render();
+ expect(h.badge()).toBe("#12 pull request open");
+ expect(h.passive()).toBe(passive);
+ expect(h.mounts).toBe(1);
+ expect(h.requests.length - before).toBe(1);
+ });
+ }),
+);
+
+it.effect(
+ "allows only the existing idle-grace poll when the sidebar closes just before a tick",
+ () =>
+ Effect.gen(function* () {
+ using h = yield* setup();
+ yield* Effect.promise(async () => {
+ await h.render();
+ await h.advance(59_000);
+ await h.render({ visible: false });
+ await h.advance(10_000);
+ const afterGrace = h.requests.length;
+ expect(afterGrace).toBeLessThanOrEqual(2);
+ await h.advance(180_000);
+ expect(h.requests.length).toBe(afterGrace);
+ });
+ }),
+);
diff --git a/apps/mobile/src/state/use-thread-pr.ts b/apps/mobile/src/state/use-thread-pr.ts
index 566a5f6981f4f..3f61a2e89076e 100644
--- a/apps/mobile/src/state/use-thread-pr.ts
+++ b/apps/mobile/src/state/use-thread-pr.ts
@@ -6,12 +6,13 @@ import {
pullRequestDetailToVcsStatus,
} from "@t3tools/client-runtime/state/pull-requests";
import { Atom } from "effect/reactivity";
-import { useCallback, useEffect, useMemo } from "react";
+import { use, useCallback, useEffect, useMemo } from "react";
import { connectionAtomRuntime } from "../connection/runtime";
import { appAtomRegistry } from "./atom-registry";
import { serverEnvironment } from "./server";
import { useEnvironmentQuery } from "./query";
+import { ThreadPrDemandContext } from "./thread-pr-demand";
import {
resolveThreadPrSource,
presentThreadPr,
@@ -39,6 +40,7 @@ export {
} from "./thread-pr-presentation";
export function useThreadPr(thread: EnvironmentThreadShell): ThreadPrPresentation | null {
+ const demand = use(ThreadPrDemandContext);
const supportsLinks = useAtomValue(
serverEnvironment.configValueAtom(thread.environmentId),
(config) => config?.environment.capabilities.threadPullRequests === true,
@@ -66,7 +68,7 @@ export function useThreadPr(thread: EnvironmentThreadShell): ThreadPrPresentatio
);
const snapshot = snapshotEntry?.identity === snapshotIdentity ? snapshotEntry.presentation : null;
const pullRequestSummary = useEnvironmentQuery(
- pullRequestRef === null
+ pullRequestRef === null || !demand
? null
: pullRequestSummaryAtom({
environmentId: thread.environmentId,
diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts
index 77fffb6cb6117..ebaeba6a1c85c 100644
--- a/apps/mobile/vite.config.ts
+++ b/apps/mobile/vite.config.ts
@@ -2,6 +2,7 @@ import { defineConfig } from "vite-plus";
import "vite-plus/test/config";
const reactTests = [
+ "src/state/use-thread-pr.demand.test.tsx",
"src/features/pull-requests/usePullRequestEnvironmentFallback.test.tsx",
"src/lib/filePreview.test.ts",
"src/features/threads/thread-inspector-navigation.test.tsx",
From 16529822f5c66ecbbfb510ad18f51aefa0c7ee0b Mon Sep 17 00:00:00 2001
From: maria-rcks
Date: Tue, 27 Oct 2026 02:51:12 +0000
Subject: [PATCH 2/2] test(mobile): focus sidebar summary demand coverage
---
.../src/state/use-thread-pr.demand.test.tsx | 179 ------------------
1 file changed, 179 deletions(-)
diff --git a/apps/mobile/src/state/use-thread-pr.demand.test.tsx b/apps/mobile/src/state/use-thread-pr.demand.test.tsx
index 03a1e654ff050..b3e6965531f63 100644
--- a/apps/mobile/src/state/use-thread-pr.demand.test.tsx
+++ b/apps/mobile/src/state/use-thread-pr.demand.test.tsx
@@ -29,7 +29,6 @@ import { expect, it } from "@effect/vitest";
import { vi } from "vite-plus/test";
const fixture = vi.hoisted(() => ({
- registry: undefined as AtomRegistry.AtomRegistry | undefined,
environment: undefined as EnvironmentRegistry.EnvironmentRegistry["Service"] | undefined,
active: "active",
listeners: new Set<() => void>(),
@@ -58,11 +57,6 @@ vi.mock("../connection/runtime", async () => {
),
};
});
-vi.mock("./atom-registry", () => ({
- get appAtomRegistry() {
- return fixture.registry!;
- },
-}));
vi.mock("./server", async () => {
const { Atom } = await import("effect/reactivity");
const config = Atom.make({ environment: { capabilities: { threadPullRequests: true } } });
@@ -71,7 +65,6 @@ vi.mock("./server", async () => {
import { ThreadPrDemandProvider } from "./thread-pr-demand";
import { useThreadPr } from "./use-thread-pr";
-import { deriveLayout, deriveWorkspacePaneLayout, isPrimarySidebarPinned } from "../lib/layout";
const environmentId = EnvironmentId.make("sidebar-environment");
const reference = {
@@ -168,7 +161,6 @@ const setup = Effect.fn("TestThreadPr.setup")(function* () {
fixture.environment =
environment as unknown as EnvironmentRegistry.EnvironmentRegistry["Service"];
const registry = AtomRegistry.make();
- fixture.registry = registry;
const container = document.createElement("div");
const root = createRoot(container);
let mounts = 0;
@@ -248,61 +240,6 @@ const setup = Effect.fn("TestThreadPr.setup")(function* () {
};
});
-it.effect.each([
- {
- name: "root pins the sidebar despite a closed preference",
- pathname: "/",
- preferred: false,
- inspector: false,
- visible: true,
- },
- {
- name: "detail route keeps an open split sidebar live",
- pathname: "/threads/1",
- preferred: true,
- inspector: false,
- visible: true,
- },
- {
- name: "detail route respects a closed sidebar",
- pathname: "/threads/1",
- preferred: false,
- inspector: false,
- visible: false,
- },
- {
- name: "inspector suppresses a retained sidebar when the split has no room",
- pathname: "/threads/1",
- preferred: true,
- inspector: true,
- visible: false,
- },
-])("uses the actual pane visibility: $name", ({ pathname, preferred, inspector, visible }) =>
- Effect.gen(function* () {
- using h = yield* setup();
- const layout = deriveLayout({ width: 1024, height: 834 });
- const pinned = isPrimarySidebarPinned({
- pathname,
- nativeWorkspace: false,
- usesSplitView: layout.usesSplitView,
- });
- const panes = deriveWorkspacePaneLayout({
- layout,
- viewportWidth: 1024,
- primarySidebarPreferredVisible: pinned || preferred,
- auxiliaryPanePreferredVisible: inspector,
- auxiliaryPaneRole: "inspector",
- });
- expect(panes.primarySidebarVisible).toBe(visible);
- yield* Effect.promise(async () => {
- await h.render({ visible: panes.primarySidebarVisible });
- await h.advance(190_000);
- expect(h.requests).toHaveLength(visible ? 4 : 0);
- expect(h.badge()).toBe(visible ? "#12 pull request open" : "none");
- });
- }),
-);
-
it.effect(
"releases retained hidden sidebar demand and keeps the badge while reveal revalidates",
() =>
@@ -386,104 +323,6 @@ it.effect.each(["inactive", "background"])("releases sidebar demand while the OS
}),
);
-it.effect.each([
- { field: "environment", changed: { environmentId: EnvironmentId.make("other") } },
- {
- field: "project",
- changed: { branchPullRequest: { ...reference, projectId: ProjectId.make("other") } },
- },
- {
- field: "repository",
- changed: { branchPullRequest: { ...reference, repository: "acme/other" } },
- },
- { field: "PR", changed: { branchPullRequest: { ...reference, number: 13 } } },
-])("does not show an old badge after a hidden $field change", ({ changed }) =>
- Effect.gen(function* () {
- using h = yield* setup();
- yield* Effect.promise(async () => {
- await h.render();
- await h.render({ visible: false });
- await h.advance(10_000);
- const before = h.requests.length;
- const value = thread(changed);
- await h.render({ visible: false, value });
- expect(h.badge()).toBe("none");
- await h.advance(180_000);
- expect(h.requests.length).toBe(before);
- h.holdReplies();
- await h.render({ value });
- expect(h.badge()).toBe("none");
- expect(h.requests.length).toBe(before + 1);
- expect(h.requests.at(-1)?.environmentId).toBe(value.environmentId);
- expect(h.requests.at(-1)?.input).toMatchObject({
- projectId: value.branchPullRequest!.projectId,
- repository: value.branchPullRequest!.repository,
- number: value.branchPullRequest!.number,
- });
- await h.releaseReplies();
- expect(h.badge()).toBe(`#${value.branchPullRequest!.number} pull request open`);
- expect(h.mounts).toBe(1);
- });
- }),
-);
-
-it.effect("clears a removed PR while hidden instead of resurrecting its cached badge", () =>
- Effect.gen(function* () {
- using h = yield* setup();
- yield* Effect.promise(async () => {
- await h.render();
- const value = thread({ branchPullRequest: null });
- await h.render({ visible: false, value });
- expect(h.badge()).toBe("none");
- await h.advance(190_000);
- await h.render({ value });
- expect(h.requests).toHaveLength(1);
- expect(h.badge()).toBe("none");
- });
- }),
-);
-
-it.effect.each([true, false])(
- "renders linked presentations with no fallback RPC, snapshot present: %s",
- (snapshot) =>
- Effect.gen(function* () {
- using h = yield* setup();
- yield* Effect.promise(async () => {
- const value = thread({
- pullRequests: [
- {
- host: "github.com",
- repository: "acme/app",
- number: 14,
- url: "https://github.com/acme/app/pull/14",
- source: "manual",
- linkedAt: "2026-10-26T00:00:00Z",
- stack: null,
- snapshot: snapshot
- ? {
- title: "Linked",
- state: "open",
- isDraft: false,
- headBranch: "feature",
- baseBranch: "main",
- updatedAt: null,
- syncedAt: "2026-10-26T00:00:00Z",
- }
- : null,
- },
- ],
- });
- await h.render({ value });
- expect(h.badge()).toBe(`#14 pull request ${snapshot ? "open" : "status pending"}`);
- await h.render({ value, visible: false });
- await h.advance(190_000);
- await h.render({ value });
- expect(h.requests).toHaveLength(0);
- expect(h.badge()).toBe(`#14 pull request ${snapshot ? "open" : "status pending"}`);
- });
- }),
-);
-
it.effect("keeps mounted passive state through an Android-style Activity tab switch", () =>
Effect.gen(function* () {
using h = yield* setup();
@@ -504,21 +343,3 @@ it.effect("keeps mounted passive state through an Android-style Activity tab swi
});
}),
);
-
-it.effect(
- "allows only the existing idle-grace poll when the sidebar closes just before a tick",
- () =>
- Effect.gen(function* () {
- using h = yield* setup();
- yield* Effect.promise(async () => {
- await h.render();
- await h.advance(59_000);
- await h.render({ visible: false });
- await h.advance(10_000);
- const afterGrace = h.requests.length;
- expect(afterGrace).toBeLessThanOrEqual(2);
- await h.advance(180_000);
- expect(h.requests.length).toBe(afterGrace);
- });
- }),
-);