first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+64
View File
@@ -0,0 +1,64 @@
# Queued Prompts in Cloud Mode Setup
Linear: APP-4562. Builds on the regular Agent Mode queued-prompts panel from [`specs/REMOTE-1543/PRODUCT.md`](../REMOTE-1543/PRODUCT.md), extending it to Cloud Mode runs.
Figma: none provided.
## Summary
Extend the multi-prompt queued-prompts panel to Cloud Mode runs so the initial cloud prompt and any follow-ups queued during environment setup render in the same panel as regular Agent Mode queued prompts, with the initial prompt rendered as a locked first row. Subsequent queued rows fire automatically as the cloud agent finishes each exchange.
## Problem
Today in Cloud Mode setup, the user's submitted prompt appears as a separate "pending user query" indicator block, and pressing Enter while the cloud environment is setting up does nothing — the prompt is dropped. Queued prompts also do not drain for cloud runs, because the local "response finished" signal that drives draining does not fire when the response is being streamed by a remote cloud agent. As a result, users cannot queue follow-up work on a cloud run.
## Goals
- Render the initial Cloud Mode prompt as a row in the regular queued-prompts panel instead of as the legacy pending-user-query block.
- Let users queue any number of follow-up prompts while the cloud environment is setting up, and have them auto-fire in order once the cloud agent is live.
- Keep the panel's row interactions (drag-to-reorder, edit, delete) for follow-up rows, while preventing them on the initial row that the cloud agent has already accepted.
- Gate everything behind a new `QueuedPromptsV2` feature flag, dogfood-only.
## Non-goals
- Changing the regular Agent Mode queued-prompts panel behavior described in `specs/REMOTE-1543/PRODUCT.md`.
- Persisting the cloud-mode queue across app restarts.
- Letting users edit, delete, or reorder the initial prompt after it has been dispatched to the cloud.
- Exposing this behavior to non-dogfood builds.
## Behavior
### Feature gating
1. All behavior described below is gated on the `QueuedPromptsV2` feature flag. When the flag is off, Cloud Mode setup behaves exactly as it does today: initial prompts render as the legacy pending-user-query block, submitting a prompt while the environment is setting up is a no-op, and queued prompts do not drain for cloud runs.
2. The `QueuedPromptsV2` flag transitively enables the regular `QueueSlashCommand` feature. When V2 is on, every existing regular Agent Mode queued-prompts surface (the auto-queue toggle, `/queue` slash command, queue panel) is also available.
### Initial cloud-mode prompt
3. When the user submits the initial prompt in a Cloud Mode pane, the prompt appears as the first row of the queued-prompts panel for that conversation. The panel renders in the same position relative to the V2 cloud-mode composing input that it renders in for regular Agent Mode (above the input editor, inside the centered V2 layout).
4. The initial cloud-mode prompt row is *locked*:
- Its drag handle is rendered in a visually disabled state and does not respond to drag gestures.
- Its edit (pencil) and delete (trash) icon-buttons remain rendered on hover and render with the same naked styling as their interactive counterparts on other rows — no greyed-out background or text. They are not clickable.
- Hovering the drag handle or either icon-button shows the same short tooltip — "The first cloud-mode prompt cannot be changed." — explaining why no interaction is possible.
- The static preview text renders identically to other rows.
5. The locked row's preview text is the prompt as the user typed it, including any `/plan`, `/orchestrate`, or other prefix the user included (matching today's pending-user-query block treatment).
6. When the cloud agent picks up the prompt — i.e. the first real exchange shows up in the conversation transcript, or the harness reports that the command has started (Oz harnesses use the harness-command-started signal; oz local-to-cloud handoff uses the first appended exchange) — the locked row is removed from the panel. After removal, the second row (if any) becomes the next row to fire, but is still considered a follow-up, not the initial prompt.
7. If the cloud run fails before the prompt is picked up (failure, cancellation, GitHub-auth required, snapshot upload failure), the locked row is removed from the panel at the same moment the legacy pending-user-query block would be removed today. Any follow-up rows queued behind it remain in the panel, available for review, edit, deletion, or reordering, exactly like regular queued rows.
### Submission during environment setup
8. Whenever the cloud pane is an ambient-agent pane that is not currently composing and not currently running, submitting the input editor queues the prompt instead of doing nothing. The queued prompt appears as a new row in the panel, after the locked initial row. In practice this covers every pre-run cloud state where the user can reach the editor — `WaitingForSession`, `Failed`, `Cancelled`, `NeedsGithubAuth`, and `Setup` (the last is normally unreachable because the first-time-setup modal owns the focus, but the predicate matches it for completeness).
9. Follow-up rows queued during setup are *not* locked. They support the same interactions as regular Agent Mode queued rows: drag-to-reorder among themselves, hover-revealed edit and delete buttons, and so on.
10. The locked initial row stays pinned at index 0 regardless of how follow-up rows are reordered. Dragging another row above the locked row is not possible — the panel keeps the locked row at the top.
11. Submitting an empty prompt does not append a new row (existing trim-and-skip behavior).
12. Submitting in shell mode is unaffected — the shell command runs in the terminal as today, regardless of whether the cloud agent is setting up.
### Drain behavior (after the initial prompt is picked up)
13. Once the locked initial row has been removed (per §6), the panel behaves as the regular Agent Mode queued-prompts panel: every time the active cloud conversation finishes an exchange cleanly, the first remaining row is removed from the panel and submitted as a follow-up prompt to the same cloud conversation.
14. The queued prompt is submitted through the same path that user-initiated cloud follow-ups use — it reaches the cloud agent, not the local agent controller. From the user's perspective, an auto-fired queued prompt is indistinguishable from a prompt the user typed and submitted manually after the agent finished.
15. When the active cloud conversation finishes for a non-clean reason (error, cancellation, cancellation during requested command execution), auto-fire pauses immediately. The queue is not flushed:
- If the input editor is currently empty, the first remaining queued row is removed from the panel and its text is placed in the input editor. The user can edit and re-submit it manually.
- If the input editor is non-empty, no rows are removed and the input is not modified.
- In both cases, remaining queued rows beyond the first stay intact.
16. Auto-fire resumes naturally the next time the active cloud conversation completes an exchange cleanly — from that completion onward, the queue resumes draining from the top.
### Conversation lifecycle interactions
17. The queued-prompts panel is owned by the terminal view and implicitly scoped to whichever conversation is currently active in that view. Switching to a different conversation goes through agent-view exit (which clears the queue) before re-entering for the new conversation, so the panel always reflects the active conversation and never carries follow-up rows across conversation switches.
18. Exiting the cloud pane, closing the tab, or removing the conversation discards the queue (including any locked initial row).
19. The collapsed/expanded state of the panel, the row-level edit state, and reorder behavior all match the regular Agent Mode queued-prompts panel for follow-up rows.
### Telemetry
20. Existing queued-prompts panel telemetry (edit committed, row deleted, row reordered, panel collapse toggled) continues to fire for follow-up rows. The locked initial row does not emit edit/delete/reorder events because those interactions are disabled.
+105
View File
@@ -0,0 +1,105 @@
# Queued Prompts in Cloud Mode Setup — Tech Spec
See `specs/APP-4562/PRODUCT.md` for user-visible behavior. This document covers the implementation that supports that behavior, layered on top of the regular Agent Mode queued-prompts panel introduced in `specs/REMOTE-1543/`.
## Context
The regular queued-prompts panel (`specs/REMOTE-1543/`) is a terminal-owned queue that appears between the warping indicator and the input editor in `TerminalView`. The queue is per-`TerminalView` and implicitly scoped to whichever conversation owns the agent view — entries are wiped on agent-view exit and on `ClearedConversationsInTerminalView`, so it never holds rows for more than one conversation at a time. Its data model lives in `app/src/ai/blocklist/queued_query.rs`, its view lives in `app/src/ai/blocklist/queued_prompts_panel.rs`, and the trigger/drain glue lives in `app/src/terminal/input.rs` and `app/src/terminal/view.rs`. Cloud Mode is currently outside that surface — see `specs/REMOTE-1543/PRODUCT.md (13, 30, 62)` and the panel's `should_render` gate at `app/src/ai/blocklist/queued_prompts_panel.rs`.
For Cloud Mode today:
- The initial submitted cloud prompt is shown as a legacy pending-user-query block inserted by `TerminalView::insert_cloud_mode_queued_user_query_block` (`app/src/terminal/view/pending_user_query.rs:90`), called from `app/src/terminal/view/ambient_agent/view_impl.rs:173` (`DispatchedAgent`) and `:212` (`FollowupDispatched`).
- Pressing Enter while the cloud environment is still setting up is suppressed by `Input::should_block_cloud_mode_setup_submission` (`app/src/terminal/input.rs:6575`), short-circuited at `app/src/terminal/input.rs:12617`.
- Queued prompts drain off `BlocklistAIControllerEvent::FinishedReceivingOutput` in `TerminalView::handle_ai_controller_event` (`app/src/terminal/view.rs:4930`). That event does not fire for a cloud-mode pane because the response stream lives on the cloud side, so subsequent queued prompts never fire.
- `QueuedQueryOrigin::InitialCloudMode` is already defined at `app/src/ai/blocklist/queued_query.rs:24` but is currently unused — this spec wires it up.
## Proposed changes
### 1. Feature flag `QueuedPromptsV2`
Add a compile-time + runtime feature flag.
- `app/Cargo.toml`: add `queued_prompts_v2 = ["queue_slash_command"]` under `[features]`. The cargo dependency means enabling V2 transitively enables the existing queue feature, so every existing `FeatureFlag::QueueSlashCommand.is_enabled()` site still works without modification. Do not add to `default`.
- `crates/warp_features/src/lib.rs`: add `QueuedPromptsV2` to the `FeatureFlag` enum, alongside the existing `QueueSlashCommand` entry. Add the variant to `DOGFOOD_FLAGS`.
- `app/src/features.rs:432-433`: register the runtime flag under `#[cfg(feature = "queued_prompts_v2")]`.
All cloud-mode-aware sites described below gate on `FeatureFlag::QueuedPromptsV2.is_enabled()` directly.
### 2. `QueuedQueryOrigin::InitialCloudMode` is now load-bearing
`QueuedQueryModel` (`app/src/ai/blocklist/queued_query.rs`) gains origin-aware no-ops so the locked-row contract is enforced at the model layer, not just the panel. The shared check lives on `QueuedQuery::is_locked()` so every mutator can ask the same question. Currently a row is locked iff its origin is `InitialCloudMode`; lifecycle code removes it explicitly via `QueuedQueryModel::remove_initial_cloud_mode_row`.
- `pop_front(ctx)`: returns `None` if the head is locked. The non-clean drain path in `TerminalView::drain_queued_prompts` reaches `pop_front` whenever a server-pushed `UpdatedConversationStatus → Error/Cancelled` arrives, so this gate prevents an in-flight status transition from clobbering the locked initial Cloud Mode row before the matching ambient-agent cleanup event has run.
- `pop_for_autofire(edit_text_override, ctx)`: returns `None` if the first row is locked. The cloud-setup lifecycle removes that row via §5, not autofire.
- `remove_by_id(query_id, ctx)`: no-op if the target row is locked. Lifecycle code uses `remove_initial_cloud_mode_row` instead.
- `reorder(source_id, target_index, ctx)`: no-op if `source_id` is locked, or if `target_index == 0` would displace a locked row currently at the head.
- `enter_edit_mode(query_id, ctx)`: no-op if the target row is locked.
- Add `remove_initial_cloud_mode_row(ctx)` that removes the first row of the queue if and only if its origin is `InitialCloudMode`. Used by §5.
All of these signatures are conversation-id-free because the model owns a single `Vec<QueuedQuery>` per terminal view — see the Context section. This division means the panel UI in §3 only needs to *render* the lock; it does not need to gate handler dispatch, because the model rejects forbidden mutations even if a click somehow gets through.
### 3. Panel: lock the InitialCloudMode row visually
In `app/src/ai/blocklist/queued_prompts_panel.rs`:
- All locked-row hover affordances share a single tooltip constant `INITIAL_CLOUD_MODE_PROMPT_TOOLTIP = "The first cloud-mode prompt cannot be changed."` (`app/src/ai/blocklist/queued_prompts_panel.rs:44`) so the drag handle, edit button, and delete button all surface the same short explanation.
- In the row rendering inside `render` (`app/src/ai/blocklist/queued_prompts_panel.rs:597`), when the rendered query's `origin()` is `QueuedQueryOrigin::InitialCloudMode`:
- Render the drag handle in a visually disabled state without wrapping the row in `Draggable`, and show the shared tooltip on hover.
- Keep the edit and delete `ActionButton`s revealed on hover and call `set_disabled(true)` on each so the click handler is gated and the disabled tooltip is surfaced. Pair that with `with_disabled_theme(NakedTheme)` so the disabled state reuses the regular naked appearance instead of picking up the default greyed-out `DisabledTheme` fill/text. Both buttons reuse the shared tooltip.
- Static preview text renders identically to other rows.
- `should_render` (`app/src/ai/blocklist/queued_prompts_panel.rs:547`) is unchanged. Because the cargo feature transitively enables `queue_slash_command`, the existing `FeatureFlag::QueueSlashCommand.is_enabled()` check passes when V2 is on.
- Wire the panel into the V2 cloud-mode composing input in `Input::render_cloud_mode_v2_composing_input` (`app/src/terminal/input/agent.rs:345`). Render the panel as a sibling above the input card, inside the same `ConstrainedBox` constrained to `CLOUD_MODE_V2_MAX_WIDTH` (`app/src/terminal/input/agent.rs:34`). The non-V2 placement at `app/src/terminal/input/agent.rs:324` is unchanged.
### 4. Route initial + follow-up cloud-mode prompts into the queue
Branch both existing `insert_cloud_mode_queued_user_query_block` call sites on `FeatureFlag::QueuedPromptsV2.is_enabled()`:
- `app/src/terminal/view/ambient_agent/view_impl.rs` initial `DispatchedAgent`.
- `app/src/terminal/view/ambient_agent/view_impl.rs` `FollowupDispatched`.
When V2 is on, call `TerminalView::enqueue_initial_cloud_mode_prompt(prompt, ctx)` which delegates to `enqueue_prompt` with `QueuedQueryOrigin::InitialCloudMode`. `enqueue_prompt` keeps `BlocklistAIContextModel::selected_conversation_id` as a *gate* — it bails when no conversation is selected so prompts are not stranded outside the agent view — but the queue itself is not keyed by the conversation id. If the gate fails, fall back to `insert_cloud_mode_queued_user_query_block` so the visual indicator is never lost.
The `FollowupDispatched` path also enqueues an `InitialCloudMode` row. Every cloud-side dispatch — whether the first execution or a follow-up — produces a locked row that the lifecycle events in §5 retire when the cloud agent picks up the prompt.
### 5. Mirror legacy block-removal sites onto the panel row
Introduce `TerminalView::remove_cloud_mode_queue_row(&mut self, ctx)` (`app/src/terminal/view/pending_user_query.rs`) that calls `QueuedQueryModel::remove_initial_cloud_mode_row` from §2. The helper is a no-op when V2 is off because no `InitialCloudMode` row was ever appended.
Affected sites:
- `app/src/terminal/view/ambient_agent/view_impl.rs:114-127``should_clean_up_pending_cloud_query` covers `HarnessCommandStarted`, `NeedsGithubAuth`, `Cancelled`, `HandoffSnapshotUploadFailed`, and `Failed` when `!CloudModeSetupV2.is_enabled()`. Both the legacy pending-user-query block and the V2 queue row are removed on the *same* condition so the two surfaces cannot diverge.
- `app/src/terminal/view.rs:5452``remove_pending_cloud_mode_query_if_exchange_has_renderable_user_query`, called from `AppendedExchange` at `app/src/terminal/view.rs:5813`. The pending-block removal stays scoped to the legacy `CloudMode` kind so unrelated `PendingUserQueryKind::QueuedPrompt` blocks (the `/queue` slash-command surface) are not torn down under V2; the queue-row removal is V2-gated independently and is a no-op when the V2 row is not present.
- `app/src/terminal/view.rs:5613-5620` — oz local-to-cloud handoff first `AppendedExchange`.
### 6. Allow submission while environment is setting up (queue instead of block)
`Input::should_block_cloud_mode_setup_submission` currently short-circuits Enter to a no-op when the cloud pane is in `WaitingForSession` / `Failed` / `Cancelled` / `NeedsGithubAuth`. This stays untouched. The new path is purely additive and gated.
Add `Input::maybe_queue_input_during_cloud_setup(ctx)` next to `maybe_queue_input_for_in_progress_conversation`. It:
1. **Hard-gates** on `FeatureFlag::QueuedPromptsV2.is_enabled()` as the very first check; returns false immediately if the flag is off, without inspecting any other state.
2. Returns false unless the cloud-pane predicate `is_ambient_agent() && !is_configuring_ambient_agent() && !is_agent_running()` holds (same predicate as today's block check).
3. Gates on `BlocklistAIContextModel::selected_conversation_id` being `Some` so we don't queue while the agent view is closed; the conversation id itself is not used to key the queue (see Context).
4. Reads and trims the editor buffer; returns false if empty.
5. Clears the editor buffer and pending attachments.
6. Appends a row with `QueuedQueryOrigin::AutoQueueToggle` to the single per-view queue.
7. Returns true so the submit handler short-circuits.
Call `maybe_queue_input_during_cloud_setup` in the submit handler immediately alongside `maybe_queue_input_for_in_progress_conversation`, before `should_block_cloud_mode_setup_submission` is evaluated. When V2 is off, the new helper returns false and the existing block check still short-circuits submission to a no-op exactly as today.
### 7. Drain via conversation-status path, submit via cloud follow-up path
Two pieces change for cloud-mode draining to work end-to-end.
#### 7a. Detect finish via the conversation-status path
Add a second drain entry point inside `TerminalView::handle_ai_history_model_event`, in the `BlocklistAIHistoryEvent::UpdatedConversationStatus` arm:
- Gate on `FeatureFlag::QueuedPromptsV2.is_enabled()` and `self.is_ambient_agent_session(ctx)`.
- Detect a transition from in-progress/blocked to a terminal status using a `last_observed_conversation_status: HashMap<AIConversationId, ConversationStatus>` field on `TerminalView`. The map is cleared on `ClearedConversationsInTerminalView`, and individual entries are removed in the `RemoveConversation` / `DeletedConversation` handlers. The per-conversation status map remains keyed by conversation id even though the queue is not — see Context — because we have to distinguish transitions per conversation when multiple conversations have lived in the same terminal view.
- Translate the terminal status into a `FinishReason` via an exhaustive match: `Success → Complete`, `Error → Error`, `Cancelled → Cancelled`, `InProgress | Blocked → None`. The `None` arm matters: a status update that is not a transition to a terminal status must not drain the queue.
- Call `self.handle_finished_conversation(finish_reason, ctx)`, which already routes through `drain_queued_prompts` and any registered `queued_prompt_callback`s.
The local AI controller path continues to feed `handle_finished_conversation` for local Agent Mode; the new history-model path feeds it for cloud-mode panes. Both converge on the same drain logic.
#### 7b. Route the popped prompt through the cloud submission path
`TerminalView::drain_queued_prompts` currently submits popped rows via `Input::submit_queued_prompt`, which goes through the local `BlocklistAIController`. That path does nothing useful for a cloud pane.
Add `Input::submit_queued_prompt_for_active_pane(prompt, ctx)` next to `submit_queued_prompt`. It selects the submission path based on pane kind, in order:
- **Cloud follow-up first.** If `ambient_agent_view_model.as_ref(ctx).is_ready_for_cloud_followup_prompt()`, emit `InputEvent::SubmitCloudFollowup { prompt }`. That event is handled by `TerminalView` and routes through `AmbientAgentViewModel::submit_cloud_followup`, the same path used for user-initiated cloud follow-ups. This branch wins over the viewer path because the old shared session is no longer live to receive a `SendAgentPrompt`.
- **Shared-session viewer next.** If `self.model.lock().shared_session_status().is_viewer()`, send the prompt straight to the sharer via `Event::SendAgentPrompt` — no buffer replace, no pending-attachment piggyback, and no use of `submit_viewer_ai_query`. When the user's editor is empty we also surface the standard `"<prompt> ◌"` loading affordance so the queued submission has visible feedback while the sharer ack flight is in flight; the `NetworkEvent::AgentPromptRequestInFlight → unfreeze_and_clear_agent_input` hop will clear it once the sharer acknowledges receipt. If the user has typed something locally, leave the buffer alone so their in-progress prompt is not clobbered.
- **Local Agent Mode fallback.** Otherwise call `submit_queued_prompt` so non-cloud queues are unaffected.
Change `drain_queued_prompts`'s `AutofireAction::Submit { text }` branch to call `submit_queued_prompt_for_active_pane(text, ctx)` instead of `submit_queued_prompt(text, ctx)`. The `PopFromEditMode` branch is unchanged.
## End-to-end flow (V2 on)
```mermaid
flowchart LR
A["User submits initial prompt"] --> B["spawn_agent emits DispatchedAgent"]
B --> C["TerminalView enqueues InitialCloudMode row"]
C --> D["Queue panel renders locked first row"]
E["User submits during setup"] --> F["Input::maybe_queue_input_during_cloud_setup"]
F --> G["AutoQueueToggle row appended after locked row"]
H["HarnessCommandStarted / AppendedExchange / failure / cancel"] --> I["TerminalView removes InitialCloudMode row"]
J["Cloud exchange completes (UpdatedConversationStatus)"] --> K["TerminalView::handle_finished_conversation"]
K --> L["drain_queued_prompts pops next row"]
L --> M["Input::submit_queued_prompt_for_active_pane → SubmitCloudFollowup"]
```
## Testing and validation
Map tests directly to the product invariants in `specs/APP-4562/PRODUCT.md`:
- **§1, §2 (feature gating)**: compile both with and without the cargo feature (`cargo check -p warp` and `cargo check -p warp --features queued_prompts_v2`); unit-test that all new helpers no-op when V2 is off.
- **§3, §4 (initial cloud-mode prompt as locked row)**: `app/src/terminal/view/queued_prompts_test.rs` covers (a) `DispatchedAgent` appends an `InitialCloudMode` row when V2 is on (`dispatched_cloud_prompt_uses_locked_queue_row_when_v2_is_enabled`), (b) `FollowupDispatched` does the same (`dispatched_cloud_followup_uses_locked_queue_row_when_v2_is_enabled`), and (c) the legacy block is not inserted when V2 is on.
- **§4 (lock semantics at the model level)**: `app/src/ai/blocklist/queued_query_tests.rs` includes `initial_cloud_mode_head_rejects_user_mutations_and_autofire` proving `enter_edit_mode`, `remove_by_id`, `reorder` (both `source_id` and `target_index == 0`), and `pop_for_autofire` no-op for `InitialCloudMode` rows; `pop_front_no_ops_when_head_is_locked` covers the non-clean drain path; `remove_initial_cloud_mode_row_only_removes_the_locked_head` covers the lifecycle removal path.
- **§6, §7 (removal sites)**: `cloud_setup_cleanup_events_remove_the_locked_queue_row` covers `HarnessCommandStarted`, `Cancelled`, `NeedsGithubAuth`, and `HandoffSnapshotUploadFailed`; `failed_event_keeps_locked_queue_row_under_cloud_mode_setup_v2` and `failed_event_removes_locked_queue_row_without_cloud_mode_setup_v2` cover the `Failed` event under both `CloudModeSetupV2` configurations, mirroring the legacy block's gating. `AppendedExchange` with renderable user query, and the oz local-to-cloud handoff first exchange, are exercised by existing terminal-view test coverage of the legacy removal sites — both paths now share the same `remove_cloud_mode_queue_row` helper.
- **§8 (during-setup queueing)**: `cloud_setup_enter_queues_followup_input_when_v2_is_enabled` asserts `maybe_queue_input_during_cloud_setup` appends an `AutoQueueToggle` row in the `WaitingForSession` state; `cloud_setup_enter_remains_blocked_when_v2_is_disabled` asserts no row is appended and the editor buffer is preserved when V2 is off.
- **§13–§16 (drain)**: `terminal_cloud_status_transition_drains_once_through_cloud_followup_input_event` asserts (a) `UpdatedConversationStatus` transitioning to a terminal status calls `handle_finished_conversation` once per transition, not per status event (the test feeds two `Success` events back-to-back and verifies a single drain), and (b) the resulting submission path goes through `InputEvent::SubmitCloudFollowup` for cloud panes via the `is_ready_for_cloud_followup_prompt` predicate.
- Full presubmit: `./script/presubmit`.
Do not run the app to test.
## Parallelization
Single workstream. The queue model, panel rendering, cloud-mode dispatch sites, conversation-status drain hook, and submission routing all share types (`QueuedQueryOrigin`, `FinishReason`, `ConversationStatus`, `AmbientAgentViewModelEvent`) that must stay consistent across edits. Splitting across child agents would create merge churn on the same files without reducing wall-clock time.
## Risks and mitigations
- **`InitialCloudMode` row leaking past run start**: every existing call to `remove_pending_user_query_block` for cloud kind has a sibling call to `TerminalView::remove_cloud_mode_queue_row` (via a unified `should_clean_up_pending_cloud_query` gate in §5), and the model-level `pop_for_autofire` and `pop_front` no-ops prevent accidental autofire or non-clean-drain pop of the locked row. The shared `QueuedQuery::is_locked()` helper centralises the check so every mutator agrees on the lock contract.
- **Double drain**: `FinishedReceivingOutput` (local) and `UpdatedConversationStatus` (history) could both call `handle_finished_conversation`. Mitigation: the new history-model entry is gated on `is_ambient_agent_session` and on a status *transition* tracked via `last_observed_conversation_status`, not on raw status updates. The `FinishReason` translation uses an exhaustive match that returns `None` for `InProgress | Blocked` so partial-status updates do not re-fire the queue.
- **Cross-surface block removal regression**: `remove_pending_cloud_mode_query_if_exchange_has_renderable_user_query` gates the legacy pending-user-query block removal on the `CloudMode` kind explicitly (independent of the V2 flag) so unrelated `PendingUserQueryKind::QueuedPrompt` blocks are not torn down under V2; the V2 queue-row removal is layered on top as a no-op when no `InitialCloudMode` row exists.
- **Conversation id not yet assigned at `DispatchedAgent`**: §4 falls back to the legacy pending-user-query block so the visual indicator is never lost.
- **Cargo feature dependency mistake**: if `queued_prompts_v2 = ["queue_slash_command"]` is omitted from `app/Cargo.toml`, V2 appears to work but the regular queue surfaces are silently dark. Mitigation: presubmit + the explicit compile-check on `--features queued_prompts_v2` in isolation.
- **Panel placement inside the V2 centered input**: the panel must match `CLOUD_MODE_V2_MAX_WIDTH` so it stays visually attached to the input card. Mitigation: render it inside the same `ConstrainedBox`/`Align` wrapper that the V2 input uses (§3).