User Settings

/settings is the CardMirror editor's settings page, and a signed-in user's choices there follow them across devices instead of staying in one browser's localStorage.

  • Route: /settings (the panel itself renders in an isolated iframe at /settings/editor-panel)
  • Research agent settings: tabs of /settings itself, listed after the editor's under a Research agent heading in the same sidebar — one per section (account, language models, connectors, skills and memory, search sources, search settings, cloud storage, rewrite modes, voice). Each is linkable as /settings?category=research-<section>. The section list is research-agent-ui's own (research-agent-ui/settings); the panes load lazily, the first time one of those tabs is opened. The old /settings/research and /settings/research/<section> pages now redirect to the matching tab (editor-<tab> sections to the editor's own tab).
  • Package: debate-editor (the settings store, SETTING_METADATA, and the embedded panel builder), apps/debate-ai.com (lib/editor-preferences.ts, /api/settings, user_settings D1 table)

What it shows

The editor's whole category set, listed in a searchable sidebar — General, Files, Appearance, Accessibility, Editing, Keyboard, Comments & AI, Collaboration — from debate-editor's exported tab list (CARDMIRROR_SETTINGS_TABS, @debate/editor/settings-tabs) and rendered by its CardMirrorSettingsSection (@debate/editor/settings-section), which wraps buildEmbeddedSettingsPanel, so each tab is the same rows the editor's gear-icon modal renders under it, minus the dialog chrome. General also carries the personal dictionary, the performance benchmark and this install's version info.

The page fills the whole window with no padding around it. The sidebar runs its full height: the Settings title and a Back link sit at its top, and the site's footer links (Docs, Github, Support, Privacy, Reddit — the same FOOTER_LINKS list the app sidebar's footer reads) sit at its bottom.

Appearance and Accessibility appear only here: they moved out of that modal when they moved to this page. Everything else is a second surface onto the same local store, so a change here shows up in the editor and the other way round.

EDITOR_SETTINGS_TABS (apps/debate-ai.com/lib/editor-preferences.ts) is the one list of which categories those are, read both by the panel route that renders them and by the API route that validates what they save. plugins is deliberately not among them — every row in it is Electron-only, so the tab would render empty on the web.

What it no longer shows

