# REMOTE-1454: Tech Spec — Cloud Mode setup UI for non-oz harnesses ## Problem The `CloudModeSetupV2` setup UI only transitions out of the "setup commands" phase when an Oz `AppendedExchange` event fires. Non-oz harness runs (claude, gemini) never produce an Oz exchange — the sandboxed `oz agent run --harness=` invokes the harness CLI (e.g. `claude --session-id … < /tmp/oz_prompt`) as a shell command in the shared session, and that command is permanently flagged as an environment startup command. See `specs/REMOTE-1454/PRODUCT.md` for the desired UX. ## Relevant code - `crates/warp_features/src/lib.rs:757,826` — `FeatureFlag::AgentHarness`, `FeatureFlag::CloudModeSetupV2`. - `app/src/terminal/view/ambient_agent/model.rs:79-151,240-250,384-418,442-488,726-727,886-899` — `AmbientAgentViewModel` (status, `harness` field, `spawn_agent`, `enter_viewing_existing_session`, `AmbientAgentViewModelEvent`, `DispatchedAgent` emission). - `app/src/terminal/view/ambient_agent/view_impl.rs:91-228,230-300` — `handle_ambient_agent_event` (inserts `CloudModeInitialUserQuery` on `DispatchedAgent`), `maybe_insert_setup_command_blocks` (gated on `is_cloud_agent_pre_first_exchange`). - `app/src/terminal/view/ambient_agent/mod.rs:81-114` — `is_cloud_agent_pre_first_exchange` helper (checks `exchange_count() == 0`). - `app/src/terminal/view/ambient_agent/block/query.rs` — `CloudModeInitialUserQuery` rich content (top-of-conversation prompt block). - `app/src/terminal/view/ambient_agent/block/setup_command_text.rs:122-157` — summary row copy keys off `is_cloud_agent_pre_first_exchange`. - `app/src/terminal/view/ambient_agent/block/setup_command.rs:56-91` — per-command row, listens for `UpdatedSetupCommandVisibility` and `BlockCompleted`. - `app/src/terminal/model/terminal_model.rs:1213-1241` — `new_for_cloud_mode_shared_session_viewer` sets `is_executing_oz_environment_startup_commands = true` when `CloudModeSetupV2` is enabled. - `app/src/terminal/model/blocks.rs:1284-1302,2486-2489` — block-list flag and per-block `is_oz_environment_startup_command`. - `app/src/terminal/model/block.rs:1462-1468` — `Block::is_oz_environment_startup_command` / setter. - `app/src/terminal/view.rs:5042-5053` — existing `AppendedExchange` handler that flips the block-list flag off (Oz path). - `app/src/terminal/view.rs:10413` — `ModelEvent::AfterBlockStarted` branch calls `maybe_insert_setup_command_blocks(block_id, ctx)`. - `app/src/terminal/view.rs:6609-6634,6785-6793,6841-6863` — uses of `is_cloud_agent_pre_first_exchange` (tombstone, input visibility, input hide logic). - `app/src/ai/blocklist/block/pending_user_query_block.rs` — `PendingUserQueryBlock` view, takes `interruptible: bool`, always renders a close button plus optional "Send now". - `app/src/terminal/view/pending_user_query.rs` — `TerminalView::insert_pending_user_query_block`, `remove_pending_user_query_block`, `send_user_query_after_next_conversation_finished`. Tracked by `TerminalView::pending_user_query_view_id`. - `app/src/terminal/view.rs:2789-2796,4160` — `TerminalView::pending_user_query_view_id`, `queued_prompt_callback` fields. - `app/src/terminal/view.rs:3237-3246,4648-4650` — existing removal paths for the pending-query block on agent-view/conversation transitions. - `app/src/ai/ambient_agents/task.rs:26-122,212-238` — `AgentConfigSnapshot.harness: Option`, `AmbientAgentTask.agent_config_snapshot`. - `app/src/terminal/cli_agent.rs:107-260` — `CLIAgent` enum (`Claude`, `Gemini`, …) with `command_prefix`, `CLIAgent::detect`. - `app/src/terminal/view/use_agent_footer/mod.rs:354-387` — `TerminalView::detect_cli_agent_from_model` (returns the `CLIAgent` for the active long-running block using alias-aware detection). - `app/src/terminal/shared_session/viewer/terminal_manager.rs:871-896,1186-1209` — `is_cloud_agent_pre_first_exchange` use in remote input / input-mode gating. - `app/src/terminal/view/shared_session/view_impl.rs:1700-1721` — viewer-driven sizing helpers: `restore_pty_to_sharer_size`, `resize_from_viewer_report`, and the new `force_report_viewer_terminal_size` entry point used at harness start. - `app/src/terminal/view/ambient_agent/block/setup_command.rs:176-197` — per-command row, currently renders via `with_agent_output_item_spacing`; needs a third-party-harness branch that uses `*terminal::view::PADDING_LEFT` horizontal margins. - `app/src/terminal/view/ambient_agent/block/setup_command_text.rs:184-206` — summary row, same story as the per-command row. - `app/src/ai/blocklist/block/view_impl.rs:1264-1312` — `CONTENT_HORIZONTAL_PADDING`, `CONTENT_ITEM_VERTICAL_MARGIN`, and the `WithContentItemSpacing::with_agent_output_item_spacing` helper used by the oz setup UI. - `app/src/terminal/view.rs:746-752` — `PADDING_LEFT` (20px, or 16px behind `LessHorizontalTerminalPadding`) — the horizontal padding used by regular terminal command blocks. - `app/src/pane_group/mod.rs:3763-3792,5682-5692` — CLI-agent conversation restoration (`FeatureFlag::AgentHarness` gate for replay). - `warp_cli::agent::Harness` (`crates/warp_cli/src/agent.rs:120-131`) — `Oz`, `Claude`, `Gemini`. ## Current state `AmbientAgentViewModel::harness` is populated on spawn for the local spawner via the harness selector. For viewers that join a shared cloud session, the field is left at its default `Oz` even for claude / gemini runs — `enter_viewing_existing_session` fetches the task but only reads `agent_config_snapshot.environment_id`. The viewer therefore reports the wrong harness until this is fixed. When the cloud-mode terminal model is created, `is_executing_oz_environment_startup_commands` is set to `true` unconditionally under `CloudModeSetupV2`. The block-list flag: - Marks every new block `is_oz_environment_startup_command = true` and hides it. - Is flipped to `false` only in `ModelEvent::AfterBlockStarted`'s sibling `BlocklistAIHistoryEvent::AppendedExchange` handler (view.rs:5042-5053). `is_cloud_agent_pre_first_exchange` is a view-level helper that returns `true` while the active cloud-agent conversation has `exchange_count() == 0`. It gates: the "Setting up environment" warping indicator, the pre-harness input-box hiding, remote-input suppression in `shared_session/viewer/terminal_manager.rs`, and `maybe_insert_setup_command_blocks`. For non-oz there are no exchanges, so this stays `true` forever. `maybe_insert_setup_command_blocks` runs in the `AfterBlockStarted` path and, when pre-first-exchange, inserts the `CloudModeSetupTextBlock` summary row (once) plus a `CloudModeSetupCommandBlock` row for the block. It has no knowledge of whether the block is the harness CLI command. `PendingUserQueryBlock::new` currently takes `interruptible: bool` and always renders a close button; the "Send now" button is added only when `interruptible`. `TerminalView::insert_pending_user_query_block` subscribes to `Dismissed`/`SendNow` events; `send_user_query_after_next_conversation_finished` also registers a `queued_prompt_callback` so the prompt is re-submitted when the current conversation finishes. ## Proposed changes ### 1. `PendingUserQueryBlock` — per-button bools `app/src/ai/blocklist/block/pending_user_query_block.rs` Replace the `interruptible: bool` constructor param with two bool fields on the block itself: - `show_close_button: bool` — whether to render the dismiss ("X") button. - `show_send_now_button: bool` — whether to render the "Send now" button. Update `PendingUserQueryBlock`: - Make `close_button: Option>` (was unconditional), constructed only when `show_close_button` is true. - `send_now_button: Option>` stays optional, constructed only when `show_send_now_button` is true. - Build the buttons column with 0, 1, or 2 children. If it ends up empty, skip the whole "buttons" container. - Constructor signature becomes `PendingUserQueryBlock::new(prompt, user_display_name, profile_image_path, show_close_button, show_send_now_button, ctx)`. This keeps the wire-up trivial for each call site (pass the two bools) and avoids introducing an enum just to express three combinations. ### 2. `TerminalView` helpers — thread the button bools and add a cloud-mode insertion path `app/src/terminal/view/pending_user_query.rs` - Change `insert_pending_user_query_block`'s `interruptible: bool` param to `show_close_button: bool, show_send_now_button: bool` and pass them to `PendingUserQueryBlock::new`. - Keep subscribing to `PendingUserQueryBlockEvent::Dismissed` and `PendingUserQueryBlockEvent::SendNow`. When both bools are `false`, the block never emits these events, so the subscription is effectively inert. - Change `send_user_query_after_next_conversation_finished`'s `interruptible: bool` param to the same two bools and pass them through. Update existing callers: - `app/src/workspace/view.rs:11605` (`handle_forked_conversation_prompts`, `/fork-and-compact`) — `show_close_button: true, show_send_now_button: false` (was `false`). - `app/src/workspace/view.rs:11701` (`summarize_active_ai_conversation`, `/compact-and`) — `show_close_button: true, show_send_now_button: false` (was `false`). - `app/src/workspace/view.rs:20955` (`QueuePromptForConversation`, `/queue`) — `show_close_button: true, show_send_now_button: true` (was `true`). - Add a new public method used by Cloud Mode: ```rust pub(in crate::terminal::view) fn insert_cloud_mode_queued_user_query_block( &mut self, prompt: String, ctx: &mut ViewContext, ) { self.remove_pending_user_query_block(ctx); self.insert_pending_user_query_block( prompt, /* show_close_button */ false, /* show_send_now_button */ false, ctx, ); } ``` This does NOT register a `queued_prompt_callback` — the prompt is not re-submitted; it is already being carried by the harness inside the sandbox. `remove_pending_user_query_block` already clears `queued_prompt_callback` on top of removing the view. It remains the single teardown entry point. ### 3. `AmbientAgentViewModel` — resolve harness for viewers, track harness-command-started, new event `app/src/terminal/view/ambient_agent/model.rs` Add a helper for the non-oz check: ```rust impl AmbientAgentViewModel { /// True when the run is configured to use a non-Oz execution harness and the /// required feature flags are enabled. pub fn is_third_party_harness(&self) -> bool { FeatureFlag::AgentHarness.is_enabled() && self.harness != Harness::Oz } } ``` Fix the viewer-side harness resolution so `AmbientAgentViewModel::harness` reflects the run's actual harness for every client, not just the spawner. In `enter_viewing_existing_session`, extend the success branch to also resolve the harness from `task.agent_config_snapshot.harness` and call `set_harness`: ```rust Ok(task) => { let snapshot = task.agent_config_snapshot.as_ref(); let environment_id = snapshot .and_then(|s| s.environment_id.as_deref()) .and_then(|id| ServerId::try_from(id).ok()) .map(SyncId::ServerId); let harness = snapshot .and_then(|s| s.harness.as_ref()) .map(|h| parse_harness_config_name(&h.harness_type)) .unwrap_or(Harness::Oz); me.set_environment_id(environment_id, ctx); me.set_harness(harness, ctx); } ``` `parse_harness_config_name` is a small module-local helper that matches `"claude"` / `"gemini"` / `"oz"` explicitly and falls back to `Harness::Oz` (with a warn log) on unknown values. `set_harness` is a no-op when unchanged. Add a harness-command-started tracker: ```rust pub struct AmbientAgentViewModel { // ... existing fields ... harness_command_started: bool, } impl AmbientAgentViewModel { pub fn harness_command_started(&self) -> bool { self.harness_command_started } pub fn mark_harness_command_started(&mut self, ctx: &mut ModelContext) { if self.harness_command_started { return; } self.harness_command_started = true; ctx.emit(AmbientAgentViewModelEvent::HarnessCommandStarted); } } ``` Initialize it to `false` in `new` and `reset_status`. Add the event variant: ```rust pub enum AmbientAgentViewModelEvent { // ... existing ... HarnessCommandStarted, } ``` ### 4. `is_cloud_agent_pre_first_exchange` — also honor harness-started `app/src/terminal/view/ambient_agent/mod.rs` Extend the helper so both Oz's "first exchange" and non-oz's "harness command started" transitions end the pre-first-exchange phase. At the site that currently returns based on `exchange_count() == 0`: ```rust if ambient_agent_view_model.as_ref(app).harness_command_started() { return false; } BlocklistAIHistoryModel::as_ref(app) .conversation(&conversation_id) .is_some_and(|conversation| conversation.exchange_count() == 0) ``` This keeps every existing consumer (`status_bar`, input-box visibility, remote-input suppression, `setup_command_text`) correctly reading `false` after the harness block is detected. ### 5. Detect the harness block and emit `HarnessCommandStarted` `app/src/terminal/view/ambient_agent/view_impl.rs` — `maybe_insert_setup_command_blocks` This function is already the single hook called from `ModelEvent::AfterBlockStarted` for cloud-mode setup bookkeeping, so it's the right place to detect the harness block. Before doing any setup-row insertion, short-circuit if the active block's CLI-agent matches the run's harness: ```rust pub(in crate::terminal::view) fn maybe_insert_setup_command_blocks( &mut self, block_id: &BlockId, ctx: &mut ViewContext, ) { if !FeatureFlag::CloudModeSetupV2.is_enabled() { return; } // Only act while we're still in the pre-first-exchange phase. if !super::is_cloud_agent_pre_first_exchange( &self.ambient_agent_view_model, &self.agent_view_controller, ctx, ) { return; } // For non-oz harness runs, transition out of the setup phase when the // harness CLI starts. The block becomes a normal CLI-agent session rather // than a setup command. if self.ambient_agent_view_model.as_ref(ctx).is_third_party_harness() && self.active_block_matches_run_harness(ctx) { self.ambient_agent_view_model.update(ctx, |m, ctx| { m.mark_harness_command_started(ctx); }); return; } // ... existing setup-row insertion logic, now guarded by the // is_cloud_agent_pre_first_exchange check above ... } ``` `active_block_matches_run_harness` is a small new helper on `TerminalView` that runs `CLIAgent::detect` directly (no alias or shell-escape handling) and matches against the run's harness: ```rust fn active_block_matches_run_harness(&self, ctx: &AppContext) -> bool { let command = self .model .lock() .block_list() .active_block() .command_with_secrets_obfuscated(false); let Some(cli_agent) = CLIAgent::detect(&command, None, None, ctx) else { return false; }; match self.ambient_agent_view_model.as_ref(ctx).selected_harness() { Harness::Oz => false, Harness::Claude => matches!(cli_agent, CLIAgent::Claude), Harness::Gemini => matches!(cli_agent, CLIAgent::Gemini), } } ``` Cloud-mode harness runs always invoke the canonical `claude` / `gemini` CLI prefix from the harness sidecar (hardcoded in `app/src/ai/agent_sdk/driver/harness/claude_code.rs:84-95` and the matching `gemini.rs`). Passing `None` for both the escape char and aliases is sufficient; if a future wrapper (e.g. `bash -l -c 'claude …'`) breaks canonical detection, the fallback trigger under *Risks and mitigations* applies. We deliberately avoid `detect_cli_agent_from_model` because it gates on `is_active_and_long_running`, which has not yet elapsed at `AfterBlockStarted` time. ### 6. Handle `HarnessCommandStarted` in the view `app/src/terminal/view/ambient_agent/view_impl.rs` — `handle_ambient_agent_event` Add a new arm that mirrors how `AppendedExchange` already tears down the setup phase for Oz, removes the cloud-mode queued prompt block, and forces a fresh viewer-size report to the sharer so the harness CLI lays out at our current dimensions: ```rust AmbientAgentViewModelEvent::HarnessCommandStarted => { // Stop classifying new blocks as environment setup commands and let the // existing CLI-agent rendering take over for the harness block. let mut model = self.model.lock(); if model.block_list().is_executing_oz_environment_startup_commands() { model .block_list_mut() .set_is_executing_oz_environment_startup_commands(false); } drop(model); self.remove_pending_user_query_block(ctx); // Collapse the setup-commands summary now that we're past setup (matches // the existing behavior when the first AI exchange appears). self.ambient_agent_view_model.update(ctx, |model, ctx| { model.set_setup_command_visibility(false, ctx); }); // Force a fresh viewer size report to the sharer so the harness CLI // (e.g. the claude TUI) starts at the viewer's actual dimensions // instead of whatever the sandbox PTY was sized to during setup. self.force_report_viewer_terminal_size(ctx); ctx.notify(); } ``` The Oz `AppendedExchange` handler in `view.rs:5042-5053` stays as-is. ### 6a. `force_report_viewer_terminal_size` helper `app/src/terminal/view/shared_session/view_impl.rs` Viewer-driven resize normally dedups on the last reported natural size (`SharedSessionViewer::last_reported_natural_size`) so a noop refresh doesn't spam the sharer. At harness start we specifically want to break that dedup: the sandbox PTY was resized during setup (to whatever size the pre-harness commands dictated), and the harness TUI will fail to lay out correctly if we don't re-issue our report. Add: ```rust /// Forces a fresh viewer-size report to the sharer by clearing the dedup cache and /// refreshing size. No-op when not an active viewer or when viewer-driven sizing is /// not eligible. pub(in crate::terminal::view) fn force_report_viewer_terminal_size( &mut self, ctx: &mut ViewContext, ) { if let Some(viewer) = self.shared_session_viewer_mut() { viewer.last_reported_natural_size = None; } self.refresh_size(ctx); } ``` `refresh_size` already funnels through the standard viewer-driven-sizing eligibility checks (`is_viewer_driven_sizing_eligible`), so this is a no-op for sharers, for multi-viewer shared sessions, and for non-cloud-agent shared sessions. Cloud-agent shared sessions bypass the same-user check (`is_shared_session_for_ambient_agent`), so the viewer will always resize the sandbox PTY on harness start. ### 7. Route the initial prompt to the queued-prompt UI for non-oz runs `app/src/terminal/view/ambient_agent/view_impl.rs` — `DispatchedAgent` handler (currently view_impl.rs:106-135) Branch on the run's harness: ```rust AmbientAgentViewModelEvent::DispatchedAgent => { self.update_pane_configuration(ctx); if FeatureFlag::CloudModeSetupV2.is_enabled() { let model = self.ambient_agent_view_model.as_ref(ctx); if model.is_third_party_harness() { let prompt = model .request() .map(|request| request.prompt.clone()) .unwrap_or_default(); if !prompt.is_empty() { self.insert_cloud_mode_queued_user_query_block(prompt, ctx); } // Do NOT set has_inserted_cloud_mode_user_query_block for third-party // harnesses — the queued block path handles its own lifecycle and the // flag only gates Oz-specific first-exchange dedup logic. } else { // ... existing Oz path: insert CloudModeInitialUserQuery, set // has_inserted_cloud_mode_user_query_block(true) ... } } else { // ... existing tip-reset branch, unchanged ... } ctx.notify(); } ``` For viewers / historical replay, `request()` is `None` on the viewer's model (the viewer doesn't spawn), so the queued block is not inserted — matching the product-spec rule that late joiners don't see it. ### 8. Remove pending-query block on pre-harness terminal states `app/src/terminal/view/ambient_agent/view_impl.rs` — extend existing `Failed`, `Cancelled`, and `NeedsGithubAuth` arms: Each arm gets the same additional line (idempotent and cheap; no-op if no block present): ```rust self.remove_pending_user_query_block(ctx); ``` This covers the product-spec requirement that failing/cancelling/auth-required runs before the harness block starts remove the queued indicator and fall back to the existing error/cancel/auth UI. The fallback UI is already rendered by the existing status-bar and loading-screen code paths under `CloudModeSetupV2`. ### 9. Cloud-mode submission-block guard remains harness-agnostic `app/src/terminal/input.rs:5725-5734` — `should_block_cloud_mode_setup_submission` currently blocks local input submission during the pre-harness/pre-first-exchange window. Because we updated `is_cloud_agent_pre_first_exchange` to also return false after `HarnessCommandStarted`, this guard already stops firing once the harness block starts, allowing input to flow to the harness TUI normally. No change needed. ### 10. Setup UI horizontal padding for non-oz harnesses `app/src/terminal/view/ambient_agent/block.rs`, `block/setup_command.rs`, `block/setup_command_text.rs` The oz setup UI wraps its per-command row and summary row in `WithContentItemSpacing::with_agent_output_item_spacing`, which applies a left margin of `CONTENT_HORIZONTAL_PADDING + icon_size + 16.` so the setup rows line up with other oz agent-output items (reasoning, actions, etc.). For non-oz harness runs, the harness CLI block that takes over after setup is a regular terminal command block and uses `*terminal::view::PADDING_LEFT` (20px, or 16px with `LessHorizontalTerminalPadding`) as its horizontal padding. Using the oz indent for non-oz setup rows causes a visible horizontal jump at harness start. A shared helper `cloud_mode_setup_row_spacing` in `block.rs` picks the right spacing per harness: ```rust pub(super) fn cloud_mode_setup_row_spacing( element: Box, ambient_agent_view_model: &ModelHandle, app: &AppContext, ) -> Container { if ambient_agent_view_model .as_ref(app) .is_third_party_harness() { Container::new(element) .with_margin_left(*PADDING_LEFT) .with_margin_right(*PADDING_LEFT) .with_margin_bottom(CONTENT_ITEM_VERTICAL_MARGIN) } else { element.with_agent_output_item_spacing(app) } } ``` `CloudModeSetupCommandBlock` calls this in its collapsed-header branch (the expanded detail view keeps its own corner-radius / container treatment). `CloudModeSetupTextBlock` calls this at the end of `render` instead of the standalone `.with_agent_output_item_spacing(app).finish()`. ### 11. Feature flag gating - All new code paths that check `is_third_party_harness` also implicitly depend on `FeatureFlag::AgentHarness` (the helper checks it). - The outer `if FeatureFlag::CloudModeSetupV2.is_enabled()` guard in `DispatchedAgent` and `maybe_insert_setup_command_blocks` continues to gate the whole setup-v2 UI; when the flag is off, the legacy loading screen / full-screen overlay path is unchanged. - When `AgentHarness` is disabled, `is_third_party_harness` always returns `false`, the Oz paths are taken for everything, and the spec's new behavior never activates. Replay of CLI-agent conversations is already guarded by the same flag in `pane_group/mod.rs:3763,5682` and `terminal/view/load_ai_conversation.rs:268`. ## End-to-end flow (non-oz Cloud Mode run) 1. User selects claude/gemini in the harness selector and submits a prompt. 2. `AmbientAgentViewModel::spawn_agent` builds a `SpawnAgentRequest` with `HarnessConfig::from_harness_type`; `self.harness` is non-oz. Stream emits `DispatchedAgent`. 3. `handle_ambient_agent_event` sees `is_third_party_harness()` and calls `insert_cloud_mode_queued_user_query_block(prompt)`. `PendingUserQueryBlock` is rendered with `show_close_button: false` and `show_send_now_button: false`. 4. Spawn stream progresses to `SessionReady`; the cloud-mode terminal-model viewer is built with `is_executing_oz_environment_startup_commands = true`. 5. Environment setup blocks start. `maybe_insert_setup_command_blocks` sees `is_cloud_agent_pre_first_exchange == true` and `active_block_matches_run_harness == false`, so it inserts the `CloudModeSetupTextBlock` summary row and a `CloudModeSetupCommandBlock` per block. Blocks are marked `is_oz_environment_startup_command` by the existing block-list logic. 6. Sandbox's `oz agent run` executes `claude --session-id … < /tmp/oz_prompt` (from `agent_sdk/driver/harness/claude_code.rs`). The viewer sees this as a new long-running block. 7. `maybe_insert_setup_command_blocks` detects `CLIAgent::Claude` matches `Harness::Claude`, calls `mark_harness_command_started`, and returns without inserting setup rows for this block. 8. `HarnessCommandStarted` fires → view arm flips `is_executing_oz_environment_startup_commands = false`, removes the pending-query block, collapses the setup-commands summary, and calls `force_report_viewer_terminal_size` so the sandbox PTY is resized to our current dimensions before claude's TUI lays out its first frame. 9. From now on, `is_cloud_agent_pre_first_exchange` returns false. The claude block is a normal CLI-agent session: `detect_cli_agent_from_model` + `CLIAgentSessionsModel::set_session` already run in `view.rs:10343-10378`, so the CLI-agent footer and rich-input UI activate. Setup rows rendered earlier in the run use `PADDING_LEFT` margins so the visible left edge of content does not shift when the harness block takes over. 10. On pre-harness `Failed` / `Cancelled` / `NeedsGithubAuth`: the new `remove_pending_user_query_block` calls tear down the queued indicator; the existing error/cancel/auth UI remains. ## Risks and mitigations **Missing harness match.** If `CLIAgent::detect` fails to identify the harness (e.g. a future wrapper like `bash -c 'claude …'`), we never transition out of setup and the claude block stays flagged as a setup command. The short-circuit only covers the sandbox's canonical invocation; if detection fails, behavior regresses to the current broken state rather than getting worse. A fallback trigger (e.g. timeout-based transition) can be added as a follow-up if this becomes a problem. **Viewer harness resolution races.** `enter_viewing_existing_session` fetches the task asynchronously. If the harness block starts before the fetch completes, `is_third_party_harness()` returns false at the time of detection and we treat the block as a setup command. In practice the task fetch is issued on join and the viewer only sees blocks after the shared session connects, which is later. If needed, we can re-evaluate the harness on task fetch completion and fire `mark_harness_command_started` retroactively. **`is_cloud_agent_pre_first_exchange` semantics shift.** Consumers were designed around "conversation has no exchanges yet". After this change, the helper also returns false when a harness command has started. Every caller (`status_bar`, `is_input_box_visible`, `shared_session/viewer/terminal_manager`, `setup_command_text`, `maybe_insert_setup_command_blocks`) behaves correctly under the new semantics for non-oz runs and is a no-op for Oz runs (since `harness_command_started` is never set on Oz). ## Testing and validation Unit coverage: - Add a test for `PendingUserQueryBlock` rendering for each `(show_close_button, show_send_now_button)` combination (both-true, close-only, neither). - Add a test for `AmbientAgentViewModel::mark_harness_command_started` idempotency and event emission, and for `is_third_party_harness` gating on `AgentHarness` + `harness` field. - Add a test that `enter_viewing_existing_session` populates `harness` from the fetched `AmbientAgentTask.agent_config_snapshot.harness` for claude / gemini / oz. - Extend `is_cloud_agent_pre_first_exchange` tests (or add one) to cover the harness-started early-return. - Add a test that `force_report_viewer_terminal_size` clears `last_reported_natural_size` and calls `refresh_size`, and is a no-op for sharers / when viewer-driven sizing is ineligible. Integration / manual validation (mirrors the product spec's validation list): - Spawn a claude-code Cloud Mode run with multiple startup commands; confirm queued prompt block (no buttons) appears, setup-commands summary expands during setup, and on claude start the queued block is removed, summary collapses to "Ran setup commands", and the claude TUI is the active CLI-agent block. Confirm the claude TUI reports the viewer's current rows/cols (resize the pane during setup and verify the TUI lays out at the new size on harness start) and that the setup-command rows are horizontally flush with the subsequent harness terminal block (no visible left-edge jump). - Spawn a gemini run; confirm the same behavior when `gemini` is detected. - Spawn a claude run with no environment setup commands; confirm the queued block is removed when the harness block starts without a setup-commands summary having been inserted. - Spawn a claude run that fails during env setup; confirm queued block is removed on `Failed` and the existing error UI renders. - Cancel a claude run pre-harness; confirm queued block removal + cancelled UI. - Trigger GitHub-auth-required pre-harness; confirm queued block removal + auth UI. - Join an existing claude shared session after the harness has started; confirm no queued block, collapsed setup summary (from prior blocks), claude TUI visible. - Replay a completed claude conversation; confirm no queued block, transcript rendered correctly via the existing CLI-agent block-snapshot path. - Re-run all Oz validation cases from `specs/REMOTE-172/PRODUCT.md` to confirm no regression. - Toggle `CloudModeSetupV2` off; confirm legacy cloud-mode behavior restored for all harnesses. - Toggle `AgentHarness` off; confirm harness selector hides non-oz options and nothing in this spec activates. ## Follow-ups - Consider telemetry for `HarnessCommandStarted` (time from `DispatchedAgent` / `SessionReady` to harness start) so we can monitor setup latency per-harness. - If detection proves flaky in practice, add a timeout-based fallback transition (e.g. "still in setup 60s after session ready with no matching CLI agent detection → transition anyway and log a warning"). - Once `CloudModeSetupV2` ships to stable, consider unifying the "pre-first-exchange" and "pre-harness" concepts behind a single view-model state rather than the current combination of block-list flag + exchange count + harness-started bool.