# `MingaEditor.State`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga_editor/state.ex#L1)

Internal state for the Editor GenServer.

## Field categories

EditorState fields fall into three categories:

**Workspace fields** live in `state.workspace` (`MingaEditor.Workspace.State`)
and are saved/restored when switching tabs. Each tab carries a snapshot
of the workspace so switching tabs restores the full editing context.

**Shell fields** live in `state.shell_state` (`MingaEditor.Shell.Traditional.State`)
and hold presentation concerns: chrome, overlays, transient UI state.
The active shell module is `state.shell`. See `MingaEditor.Shell` for the
behaviour definition.

**Global fields** are shared across all tabs and never snapshotted:
`port_manager`, `theme`, `render_timer`, `focus_stack`,
`tab_bar`, `capabilities`, `layout`, `modeline_click_regions`,
`tab_bar_click_regions`, `agent`, `picker_ui`, `whichkey`.

## Composed sub-structs

* `MingaEditor.Workspace.State`           — per-tab editing context (buffers, windows, vim, etc.)
* `MingaEditor.Shell.Traditional.State`   — presentation state (nav_flash, hover, dashboard, etc.)
* `MingaEditor.State.Picker`       — picker instance, source, restore index
* `MingaEditor.State.WhichKey`     — which-key popup node, timer, visibility
* `MingaEditor.State.Registers`    — named registers and active register selection

# `backend`

```elixir
@type backend() :: :tui | :native_gui | :headless
```

# `content_context`

```elixir
@type content_context() :: %{
  type: :buffer | :agent,
  display_name: String.t(),
  directory: String.t(),
  dirty: boolean(),
  filetype: atom()
}
```

Display metadata derived from the active window's content type.

Used by title, modeline, and any other subsystem that needs to answer
"what is the user looking at?" without assuming a buffer is active.

# `document_highlight`

```elixir
@type document_highlight() :: Minga.LSP.DocumentHighlight.t()
```

A document highlight range from the LSP server.

# `line_number_style`

```elixir
@type line_number_style() :: :hybrid | :absolute | :relative | :none
```

Line number display style.

# `t`

```elixir
@type t() :: %MingaEditor.State{
  backend: backend(),
  buffer_add_context: MingaEditor.Shell.buffer_add_context(),
  buffer_monitors: %{required(pid()) =&gt; reference()},
  capabilities: MingaEditor.Frontend.Capabilities.t(),
  editing_model: :vim | :cua,
  face_override_registries: %{
    required(pid()) =&gt; MingaEditor.UI.Face.Registry.t()
  },
  focus_stack: [module()],
  font_registry: MingaEditor.UI.FontRegistry.t(),
  git_remote_op:
    {msg_ref :: reference(), task_monitor :: reference(),
     {git_root :: String.t(), success_msg :: String.t(),
      error_prefix :: String.t()}}
    | nil,
  last_cursor_line: non_neg_integer() | nil,
  last_test_command: {String.t(), String.t()} | nil,
  layout: MingaEditor.Layout.t() | nil,
  lsp: MingaEditor.State.LSP.t(),
  message_store: MingaEditor.UI.Panel.MessageStore.t(),
  parser_status: MingaEditor.Shell.Traditional.Modeline.parser_status(),
  pending_quit: :quit | :quit_all | nil,
  port_manager: GenServer.server() | nil,
  render_timer: reference() | nil,
  session: MingaEditor.State.Session.t(),
  shell: module(),
  shell_state:
    MingaEditor.Shell.Traditional.State.t() | MingaEditor.Shell.Board.State.t(),
  space_leader_pending: boolean(),
  space_leader_timer: reference() | nil,
  stashed_board_state: MingaEditor.Shell.Board.State.t() | nil,
  theme: MingaEditor.UI.Theme.t(),
  workspace: MingaEditor.Workspace.State.t()
}
```

# `active_buffer`

```elixir
@spec active_buffer(t()) :: non_neg_integer()
```

Returns the active buffer index.

# `active_content_context`

```elixir
@spec active_content_context(t()) :: content_context()
```

