10 KiB
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 viewOpenAmbientAgentSession, or conversation listOpenAmbientAgentSession). - Transcript viewer load (from
warp://conversation/{id}or management / conversation-listOpenConversationTranscriptViewer).
Out of scope:
AgentViewEntryBlockclick for CLI agents: this block is only rendered when anAIConversationis inBlocklistAIHistoryModel, which never happens for CLI agent conversations.HistoricalCLIAgentrestore viareplace_loading_pane_with_terminal: only reachable fromRestoreOrNavigateToConversation, whichConversationOrTask::get_open_actiondispatches only for localConversationitems, not cloudTaskitems.
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_sessioncallingset_harnessonAmbientAgentViewModel, which emitsHarnessSelected. TheHarnessSelectedhandler inapp/src/terminal/view/ambient_agent/view_impl.rsinvokesmaybe_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;HarnessSelectedcan fire more than once.is_third_party_harness()— only 3p runs; oz is unchanged. This implicitly checksFeatureFlag::AgentHarness.is_shared_ambient_agent_session()— only the live shared-session context. Load-bearing:HarnessSelectedalso 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.
AgentHarnessdisabled — confirmis_third_party_harness()returns false and both new paths no-op.- Idempotency — a run that spams
HarnessSelectedonly enters agent view once.
Unit:
AgentViewEntryOrigin::ThirdPartyCloudAgentdoes not triggerAmbientAgentViewModel::enter_setup/enter_composing_from_setupwhen passed toenter_agent_view_internal.maybe_enter_agent_view_for_shared_third_party_viewerno-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_viewerwith aCloudConversationData::CLIAgentargument leaves theAgentViewControllerin an active state with the restored block still in the terminal block list.
Risks and mitigations
HarnessSelectedis multi-purpose: fires both on viewer-side task-fetch resolution and on local spawner selector changes. Theis_shared_ambient_agent_session()guard inmaybe_enter_agent_view_for_shared_third_party_vieweris 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
ExitedAgentViewremoves 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_exchangeandis_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
AgentViewEntryBlockandHistoricalCLIAgentrestore paths should also route into agent view for any future flows that materialize CLI agent conversations locally.