Skip to content
Open
34 changes: 34 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
getFallbackThreadIdAfterDelete,
hasUnseenCompletion,
resolveSidebarRowThreadId,
belongsInSidebarWorkingShelf,
isSidebarEnvironmentDisconnected,
orderItemsByPreferredIds,
pinOrderKeyBetween,
resolveProjectStatusIndicator,
Expand Down Expand Up @@ -1089,6 +1091,38 @@ 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<EnvironmentId>, 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,
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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,
Expand Down Expand Up @@ -184,6 +185,29 @@ 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<T extends { readonly environmentId: EnvironmentId }>(
thread: T,
input: {
readonly enabled: boolean;
readonly disconnectedEnvironmentIds: ReadonlySet<EnvironmentId>;
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 =
Expand Down
26 changes: 24 additions & 2 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { liveShellEnvironmentIdsAtom } from "../state/shell";
import { attachSidebarTree, SIDEBAR_TREE_PROPS } from "../lib/sidebarTree";
import { type EnvironmentId, type ScopedProjectRef } from "@t3tools/contracts";
import type { EnvironmentServerConfig } from "@t3tools/client-runtime/connection";
Expand Down Expand Up @@ -138,6 +139,7 @@ import { useNowMinute } from "../hooks/useNowMinute";
import {
useEnvironmentIdentities,
useConnectedEnvironmentIds,
useEnvironmentConnectionSummaries,
useEnvironmentMachines,
usePrimaryEnvironmentId,
} from "../state/environments";
Expand Down Expand Up @@ -184,6 +186,8 @@ import {
deleteSelectedThreadEntries,
filterSidebarProjectScopeItems,
firstValidTimestampMs,
belongsInSidebarWorkingShelf,
isSidebarEnvironmentDisconnected,
isSidebarNestedLinkClick,
isTrailingDoubleClick,
orderItemsByPreferredIds,
Expand Down Expand Up @@ -351,6 +355,7 @@ interface SidebarThreadPartitionInput {
readonly workingShelfEnabled: boolean;
/** Boring mode: folders list active threads by the user's latest message. */
readonly sortActiveBySend: boolean;
readonly disconnectedEnvironmentIds: ReadonlySet<EnvironmentId>;
// 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.
Expand Down Expand Up @@ -432,6 +437,18 @@ 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,
Expand Down Expand Up @@ -476,6 +493,7 @@ function useSidebarThreads(input: {
optimisticDrop,
workingShelfEnabled,
sortActiveBySend: folderGroups !== null,
disconnectedEnvironmentIds,
clockKey: `${nowMinute}:${snoozeWakeClock}:${inboxReturnsVersion}`,
});

Expand Down Expand Up @@ -611,6 +629,7 @@ function partitionSidebarThreads(input: SidebarThreadPartitionInput) {
optimisticDrop,
workingShelfEnabled,
sortActiveBySend,
disconnectedEnvironmentIds,
} = input;
const preciseNow = new Date().toISOString();
const visible = filterSidebarV2VisibleThreads(threads, scopedProjectKeys, delegatedUnderParent);
Expand All @@ -619,8 +638,11 @@ function partitionSidebarThreads(input: SidebarThreadPartitionInput) {
const active: EnvironmentThreadShell[] = [];
const working: EnvironmentThreadShell[] = [];
const inbox = (thread: EnvironmentThreadShell, manuallyMoved = false) =>
workingShelfEnabled &&
(manuallyMoved ? isThreadWorking(thread) : inboxObservation.isWorking(thread))
belongsInSidebarWorkingShelf(thread, {
enabled: workingShelfEnabled,
disconnectedEnvironmentIds,
isWorking: manuallyMoved ? isThreadWorking : inboxObservation.isWorking,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
? working
: active;
const snoozed: EnvironmentThreadShell[] = [];
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/state/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,7 @@ export function useEnvironmentMachines() {
export function useConnectedEnvironmentIds() {
return useAtomValue(environmentSummaries.connectedEnvironmentIdsAtom);
}

export function useEnvironmentConnectionSummaries() {
return useAtomValue(environmentSummaries.environmentsAtom);
}
Loading