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:
app/src/terminal/writeable_pty/terminal_manager_util.rs:23 @ 0b2273dawiredPtyControllerdirectly toViewHandle<TerminalView>and matchedterminal::view::Eventvariants for PTY writes, resize, command execution, and native completions.app/src/terminal/local_tty/terminal_manager.rs:139 @ 0b2273dastoredViewHandle<TerminalView>directly and called GUI-specific lifecycle hooks for shell startup, spawn failures, shell launch-data updates, and Unix terminal-attributes/password-prompt handling.app/src/terminal/terminal_manager.rs:24 @ 0b2273daexposedview() -> ViewHandle<TerminalView>from the object-safeTerminalManagertrait, so callers recovered the GUI view from the boxed manager.
The current design splits the terminal manager responsibilities into three layers:
TerminalSurfaceandPtyIntentdefine the narrow frontend-to-PTY boundary.TerminalManager<S>owns local terminal model/session/controller/event-loop state for any terminal surface.TerminalManager<S>::create_modelis the generic model-producing construction path. The current GUI pane system calls it withS = TerminalViewand 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:
CtrlDShutdownPtyWriteBytesToPtyWriteAgentInputToPtyResizeExecuteCommandRunNativeShellCompletions
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 asTerminalManager::<TerminalView>::create_modelremote_tty::TerminalManager::create_modelMockTerminalManager::create_modelshared_session::viewer::TerminalManager::newshared_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:
TerminalModelSessionsModelEventDispatcher- 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:
- Resolves GUI-specific restored blocks from explicit restored blocks and conversation restoration.
- Uses a surface setup callback that creates
CurrentPrompt,PromptType, andTerminalViewfrom the manager-created surface components, then returns(view, post_wire). - The returned
post_wireclosure appends the GUI restoration separator when needed, wires remote-server choice UI, and wiresTerminalView-specific session sharing. - 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
Networksetup - 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 ifsurface.should_poll_for_password_prompt(ctx)returns true.ModelEvent::BlockCompleted(completed)stops polling and callssurface.on_polled_block_completed(completed, ctx).TerminalAttributesPollerEvent::TermiosQueryFinishedchecks for ECHO off and ICANON on, callssurface.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/formatcargo check -p warpcargo 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
TerminalViewseparately - shared-session start/stop behavior
Validation performed during implementation:
./script/formatcargo 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
PtyIntentlimited 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
TerminalViewexplicitly from GUI constructors and update pane helpers at the same call sites that previously usedmanager.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
TerminalSurfacehooks 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 theTerminalViewmanager.