The page used to be this app's own account form, and that whole section is gone:

  • the Debate style / Font size / Font family / Colour theme / Light-dark pickers (debate-round's UserSettingsPanel) — the dock's gear-icon menu still carries the colour theme and light/dark rows. The whole form is back as the first tab of /settings, Preferences (still UserSettingsPanel), ahead of the editor's own tabs; /settings/preferences now redirects to it
  • Favorite tools — a tool is starred from its own page or the command palette (the /tools page was removed), which is also where a renamed or removed tool now gets pruned from the saved list
  • Tool data — each tool page's header now carries a small badge (Saved to account / Syncing… / Not synced · Retry / Saved on this browser when signed out) for tools whose data syncs; see Tool Data Sync's Known gaps for what went with it (the sync itself is untouched and still runs app-wide)
  • Word limit presets (debate-round's WordLimitPresetsPanel) — now editable directly on /word-count, in a collapsible "Manage word limit presets" section below the word-count form, rather than from here
  • Ebb Flow preferences — reachable with Cmd/Ctrl+, inside a flow, where they apply
  • Default format for new speech documents (Files → New documents) — a new speech document is now always created as .cmir, CardMirror's native format, so autosave always applies to it (autosave only writes .cmir). Save As still exports a .docx for teammates on Verbatim. The format is SPEECH_DOC_FORMAT in debate-editor's src/editor/speech-filename.ts

Their /api/settings fields (debateStyle, fontSize, wordLimitPresets, …) are all still stored and served; what changed is that nothing writes them from here any more.

Your own AI key

The Preferences tab has a Your own AI key box. Paste an OpenRouter key (sk-or-…) or an Anthropic key (sk-ant-…) and the AI features — card AI analysis, AI coach and judge, citation and OCR fixes, every caller of /api/reason-ai and /api/card-ai-analysis — run on your key instead of Debate AI's shared one. You set your own spending limit on that key with OpenRouter (or Anthropic), and Debate AI's daily AI request and card-analysis limits stop applying to you.

The same box appears:

  • in the pricing dialog when you hit a daily AI limit, as an alternative to upgrading, and
  • in an AI limit reached dialog when Debate AI's shared key itself has run out of credit or hit its spending cap (the route answers 503 with an x-ai-key-needed header). That dialog is an upgrade box: it offers the plan cards to upgrade your account first, with bringing your own key as the other option. If the plans can't be loaded, only the key option shows.

The key is kept in this browser's localStorage only — never in your account's settings row — and is sent, as the x-user-ai-key header, only on same-origin requests to the two AI routes. The server uses it for that one request and never stores or logs it. Remove it to go back to the shared key.

  • Packages: debate-webview (lib/ai/own-ai-key.ts, components/settings/OwnAiKeySection.tsx, components/pricing/OwnAiKeyDialog.tsx), apps/debate-ai.com (lib/ai/provider-key.ts)

Flow editor preferences follow your account

The Ebb flow editor's display preferences (Cmd/Ctrl+,) and keybinding overrides sync to your account when you are signed in, through the same tool data sync as the other tools (flowEditorDisplaySettings and flowKeymap in debate-data-sync's catalog). What follows you: flow font, default grid zoom, the editing toggles (insert-paste, append-edit, scroll-zoom, align speeches, tooltips, Vim in the RFD pane), CardMirror options, light/dark theme, side colours, and every keymap override.

What stays on the device it was set on: the flows folder, whether the sidebar and RFD pane are open, and the live-collaboration switches, display name and contacts. A pull from the account never resets those. Engineering notes: debate-data-sync's state/flow-editor-settings-codec.ts — a field joins the account only by being added to SYNCED_FLOW_DISPLAY_FIELDS.

Saved-to-account marker on flow tabs

Each flow tab in the sidebar's Open Tabs group shows a small cloud icon once that flow has been saved to your account from the Flow History dialog (including bulk saves): a plain cloud means the account copy matches what you see, a crossed-out amber cloud means you've edited it since. A tab with no icon has no baseline yet. The marker survives a reload: each save remembers the version the account stamped on it, and opening the /debate start screen or a round's flow tabs (or the Saved to account tab in Flow History) re-adopts that baseline only if your account still holds exactly that version, so edits made elsewhere (or by a different account on the same browser) never show as saved. Engineering notes: debate-round's state/flowAccountStatus.ts; the on-mount restore is state/restoreAccountBaselines.ts (one fetch per page session, silent when signed out).

The same cloud icon appears on the round cards of the /debate start screen (Featured and Recent) once the round itself has been saved to your account from the Flow History dialog, and flips to the amber crossed-out cloud when the round is edited afterwards. Saving a round's flows does not by itself mark the round.

Auto-save for flows already on your account

Once a flow has been saved to your account, further edits to it are saved automatically about five seconds after you stop typing, and the tab's cloud icon flips back to saved. Flows you have never saved are never uploaded automatically, and archived flows are skipped. Auto-save never overwrites: if the account holds a newer copy (saved from another device) the flow stays amber and you resolve it from Flow History. A failed save is retried only after the flow changes again. Turn it off with Auto-save flows to my account on the Preferences tab; it is on by default, applies immediately, and is stored in this browser only (not synced to other devices). Turning it off discards any save still waiting on the timer. Engineering notes: debate-round's state/flowAutoSave.ts, mounted by useFlowAutoSave in OpenTabsGroup; the preference lives in state/flowAutoSavePreference.ts.

Choose how far auto-save goes under Flow auto-save in Settings → Preferences:

  • Flows already on my account (default) — the behavior above.
  • All flows — also uploads flows you have never saved. If your account already holds a flow with that id, it is treated as a conflict and left alone.
  • Off — nothing is saved automatically; use Save now or Flow History.

The choice applies immediately in this browser. When you are signed in it is also saved to your account (PUT /api/settings { flowAutoSave }, the user_settings.flow_auto_save column) and adopted on your other devices the next time Settings loads. Engineering notes: state/flowAutoSaveSettings.ts (debate:flow-auto-save in localStorage, normalizeFlowAutoSavePatch/parseFlowAutoSave for the route), read at each run by createFlowAutoSaver's getMode. Deploy must add the new nullable column (schema.ts is the only source of truth).

Data flow

debate-editor/settings.ts (the store, SETTING_METADATA, SECRET_SETTING_KEYS)
debate-editor/settings-ui.ts (buildEmbeddedSettingsPanel)
app/settings/editor-panel/page.tsx (iframe document: category sidebar, hydrate, debounced push)
  → GET/PUT /api/settings { editorPreferences }
lib/editor-preferences.ts (EDITOR_SETTINGS_TABS, EDITOR_PREFERENCE_KEYS, validation)
components/settings/CardMirrorSettingsPanel.tsx (the iframe wrapper, self-sizing)

The local store stays authoritative: the page hydrates from the account on load (a 401 just means signed out), and every change writes localStorage immediately and mirrors to the account on a 600ms debounce. A failed mirror never undoes the local change.

Credentials stay in the browser

The mirrored keys are the hosted categories' settings minus the credentials among them — the provider API keys and the translation key that debate-editor's SECRET_SETTING_KEYS already keeps out of a settings export, plus the self-hosted relay's bearer token. EDITOR_PREFERENCE_KEYS excludes them, so they are refused on the way in (reported as unknown keys, not silently dropped) and filtered on the way back out of a stored row. apps/debate-ai.com/lib/__tests__/editor-preferences.test.ts pins that.

The push is also limited to the rows that actually rendered — each one tagged data-setting-key by the editor — rather than to every key in a hosted category. A row this host hides (desktop-only card sharing, say) would otherwise be mirrored at its default and overwrite whatever the account holds for it.

Flow editor preferences follow your account

Signed in, the Ebb flow editor's look-and-behaviour preferences (font, default grid zoom, theme, Aff/Neg colours, the Vim/insert-paste/append-edit/scroll-zoom/ align-speeches/tooltip toggles, the CardMirror options and your custom keybindings) are saved to your account as flowEditorSettings and applied the next time you open a flow on any device. Anything specific to one device — the flows folder, collaboration name and relay toggles, contacts, and panel layout — stays in that browser. Signed out, nothing changes: settings stay in localStorage.

The welcome dialog shows once per account

The "Welcome to Debate AI" dialog of key links appears the first time an account signs in, and never again on any device. Showing it records user_settings.welcome_dismissed_at through GET/POST /api/settings/welcome (FirstLoginWelcomeProvider in @debate/webview, helpers in lib/first-login-welcome.ts). A localStorage copy of the flag saves the request on browsers that already know, and a browser that showed the dialog before the flag moved to the account records it there once. If the account's answer cannot be read (offline, an error), the dialog stays hidden rather than risk showing it twice. A second account signing in on a browser that already showed it does not see it.

Privacy

The Privacy tab (/settings?category=privacy) is where you decide what Debate AI may publish of your recordings and how you are named on them.

  • Recording release. A few paragraphs explain what publishing means. Until you pick I agree, nothing you record is published; Don't publish my recordings is the default and withdraws an earlier agreement. Agreeing records the date and the version of the wording you agreed to, and if the wording changes you are asked again.
  • What is published: Audio and video, or Audio only, which leaves your camera out.
  • How you are credited: Anonymous contributor (the default, your name hidden), a Username you type in, or your Real name, either in full or as first name and last initial ("Ada L."). The tab previews how you will appear.
  • Privacy policy: a link to /legal/privacy.
  • Request deletion of your data: ask for everything (including your account) or only some of it (published recordings, saved speech audio, flows, documents, comments, account settings), with a box for suggested deletions such as links to specific recordings. Requests go to the site's admins as a notification, and your past requests and their status are listed under the form. Nothing is deleted automatically.

Engineering notes: the rules live in @debate/webview's lib/privacy/recording-release.ts (mayPublishRecording, includeVideo, publicCreditName, the validators and the release text). The choices are stored as JSON in user_settings.recording_privacy through GET/PUT /api/settings/privacy; requests go to GET/POST /api/settings/privacy/deletion-requests and the data_deletion_requests table, created on first use by apps/debate-ai.com/lib/privacy/store.ts, which also exports getRecordingPrivacy(db, userId) for any server code that publishes a recording.

Known gaps: the release text is a plain-language placeholder that has not had legal review. No feature publishes user recordings yet, so the settings take effect as publishing features read them. There is no admin screen for the deletion queue yet; admins see the notification and work the data_deletion_requests table by hand.

Known gaps

No optimistic-concurrency handling in general — two tabs editing the same field at once has the last PUT win. favoriteTools no longer has this problem: every code path that changes it (toggleFavorite/removeFavorite's single add/remove op, and pruneUnknown's batch removeFavoriteTools op) now resolves against the account's row as it stands at write time, not a client-computed whole-list replace, so two tabs adding, removing or pruning different entries at once can no longer drop each other's change. A plain whole-list favoriteTools replace is still accepted by the route for a caller that genuinely needs one, but nothing in the app sends one anymore. recentTools (see Command Palette's "Recent" section) joins it as an op-based field — each tool visit sends a single recordRecentTool op, resolved the same read-then-write way — since it writes far more often than any picker-style field here (every tool open, not just an explicit Save) and two tabs opening different tools in quick succession would otherwise race the same way favorites used to.

editorPreferences is a plain merge of whatever the page last pushed, so it has the same last-write-wins property: the editor's settings open in two tabs, changed in both, end at whichever debounce landed second. Every other field (debateStyle, colorTheme, etc.) is a plain whole-value replace with no such protection — debateStyle/fontSize/colorTheme/themeMode are written from /settings' Preferences tab and the dock's Theme submenu, both of which apply and (when signed in) sync a single field at a time rather than a whole form Save, so this mostly shows up as "two tabs pick a different Debate style within the same second."

wordLimitPresets (debate-round's WordLimitPresetsPanel, rendered from /word-count rather than from this page — see "What it no longer shows" above) is also op-based now: addPreset/updatePreset/removePreset each send a single addWordLimitPreset/updateWordLimitPreset/ removeWordLimitPreset op, resolved against the account's current list at write time instead of a client-computed whole-list replace, mirroring favoriteTools's fix above. A duplicate name, an unknown preset, or an invalid word limit is refused with a 400 rather than silently corrupting the stored list. A plain whole-list wordLimitPresets replace is still accepted by the route for a caller that genuinely needs one, but useWordLimitPresets.ts no longer sends one.

outlineFilterPresets (debate-practice-drills's named Outline filter presets bar — see Argument Tree Outline) is op-based the same way: addPreset/removePreset each send a single addOutlineFilterPreset/removeOutlineFilterPreset op, resolved against the account's current list at write time instead of a client-computed whole-list replace, mirroring wordLimitPresets's fix above. A duplicate name, an at-capacity list, or an invalid filter is refused with a 400 rather than silently corrupting the stored list. A plain whole-list outlineFilterPresets replace is still accepted by the route for a caller that genuinely needs one, but useOutlineFilterPresets.ts no longer sends one.

Pinned debates

The debates you pin to the start screen's Featured section follow your account. Each pin is a { roundId, pinnedAt } record in the pinnedDebates collection of the tool-record sync (debate-data-sync's TOOL_RECORD_COLLECTIONS, stored in saved_tool_records), so it syncs like every other tool store and appears under Settings → Account → Tool data as "Pinned Debates". A round's id is its saved_rounds.client_id, which a cloud save keeps, so the pin names the same round on every device; a pin whose round isn't on this device yet is skipped until the round arrives. Browsers that stored pins as a bare list of ids are upgraded to records the first time the list is read.

Known gap: pins are capped at 12 per browser, and the merge on sign-in is a union, so a pin removed on one device while another is offline can reappear until that device syncs.

Flow editor shortcuts

Shortcuts you rebind in the flow editor sync to your account as the flowKeymap collection (listed in Tool data as "Flow Editor Shortcuts"), one record per rebound action. They are stored under ebb-keymap-settings and adapted by flow-keymap-codec.ts in debate-data-sync.

Known gap: the flow editor reads shortcuts once at startup, so a rebinding made on another device applies the next time the editor loads. Display preferences (ebb-display-settings) are not synced: they include device-local values such as the flows folder path.

Save status on each tool

A tool page whose data syncs shows a small badge in its header: Saved to your account once everything the tool stores has reached your account, Saving to your account… while recent changes are on their way, Syncing… during the first merge after you sign in, and Saved on this device when you're signed out. It is read from the same auto-sync that powers Tool data (see getToolRecordCollectionSyncStatus in debate-data-sync), so it never starts a second sync, and it refreshes every few seconds rather than instantly.

While changes are waiting to be saved, a Save now button appears next to the badge and pushes that tool's data to your account immediately instead of waiting for the next automatic save. If the save fails the button turns into Retry save and its tooltip says why. Starring a tool as a favorite uses the star at the end of the same header row.

The /debate start screen has its own header, so it shows the same badge (and Save now) beside its New flow button; DebateFlowPage takes it as a startScreenActions prop because debate-round can't import the webview's component. Once a round or flow is open, the same badge sits at the start of the speech controls bar above the workspace (roundActions, handed to SpeechControlsTopBar as leadingActions).

The shared-cards sub-pages (Evidence Library, Argument Library and Card Revision History) keep their data under the /research/cards hub rather than under their own route, so their headers name the collections explicitly (ToolPageHeader's syncCollections prop, resolved by resolveToolSyncKeys) and show the badge too.

The video library at /videos (and each /videos/<category> view of it) shows the badge (and Save now) beside its search bar; LecturesPage takes it as a headerActionsSlot prop because debate-videos can't import the webview's component.

The /research/cards search workspace has no ToolPageHeader, so its route renders a slim strip above the workspace holding the badge (and Save now) for the Evidence Library collections.

Known gap: the open-flow tabs and the /doc header are custom too and don't show the badge yet.