Files
galaxy/specs/terminal-manager-view-abstraction/TECH.md
T

11 KiB

Terminal manager view abstraction — TECH

Context

The local TTY terminal manager now has a reusable construction path for terminal frontends. The GUI remains the only frontend in this change, but the manager no longer needs to treat TerminalView as part of the object-safe manager contract.

The pre-change local manager was tightly coupled to TerminalView:

The current design splits the terminal manager responsibilities into three layers:

  1. TerminalSurface and PtyIntent define the narrow frontend-to-PTY boundary.
  2. TerminalManager<S> owns local terminal model/session/controller/event-loop state for any terminal surface.
  3. TerminalManager<S>::create_model is the generic model-producing construction path. The current GUI pane system calls it with S = TerminalView and a GUI surface setup callback.

Proposed changes

Terminal surface API

app/src/terminal/writeable_pty/terminal_surface.rs defines PtyIntent, PtyIntentEvent, and TerminalSurface.

PtyIntent is the only event vocabulary the generic PTY wiring understands. It is intentionally limited to PTY/session-driving actions: process control, byte writes, resize, command execution, and native shell completions. UI events, pane orchestration, remote-server choice UI, and shared-session protocol events remain on concrete surface event types.

PtyIntentEvent preserves the From<&SurfaceEvent> for Option<PtyIntent> projection pattern while hiding the higher-ranked bound behind a simple method:

pub(crate) trait PtyIntentEvent {
    fn pty_intent(&self) -> Option<PtyIntent>;
}

impl<T> PtyIntentEvent for T
where
    for<'a> Option<PtyIntent>: From<&'a T>,
{
    fn pty_intent(&self) -> Option<PtyIntent> {
        Option::<PtyIntent>::from(self)
    }
}

TerminalSurface is the frontend contract consumed by TerminalManager<S>. It requires a surface event type that implements PtyIntentEvent and lifecycle hooks for shell startup, PTY spawn failure, launch-data updates, and Unix password-prompt polling.

TerminalView implements this contract and maps only the existing PTY-driving terminal::view::Event variants to PtyIntent:

  • CtrlD
  • ShutdownPty
  • WriteBytesToPty
  • WriteAgentInputToPty
  • Resize
  • ExecuteCommand
  • RunNativeShellCompletions

Every other TerminalView event maps to None.

View-agnostic terminal manager trait

crate::terminal::TerminalManager no longer exposes view() -> ViewHandle<TerminalView>. The object-safe trait now describes terminal model/session lifecycle:

  • model()
  • on_view_detached(...)
  • as_any()
  • as_any_mut()

Constructors that create a TerminalView return the view separately alongside the boxed manager. This keeps the boxed manager view-agnostic while preserving existing pane code that needs the TerminalView handle.

Current constructors that return (manager, surface):

  • local_tty::TerminalManager<S>::create_model, called by the GUI path as TerminalManager::<TerminalView>::create_model
  • remote_tty::TerminalManager::create_model
  • MockTerminalManager::create_model
  • shared_session::viewer::TerminalManager::new
  • shared_session::viewer::TerminalManager::new_deferred

Generic local manager construction

app/src/terminal/local_tty/terminal_manager.rs defines the local manager as:

pub struct TerminalManager<S> {
    view: ViewHandle<S>,
    // model, controllers, event-loop state, and retained handles
}

TerminalManager<S>::create_model(...) is the canonical local terminal construction API. It owns the local terminal session construction order, creates the manager-owned channels/models/controllers internally, calls one surface setup callback that returns (surface, post_wire), wires the surface to the PtyController through wire_up_pty_controller_with_surface, runs post_wire, boxes the manager as a WarpUI model, schedules shell determination, and returns (manager_model, surface).

The surface factory callback receives only the components a surface needs:

  • TerminalModel
  • Sessions
  • ModelEventDispatcher
  • wakeup receiver
  • inactive PTY reads receiver
  • terminal colors
  • current terminal size

The manager stores the remaining startup and lifetime components itself, including the event-loop sender/receiver, channel event proxy, PtyController, RemoteServerController, and shell-starter source. Shell determination later consumes that stored startup state from the manager after it has been registered as a WarpUI model.

The surface setup callback can return a deferred post_wire closure for surface-specific wiring. The constructor runs that closure after the PTY controller is wired and the manager has been assembled. This keeps ordering-sensitive post-wiring inside the manager-owned construction flow without requiring the generic manager core to import TerminalView.

