Files
galaxy/specs/APP-4274/TECH.md
T

9.3 KiB
Raw Blame History

Refactor AmbientAgentViewModel to be cloud-agent scoped

Linear: APP-4274

Context

This refactor simplifies the relationship between AmbientAgentViewModel, AgentViewController, and cloud-mode terminal views. Before the change, AmbientAgentViewModel existed more broadly than its actual responsibility. It represented cloud-agent-specific state, but it could also be present in local/non-cloud agent flows, which made TerminalView, Input, and AgentViewController look like they all owned parallel pieces of agent state. This made it unclear which abstraction was the source of truth for local Agent View state, cloud-agent lifecycle state, root vs nested cloud-mode navigation state, and cloud-only UI like harness selection and ambient progress. The final model is:

  • AgentViewController owns local/fullscreen/inline Agent View conversation state.
  • AmbientAgentViewModel owns cloud-agent conversation/session state only.
  • TerminalView optionally has an AmbientAgentViewModel only when the terminal represents cloud mode.
  • Root vs nested cloud mode is derived from PaneStack, not stored as parallel navigation state. Relevant files:
  • app/src/terminal/view.rs:2849TerminalView::is_nested_cloud_mode
  • app/src/terminal/view.rs:2970TerminalView::new(..., is_cloud_mode: bool, ...)
  • app/src/terminal/input.rs:1660Input.ambient_agent_view_state
  • app/src/terminal/input.rs:1669 — private AmbientAgentViewState
  • app/src/terminal/input.rs:2176 — construction of AmbientAgentViewState
  • app/src/terminal/view/pane_impl.rs:939TerminalView::is_ambient_agent_session
  • app/src/terminal/view/ambient_agent/model.rs:77AmbientAgentViewModel
  • app/src/terminal/view_test.rs:338 — root/nested cloud-mode keymap regression test

Goals

Make AmbientAgentViewModel cloud-scoped, avoid constructing dummy ambient models for local/non-cloud Agent View flows, keep cloud-agent state attached to the terminal/view entry for the cloud conversation, remove denormalized cloud navigation state from TerminalView, make the schema encode invariants around cloud-only UI state, and preserve cloud setup, composing, spawning, shared-session viewer, progress, and cancellation flows.

Non-goals

This does not redesign AgentViewController, change local Agent View behavior except by removing ambient-model coupling, change cloud-agent server APIs or task spawning semantics, or change PaneStack ownership semantics.

Proposed changes

AmbientAgentViewModel is optional on TerminalView

TerminalView::new takes is_cloud_mode: bool and constructs an ambient model only when that flag is true. Cloud-mode terminals get Some(ModelHandle<AmbientAgentViewModel>); normal/local terminals get None. This makes the presence of AmbientAgentViewModel meaningful: if it exists, the terminal is capable of cloud-agent UI and lifecycle behavior.

AgentViewController no longer carries ambient model state

AgentViewController remains responsible for Agent View entry/exit/conversation display state. Cloud-agent-specific progress, setup, harness, task, environment, and cancellation state lives in AmbientAgentViewModel. This removes the previous parallel-state ambiguity where local Agent View and ambient-agent state appeared coupled even when local flows did not need ambient state.

AmbientAgentViewModel represents only cloud-agent lifecycle state

AmbientAgentViewModel starts in a cloud-relevant state (Composing) and tracks cloud-only concepts:

  • setup/composing/waiting/running/failure/cancelled status
  • selected cloud environment
  • selected harness
  • spawned task ID
  • cloud-agent request
  • progress timing
  • setup command state Because non-cloud terminals do not construct this model, the model no longer needs a “not ambient agent” status variant or parent-terminal bookkeeping.

Input groups ambient-only UI state

Input now stores ambient_agent_view_state: Option<AmbientAgentViewState> instead of independent optional ambient-model and harness-selector fields. AmbientAgentViewState contains:

  • view_model: ModelHandle<AmbientAgentViewModel>
  • harness_selector: ViewHandle<HarnessSelector> The grouped state is constructed only when ambient_agent_view_model exists. This encodes the invariant directly: no ambient model means no harness selector, and if a harness selector exists then an ambient model exists. Accessors on Input preserve existing call-site ergonomics:
  • ambient_agent_view_model()
  • harness_selector()

Cloud navigation is derived from PaneStack

