diff --git a/apps/mobile/src/features/home/workspace-connection-status.test.ts b/apps/mobile/src/features/home/workspace-connection-status.test.ts index 3d56719d3fe5..1d7239fc018f 100644 --- a/apps/mobile/src/features/home/workspace-connection-status.test.ts +++ b/apps/mobile/src/features/home/workspace-connection-status.test.ts @@ -15,7 +15,6 @@ function workspaceState(overrides: Partial = {}): WorkspaceState connectionState: "connected", connectionError: null, shellSnapshotError: null, - latestCachedSnapshotReceivedAt: null, networkStatus: "online", ...overrides, }; diff --git a/apps/mobile/src/state/workspace-connection-atoms.test.ts b/apps/mobile/src/state/workspace-connection-atoms.test.ts index a48daf76628d..a46499f8b0ce 100644 --- a/apps/mobile/src/state/workspace-connection-atoms.test.ts +++ b/apps/mobile/src/state/workspace-connection-atoms.test.ts @@ -55,7 +55,6 @@ describe("workspace connection subscriptions", () => { hasCachedShell: false, hasLiveShell: true, firstError: null, - latestSnapshotUpdatedAt: null, }); const broadState = Atom.make((get) => projectWorkspaceState({ @@ -75,7 +74,7 @@ describe("workspace connection subscriptions", () => { for (let index = 0; index < 20; index++) { h.registry.set(shell, { ...h.registry.get(shell), - latestSnapshotUpdatedAt: new Date(index * 1000).toISOString(), + hasSynchronizingShell: index % 2 === 0, }); h.registry.get(broadState); h.registry.get(h.stateAtom); diff --git a/apps/mobile/src/state/workspaceModel.test.ts b/apps/mobile/src/state/workspaceModel.test.ts index a751ca281e4d..60bc6830321f 100644 --- a/apps/mobile/src/state/workspaceModel.test.ts +++ b/apps/mobile/src/state/workspaceModel.test.ts @@ -53,7 +53,6 @@ const EMPTY_SHELL_SUMMARY: EnvironmentShellSummary = { hasCachedShell: false, hasLiveShell: false, firstError: null, - latestSnapshotUpdatedAt: null, }; const CACHED_SHELL_SUMMARY: EnvironmentShellSummary = { @@ -61,7 +60,6 @@ const CACHED_SHELL_SUMMARY: EnvironmentShellSummary = { hasSnapshot: true, hasSynchronizingShell: true, hasCachedShell: true, - latestSnapshotUpdatedAt: "2026-06-07T00:00:00.000Z", }; describe("mobile workspace projection", () => { diff --git a/apps/mobile/src/state/workspaceModel.ts b/apps/mobile/src/state/workspaceModel.ts index 6b03c6f8340f..8600c1099d1e 100644 --- a/apps/mobile/src/state/workspaceModel.ts +++ b/apps/mobile/src/state/workspaceModel.ts @@ -26,7 +26,6 @@ export interface WorkspaceState extends WorkspaceConnectionState { readonly hasLoadedShellSnapshot: boolean; readonly hasPendingShellSnapshot: boolean; readonly shellSnapshotError: string | null; - readonly latestCachedSnapshotReceivedAt: string | null; } function overallConnectionState( @@ -101,7 +100,6 @@ export function projectWorkspaceState(input: { hasLoadedShellSnapshot: input.shellSummary.hasSnapshot, hasPendingShellSnapshot: input.shellSummary.hasSynchronizingShell, shellSnapshotError: input.shellSummary.firstError, - latestCachedSnapshotReceivedAt: input.shellSummary.latestSnapshotUpdatedAt, }; } diff --git a/packages/client-runtime/src/state/shell.test.ts b/packages/client-runtime/src/state/shell.test.ts index 980d5f230f34..4310aedb7cba 100644 --- a/packages/client-runtime/src/state/shell.test.ts +++ b/packages/client-runtime/src/state/shell.test.ts @@ -88,6 +88,7 @@ function makeHarness() { }); return { + catalogValueAtom, registry: AtomRegistry.make(), shellStateAtom: shellStateAtoms, configAtom: configAtoms, @@ -107,7 +108,6 @@ describe("environment shell projections", () => { hasCachedShell: true, hasLiveShell: false, firstError: "Retrying.", - latestSnapshotUpdatedAt: "2026-06-02T00:00:00.000Z", }); harness.registry.set( @@ -122,6 +122,101 @@ describe("environment shell projections", () => { expect(harness.registry.get(harness.summaryAtom)).toBe(summary); }); + it("does not notify summary subscribers when thread timestamps advance", () => { + const harness = makeHarness(); + const initial = harness.registry.get(harness.summaryAtom); + let changes = 0; + const unsubscribe = harness.registry.subscribe(harness.summaryAtom, () => changes++); + try { + for (let index = 0; index < 20; index++) { + harness.registry.set( + harness.shellStateAtom(ENVIRONMENT_ID), + shellState({ + status: "cached", + updatedAt: `2026-07-01T00:00:${String(index).padStart(2, "0")}.000Z`, + snapshotSequence: index + 2, + }), + ); + expect(harness.registry.get(harness.summaryAtom)).toBe(initial); + } + expect(changes).toBe(0); + } finally { + unsubscribe(); + harness.registry.dispose(); + } + }); + + it("notifies for shell availability, status, errors, and enabled environment changes", () => { + const harness = makeHarness(); + let changes = 0; + const unsubscribe = harness.registry.subscribe(harness.summaryAtom, () => changes++); + const setCatalog = (entries: ReturnType[]) => { + harness.registry.set(harness.catalogValueAtom, { + isReady: true, + entries: new Map(entries.map((entry) => [entry.target.environmentId, entry])), + }); + return harness.registry.get(harness.summaryAtom); + }; + try { + expect(setCatalog([])).toEqual({ + hasSnapshot: false, + hasSynchronizingShell: false, + hasCachedShell: false, + hasLiveShell: false, + firstError: null, + }); + expect(setCatalog([environmentEntry(ENVIRONMENT_ID, "Environment")])).toMatchObject({ + hasSnapshot: true, + hasCachedShell: true, + }); + harness.registry.set(harness.shellStateAtom(ENVIRONMENT_ID), shellState({ status: "empty" })); + expect(harness.registry.get(harness.summaryAtom)).toMatchObject({ + hasSnapshot: false, + hasCachedShell: false, + }); + harness.registry.set( + harness.shellStateAtom(ENVIRONMENT_ID), + shellState({ status: "synchronizing", error: "Retrying." }), + ); + expect(harness.registry.get(harness.summaryAtom)).toMatchObject({ + hasSnapshot: false, + hasSynchronizingShell: true, + firstError: "Retrying.", + }); + harness.registry.set( + harness.shellStateAtom(ENVIRONMENT_ID), + shellState({ status: "live", updatedAt: "2026-07-01T00:00:00.000Z" }), + ); + expect(harness.registry.get(harness.summaryAtom)).toMatchObject({ + hasSnapshot: true, + hasSynchronizingShell: false, + hasLiveShell: true, + firstError: null, + }); + harness.registry.set( + harness.shellStateAtom(ENVIRONMENT_ID), + shellState({ + status: "live", + updatedAt: "2026-07-01T00:00:00.000Z", + error: "Disconnected.", + }), + ); + expect(harness.registry.get(harness.summaryAtom).firstError).toBe("Disconnected."); + harness.registry.set( + harness.shellStateAtom(ENVIRONMENT_ID), + shellState({ status: "live", updatedAt: "2026-07-01T00:00:00.000Z" }), + ); + expect(harness.registry.get(harness.summaryAtom).firstError).toBeNull(); + expect( + setCatalog([{ ...environmentEntry(ENVIRONMENT_ID, "Environment"), enabled: false }]), + ).toMatchObject({ hasSnapshot: false, hasLiveShell: false }); + expect(changes).toBe(8); + } finally { + unsubscribe(); + harness.registry.dispose(); + } + }); + it("preserves server-config map identity until a config reference changes", () => { const harness = makeHarness(); const empty = harness.registry.get(harness.serverConfigsAtom); diff --git a/packages/client-runtime/src/state/shell.ts b/packages/client-runtime/src/state/shell.ts index 4b7ea62819a9..e21a36226ce4 100644 --- a/packages/client-runtime/src/state/shell.ts +++ b/packages/client-runtime/src/state/shell.ts @@ -6,7 +6,6 @@ import { type ServerConfig, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; -import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; @@ -321,7 +320,6 @@ export interface EnvironmentShellSummary { readonly hasCachedShell: boolean; readonly hasLiveShell: boolean; readonly firstError: string | null; - readonly latestSnapshotUpdatedAt: string | null; } const EMPTY_ENVIRONMENT_SHELL_SUMMARY: EnvironmentShellSummary = Object.freeze({ @@ -330,7 +328,6 @@ const EMPTY_ENVIRONMENT_SHELL_SUMMARY: EnvironmentShellSummary = Object.freeze({ hasCachedShell: false, hasLiveShell: false, firstError: null, - latestSnapshotUpdatedAt: null, }); const EMPTY_SERVER_CONFIGS: ReadonlyMap = new Map(); @@ -344,8 +341,7 @@ function shellSummariesEqual( left.hasSynchronizingShell === right.hasSynchronizingShell && left.hasCachedShell === right.hasCachedShell && left.hasLiveShell === right.hasLiveShell && - left.firstError === right.firstError && - left.latestSnapshotUpdatedAt === right.latestSnapshotUpdatedAt + left.firstError === right.firstError ); } @@ -372,7 +368,6 @@ export function createEnvironmentShellSummaryAtom(input: { let hasCachedShell = false; let hasLiveShell = false; let firstError: string | null = null; - let latestSnapshotUpdatedAt: string | null = null; for (const environmentId of enabledEnvironmentIds(get(input.catalogValueAtom))) { const state = get(input.shellStateValueAtom(environmentId)); @@ -386,22 +381,6 @@ export function createEnvironmentShellSummaryAtom(input: { continue; } hasSnapshot = true; - const snapshot = state.snapshot.value; - const updatedAt = snapshot.threads.concat(snapshot.archivedThreads).reduce( - (latest, thread) => { - const value = DateTime.formatIso(thread.updatedAt); - return latest === null || value > latest ? value : latest; - }, - snapshot.projects.reduce((latest, project) => { - return latest === null || project.updatedAt > latest ? project.updatedAt : latest; - }, null), - ); - if ( - updatedAt !== null && - (latestSnapshotUpdatedAt === null || updatedAt > latestSnapshotUpdatedAt) - ) { - latestSnapshotUpdatedAt = updatedAt; - } } const next: EnvironmentShellSummary = { @@ -410,7 +389,6 @@ export function createEnvironmentShellSummaryAtom(input: { hasCachedShell, hasLiveShell, firstError, - latestSnapshotUpdatedAt, }; if (shellSummariesEqual(previousSummary, next)) { return previousSummary;