For the GUI caller, TerminalManager::<TerminalView>::create_model(...) now:

  1. Resolves GUI-specific restored blocks from explicit restored blocks and conversation restoration.
  2. Uses a surface setup callback that creates CurrentPrompt, PromptType, and TerminalView from the manager-created surface components, then returns (view, post_wire).
  3. The returned post_wire closure appends the GUI restoration separator when needed, wires remote-server choice UI, and wires TerminalView-specific session sharing.
  4. The generic constructor boxes the manager, schedules shell determination, and returns (manager_model, terminal_view).

This keeps the end-to-end local terminal construction protocol in TerminalManager<S>::create_model while keeping GUI-specific work in the TerminalView surface setup callback. A future TUI surface can call the same function with a different surface setup callback.

Generic PTY wiring

wire_up_pty_controller_with_surface<T, S>(...) replaces the direct TerminalView wiring. It subscribes to the surface event stream, calls event.pty_intent(), and handles only PtyIntent values.

The behavior of each intent matches the old TerminalView event match:

  • raw bytes and agent bytes write to the PTY controller
  • resize resizes the PTY
  • command execution resolves shell type from Sessions, sets workflow state, writes the command, and updates command history when requested
  • native completions call through to PtyController::run_native_shell_completions
  • PTY disconnection exits the TerminalModel

wire_up_remote_server_controller_with_view remains GUI-specific because the remote-server install/skip choice is rendered as TerminalView rich content.

TerminalView-specific session sharing boundary

Local session sharing remains GUI-specific in this change. The reusable lower layers stay unchanged:

  • shared_session::sharer::Network
  • ordered terminal event flow from TerminalModel
  • existing shared-session handler helpers

The local sharer wiring is grouped behind wire_up_terminal_view_session_sharing(...). That helper owns the current TerminalView adapter responsibilities:

  • prompt updates
  • presence selection
  • LLM/input-mode/conversation broadcasts
  • AgentView and active-agent registration
  • CLI-agent session broadcasts
  • local sharer Network setup
  • network status UI reactions

shared_session::manager::Manager remains GUI-shaped and continues storing TerminalView handles. The sharing boundary is now easier to locate and refactor later without making this change TUI-sharing-ready.

Unix password-prompt polling

The local manager still owns the Unix termios poller, but the surface decides whether polling is useful and how to react.

The poller now subscribes to ModelEventDispatcher rather than TerminalView events:

  • ModelEvent::AfterBlockStarted { is_for_in_band_command: false, .. } records the active block index and starts polling only if surface.should_poll_for_password_prompt(ctx) returns true.
  • ModelEvent::BlockCompleted(completed) stops polling and calls surface.on_polled_block_completed(completed, ctx).
  • TerminalAttributesPollerEvent::TermiosQueryFinished checks for ECHO off and ICANON on, calls surface.on_possible_password_prompt(block_index, ctx), and stops polling after the first detected prompt.

For TerminalView, these hooks preserve existing password notification and SSH upload behavior.

Testing and validation

Run formatting and compile checks:

  • ./script/format
  • cargo check -p warp
  • cargo clippy -p warp --all-targets --tests -- -D warnings

Run focused tests covering:

  • terminal manager/view constructor paths
  • PTY write and command execution events
  • pane creation paths that now receive the TerminalView separately
  • shared-session start/stop behavior

Validation performed during implementation:

  • ./script/format
  • cargo check -p warp

Parallelization

Do not parallelize implementation across child agents. The core changes touch the same local manager, object-safe trait, PTY wiring, and pane construction call sites; parallel work would create overlapping edits.

Validation can be performed separately after the code compiles.

Risks and mitigations

  • Surface boundary grows too broad. Keep PtyIntent limited to PTY/session-driving actions and leave pane, UI, remote-server choice, and shared-session protocol events on concrete surface event types.
  • Constructor churn breaks pane creation. Return TerminalView explicitly from GUI constructors and update pane helpers at the same call sites that previously used manager.view().
  • Shared-session behavior regresses. Keep protocol/model layers unchanged and move the GUI-specific sharer setup as a group rather than rewriting it.
  • Password-prompt behavior changes. Route the same termios detection through explicit TerminalSurface hooks and preserve one-notification-per-command behavior.

Out of scope

  • No TUI backend or LaunchMode::Tui.
  • No terminal-history rendering, virtual list, or key-passthrough work.
  • No generic shared-session frontend abstraction.
  • No blanket object-safe trait implementation for every TerminalManager<S>; current detach behavior is still specific to the TerminalView manager.