feat: account-level Chat setting (user_settings), out of PostHog

Merged
HA
2d ago
+17835-397

Stacked on #9155 (user_settings table); merge that first and let it deploy. Supersedes #9154.

Summary

  • Chat leaves PostHog. Desktop and mobile get Settings → Experimental → Chat, stored per account in user_settings.experimental_chat and off by default. Turn it on once and it applies on every device and on the server.
  • One rule everywhere: new sessions open as chat only when the setting is on and the agent supports ACP (Claude, Codex, OpenCode, Pi). Every other case opens a terminal. This applies to desktop, mobile, automations (the owner's setting) and cloud workspace starts.
  • Existing chats always show. Chats from automations, another device, or an older build are adopted and listed whatever the setting says; the setting only decides how new sessions open.
  • Presets bar is back on chat tabs, and "Fork into chat" is hidden while Chat is off.

How It Works

PieceWhat
userSettings.get / userSettings.updateNew router, mirroring channelNotificationPrefs. No row means the defaults; update upserts only the fields it was given and returns the settings
lib/user-settings.tsloadUserSettings / acpChatEnabled(userId). Used by automations (relay and cloud) and cloudWorkspace.create
DesktopuseAcpChatEnabled() is a cloudTrpc.userSettings.get query. AcpChatSetting writes the result back with setData. The old local acp-chat store is deleted and its key moved to DEAD_KEYS
MobileuseAcpChatEnabled() is a React Query userSettings.get (refetched on foreground through the existing focus manager); the Settings switch writes the result into the cache

Show vs open

  • Showing no longer depends on the setting: desktop useAutoAdoptChatSessions, useConsumeAutomationRunLink, seedPendingCloudChat; mobile home list, "Manage sessions", chat tabs and stop button, ended chats.
  • Opening new sessions checks the setting and ACP support: desktop launcher, presets, new chat (button and hotkey), new workspace, fork into chat; mobile new-session sheet and new terminal workspace.

Auth and the session are untouched.

Compatibility

  • Old desktop and mobile builds still read the PostHog flag for what they show. Chat is off by default, so automations and cloud starts open terminals, which every build shows.
  • Edge case: someone who turns Chat on in a new build but also uses an old build outside the flag won't see chats there until they update.
  • tRPC: only additions (a new userSettings router). Nothing released loses an input or output.
  • Behaviour change: people in acp-chat lose chat (and their automations switch to terminals) until they turn the setting on. An automation that continues a previous chat starts a fresh terminal while its owner has Chat off (hostSession.test.ts:156).

Manual QA

Earlier iteration (#9154, device-local setting), desktop dev app over CDP, before (main) vs after:

BeforeAfter
Settings → ExperimentalNo Chat switchChat switch, off
Chat off: Claude presetTerminalTerminal
Chat off: terminal "CLI mode" menuOffers Fork into chatFork session / Continue only
Chat on: Claude presetn/aChat pane
Chat on: Copilot preset (no ACP)n/aTerminal
Presets bar on a chat tabHiddenShown
  • Not re-run against this version. The table isn't on the workspace's Neon dev branch yet (applying migrations was blocked here). Re-check the rows above, plus: toggle on desktop, mobile follows after a foreground refetch
  • Not run: mobile on a simulator; a real automation run end to end

Testing

  • tsc: packages/trpc, packages/auth, apps/desktop clean. apps/mobile: same 188 errors as main, none in changed files
  • bun test: trpc 817 (new user-settings.test.ts), desktop full suite (useCreateAgentSession fixture now mocks useAcpChatEnabled), mobile 248, auth 90
  • bun run check:i18n clean (2 new strings translated in all 16 locales); biome check clean
  • An independent review agent read the account-column version: no correctness bugs; its compatibility and session-cache findings are resolved by moving to user_settings and having automations follow the owner's setting

Follow-ups

  • Delete the acp-chat PostHog flag once no live build reads it
  • Settings copy says "Claude and Codex"; OpenCode and Pi also chat (re-keys 2 strings)

Summary by cubic

Moves the Chat experimental setting out of PostHog and per-device storage into an account-level user_settings row, so desktop, mobile, and server-side automations all agree on one value that's off by default.

  • Adds a userSettings tRPC router (get/update) that reads and upserts experimentalChat; no row means off.
  • Desktop and mobile replace their acp-chat PostHog flag and local store with userSettings.get queries and persist via userSettings.update.
  • Launch paths wait for the setting (useResolveAcpChatEnabled) instead of reading "off" while it loads; desktop sign-out resets the query so the next account never sees the previous one's value.
  • Existing chats are always shown regardless of the setting; the setting only decides whether new sessions open as chat (and only for agents that support ACP).
  • Automations open as chat only when the owner's setting is on; the old acp-chat PostHog flag remains only for released builds and can be removed later.
  • The Chat switch is disabled while a save is in flight; "Track file changes per turn" stays visible but disabled while Chat is off.
  • The desktop device-local acp-chat store is deleted and its key moves to DEAD_KEYS, so anyone with the setting enabled in a previous build will see terminals until they flip the new account-level toggle.

Migration
Existing acp-chat feature-flag users lose chat access until they enable the new setting in Settings → Experimental → Chat; the old local key is dead.