Returns display metadata for the active window's content.

Buffer windows return file/buffer metadata. Agent chat windows return
agent-specific display info. Falls back to buffer metadata when the
active window is nil or unrecognized.

# `active_tab`

```elixir
@spec active_tab(t()) :: MingaEditor.State.Tab.t() | nil
```

# `active_tab_kind`

```elixir
@spec active_tab_kind(t()) :: MingaEditor.State.Tab.kind()
```

# `active_window_struct`

```elixir
@spec active_window_struct(t()) :: MingaEditor.Window.t() | nil
```

Returns the active window struct, or nil if windows aren't initialized.

# `active_window_viewport`

```elixir
@spec active_window_viewport(t()) :: MingaEditor.Viewport.t()
```

Returns the active window's viewport, falling back to `state.workspace.viewport`
when no window is active. Use this for scroll commands that need to
read/write the viewport of the focused window (not the terminal-level
viewport).

# `add_buffer`

```elixir
@spec add_buffer(t(), pid(), keyword()) :: t()
```

Adds a new buffer and makes it the active buffer for the current window.

Thin wrapper around `add_buffer_pure/3` that applies effects inline.

# `add_buffer_pure`

```elixir
@spec add_buffer_pure(t(), pid(), keyword()) :: {t(), [MingaEditor.effect()]}
```

Pure variant of `add_buffer/2`. Returns `{state, effects}` instead of
performing side effects directly.

Generic concerns (buffer pool) are handled here. Shell-specific
presentation logic (tab bar, card routing) is dispatched through
`shell.on_buffer_added/4`. The only effect returned is `{:monitor, pid}`.

The buffer-add context is read from `state.buffer_add_context` (set by
picker preview) or overridden via `opts[:context]`. After dispatch the
field is reset to `:open`.

# `agent`

```elixir
@spec agent(t()) :: MingaEditor.State.Agent.t()
```

# `apply_render_output`

```elixir
@spec apply_render_output(t(), MingaEditor.RenderPipeline.Input.t()) :: t()
```

Applies render pipeline mutations back to the editor state.

The render pipeline updates window caches (invalidation tracking,
context fingerprints), click regions, and layout during rendering.
This function writes those mutations back after the pipeline completes.

The `render_output` is a `RenderPipeline.Input` struct with the mutated
fields. Only `windows`, `shell_state`, and `layout` carry meaningful
changes; other fields are unchanged.

# `bottom_panel`

```elixir
@spec bottom_panel(t()) :: MingaEditor.BottomPanel.t()
```

# `buffer`

```elixir
@spec buffer(t()) :: pid() | nil
```

Returns the active buffer pid.

# `buffers`

```elixir
@spec buffers(t()) :: [pid()]
```

Returns the buffer list.

# `build_agent_tab_defaults`

```elixir
@spec build_agent_tab_defaults(t(), MingaEditor.State.Windows.t(), pid() | nil) ::
  MingaEditor.State.Tab.context()
```

Builds a complete per-tab context for an agent tab.

Used by agent tab creation paths to ensure all `@per_tab_fields` are
populated. Accepts a pre-built `Windows` struct for the agent chat
window and the agent buffer pid.

# `cancel_nav_flash`

```elixir
@spec cancel_nav_flash(t()) :: t()
```

# `clear_status`

```elixir
@spec clear_status(t()) :: t()
```

# `close_buffer_pure`

```elixir
@spec close_buffer_pure(t(), pid()) :: {t(), [MingaEditor.effect()]}
```

Pure variant of `remove_dead_buffer/2`. Returns `{state, effects}` instead
of performing side effects directly.

Removes the pid from the buffer list, clears it from special buffer slots,
switches to another buffer if the active one died, and cleans up the
monitor ref. This function is already pure (no process calls), so the
effects list is always empty.

# `close_dashboard`

```elixir
@spec close_dashboard(t()) :: t()
```

# `close_git_status_panel`

```elixir
@spec close_git_status_panel(t()) :: t()
```

# `dashboard`

```elixir
@spec dashboard(t()) :: MingaEditor.Dashboard.state() | nil
```

