diff --git a/apps/web/src/components/Sidebar.browser.test.tsx b/apps/web/src/components/Sidebar.browser.test.tsx
index 875a2225d70a1..378df7cb97e5c 100644
--- a/apps/web/src/components/Sidebar.browser.test.tsx
+++ b/apps/web/src/components/Sidebar.browser.test.tsx
@@ -1,4 +1,10 @@
-import { DEFAULT_CLIENT_SETTINGS, EnvironmentId, ProjectId } from "@t3tools/contracts";
+import {
+ DEFAULT_CLIENT_SETTINGS,
+ EnvironmentId,
+ ProjectId,
+ ProviderInstanceId,
+ ThreadId,
+} from "@t3tools/contracts";
import { describe, expect, it, vi } from "vite-plus/test";
import { userEvent } from "vite-plus/test/browser";
@@ -6,12 +12,15 @@ import { render } from "../../test/render";
import { openCommandPalette } from "../commandPaletteBus";
import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { __setClientSettingsForTests, getClientSettings } from "../hooks/useSettings";
-import { useProjects } from "../state/entities";
+import { useProjects, useQuietThreadShells } from "../state/entities";
+import { useEnvironmentConnectionSummaries } from "../state/environments";
+import { makeThreadFixture } from "../test-fixtures";
import type { Project } from "../types";
import Sidebar from "./Sidebar";
import { SidebarProvider } from "./ui/sidebar";
vi.mock("../state/entities", { spy: true });
+vi.mock("../state/environments", { spy: true });
vi.mock("../hooks/useHandleNewThread", { spy: true });
vi.mock("../commandPaletteBus", () => ({
openCommandPalette: vi.fn(),
@@ -87,3 +96,93 @@ describe("sidebar header new thread", () => {
}
});
});
+
+describe("Working shelf connections", () => {
+ it("keeps running and waiting threads shelved through reconnects and resubscription", async () => {
+ using stack = new DisposableStack();
+ const previousSettings = getClientSettings();
+ stack.defer(() => {
+ __setClientSettingsForTests(previousSettings);
+ vi.mocked(useQuietThreadShells).mockRestore();
+ vi.mocked(useEnvironmentConnectionSummaries).mockRestore();
+ });
+ __setClientSettingsForTests({
+ ...DEFAULT_CLIENT_SETTINGS,
+ sidebarWorkingShelfEnabled: true,
+ });
+ const remote = project("remote");
+ const runtime = {
+ status: "running" as const,
+ activeRunId: null,
+ providerInstanceId: ProviderInstanceId.make("codex"),
+ providerName: "Codex",
+ lastError: null,
+ updatedAt: "2026-11-04T10:00:00.000Z",
+ };
+ const thread = (id: string) =>
+ makeThreadFixture({
+ id: ThreadId.make(id),
+ title: id,
+ projectId: remote.id,
+ environmentId: remote.environmentId,
+ });
+ vi.mocked(useProjects).mockReturnValue([remote]);
+ vi.mocked(useQuietThreadShells).mockReturnValue([
+ { ...thread("Running remotely"), runtime },
+ {
+ ...thread("Waiting remotely"),
+ runtime: { ...runtime, status: "idle" },
+ pendingBackgroundTasks: [
+ { taskId: "background", description: "Watch build", kind: "monitor", awaited: true },
+ ],
+ },
+ thread("Ready for input"),
+ ]);
+ vi.mocked(useHandleNewThread).mockReturnValue({
+ activeDraftThread: null,
+ activeThread: null,
+ defaultProjectRef: null,
+ handleNewThread: vi.fn(),
+ routeDraftId: null,
+ routeThreadRef: null,
+ });
+ const connection = {
+ environmentId: remote.environmentId,
+ environmentLabel: "Remote",
+ displayUrl: "https://remote.example",
+ isRelayManaged: true,
+ isEnabled: true,
+ connectionError: null,
+ connectionErrorTraceId: null,
+ };
+ vi.mocked(useEnvironmentConnectionSummaries).mockReturnValue([
+ { ...connection, connectionState: "connecting" },
+ ]);
+ const sidebar = () => (
+
+
+
+ );
+ await using screen = await render(sidebar());
+ await expect.element(screen.getByRole("button", { name: /^Working\s*2$/ })).toBeInTheDocument();
+
+ for (const connectionState of ["reconnecting", "connected", "offline", "connecting"] as const) {
+ vi.mocked(useEnvironmentConnectionSummaries).mockReturnValue([
+ { ...connection, connectionState },
+ ]);
+ await screen.rerender(sidebar());
+ await expect
+ .element(screen.getByRole("button", { name: /^Working\s*2$/ }))
+ .toBeInTheDocument();
+ await expect
+ .element(screen.getByRole("button", { name: /^Running remotely,/ }))
+ .not.toBeInTheDocument();
+ await expect
+ .element(screen.getByRole("button", { name: /^Waiting remotely,/ }))
+ .not.toBeInTheDocument();
+ await expect
+ .element(screen.getByRole("button", { name: "Ready for input, remote", exact: true }))
+ .toBeInTheDocument();
+ }
+ });
+});
diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts
index 3e2e8be2ce18d..8a6a05c9975a1 100644
--- a/apps/web/src/components/Sidebar.logic.test.ts
+++ b/apps/web/src/components/Sidebar.logic.test.ts
@@ -11,8 +11,6 @@ import {
getFallbackThreadIdAfterDelete,
hasUnseenCompletion,
resolveSidebarRowThreadId,
- belongsInSidebarWorkingShelf,
- isSidebarEnvironmentDisconnected,
orderItemsByPreferredIds,
pinOrderKeyBetween,
resolveSidebarSweepKeys,
@@ -1004,38 +1002,6 @@ describe("Working shelf (beta)", () => {
).toBe(false);
});
- it.each([
- ["connected", true, false],
- ["connected", false, true],
- ["connecting", false, false],
- ["reconnecting", false, true],
- ["offline", false, true],
- ["error", false, true],
- ["unsupported", false, true],
- ["available", false, true],
- ] as const)(
- "lists threads outside the shelf on a %s environment: %s",
- (phase, shellIsLive, disconnected) => {
- expect(isSidebarEnvironmentDisconnected(phase, shellIsLive)).toBe(disconnected);
- },
- );
-
- it("lists a running thread outside the shelf while its environment is disconnected", () => {
- const thread = { ...idle, runtime, environmentId: localEnvironmentId };
- const shelve = (disconnected: ReadonlyArray, enabled = true) =>
- belongsInSidebarWorkingShelf(thread, {
- enabled,
- disconnectedEnvironmentIds: new Set(disconnected),
- isWorking: isSidebarThreadWorking,
- });
-
- expect(shelve([])).toBe(true);
- expect(shelve([localEnvironmentId])).toBe(false);
- expect(shelve([])).toBe(true);
- expect(shelve([EnvironmentId.make("environment-other")])).toBe(true);
- expect(shelve([], false)).toBe(false);
- });
-
describe("sortInboxThreadsByReturn", () => {
const thread = (
id: string,
diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts
index c20789e2a9160..6acb1438778ca 100644
--- a/apps/web/src/components/Sidebar.logic.ts
+++ b/apps/web/src/components/Sidebar.logic.ts
@@ -16,7 +16,6 @@ import {
type ThreadSearchShell,
} from "@t3tools/client-runtime/state/thread-search";
import { foldSearchQuery, rankSearchItems, scoreThreadSearch } from "@t3tools/shared/paletteSearch";
-import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
import {
isListedThreadLineage,
type ContextMenuItem,
@@ -185,29 +184,6 @@ export function resolveSidebarThreadSection(input: {
return "active";
}
-// Keep cached placement on the first connection attempt, matching mobile's threadRouteIsHydrating.
-export function isSidebarEnvironmentDisconnected(
- phase: EnvironmentConnectionPhase,
- shellIsLive: boolean,
-): boolean {
- return phase !== "connecting" && (phase !== "connected" || !shellIsLive);
-}
-
-export function belongsInSidebarWorkingShelf(
- thread: T,
- input: {
- readonly enabled: boolean;
- readonly disconnectedEnvironmentIds: ReadonlySet;
- readonly isWorking: (thread: T) => boolean;
- },
-): boolean {
- return (
- input.enabled &&
- !input.disconnectedEnvironmentIds.has(thread.environmentId) &&
- input.isWorking(thread)
- );
-}
-
const SIDEBAR_MARKER_PREFIX = "sidebar-marker-";
export type SidebarListMarker =
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
index 18aa27709e4c0..a10183a32d8f0 100644
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -1,4 +1,3 @@
-import { liveShellEnvironmentIdsAtom } from "../state/shell";
import { attachSidebarTree } from "../lib/sidebarTree";
import { type EnvironmentId, type ScopedProjectRef } from "@t3tools/contracts";
import type { EnvironmentServerConfig } from "@t3tools/client-runtime/connection";
@@ -140,7 +139,6 @@ import { useNowMinute } from "../hooks/useNowMinute";
import {
useEnvironmentIdentities,
useConnectedEnvironmentIds,
- useEnvironmentConnectionSummaries,
useEnvironmentMachines,
usePrimaryEnvironmentId,
} from "../state/environments";
@@ -187,8 +185,6 @@ import {
deleteSelectedThreadEntries,
filterSidebarProjectScopeItems,
firstValidTimestampMs,
- belongsInSidebarWorkingShelf,
- isSidebarEnvironmentDisconnected,
isSidebarNestedLinkClick,
isTrailingDoubleClick,
orderItemsByPreferredIds,
@@ -356,7 +352,6 @@ interface SidebarThreadPartitionInput {
readonly workingShelfEnabled: boolean;
/** Boring mode: folders list active threads by the user's latest message. */
readonly sortActiveBySend: boolean;
- readonly disconnectedEnvironmentIds: ReadonlySet;
// Snooze wakes, the queued-turn grace and the Working shelf's exit grace depend
// on the clock, so the minute tick, the precise snooze-wake tick and the inbox
// tracker's expiry version are inputs: a new key repartitions.
@@ -442,18 +437,6 @@ function useSidebarThreads(input: {
// Folders already separate the work, so boring mode has no Working shelf.
const workingShelfEnabled =
useClientSettings((s) => s.sidebarWorkingShelfEnabled) && folderGroups === null;
- const environmentConnections = useEnvironmentConnectionSummaries();
- const liveShellEnvironmentIds = useAtomValue(liveShellEnvironmentIdsAtom);
- const disconnectedEnvironmentIds = new Set(
- environmentConnections
- .filter((environment) =>
- isSidebarEnvironmentDisconnected(
- environment.connectionState,
- liveShellEnvironmentIds.has(environment.environmentId),
- ),
- )
- .map((environment) => environment.environmentId),
- );
const inboxReturnsVersion = useSyncExternalStore(
inboxReturns.subscribe,
inboxReturns.getSnapshot,
@@ -498,7 +481,6 @@ function useSidebarThreads(input: {
optimisticDrop,
workingShelfEnabled,
sortActiveBySend: folderGroups !== null,
- disconnectedEnvironmentIds,
clockKey: `${nowMinute}:${snoozeWakeClock}:${inboxReturnsVersion}`,
});
@@ -634,7 +616,6 @@ function partitionSidebarThreads(input: SidebarThreadPartitionInput) {
optimisticDrop,
workingShelfEnabled,
sortActiveBySend,
- disconnectedEnvironmentIds,
} = input;
const preciseNow = new Date().toISOString();
const visible = filterSidebarV2VisibleThreads(threads, scopedProjectKeys, delegatedUnderParent);
@@ -643,11 +624,8 @@ function partitionSidebarThreads(input: SidebarThreadPartitionInput) {
const active: EnvironmentThreadShell[] = [];
const working: EnvironmentThreadShell[] = [];
const inbox = (thread: EnvironmentThreadShell, manuallyMoved = false) =>
- belongsInSidebarWorkingShelf(thread, {
- enabled: workingShelfEnabled,
- disconnectedEnvironmentIds,
- isWorking: manuallyMoved ? isThreadWorking : inboxObservation.isWorking,
- })
+ workingShelfEnabled &&
+ (manuallyMoved ? isThreadWorking(thread) : inboxObservation.isWorking(thread))
? working
: active;
const snoozed: EnvironmentThreadShell[] = [];