The refactor removes CloudAgentNavigation and TerminalView.cloud_agent_navigation. TerminalView::is_nested_cloud_mode now checks:

  1. The terminal is an ambient/cloud session.
  2. The terminal has an owning PaneStack.
  3. The terminals view appears in the stack.
  4. The terminal is not the first entry. This makes PaneStack the source of truth:
  • first stack entry = root cloud-mode pane
  • later stack entries = nested cloud-mode panes
  • no stack = not nested This avoids stale cached navigation state and handles root/nested state consistently after push/pop. The important subtlety is that stack depth alone is insufficient: a root pane in a stack with depth greater than one is still root. The implementation checks the terminals actual position in the stack instead.

Root cloud-mode keymap context uses derived state

The root cloud-mode key is set only when the terminal is an ambient/cloud session and is not nested according to PaneStack. This preserves the intended behavior for bindings like setting input mode to Agent Mode: root cloud-mode panes should not accidentally enter local Agent View.

Pane chrome and cloud-mode entry use derived nested state

The pane header and cloud-agent entry paths now consult is_nested_cloud_mode instead of cached navigation state. Important behavior:

  • nested cloud-mode panes show parent/back navigation where appropriate
  • starting cloud mode from a nested cloud-mode pane can pop back to the parent and start a sibling run
  • root cloud-mode panes remain distinguishable from nested panes even when the stack depth is greater than one

End-to-end flow

Local/non-cloud Agent View

  1. TerminalView::new(..., is_cloud_mode: false, ...)
  2. ambient_agent_view_model = None
  3. Input.ambient_agent_view_state = None
  4. AgentViewController handles Agent View state
  5. Cloud-only UI/state is absent

Cloud-mode terminal

  1. TerminalView::new(..., is_cloud_mode: true, ...)
  2. AmbientAgentViewModel is constructed for that terminal view
  3. Input creates AmbientAgentViewState
  4. Cloud setup/composing/progress/cancellation use the ambient model
  5. Root vs nested navigation is derived from PaneStack

Nested cloud-mode pane

  1. A cloud-mode terminal is pushed onto a PaneStack
  2. PaneStack::push calls TerminalView::set_pane_stack
  3. is_nested_cloud_mode finds the terminals index in PaneStack::entries
  4. Index > 0 means nested
  5. Keymap/chrome/cloud-entry behavior follows from that derived state

Review comment resolutions

Group AmbientAgentViewModel and HarnessSelector

Resolved by introducing private AmbientAgentViewState in Input. This prevents impossible/ambiguous optional-state combinations and makes the schema match the real invariant.

Remove denormalized cloud navigation

Resolved by removing CloudAgentNavigation and TerminalView.cloud_agent_navigation. Root/nested state now comes from the owning PaneStack.

Testing and validation

Focused validation passed:

  • cargo test -p warp root_cloud_mode_pane_sets_root_cloud_mode_context_key --features local_tty,local_fs
  • cargo test -p warp set_input_mode_agent_does_not_enter_local_agent_from_root_cloud_mode_pane --features local_tty,local_fs
  • cargo check -p warp --features local_tty,local_fs The key regression test is root_cloud_mode_pane_sets_root_cloud_mode_context_key in app/src/terminal/view_test.rs:338. It verifies:
  1. A standalone cloud-mode terminal gets ROOT_CLOUD_MODE_PANE_KEY.
  2. After creating a PaneStack, the root terminal still gets the key.
  3. A pushed nested cloud-mode terminal does not get the key. The test keeps the PaneStack handle alive through assertions so TerminalViews weak stack handle can upgrade during keymap evaluation.

Risks and mitigations

Optional ambient model requires many call sites to handle None

Call sites use optional accessors or only pass the model into cloud-capable components when present. This makes non-cloud behavior explicit rather than hidden behind dummy state.

Deriving nested state from PaneStack could misclassify panes

The helper checks stack membership and entry index, not only stack depth. This keeps root panes root even when they have nested children.

Cloud shared-session viewers still need ambient state

Shared-session viewer construction passes is_cloud_mode through to TerminalView::new, so cloud-mode shared session viewers still construct AmbientAgentViewModel.

Follow-ups

  • Cloud UI verification requires pushing the branch so a cloud agent can build/test the changed client state.
  • If more cloud-only UI state is added to Input, it should live under AmbientAgentViewState rather than as independent optional fields.
  • If more root/nested cloud behavior appears, it should continue to derive from PaneStack rather than reintroducing cached navigation state.