Skip to content

Persist desktop preferences across builds and restarts#4408

Open
danieliser wants to merge 5 commits into
pingdotgg:mainfrom
danieliser:agent/durable-ui-preferences
Open

Persist desktop preferences across builds and restarts#4408
danieliser wants to merge 5 commits into
pingdotgg:mainfrom
danieliser:agent/durable-ui-preferences

Conversation

@danieliser

@danieliser danieliser commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • Standardized packaged Alpha, Nightly, and stable builds on the canonical t3code Electron profile while keeping unpackaged development builds isolated in t3code-dev.
  • Added durable desktop-backed storage for renderer UI state and sticky composer preferences instead of relying only on origin-scoped localStorage.
  • Hydrated client settings, UI state, and sticky model/provider choices before first render using guarded three-way reconciliation so startup reads cannot overwrite newer user edits.
  • Serialized writes, surfaced persistence failures, and added bounded flush barriers for direct window close, app quit, relaunch, signals, and update installation.
  • Migrated valid legacy Alpha/Nightly browser state and pre-v3 composer preference shapes without promoting malformed or partial documents.
  • Kept thread drafts, images, and thread-local composer content out of the durable preference document.

Why

Desktop preferences were split across Electron profile directories, renderer-origin storage, and server-side settings. Build identity and origin changes could therefore make labels, manual project ordering, explicit unread state, and default model selections appear to reset even though other durable state still existed.

This gives all packaged channels one stable source of truth, preserves the desired Alpha/Nightly sharing behavior, and prevents unpackaged development sessions from modifying that packaged profile.

Data Safety

  • Canonical data wins; legacy profile migration runs only when the canonical profile is absent.
  • Malformed durable or browser documents fail closed and do not overwrite valid state with defaults.
  • Startup reconciliation preserves edits made while hydration is in flight.
  • Writes use serialized queues and atomic replacement.
  • Close and shutdown paths wait for renderer acknowledgement with a bounded timeout so Quit cannot hang indefinitely.
  • Legacy cleanup happens only after the durable write succeeds.

Verification

  • 111 focused web persistence tests passed.
  • 40 focused desktop persistence tests passed.
  • Web and desktop TypeScript checks passed.
  • Targeted lint, formatting, and git diff --check passed.
  • The combined patched-nightly integration passed 192 focused persistence tests plus web and desktop typechecks.
  • Independent review findings covering migration validation, write ordering, close handling, and updater shutdown were reproduced where valid and fixed with focused regressions.

Checklist

  • Packaged channels share one canonical profile
  • Unpackaged development remains isolated
  • Legacy preferences migrate without accepting corrupt documents
  • Direct close, quit, relaunch, signals, and updater install flush pending state
  • No user-facing layout or animation changes are included

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2db24c2d-4c2f-46f9-bdc7-b5d52931499e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 23, 2026
Comment thread apps/web/src/uiStateStore.ts Outdated
Comment thread apps/web/src/uiStateStore.ts
Comment thread apps/web/src/hooks/useSettings.ts
Comment thread apps/web/src/uiStateStore.ts Outdated
Comment thread apps/web/src/composerDraftStore.ts Outdated
Comment thread apps/web/src/composerDraftStore.ts
Comment thread apps/web/src/uiStateStore.ts
Comment thread apps/web/src/hooks/useSettings.ts
Comment thread apps/web/src/composerDraftStore.ts
Comment thread apps/desktop/src/app/DesktopEnvironment.ts Outdated
Comment thread apps/web/src/localApi.ts
@macroscopeapp

macroscopeapp Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces significant new persistence functionality with complex state management and migration logic. Multiple unresolved review comments identify potential bugs including race conditions during shutdown, data loss scenarios, and error handling gaps that warrant careful human review.

You can customize Macroscope's approvability policy. Learn more.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: caf75c3371

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

