MingaEditor.Shell behaviour (Minga v0.1.0)

Copy Markdown View Source

Behaviour for pluggable presentation shells.

A shell owns layout, chrome, input routing, and rendering for a specific UX model. The traditional editor, The Board, and headless mode are all shells. The workspace (core editing state) is shared; the shell decides how to present it.

Implementation

Implement all callbacks in a module and set it as the :shell field on MingaEditor.State. The Editor GenServer dispatches to the active shell for presentation concerns.

Available shells

Summary

Types

Why a buffer was added. Shells use this to decide tab presentation.

Shell-specific state. Each shell defines its own struct.

Workspace state (the editing context shared by all shells).

Callbacks

Returns the currently active tab, or nil if the shell has no tabs.

Returns the kind (:file or :agent) of the active tab.

Build chrome (tab bar, modeline, file tree, overlays, etc.).

Compute spatial layout for the current state.

Finds the file tab whose snapshotted workspace has pid as active buffer.

Handle a shell-specific event (tool prompt, nav flash, git status, etc.).

Handle a shell-specific GUI action from the native frontend.

Initialize shell state from config and initial workspace.

Returns the input handler stack for this shell.

A background agent session emitted an event.

The active buffer changed.

Render a complete frame.

Associates a session pid with a tab. Returns updated shell state.

Types

buffer_add_context()

@type buffer_add_context() :: :open | :preview

Why a buffer was added. Shells use this to decide tab presentation.

  • :open — permanent open (file tree, :e, LSP jump, picker confirm). Creates a new tab or switches to an existing one.
  • :preview — transient picker preview. Updates the current tab in-place so navigating the picker doesn't spawn new tabs.

shell_state()

@type shell_state() :: term()

Shell-specific state. Each shell defines its own struct.

workspace()

@type workspace() :: MingaEditor.Workspace.State.t()

Workspace state (the editing context shared by all shells).

Callbacks

active_tab(shell_state)

@callback active_tab(shell_state()) :: MingaEditor.State.Tab.t() | nil

Returns the currently active tab, or nil if the shell has no tabs.

active_tab_kind(shell_state)

@callback active_tab_kind(shell_state()) :: atom()

Returns the kind (:file or :agent) of the active tab.

Shells without tabs return :file (the default content kind).

build_chrome(editor_state, layout, scrolls, cursor_info)

@callback build_chrome(
  editor_state :: term(),
  layout :: MingaEditor.Layout.t(),
  scrolls :: map(),
  cursor_info :: term()
) :: MingaEditor.RenderPipeline.Chrome.t()

Build chrome (tab bar, modeline, file tree, overlays, etc.).

Returns a chrome struct with draw lists for each UI region. The shell decides which chrome elements exist and how they render.

compute_layout(editor_state)

@callback compute_layout(editor_state :: term()) :: MingaEditor.Layout.t()

Compute spatial layout for the current state.

Returns a layout struct with named rectangles for each UI region. The shell decides what regions exist (tab bar, modeline, file tree, editor panes, agent panel, bottom panel, etc.).

find_tab_by_buffer(shell_state, pid)

@callback find_tab_by_buffer(shell_state(), pid()) :: MingaEditor.State.Tab.t() | nil

Finds the file tab whose snapshotted workspace has pid as active buffer.

Returns nil if the shell has no tabs or no matching tab exists.

handle_event(shell_state, workspace, event)

@callback handle_event(shell_state(), workspace(), event :: term()) ::
  {shell_state(), workspace()}

Handle a shell-specific event (tool prompt, nav flash, git status, etc.).

Returns the updated shell state and workspace. The Editor GenServer calls this for events that are presentation concerns, not core editing.

handle_gui_action(shell_state, workspace, action)

@callback handle_gui_action(shell_state(), workspace(), action :: term()) ::
  {shell_state(), workspace()}

Handle a shell-specific GUI action from the native frontend.

Returns the updated shell state and workspace.

init(opts)

@callback init(opts :: keyword()) :: shell_state()

Initialize shell state from config and initial workspace.

Called once during Editor startup. Returns the shell's initial state.

input_handlers(editor_state)

@callback input_handlers(editor_state :: term()) :: %{
  overlay: [module()],
  surface: [module()]
}

Returns the input handler stack for this shell.

Overlay handlers (picker, completion, conflict prompt) sit above the surface and intercept keys first. Surface handlers (dashboard, file tree, agent panel, mode dispatch) handle keys when no overlay claims them. The Editor walks overlays first, then surfaces.

on_agent_event(shell_state, workspace, session_pid, event)

@callback on_agent_event(
  shell_state(),
  workspace(),
  session_pid :: pid(),
  event :: term()
) :: {shell_state(), workspace()}

A background agent session emitted an event.

Called when session_pid is not the active session. The shell updates its presentation state (tab badges, card status, attention flags, etc.).

on_buffer_added(shell_state, workspace, buffer_pid, context)

@callback on_buffer_added(
  shell_state(),
  workspace(),
  buffer_pid :: pid(),
  context :: buffer_add_context()
) :: {shell_state(), workspace()}

A buffer was added to the workspace.

Called after the buffer pid is in workspace.buffers and monitored. The shell decides how to present it (e.g., create/update tabs, route to a card, or ignore). context tells the shell WHY the buffer was added so it can choose the right presentation strategy.

on_buffer_died(shell_state, workspace, dead_pid)

@callback on_buffer_died(shell_state(), workspace(), dead_pid :: pid()) ::
  {shell_state(), workspace()}

A buffer process died.

Called after the dead buffer has been removed from workspace.buffers. The shell cleans up any references (tab entries, card associations, etc.).

on_buffer_switched(shell_state, workspace)

@callback on_buffer_switched(shell_state(), workspace()) :: {shell_state(), workspace()}

The active buffer changed.

Called after workspace.buffers.active has been updated. The shell decides whether to sync the active window, update chrome, etc.

render(editor_state)

@callback render(editor_state :: term()) :: term()

Render a complete frame.

Runs the full render pipeline (content, chrome, compose, emit) and sends commands to the frontend. Returns updated state with cached render data.

set_tab_session(shell_state, tab_id, arg3)

@callback set_tab_session(shell_state(), tab_id :: term(), pid() | nil) :: shell_state()

Associates a session pid with a tab. Returns updated shell state.

No-op for shells without tabs.