115 lines
10 KiB
Markdown
115 lines
10 KiB
Markdown
# REMOTE-1459: Tech Spec — Consistent agent view entry for non-oz cloud conversations
|
|
|
|
## Problem
|
|
|
|
When a user opens a non-oz (claude, gemini) cloud agent conversation, the resulting UX depends on which entry point they used: some land in a shared-session viewer, some land in a plain transcript viewer, and none consistently enter the agent view. For 3p harnesses the block snapshot is a single block in the terminal model with no surrounding agent-view chrome (pane header, exit affordance, details panel), so opening a 3p conversation feels unlike every other agent interaction in Warp.
|
|
|
|
Reference issue: https://linear.app/warpdotdev/issue/REMOTE-1459/fix-agent-view-entry-with-3rd-party-harnesses
|
|
|
|
REMOTE-1454 handled the *locally-started* cloud-mode flow for 3p harnesses (setup v2 UI, queued prompt, harness-command-started transition). This spec handles the two *viewer* entry points 1454 did not cover.
|
|
|
|
## Scope
|
|
|
|
In scope — both converge on the user opening an existing 3p cloud conversation:
|
|
|
|
- Live shared-session viewer join (from `warp://shared_session/{id}`, management view `OpenAmbientAgentSession`, or conversation list `OpenAmbientAgentSession`).
|
|
- Transcript viewer load (from `warp://conversation/{id}` or management / conversation-list `OpenConversationTranscriptViewer`).
|
|
|
|
Out of scope:
|
|
|
|
- `AgentViewEntryBlock` click for CLI agents: this block is only rendered when an `AIConversation` is in `BlocklistAIHistoryModel`, which never happens for CLI agent conversations.
|
|
- `HistoricalCLIAgent` restore via `replace_loading_pane_with_terminal`: only reachable from `RestoreOrNavigateToConversation`, which `ConversationOrTask::get_open_action` dispatches only for local `Conversation` items, not cloud `Task` items.
|
|
|
|
## Design
|
|
|
|
A 3p cloud run has no materialized `AIConversation` on the client — the server API returns only a `SerializedBlock` snapshot for claude/gemini runs. To wrap the harness's content in agent-view chrome, we mint a fresh empty Oz-style `AIConversation` as the agent-view-state *vehicle* and retag the run's blocks and rich content onto that vehicle so they pass `should_hide_block`'s agent view filter.
|
|
|
|
The two entry paths are structurally different and call the entry directly from their respective setup sites:
|
|
|
|
- **Transcript viewer** — harness is known synchronously from `CloudConversationData::CLIAgent`. `load_data_into_transcript_viewer` (`app/src/pane_group/mod.rs`) restores the snapshot, enters agent view, and retags — the snapshot block is in the list before the retag runs.
|
|
- **Live shared-session viewer** — harness is resolved asynchronously by `enter_viewing_existing_session` calling `set_harness` on `AmbientAgentViewModel`, which emits `HarnessSelected`. The `HarnessSelected` handler in `app/src/terminal/view/ambient_agent/view_impl.rs` invokes `maybe_enter_agent_view_for_shared_third_party_viewer`, which enters agent view and retags blocks + pre-agent-view rich content.
|
|
|
|
## Changes
|
|
|
|
### 1. `AgentViewEntryOrigin::ThirdPartyCloudAgent` variant
|
|
|
|
`app/src/ai/blocklist/agent_view/controller.rs`
|
|
|
|
Distinct from `CloudAgent`: `CloudAgent` is emitted when *starting* a new cloud-mode run from the local session, `ThirdPartyCloudAgent` when *viewing* an existing run. The existing `origin == CloudAgent` guard in `enter_agent_view_internal` stays as-is, so the new variant does not touch `AmbientAgentViewModel::enter_setup` / `enter_composing_from_setup` — important because the live shared-session viewer is already in `Status::AgentRunning` and the transcript viewer has status `NotAmbientAgent`.
|
|
|
|
`try_enter_agent_view` adds the new variant to the set of origins exempt from the long-running-command guard, alongside transcript viewers. Without the bypass, agent view entry in the live shared-session viewer path surfaces a "cannot enter agent view while a command is long running" error toast because the harness CLI block is classified as long-running.
|
|
|
|
`enter_agent_view_for_new_conversation` bypasses its "can start new conversation" check for `ThirdPartyCloudAgent` because the vehicle conversation is not a new conversation in the user-visible sense.
|
|
|
|
Telemetry: `TelemetryAgentViewEntryOrigin::ThirdPartyCloudAgent` is added and `From<AgentViewEntryOrigin>` maps the new variant (`app/src/server/telemetry/events.rs`).
|
|
|
|
### 2. Transcript viewer entry — direct call from `load_data_into_transcript_viewer`
|
|
|
|
`app/src/pane_group/mod.rs`
|
|
|
|
The `CloudConversationData::CLIAgent` branch maps `AIAgentHarness` → `warp_cli::agent::Harness`, restores the block snapshot, calls `set_harness` to keep the viewer's `AmbientAgentViewModel::harness` in sync with the loaded run, calls `enter_agent_view_for_new_conversation(None, ThirdPartyCloudAgent, ctx)`, and then calls `attach_non_startup_blocks_to_conversation(vehicle_conversation_id)` to retag the snapshot block. All four steps run inside a single `terminal_view.update` closure so the flow reads linearly.
|
|
|
|
### 3. Live shared-session viewer entry — `HarnessSelected` handler
|
|
|
|
`app/src/terminal/view/ambient_agent/view_impl.rs`
|
|
|
|
`maybe_enter_agent_view_for_shared_third_party_viewer` is called from the existing `HarnessSelected` arm of `handle_ambient_agent_event`. Guards:
|
|
|
|
- `!agent_view_state.is_active()` — idempotency; `HarnessSelected` can fire more than once.
|
|
- `is_third_party_harness()` — only 3p runs; oz is unchanged. This implicitly checks `FeatureFlag::AgentHarness`.
|
|
- `is_shared_ambient_agent_session()` — only the live shared-session context. Load-bearing: `HarnessSelected` also fires on the local spawner's harness selector dropdown, and there the REMOTE-1454 flow handles entry; the transcript viewer path calls the entry directly (§2) so we intentionally do not handle it here.
|
|
|
|
After entering agent view, it retags non-startup blocks via `attach_non_startup_blocks_to_conversation` and retags pre-existing rich content (setup-commands summary, tombstone, …) via `set_rich_content_agent_view_conversation_id`. Retagging rich content is necessary because items with `agent_view_conversation_id == None` are hidden in fullscreen agent view by `RichContentItem::should_hide_for_agent_view_state`.
|
|
|
|
### 4. `DispatchedAgent` short-circuit for viewer surfaces
|
|
|
|
`app/src/terminal/view/ambient_agent/view_impl.rs`
|
|
|
|
`DispatchedAgent` is only meaningful on the spawner's view. The handler now short-circuits for shared-ambient-session viewers and transcript viewers to make this explicit and avoid ever inserting cloud-mode rich content on a viewer.
|
|
|
|
### 5. Zero-state block suppression
|
|
|
|
`app/src/terminal/view.rs`
|
|
|
|
`ThirdPartyCloudAgent` is added to the `EnteredAgentView` handler's skip list alongside `CreateEnvironment` and `SlashInit`, so no `AgentViewZeroStateBlock` is inserted for 3p viewer entries.
|
|
|
|
### 6. Block visibility retag — `BlockList::attach_non_startup_blocks_to_conversation`
|
|
|
|
`app/src/terminal/model/blocks.rs`
|
|
|
|
Walks the blocks and calls `Block::add_attached_conversation_id(conversation_id)` on every block that isn't flagged `is_oz_environment_startup_command` (REMOTE-1454's setup command rows, which are hidden by their own mechanism). Skips blocks already tagged with this conversation as `origin_conversation_id`. Shares AI / agent-view rich content dirty bookkeeping with `set_agent_view_state` via the private `mark_agent_view_rich_content_dirty` helper.
|
|
|
|
### 7. Rich content retag — `TerminalView::set_rich_content_agent_view_conversation_id`
|
|
|
|
`app/src/terminal/view/agent_view.rs`
|
|
|
|
Generalized from the old `move_ai_block_to_agent_view_conversation`. Updates both the local `rich_content_views` entry and the block list's `update_agent_view_conversation_id_for_rich_content` so `should_hide_for_agent_view_state` picks up the new association. Existing call sites in `terminal/view.rs` are updated to use the new name.
|
|
|
|
## Testing and validation
|
|
|
|
Manual:
|
|
|
|
- **Claude / Gemini, live, shared session link** — start a run on one client, join from another via `warp://shared_session/{id}`. Confirm the joining client lands in agent view.
|
|
- **Claude / Gemini, live, management view and conversation list** — same via `OpenAmbientAgentSession`.
|
|
- **Claude / Gemini, completed, transcript viewer** — open a completed task from management view / conversation list. Confirm the transcript viewer opens in agent view with the block snapshot as the content.
|
|
- **Claude / Gemini, completed, conversation link** — same via `warp://conversation/{id}`.
|
|
- **Oz** — repeat the live and completed cases; confirm no regression on any branch we didn't touch.
|
|
- **`AgentHarness` disabled** — confirm `is_third_party_harness()` returns false and both new paths no-op.
|
|
- **Idempotency** — a run that spams `HarnessSelected` only enters agent view once.
|
|
|
|
Unit:
|
|
|
|
- `AgentViewEntryOrigin::ThirdPartyCloudAgent` does not trigger `AmbientAgentViewModel::enter_setup` / `enter_composing_from_setup` when passed to `enter_agent_view_internal`.
|
|
- `maybe_enter_agent_view_for_shared_third_party_viewer` no-ops when agent view is already active, when the harness is oz, and when the context is not a shared ambient agent session.
|
|
- `load_data_into_transcript_viewer` with a `CloudConversationData::CLIAgent` argument leaves the `AgentViewController` in an active state with the restored block still in the terminal block list.
|
|
|
|
## Risks and mitigations
|
|
|
|
- **`HarnessSelected` is multi-purpose**: fires both on viewer-side task-fetch resolution and on local spawner selector changes. The `is_shared_ambient_agent_session()` guard in `maybe_enter_agent_view_for_shared_third_party_viewer` is load-bearing.
|
|
- **Empty vehicle conversation in the conversation list**: the fresh Oz conversation appears in the user's conversation list until they exit agent view, at which point the standard empty-conversation cleanup in `ExitedAgentView` removes it. Since the vehicle has 0 exchanges, existing empty-conversation handling hides fork / copy-link affordances and title fallbacks are never consulted (pane title comes from the harness CLI's terminal-title escape).
|
|
- **Setup-command rendering on the live viewer**: REMOTE-1454's setup-command summary and pending-prompt block key off `is_cloud_agent_pre_first_exchange` and `is_third_party_harness`. Entering agent view does not change the exchange count or harness-command-started state, so no cross-interaction.
|
|
|
|
## Follow-ups
|
|
|
|
- Revisit whether `AgentViewEntryBlock` and `HistoricalCLIAgent` restore paths should also route into agent view for any future flows that materialize CLI agent conversations locally.
|