# `dismiss_hover_popup`

```elixir
@spec dismiss_hover_popup(t()) :: t()
```

# `find_agent_chat_window`

```elixir
@spec find_agent_chat_window(t()) ::
  {MingaEditor.Window.id(), MingaEditor.Window.t()} | nil
```

Finds the agent chat window in the windows map.

Returns `{win_id, window}` or `nil` if no agent chat window exists.

# `find_buffer_by_path`

```elixir
@spec find_buffer_by_path(t() | map(), String.t()) :: non_neg_integer() | nil
```

Returns the index of the buffer whose file path matches `file_path`, or nil.

Catches `:exit` for each buffer in case a process has died but not yet been
removed from the buffer list.

# `find_tab_by_buffer`

```elixir
@spec find_tab_by_buffer(t(), pid()) :: MingaEditor.State.Tab.t() | nil
```

# `focus_window`

```elixir
@spec focus_window(t(), MingaEditor.Window.id()) :: t()
```

Switches focus to the given window, saving the current cursor to the
outgoing window and restoring the target window's stored cursor.

No-op if `target_id` is already the active window or windows aren't set up.

# `git_status_panel`

```elixir
@spec git_status_panel(t()) ::
  MingaEditor.Frontend.Protocol.GUI.git_status_data() | nil
```

# `hover_popup`

```elixir
@spec hover_popup(t()) :: MingaEditor.HoverPopup.t() | nil
```

# `invalidate_all_windows`

```elixir
@spec invalidate_all_windows(t()) :: t()
```

Invalidates render caches for all windows.

Call when the screen layout changes (file tree toggle, agent panel toggle)
because cached draws contain baked-in absolute coordinates that become
wrong when column offsets shift.

# `monitor_buffer`

```elixir
@spec monitor_buffer(t(), pid()) :: t()
```

Monitors a buffer pid so the Editor receives `:DOWN` when it dies.

Idempotent: if the pid is already monitored, returns state unchanged.

# `monitor_buffers`

```elixir
@spec monitor_buffers(t(), [pid()]) :: t()
```

Monitors a list of buffer pids. Convenience wrapper around `monitor_buffer/2`.

# `nav_flash`

```elixir
@spec nav_flash(t()) :: MingaEditor.NavFlash.t() | nil
```

# `picker_ui`

```elixir
@spec picker_ui(t()) :: MingaEditor.State.Picker.t()
```

# `prompt_ui`

```elixir
@spec prompt_ui(t()) :: MingaEditor.State.Prompt.t()
```

# `put_active_window_viewport`

```elixir
@spec put_active_window_viewport(t(), MingaEditor.Viewport.t()) :: t()
```

Updates the active window's viewport. Falls back to updating
`state.workspace.viewport` when no window is active.

# `rebuild_agent_from_session`

```elixir
@spec rebuild_agent_from_session(t(), MingaEditor.State.Tab.t()) :: t()
```

Rebuilds state.agent from the Session process when switching to an
agent tab. The Session is the source of truth for status, pending
approval, and error. The editor's agent field is a rendering cache,
not a source of truth.

# `remove_dead_buffer`

```elixir
@spec remove_dead_buffer(t(), pid()) :: t()
```

Removes a dead buffer pid from all state locations.

Called from the Editor's `:DOWN` handler. Removes the pid from the buffer
list, clears it from special buffer slots (messages, warnings, help), and
switches to another buffer if the active one died. Also cleans up the
monitor ref.

Thin wrapper around `close_buffer_pure/2` that applies effects inline.

# `restore_tab_context`

```elixir
@spec restore_tab_context(t(), MingaEditor.State.Tab.context()) :: t()
```

Writes a tab context back into the live editor state.

The context carries workspace fields as a flat map. Empty context means a
brand-new tab; we build defaults with the current active buffer and
viewport dimensions.

Backward compatibility: old contexts with nested structure are migrated
to the new flat format via `maybe_migrate_legacy_context/2`.

# `scope_for_active_window`

```elixir
@spec scope_for_active_window(t()) :: atom()
```

Returns the appropriate keymap scope for the active window's content type.

Used when leaving the file tree (toggle, close, navigate right) to restore
the correct scope. Returns :agent for agent chat windows, :editor otherwise.

# `scope_for_content`

```elixir
@spec scope_for_content(
  MingaEditor.Window.Content.t(),
  Minga.Keymap.Scope.scope_name()
) ::
  Minga.Keymap.Scope.scope_name()
```

Derives the keymap scope from a window's content type.

Agent chat windows always use `:agent` scope. Buffer windows use
`:editor` when coming from `:agent` scope, and preserve the current
scope otherwise (e.g., `:file_tree` stays as `:file_tree`).

# `screen_rect`

```elixir
@spec screen_rect(t()) :: MingaEditor.WindowTree.rect()
```

Returns the screen rect for layout computation, excluding the global
minibuffer row and reserving space for the file tree panel when open.

# `scroll_agent_chat_window`

```elixir
@spec scroll_agent_chat_window(t(), integer()) :: t()
```

Scrolls the agent chat window's viewport by `delta` lines and updates
pinned state. Delegates to `Window.scroll_viewport/3`.

Returns the state unchanged if no agent chat window exists.

# `set_agent`

```elixir
@spec set_agent(t(), MingaEditor.State.Agent.t()) :: t()
```

# `set_bottom_panel`

```elixir
@spec set_bottom_panel(t(), MingaEditor.BottomPanel.t()) :: t()
```

# `set_buffer_add_context`

```elixir
@spec set_buffer_add_context(t(), MingaEditor.Shell.buffer_add_context()) :: t()
```

Sets the context for the next `add_buffer` call.

Used by picker preview to mark buffer additions as transient previews
rather than permanent opens. The context is consumed and reset to
`:open` by `add_buffer_pure/3`.

# `set_dashboard`

```elixir
@spec set_dashboard(t(), MingaEditor.Dashboard.state()) :: t()
```

# `set_git_status_panel`

```elixir
@spec set_git_status_panel(t(), map() | nil) :: t()
```

# `set_hover_popup`

```elixir
@spec set_hover_popup(t(), MingaEditor.HoverPopup.t()) :: t()
```

# `set_nav_flash`

```elixir
@spec set_nav_flash(t(), MingaEditor.NavFlash.t()) :: t()
```

# `set_picker_ui`

```elixir
@spec set_picker_ui(t(), MingaEditor.State.Picker.t()) :: t()
```

# `set_prompt_ui`

```elixir
@spec set_prompt_ui(t(), MingaEditor.State.Prompt.t()) :: t()
```

# `set_status`

```elixir
@spec set_status(t(), String.t()) :: t()
```

# `set_tab_bar`

```elixir
@spec set_tab_bar(t(), MingaEditor.State.TabBar.t() | nil) :: t()
```

# `set_tab_session`

```elixir
@spec set_tab_session(t(), MingaEditor.State.Tab.id(), pid() | nil) :: t()
```

# `set_whichkey`

```elixir
@spec set_whichkey(t(), MingaEditor.State.WhichKey.t()) :: t()
```

# `skip_tool_prompt?`

```elixir
@spec skip_tool_prompt?(t(), atom()) :: boolean()
```

Returns true if the given tool should NOT be prompted for installation.

A tool is skipped when it's already declined this session, already
installed, currently being installed, or already in the prompt queue.

# `snapshot_tab_context`

```elixir
@spec snapshot_tab_context(t()) :: MingaEditor.State.Tab.context()
```

Captures the current per-tab fields into a context map.

The returned map is stored in the outgoing tab so it can be restored
when the user switches back.

# `split?`

```elixir
@spec split?(t()) :: boolean()
```

Returns true if the editor has more than one window.

# `start_buffer`

```elixir
@spec start_buffer(String.t()) :: {:ok, pid()} | {:error, term()}
```

Starts a new buffer under the buffer supervisor for the given file path.

# `status_msg`

```elixir
@spec status_msg(t()) :: String.t() | nil
```

# `switch_buffer`

```elixir
@spec switch_buffer(t(), non_neg_integer()) :: t()
```

Switches to the buffer at `idx`, making it active for the current window.

Centralizes `Buffers.switch_to` + window sync so callers don't need to
remember to call `sync_active_window_buffer/1`. Shell-specific
presentation logic (tab label updates, etc.) is dispatched through
`shell.on_buffer_switched/2`.

# `switch_tab`

```elixir
@spec switch_tab(t(), MingaEditor.State.Tab.id()) :: t()
```

Switches to the tab with `target_id`.

Snapshots the current tab's context, stores it, updates the tab bar's
active pointer, and restores the target tab's saved context into the
live editor state. Invalidates layout and window caches since the
entire visual context changes.

Thin wrapper around `switch_tab_pure/2` that applies effects inline.

# `switch_tab_pure`

```elixir
@spec switch_tab_pure(t(), MingaEditor.State.Tab.id()) ::
  {t(), [MingaEditor.effect()]}
```

Pure variant of `switch_tab/2`. Returns `{state, effects}` instead of
performing side effects directly.

Snapshots the current tab's context, updates the tab bar pointer,
restores the target tab's context, invalidates layout. Side effects
(spinner stop/start, agent session rebuild) are returned as effects.

The returned effects list may include:
- `:stop_spinner` — cancel the outgoing agent's spinner timer
- `{:rebuild_agent_session, tab}` — rebuild agent state from session process
- `:start_spinner` — conditionally restart spinner for incoming agent

# `sync_active_window_buffer`

```elixir
@spec sync_active_window_buffer(t()) :: t()
```

Syncs the active window's buffer reference with `state.workspace.buffers.active`.

Call this after any operation that changes `state.workspace.buffers.active` to keep the
window tree consistent. No-op when windows aren't initialized.

# `sync_active_window_cursor`

```elixir
@spec sync_active_window_cursor(t()) :: t()
```

Snapshots the active buffer's cursor into the active window struct.

Call this before rendering split views so inactive windows have a fresh
cursor position for the active window when it becomes inactive later.

# `tab_bar`

```elixir
@spec tab_bar(t()) :: MingaEditor.State.TabBar.t() | nil
```

# `transition_mode`

```elixir
@spec transition_mode(t(), Minga.Mode.mode(), Minga.Mode.state() | nil) :: t()
```

Transitions the editor to a new vim mode.

Convenience wrapper around `VimState.transition/3` that operates on
the full EditorState. This is the preferred API for call sites that
already have an EditorState.

## Examples

    # Simple transition (uses default mode_state):
    EditorState.transition_mode(state, :normal)
    EditorState.transition_mode(state, :insert)

    # With explicit mode_state (required for visual, search, etc.):
    EditorState.transition_mode(state, :visual, %VisualState{...})

# `tree_rect`

```elixir
@spec tree_rect(t()) :: MingaEditor.WindowTree.rect() | nil
```

Returns the screen rect for the file tree panel, or nil if closed.

# `update_picker_ui`

```elixir
@spec update_picker_ui(t(), (MingaEditor.State.Picker.t() -&gt;
                         MingaEditor.State.Picker.t())) :: t()
```

# `update_shell_state`

```elixir
@spec update_shell_state(t(), (MingaEditor.Shell.Traditional.State.t() -&gt;
                           MingaEditor.Shell.Traditional.State.t())) :: t()
```

Applies a function to the shell state and returns the updated state.

# `update_window`

```elixir
@spec update_window(t(), MingaEditor.Window.id(), (MingaEditor.Window.t() -&gt;
                                               MingaEditor.Window.t())) ::
  t()
```

Updates the window struct for the given window id via a mapper function.

# `update_workspace`

```elixir
@spec update_workspace(t(), (MingaEditor.Workspace.State.t() -&gt;
                         MingaEditor.Workspace.State.t())) :: t()
```

Applies a function to the workspace and returns the updated state.

# `whichkey`

```elixir
@spec whichkey(t()) :: MingaEditor.State.WhichKey.t()
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