> {
const shutdown = yield* DesktopShutdown.DesktopShutdown;
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.flushRendererState;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Flush renderer state before closing the window

When the user exits by closing the last window on Linux/Windows, window-all-closed calls app.quit only after the BrowserWindow has emitted closed; DesktopWindow then clears the main-window ref, so this new before-quit flush finds no live webContents and returns without asking the renderer to drain its debounced ui-state/composer preference writes. A preference changed shortly before closing the window can therefore be dropped. Please flush or prevent the window close before the renderer is destroyed, rather than only during before-quit.

Useful? React with 👍 / 👎.

Comment thread apps/web/src/clientPersistenceBootstrap.ts Outdated
Comment thread apps/web/src/uiStateStore.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding on the Effect service error-modeling conventions. See inline comment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/desktop/src/app/DesktopAppIdentity.ts Outdated
Comment thread apps/web/src/localApi.ts
Comment thread apps/web/src/localApi.ts Outdated
Comment thread apps/web/src/composerDraftStore.ts
Comment thread apps/web/src/composerDraftStore.ts
Comment thread apps/web/src/composerDraftStore.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09e4513bf7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/window/DesktopWindow.ts Outdated
Comment thread apps/web/src/composerPreferencesStorage.ts Outdated
Comment thread apps/web/src/localApi.ts Outdated
runFork(flushBoundsPersist);
let closeAllowed = false;
let closeFlushPending = false;
window.on("close", (event) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium window/DesktopWindow.ts:580

The close handler skips the renderer-state flush when desktopState.quitting is true, so any quit path that sets quitting without first calling flushRendererState destroys the window while durable preference writes are still pending. The update-install path (DesktopUpdates.installDownloadedUpdate) sets quitting to true and then destroys windows without flushing, so recent UI/composer state is silently lost. The quitting fast-path bypass should only apply after a flush has already completed, or the update-install path must call flushRendererState before destroying windows.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/window/DesktopWindow.ts around line 580:

The `close` handler skips the renderer-state flush when `desktopState.quitting` is true, so any quit path that sets `quitting` without first calling `flushRendererState` destroys the window while durable preference writes are still pending. The update-install path (`DesktopUpdates.installDownloadedUpdate`) sets `quitting` to `true` and then destroys windows without flushing, so recent UI/composer state is silently lost. The `quitting` fast-path bypass should only apply after a flush has already completed, or the update-install path must call `flushRendererState` before destroying windows.

Comment thread apps/web/src/composerPreferencesStorage.ts Outdated
Comment thread apps/web/src/composerPreferencesStorage.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6b194d2. Configure here.

return yield* Effect.gen(function* () {
yield* desktopWindow.flushRendererState;
yield* desktopWindow.flushMainWindowBounds;
yield* Ref.set(desktopState.quitting, true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Install flush races non-macOS quit

Medium Severity

installDownloadedUpdate keeps desktopState.quitting false until after flushRendererState, which can take up to 10s. In that window a user close can finish the close interceptor, destroy the main window, and on Windows/Linux fire window-all-closedapp.quit while install is still waiting on a dead webContents before destroyAll / quitAndInstall. That races lifecycle shutdown with update install and can stall or fail the update path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6b194d2. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b194d22bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
closeFlushPending = true;
void runPromise(
Effect.all([flushRendererStateForWindow(window), flushBoundsPersist], {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid blocking close when no renderer can flush

When the main window is closed before the web app has installed the onRendererStateFlush listener (for example after a failed/slow initial load or a crashed renderer), this path still prevents the native close and sends a flush request that no renderer can acknowledge. The only way out is the 10s timeout in flushRendererStateForWindow, so users trying to close a broken window now see the app hang instead of closing; track renderer flush readiness or skip this flush for unloaded/crashed contents.

Useful? React with 👍 / 👎.

Comment thread apps/web/src/localApi.ts
if (browserState === null) {
return null;
}
await window.desktopBridge.setRendererState(key, browserState.raw);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hydrate valid browser state even if migration fails

When the desktop renderer-state file is missing but an existing browser-local t3code:ui-state:v1 or t3code:composer-preferences:v1 value is present, this await makes hydration depend on the first durable migration write succeeding. If that write rejects due to a transient IPC/filesystem error, the already-validated browser state is not returned, so the UI/composer hydrators render defaults and keep writes guarded even though usable state is still in localStorage; apply the browser state for the current session and retry/preserve the failed durable copy separately.

Useful? React with 👍 / 👎.

Comment on lines +470 to +472
yield* desktopWindow.flushRendererState;
yield* desktopWindow.flushMainWindowBounds;
yield* Ref.set(desktopState.quitting, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark update installs as quitting before slow flushes

After an update install is accepted, flushRendererState can wait up to 10 seconds while desktopState.quitting is still false. If the user closes the last Linux/Windows window during that window, the normal window-all-closed quit path can run before this flow reaches quitAndInstall, so the requested update install may be skipped; set the quitting latch before the slow flushes or make the close/quit path honor updateInstallInFlightRef.

Useful? React with 👍 / 👎.

Comment thread apps/web/src/localApi.ts
if (!legacySettings) {
return null;
}
await window.desktopBridge.setClientSettings(legacySettings);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return legacy client settings when seeding fails

When a desktop client-settings file is absent but valid browser-local settings still exist, this migration write is now on the read path. A transient desktop write failure rejects getClientSettings, so startup hydrates with defaults and leaves the user's valid local settings unused even though they are still readable; return the legacy settings for the current session and retry the durable copy separately instead of making the first seed write mandatory.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant