# Unifying the Settings and Editor Config Surfaces: Duplication Map and Target Architecture (https://jackin.tailrocks.com/reference/research/operator-surface/settings-editor-surface-unification-design/)



**Status**: Design rationale and target architecture behind [Unify the settings and workspace-editor surfaces](/roadmap/unify-settings-editor-surfaces/). The auth source-folder validation and shared error dialog described here have shipped; the modal-enum collapse, host-trait, and per-domain convergence below are proposed target shape, not yet implemented. This page keeps the duplication inventory and architecture reasoning out of the roadmap item's day-to-day tracking.

## Problem [#problem]

The global **Settings** screen and the **workspace Editor** screen edit the same config domains — auth, mounts, env/secrets, trust, and general options — but are implemented as two largely separate surfaces. The same concepts are coded twice: row models, flatten/validation, rendering, edit flows, modal stacks, footer hints, and mouse hit-testing each exist as a left/right twin. The two screens look and behave *almost* the same, which is exactly the failure mode: any change made to one and not mirrored to the other produces a behavior that exists on one screen and not the other.

The operator-facing goal: editing config in global settings and editing it in the workspace editor is *the same thing* — same look, same keys, same behavior. It must therefore be one implementation. A change in one place must propagate to the other by construction, never by remembering to copy it.

## Root cause — why the architecture permits this whole class of bug [#root-cause--why-the-architecture-permits-this-whole-class-of-bug]

This was not a single defect; it is what the current structure guarantees. The auth source-folder picker exposed the pattern concretely:

* Source-folder validation landed in the settings commit path; the editor commit path was a different function and stayed unvalidated, so a wrong folder (for example `~/.cargo` selected for Codex) was silently accepted.
* The rejection rendered differently per surface (the file browser's inline reject line versus nothing) until both were pointed at the standard error dialog.

Auth was only the instance that bit us. The same two-implementations-for-one-concept structure exists for **mounts, env/secrets, trust, and general**, and for the cross-cutting machinery (modal stacks, tab strip, footer, mouse). Patching each twin as it bites leaves the structure — and the next divergence — in place. The correct fix removes the condition: one shared config-surface subsystem both screens drive, so a behavior cannot exist on one screen and not the other.

The established sharing pattern proves this is a convergence, not a rewrite: the form models, pickers, mount-row display, footer-hint vocabulary, and focus model already live once in `jackin-console` and are consumed by both screens. The remaining duplication is the per-domain edit flow, the modal-enum packaging, and the render/flatten glue.

## Scope [#scope]

The whole settings ↔ editor config surface, domain by domain:

* **Auth** — agent auth mode + credential + source folder, across global / workspace / role layers. (First slice; validation and the shared dialog already shipped.)
* **Mounts** — mount rows, add/edit/destination flow, scope picker.
* **Env / secrets** — masked key/value rows, scope (global/workspace vs role), enter/add/delete flow.
* **Trust** — role-source trust (a first-class tab in settings, a confirm flow in the editor).
* **General** — coauthor-trailer / DCO and the workspace general options.
* **Cross-cutting** — the modal stack, tab strip, footer hints, mouse hit-testing, dirty/discard, and the error-display mechanism.

## Full duplication map (as of the auth slice landing) [#full-duplication-map-as-of-the-auth-slice-landing]

Generic state structs live once in `jackin-console`; concrete instantiations live in the `jackin` app crate (<RepoFile path="crates/jackin/src/console/tui.rs" />). The structural asymmetry at the root: the editor uses **one** unified modal enum `ConsoleModal` (<RepoFile path="crates/jackin-console/src/tui/model/modal.rs" />); settings splits the same modal taxonomy across **three** enums (`GlobalMountModal`, `SettingsEnvModal`, `SettingsAuthModal`) plus a screen-level error-popup field (<RepoFile path="crates/jackin-console/src/tui/screens/settings/model.rs" />).

### Modal taxonomy — same concepts, 1 enum vs 3 [#modal-taxonomy--same-concepts-1-enum-vs-3]

| Modal concept          | Editor (`ConsoleModal`)                                | Settings                                                                           | Shared inner widget    |
| ---------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------- | ---------------------- |
| Text input             | `TextInput`                                            | `GlobalMountModal::Text`, `SettingsEnvModal::Text`, `SettingsAuthModal::TextInput` | `TextInputState`       |
| File browser           | `FileBrowser`                                          | `GlobalMountModal::FileBrowser`, `SettingsAuthModal::SourceFolderPicker`           | `file_browser`         |
| Mount-dst choice       | `MountDstChoice`                                       | `GlobalMountModal::MountDstChoice`                                                 | `mount_dst_choice`     |
| Confirm                | `Confirm`                                              | `GlobalMountModal::Confirm`, `SettingsEnvModal::Confirm`                           | `ConfirmState`         |
| Confirm-save / preview | `ConfirmSave`                                          | `GlobalMountModal::PreviewSave`                                                    | `save_preview`         |
| Error popup            | `ErrorPopup`                                           | screen-level `SettingsState.error_popup` (not a modal variant)                     | `error_popup`          |
| Op (1Password) picker  | `OpPicker`                                             | `SettingsEnvModal::OpPicker`, `SettingsAuthModal::OpPicker`                        | `op_picker`            |
| Role picker            | `RolePicker` / `RoleOverridePicker` / `AuthRolePicker` | `GlobalMountModal::RolePicker`, `SettingsEnvModal::RolePicker`                     | `role_picker`          |
| Source picker          | `SourcePicker` / `AuthSourcePicker`                    | `SettingsEnvModal::SourcePicker`, `SettingsAuthModal::SourcePicker`                | `source_picker`        |
| Scope picker           | `ScopePicker`                                          | `GlobalMountModal::ScopePicker`, `SettingsEnvModal::ScopePicker`                   | `scope_picker`         |
| Auth form              | `AuthForm`                                             | `SettingsAuthModal::AuthForm`                                                      | `auth_panel::AuthForm` |

Every modal concept settings has is also an editor `ConsoleModal` variant. The taxonomy is fully twinned; only the packaging differs.

### Config domains — row model, flatten, render, edit flow [#config-domains--row-model-flatten-render-edit-flow]

| Domain        | Editor impl                                                    | Settings impl                                                                          | Shared today                                                                        |
| ------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Auth          | `AuthRow<K>` + `input/auth.rs` (840+ L)                        | `SettingsAuthRow<K,M>` + `input/global_mounts/auth.rs` (660 L)                         | `AuthForm`, `AuthKind`/`AuthMode`, `auth_panel::render_form`, source-folder helpers |
| Mounts        | inline in `pending: WorkspaceConfig`; `input/editor/mounts.rs` | `GlobalMountRow` + `GlobalMountsState`; mount handlers in `input/global_mounts.rs`     | `mount_rows` (two twin fn pairs), `mount_display`, `mount_dst_choice`               |
| Env / secrets | `SecretsRow` + `secrets_flat_rows` + `input/editor/secrets.rs` | `SettingsEnvRow` + `settings_env_flat_rows` + env handlers in `input/global_mounts.rs` | `editor_rows::render_secret_key_line`, env-value component, pickers                 |
| Trust         | `ConfirmTarget::TrustRoleSource` confirm flow                  | dedicated `SettingsTrustState` tab + `handle_trust_key`                                | role-trust confirm semantics (separate code)                                        |
| General       | inline in `WorkspaceConfig`; `render_general_tab`              | `SettingsGeneralState`; `render_general_tab` (same-named twin)                         | tab strip, footer hints                                                             |

### Largest twinned function families [#largest-twinned-function-families]

Auth edit-flow (the proven slice): `open_auth_form_modal` / `open_settings_auth_form`; `apply_*_to_auth_form` vs `apply_*_to_settings_auth_form` for plain / op-picker (committed / runner / validator / failed) / source-folder; `restore_auth_form_after_op_picker_cancel` / `restore_settings_auth_form`; `persist_form` / `persist_settings_auth_form`; `clear_layer` / `clear_settings_auth_kind`. Env/secrets edit-flow: `open_secrets_enter_modal` / `open_settings_env_enter_modal`; `open_secrets_add_modal` / `open_settings_env_add_modal`; `open_secrets_delete_confirm` / `open_settings_env_delete_confirm`; mask toggles; `set_pending_env_value_typed` / `set_settings_env_value_typed`. Per-tab render twins in <RepoFile path="crates/jackin-console/src/tui/screens/settings/view.rs" /> vs the editor render in <RepoFile path="crates/jackin-console/src/tui/screens/editor/view.rs" />: `render_general_tab`, `render_mounts_tab`, `render_secrets_tab` / `render_env_tab`, `render_auth_tab`, and the `*_lines_for_state` line builders. Flatten/row-count twins (<RepoFile path="crates/jackin-console/src/tui/screens/editor/model.rs" /> update vs settings update): `auth_flat_rows` / `secrets_flat_rows` vs `settings_env_flat_rows` / `settings_auth_detail_row_count`. Modal-stack methods copy-pasted across four structs: `open_sub_modal` / `pop_modal_chain` / `clear_modal_chain` on `EditorState`, `SettingsEnvState`, `GlobalMountsState`, plus `push_auth_modal` / `restore_pending_auth_form` on `SettingsAuthState`. `is_dirty` / `discard` duplicated on every per-tab state. Mouse hit-test twins in <RepoFile path="crates/jackin-console/src/tui/input/mouse.rs" />: `try_select_editor_tab` / `try_select_settings_tab`, row-select and modal-scroll twins. Footer twins in `components/footer`: `editor_footer_items` / `settings_footer_items`, one `modal_footer_items` (editor) vs three `settings_*_modal_footer_items` (settings).

### Already shared (the established pattern to extend) [#already-shared-the-established-pattern-to-extend]

In `jackin-console` shared components, consumed by both screens: `auth_panel` (`AuthForm`, `render_form`, pickers' state constructors); `mount_rows`, `mount_display`, `mount_dst_choice`; `editor_rows` (tab strip, secret-key line, disclosure styles); `footer_hints` (the full shared hint vocabulary); every picker widget (`op_picker`, `role_picker`, `source_picker`, `scope_picker`, `github_picker`, `file_browser`, `workdir_pick`, `confirm_save`/`save_preview`, `error_popup`, `status_popup`, `container_info`); shared update helpers (`auth_flat_rows`, `secrets_flat_rows`, `settings_env_flat_rows`, change-count helpers); `FocusOwner<T>`; and the auth domain types (`AuthKind`, `AuthMode`). The widgets are shared; the per-screen *wiring* around them is not.

## Key asymmetries to resolve [#key-asymmetries-to-resolve]

1. Editor = one modal enum (`ConsoleModal`, \~22 variants); settings = three modal enums + a screen-level error field. Same concepts, different packaging.
2. Editor packs mounts/general/env into one `WorkspaceConfig` (`pending`/`original`); settings splits each tab into its own `*State` with its own `pending`/`original`/`modal`/`is_dirty`/`discard`.
3. Trust is a first-class tab in settings but only a confirm flow in the editor.
4. Editor auth supports mouse row-click + an `auth_expanded` set; settings auth uses `selected_kind` detail-rows with no row click.
5. Settings env scope (`Global`/`Role`) mirrors editor secrets scope (`Workspace`/`Role`) — the same two-level model expressed as two enums and two flatteners.

## Target architecture [#target-architecture]

End state: **one shared config-surface subsystem** that both screens drive. The two screens differ only in *which layers they edit* — settings edits global defaults; the editor edits workspace + role layers. That difference is data (the scope/layer set), not behavior, so it belongs behind a small trait, not in duplicated code.

Following the [TUI architecture rules](/reference/tui/): the config-surface **model** (rows, modal stack) is terminal-interaction state and lives in `jackin-console`; per-domain **decision logic** (validation, flatten) is a pure rule in a `domain`/update module; rendering is a pure view over the shared model. No external work runs in any of it.

### One modal enum [#one-modal-enum]

Collapse settings' three modal enums (and the screen-level error popup) into the editor's single unified modal taxonomy, so there is one modal stack type for both screens. The auth-modal slice is the worked example: retire `SettingsAuthModal::SourceFolderPicker` in favor of the shared file-browser-with-target shape, then fold the remaining settings auth/env/mount modals into the shared enum. One `open_sub_modal`/`pop_modal_chain`/`clear_modal_chain` implementation replaces the four copies.

### One per-domain edit subsystem + a scope host trait [#one-per-domain-edit-subsystem--a-scope-host-trait]

For each domain (auth, mounts, env, trust, general), one shared set of: row model, flatten/validation, render line-builder, and edit-flow handlers, generic over a host trait that supplies the screen-specific pieces:

```rust
pub trait ConfigSurfaceHost {
    // which layers this screen edits, and how to read/write them
    fn scopes(&self) -> &[ConfigScope];                 // settings: [Global]; editor: [Workspace, Role(_)]
    fn stash_modal(&mut self, child: ConfigModal);      // one modal-stack impl, not four
    fn restore_modal(&mut self);
    fn show_error(&mut self, reason: String);           // one standard dialog, same title both screens
    // domain accessors (auth rows, mount rows, env rows) keyed by scope
}
```

Neither `ConfigSurfaceHost` nor `ConfigScope`/`ConfigModal` exist in source yet — this is proposed shape, not landed code. The auth slice instantiates the pattern first (see below); mounts, env, trust, general are expected to follow the same shape once the trait exists. Each domain's edit flow (`open_form`, `apply_*`, `validate`, `restore`, `persist`, `clear`) is intended to become one generic function over `H: ConfigSurfaceHost` replacing the twin pair.

### Auth-modal slice (first increment, concrete pattern) [#auth-modal-slice-first-increment-concrete-pattern]

The auth lifecycle is the smallest complete instance and defines the shared shapes the other domains would reuse. Only the plain validation function (`validate_auth_source_folder`, shared via `ManagerEffect::ApplyFileBrowserOutcome`) has landed; the typed decision enum and host trait below are still proposed:

```rust
// jackin-console: shared auth modal model (beside AuthForm) — proposed, not yet landed
pub enum AuthModal<SourcePickerState, OpPickerState, FileBrowserState, AuthForm, AuthFormFocus> {
    Form { target: AuthFormTarget, state: Box<AuthForm>, focus: AuthFormFocus, literal_buffer: String },
    CredentialSourcePicker { state: SourcePickerState },
    SourceFolderBrowser { state: FileBrowserState },
    OpPicker { state: Box<OpPickerState> },
}

// the validate-then-apply-or-error decision as a pure rule → domain — proposed, not yet landed
pub enum AuthSourceFolderOutcome { Apply(PathBuf), Reject(String) }
pub fn decide_auth_source_folder(kind: Option<AuthKind>, committed: PathBuf) -> AuthSourceFolderOutcome;
```

`decide_auth_source_folder` would wrap the already-shared `validate_auth_source_folder` (<RepoFile path="crates/jackin/src/console.rs" />) → runtime gate `validate_sync_source_dir` (<RepoFile path="crates/jackin-instance/src/auth.rs" />). The plan: one `handle_auth_modal<H>` and one of each `apply_*`/`open_*`/`restore_*` would replace the auth twins, with the same shape generalizing to the other domains.

Infra modals that the editor shares with non-auth flows (`TextInput` for mount destinations and env keys; the env-value `OpPicker`) are meant to stay surface-owned and reached through host openers; they would not fold into a domain modal.

## Module placement [#module-placement]

Per the [TUI architecture](/reference/tui/): the shared config-surface model + a `ConfigModal` enum would live in `jackin-console` beside the existing shared components; pure decision/flatten logic lives in the screens' update modules (already partly there); dispatch/apply helpers would move into a shared `jackin-console` update module over time. The current handlers live in the app crate's input layer (<RepoFile path="crates/jackin-console/src/tui/input/editor.rs" />, <RepoFile path="crates/jackin-console/src/tui/input/global_mounts.rs" />); any move toward a `jackin-console` update layer should stage so each slice stays small and reviewable. This also relates to the [Split input/editor.rs](/roadmap/split-input-editor/) item; the app command-dispatch split has already landed there, so the remaining sequencing concern is keeping the editor/settings split from duplicating one implementation.

## Feasibility [#feasibility]

Per the project's correctness-first rule, the only valid reason to stop short is a proven impossibility. None is known.

* The widgets are already shared once; the duplication is wiring. The auth slice's plain shared-validation-function step already proves a collapse of the validate/error path is possible without new generic machinery.
* The two screens differ only in the layer set they edit — a data difference expressible behind `ConfigScope`, not behavioral.
* The state structs are already generic in `jackin-console`, so embedding shared modal/row types would need no new generic machinery.

Real constraints (capability, not cost): infra modals serve non-auth flows and should stay surface-owned, reached via host openers; unifying modal/row **data** is independent of unifying **rendering**, so render/footer/mouse convergence is its own increment per domain and must be proven not to regress modal sizing, footer hints, or hit-testing. The trust asymmetry (tab vs confirm flow) and the auth mouse-row-click asymmetry are genuine behavior gaps to reconcile deliberately, not silently drop. If a real block surfaces (for example a borrow/lifetime conflict in a host trait), it should be recorded with the exact error before any symptom-layer fallback.

## Parity invariants (to be verified by cross-surface tests, per domain) [#parity-invariants-to-be-verified-by-cross-surface-tests-per-domain]

For every domain, both screens must be identical on: open gates; modal open/stash/restore; validation accept/reject for the same input; the error dialog (same widget, same title, same dismiss target); apply semantics (set value, refocus, preserve buffers); footer hint text; Esc/cancel target; and the missing-stash path logging via the shared logger (never a silent drop). The plan is a shared test that asserts editor and settings produce identical results for the same operation.

## Decisions [#decisions]

Locked:

* One modal enum for both screens — settings' three modal enums collapse into the editor's unified taxonomy.
* Per-step commits — commit and push after each slice, suite-green and clippy-clean.
* Full collapse of the auth source-folder modal variant is the intended first structural slice.

Open (raise before the relevant slice):

* Where the shared `ConfigModal` / `AuthModal` types live (leaning `jackin-console`, beside the shared widgets).
* Whether the shared dispatch/apply helpers live in a `jackin-console` update module versus the app-crate input layer, and how to stage the move.
* How to reconcile the trust asymmetry (promote the editor to a trust tab, or model both through one trust-confirm flow) and the auth mouse-row-click asymmetry (add row-click to settings, or treat it as a deliberate per-screen affordance).
* Whether the editor's single-`WorkspaceConfig` model and settings' per-tab `*State` model converge on one scoped row model, or stay distinct behind the host trait.

## Related work [#related-work]

* [Unify the settings and workspace-editor surfaces](/roadmap/unify-settings-editor-surfaces/) — the roadmap item this design rationale supports.
* [Auth source-folder sync spec](/reference/developer-reference/specs/auth-source-folder-sync/) — the behavioral invariants the auth slice must preserve.
* [TUI architecture](/reference/tui/) — the model/decision/view split this design follows.
