diff --git a/apps/desktop/src/lib/incremental-external-store-runtime.ts b/apps/desktop/src/lib/incremental-external-store-runtime.ts index e8e5fdcee2..2e5da99cb7 100644 --- a/apps/desktop/src/lib/incremental-external-store-runtime.ts +++ b/apps/desktop/src/lib/incremental-external-store-runtime.ts @@ -146,7 +146,9 @@ class IncrementalExternalStoreThreadRuntimeCore extends ExternalStoreThreadRunti } const isRunning = store.isRunning ?? false - this.isDisabled = store.isDisabled ?? false + const newDisabled = store.isDisabled ?? false + const disabledChanged = this.isDisabled !== newDisabled + this.isDisabled = newDisabled const oldStore = self._store self._store = store @@ -158,7 +160,7 @@ class IncrementalExternalStoreThreadRuntimeCore extends ExternalStoreThreadRunti // (render -> new literal -> setAdapter -> notify -> render), which React // kills with "Maximum update depth exceeded" and takes the session tile // down with its error boundary. - let changed = false + let changed = disabledChanged if (this.extras !== store.extras) { this.extras = store.extras diff --git a/apps/desktop/src/lib/incremental-runtime-adapter-notify.test.ts b/apps/desktop/src/lib/incremental-runtime-adapter-notify.test.ts index 28067181dd..2fdcd73d76 100644 --- a/apps/desktop/src/lib/incremental-runtime-adapter-notify.test.ts +++ b/apps/desktop/src/lib/incremental-runtime-adapter-notify.test.ts @@ -115,6 +115,29 @@ describe('IncrementalExternalStoreThreadRuntimeCore adapter swap notifications', expect(notifications).toBeGreaterThan(0) }) + it('still notifies on an isDisabled flip even when transcript and run state are unchanged', () => { + const repo = repositoryOf([message('a', 'one')]) + const core = new IncrementalExternalStoreRuntimeCore(adapterWith(repo)) + const thread = core.threads.getMainThreadRuntimeCore() + + let notifications = 0 + thread.subscribe(() => { + notifications += 1 + }) + + core.setAdapter(adapterWith(repo, { isDisabled: true })) + + expect(notifications).toBeGreaterThan(0) + + // And flipping back also notifies — but an unchanged repeat stays silent. + core.setAdapter(adapterWith(repo, { isDisabled: false })) + const afterFlipBack = notifications + + core.setAdapter(adapterWith(repo, { isDisabled: false })) + + expect(notifications).toBe(afterFlipBack) + }) + it('a subscriber that swaps a fresh-but-identical adapter on every notify must not recurse unboundedly', () => { // Direct simulation of the feedback loop: the subscriber plays the role of // React re-rendering ChatRuntimeBoundary (new literal -> setAdapter). With