Skip to content

Feature: persist a default permission mode for new sessions (mirroring default_model) #591

Description

@amir-ag

Summary

The composer's permission mode picker only changes the running session. Every new session starts in interactive again, and there is no way in the app to say "start my sessions in this mode". The only lever is mode in the global config.toml: read once at sidecar start, needs an app restart to change, surfaced nowhere in the UI.

default_model already solves this exact problem for the model picker (prefs entry, POST /v1/settings/default-model, "make default" button). This asks for the same shape for the permission mode.

Verified against 9e145d9 (main, 2026-08-29); code references collapsed below.

Proposal

  • default_mode preference, mirroring default_model: set_default_mode on the manager, POST /v1/settings/default-mode, applied in __init__ next to default_model, returned by get_settings().
  • Precedence: preference, then config.toml mode, then built-in interactive (same shape as auto_approve()).
  • A "set as default" affordance in the composer's mode menu.
  • Safety cap, since a sticky default is riskier than a per-session pick: persistable are discuss and interactive; auto-approve only while its feature flag is on (falls back to interactive when the flag goes off); bypass-approvals not persistable from the UI at all — that stays a deliberate config.toml edit. Unknown values resolve to interactive. Scheduled tasks keep their hardcoded interactive and are untouched.

Question for maintainers

Personas already carry a default_permission_mode field, but today it is display-only; nothing feeds it into the engine. If the intended home for this is the persona rather than a global preference, say so and I will follow that design instead.

Offer

Happy to send the PR: preference + endpoint, settings payload, composer affordance, safety cap, tests along the lines of tests/test_auto_approve_settings.py, plus a small fix to the stale mode line in docs/config.example.toml. Flagging first per the README's roadmap note.

Code references (verified against 9e145d9)

Picker is per-session, no pref written. App.tsx:1195 changeMode → WebSocket set_mode (api.ts:2452), handled at app.py:2637 by setting engine.permissions.mode on the live engine. The value lands on the session record (manager.py:5034) and is restored on resume (manager.py:604) — that is where it stops.

New sessions take SessionManager.mode. manager.py:607: model, mode, messages = self.model, self.mode, None. Set once in __init__ (manager.py:194) from build_app (run.py:102), whose --mode argparse default is cfg.mode (run.py:141,147). The desktop shell passes only --host/--port (lib.rs:751), so the global config.toml reaches GUI sessions only via that argparse default, read once at start.

default_model precedent. Pref applied at manager.py:260, writer set_default_model at manager.py:3560, endpoint app.py:1872, UI button ManageTabs.tsx:204. No mode key exists anywhere in prefs.

Prefs-over-config precedence precedent. auto_approve() at manager.py:3406 and tests/test_auto_approve_settings.py::test_prefs_falls_back_to_config_when_unset.

Scheduled tasks. manager.py:4327 hardcodes mode=Mode.INTERACTIVE, so a default cannot widen unattended runs; worth an explicit regression test.

Persona field is display-only. default_permission_mode in personas/manifest.py:79 (parsed at :293), surfaced via loading.py:37 / manager.py:902, rendered in PersonaView.tsx:356 / PersonasTab.tsx:454. Neither build_engine call site (manager.py:649, manager.py:4323) consults it.

Two adjacent doc/code drifts (would fold fixes into the PR):

  1. The per-workspace mode override is dead code: mode is in _WORKSPACE_FIELDS (config.py:108) and tests/test_config.py asserts it resolves, but both readers call load_config() without a workspace (run.py:141, cli.py:19), and agent.py:266 consumes only allowed_commands / auto_allow / allowed_domains. The config.example.toml header promises "per-workspace, overrides global" for a key where that never applies.
  2. docs/config.example.toml:8 documents plan | interactive | auto | custom; actual server choices are discuss, plan, interactive, auto, bypass-approvals, auto-approve (run.py:146), with auto a legacy alias mapped to BYPASS_APPROVALS by Mode._missing_ (permissions.py:232). custom still works as a config value (Mode.CUSTOM, permissions.py:229) but is absent from the choices list and hidden from the picker.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions