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

94 lines
17 KiB
Markdown

# APP-4365: Use Queued Query UI for Oz Cloud Mode Queries — Tech Spec
## Context
`PRODUCT.md` defines the user-visible target: initial and follow-up Oz cloud mode prompts should use the same queued-query UI as third-party cloud agents, setup-command rich content should remain unchanged, and the real transcript query should render normally when it arrives.
Cloud mode panes are deferred shared-session viewers. `create_cloud_mode_view` creates a deferred viewer manager, subscribes it to `AmbientAgentViewModel`, and connects or appends a session on `SessionReady` / `FollowupSessionReady` in `app/src/terminal/view/ambient_agent/mod.rs (42-105)`. `is_cloud_agent_pre_first_exchange` identifies the setup interval after a session is ready but before the first exchange arrives, including the third-party harness-start escape hatch in `app/src/terminal/view/ambient_agent/mod.rs (110-162)`.
`AmbientAgentViewModel` owns cloud run state. It tracks `Status::WaitingForSession { kind: InitialRun | Followup }`, the selected harness, task/session IDs, pending follow-up prompt, and today also tracks Oz optimistic-query state via `has_inserted_cloud_mode_user_query_block` and `optimistically_rendered_user_queries` in `app/src/terminal/view/ambient_agent/model.rs (64-145)`. Initial cloud runs call `spawn_internal`, store the request, transition to `WaitingForSession`, and emit `DispatchedAgent` in `app/src/terminal/view/ambient_agent/model.rs (660-679)`. Follow-up runs call `submit_cloud_followup`, set `pending_followup_prompt`, transition to `WaitingForSession`, and emit `FollowupDispatched` in `app/src/terminal/view/ambient_agent/model.rs (492-529)`.
The queued-query visual pattern already exists as `PendingUserQueryBlock`: it renders the prompt with user avatar, dimmed text, and a `Queued` badge, with optional close/send-now controls in `app/src/ai/blocklist/block/pending_user_query_block.rs (16-175)`. `TerminalView::insert_cloud_mode_queued_user_query_block` inserts that block without buttons and without a queued callback, specifically for cloud mode lifecycle-owned prompts, in `app/src/terminal/view/pending_user_query.rs (76-91)`. It is inserted as rich content with `RichContentMetadata::PendingUserQuery` and `PinToBottom` in `app/src/terminal/view/pending_user_query.rs (28-74)` and `app/src/terminal/view/rich_content.rs (183-244)`.
The current event handling creates divergent UI. On `DispatchedAgent`, third-party harnesses rebuild the display prompt with `display_user_query_with_mode` and call `insert_cloud_mode_queued_user_query_block`, while Oz creates `CloudModeInitialUserQuery`, inserts it as rich content, sets `has_inserted_cloud_mode_user_query_block`, and records the stripped prompt as optimistic UI in `app/src/terminal/view/ambient_agent/view_impl.rs (129-189)`. On `FollowupDispatched`, Oz creates `CloudModeFollowupUserQuery`, inserts it, and records the optimistic prompt in `app/src/terminal/view/ambient_agent/view_impl.rs (200-229)`.
The bespoke optimistic views live in `app/src/terminal/view/ambient_agent/block/query.rs (20-159)`. They share normal AI query rendering and show a `Failed` label when the ambient model has an error. They are re-exported through `app/src/terminal/view/ambient_agent/block.rs (1-8)`.
The duplicate-hiding logic lives in the AI block renderer. `should_hide_ai_block_query_and_header` hides a real AI block query/header for the first cloud exchange or any recorded optimistic follow-up prompt while the viewer is live and not receiving replay in `app/src/ai/blocklist/block/view_impl.rs (99-123)`. `AIBlock::render` asks the ambient model for `has_inserted_cloud_mode_user_query_block` and `has_optimistic_user_query` before deciding whether to render the actual query/header in `app/src/ai/blocklist/block/view_impl.rs (842-907)`. Tests currently assert this hiding behavior in `app/src/ai/blocklist/block/view_impl/cloud_mode_setup_tests.rs (1-37)`.
Setup-command rich content is independent of the optimistic prompt blocks. `maybe_insert_setup_command_blocks` gates on CloudModeSetupV2 and `is_cloud_agent_pre_first_exchange`, then inserts the setup intro and setup-command block before the real terminal block in `app/src/terminal/view/ambient_agent/view_impl.rs (373-453)`. When the first Oz exchange is appended, `handle_ai_history_model_event` turns off `is_executing_oz_environment_startup_commands` before inserting the real AI block in `app/src/terminal/view.rs (5086-5159)`.
Follow-up submission is already routed away from the stale shared-session network. `try_submit_pending_cloud_followup` intercepts `InputEvent::SendAgentPrompt`, validates the task, calls `submit_cloud_followup`, and resets the input in `app/src/terminal/view.rs (19735-19794)`. If not intercepted, `TerminalViewEvent::SendAgentPrompt` still goes to the viewer network in `app/src/terminal/shared_session/viewer/terminal_manager.rs (1323-1398)`. Existing follow-up and tombstone tests cover this route in `app/src/terminal/view_test.rs (552-576)` and `app/src/terminal/view/shared_session/view_impl_test.rs (638-805)`.
## Proposed changes
### Use one cloud queued-query insertion path
Update `TerminalView::handle_ambient_agent_event` so both Oz and third-party cloud runs use `insert_cloud_mode_queued_user_query_block` while waiting for the real cloud transcript.
For `DispatchedAgent`:
- Keep the existing viewer short-circuit and `CloudModeSetupV2` gate.
- Remove the `is_third_party_harness()` branch that sends Oz down the bespoke optimistic path.
- Build the display prompt from `AmbientAgentViewModel::request()` using the existing `display_user_query_with_mode(request.mode, &request.prompt)` helper, matching the current third-party path and satisfying `PRODUCT.md` Behavior 4.
- Insert the queued-query block when the display prompt is non-empty.
- Do not call `set_has_inserted_cloud_mode_user_query_block` or `record_optimistic_user_query`.
For `FollowupDispatched`:
- Keep the conversation-status update to `ConversationStatus::InProgress`.
- Read `pending_followup_prompt()` and call `insert_cloud_mode_queued_user_query_block(prompt, ctx)` instead of creating `CloudModeFollowupUserQuery`.
- Do not record the prompt as optimistic UI.
This intentionally reuses `PendingUserQueryBlock` rather than `QueuedQueryModel`. `QueuedQueryModel` in `app/src/ai/blocklist/queued_query.rs (1-67)` is not currently wired into the terminal rich-content path, while `insert_cloud_mode_queued_user_query_block` is the existing third-party cloud UI surface the product spec names as the reference.
### Remove the queued item only when the real transcript user query arrives
Add cloud-specific removal at the point the real Oz AI exchange with a renderable user query is appended. In `handle_ai_history_model_event`, after turning off `is_executing_oz_environment_startup_commands` for ambient sessions and before inserting the `AIBlock`, call `remove_pending_user_query_block(ctx)` only when:
- `CloudModeSetupV2` is enabled,
- the terminal is an ambient agent session,
- the appended exchange belongs to the visible root task that will produce an AI block,
- the exchange contains an input that will render a user query for the submitted prompt, and
- `pending_user_query_view_id` is set.
The key invariant is that the queued item survives session attach and any intermediate shared-session output until the actual user-query element from the real shared-session response exists. `SessionReady`, `FollowupSessionReady`, progress updates, setup-command blocks, harness-start transitions, and generic output without a renderable user query must not remove the queued item.
The removal should not depend on prompt string matching unless the existing exchange/input model exposes a cheap exact display-query comparison. The cloud queued-query helper owns a single lifecycle-managed pending item, and the first visible real user-query exchange for that ambient conversation is the replacement surface. This avoids retaining a stale queued item while also avoiding the old real-query hiding logic. If the exchange is filtered out by `blocklist_filter::should_show_task_in_blocklist`, or if it does not contain a renderable user query, leave the pending item untouched until a later visible user-query exchange, failure, cancellation, auth event, or other terminal lifecycle event handles it.
Keep the existing error/auth/cancel cleanup at the top of `handle_ambient_agent_event` in `app/src/terminal/view/ambient_agent/view_impl.rs (111-126)`. That cleanup already removes lifecycle-owned cloud queued items and should continue to apply to Oz and third-party runs. Do not remove the queued item on `SessionReady` or `FollowupSessionReady`; those events mean a shared session exists, not that the real user-query transcript has rendered.
### Delete Oz optimistic-query rendering and tracking
Remove `CloudModeInitialUserQuery`, `CloudModeFollowupUserQuery`, and the private `render_user_query` helper from `app/src/terminal/view/ambient_agent/block/query.rs`. If that leaves the file empty, remove `mod query;` and `pub use query::*;` from `app/src/terminal/view/ambient_agent/block.rs`.
Remove `AmbientAgentViewModel` fields and methods that only support optimistic query hiding:
- `has_inserted_cloud_mode_user_query_block`
- `optimistically_rendered_user_queries`
- `has_inserted_cloud_mode_user_query_block()`
- `set_has_inserted_cloud_mode_user_query_block(...)`
- `record_optimistic_user_query(...)`
- `has_optimistic_user_query(...)`
Also remove their initialization and reset logic in `new` and `reset_for_new_cloud_prompt`.
Remove `should_hide_ai_block_query_and_header` from `app/src/ai/blocklist/block/view_impl.rs`, delete the ambient-model lookup in `AIBlock::render`, and let the existing `query_and_index` path render the real query/header normally. This implements `PRODUCT.md` Behavior 11-14 directly: the queued rich content is the transient placeholder, and the AI block is the real transcript representation.
### Preserve setup-command rich content
Do not change `maybe_insert_setup_command_blocks`, `CloudModeSetupTextBlock`, `CloudModeSetupCommandBlock`, `is_cloud_agent_pre_first_exchange`, or the block-list startup-command flags. The queued-query change should be limited to which pending prompt rich content is inserted and when it is removed. This preserves `PRODUCT.md` Behavior 7-8 and avoids changing the setup-command grouping/order behavior already covered by CloudModeSetupV2.
### Update comments and names where they become stale
Update comments on `insert_cloud_mode_queued_user_query_block` from “non-oz Cloud Mode run” to “Cloud Mode run waiting for the real transcript” so it accurately covers Oz and third-party runs. Update comments in `pending_user_query_view_id` only if needed; the field remains shared by normal `/queue` prompts and lifecycle-owned cloud queued prompts, but cloud insertion still uses the callback-free helper.
No new feature flag is needed. Initial-run behavior remains under `CloudModeSetupV2`, and cloud follow-up behavior remains reachable only through the existing `HandoffCloudCloud` follow-up entrypoints.
## End-to-end flow
1. User submits an initial Oz cloud prompt.
2. `AmbientAgentViewModel::spawn_internal` stores the stripped request and emits `DispatchedAgent`.
3. `TerminalView::handle_ambient_agent_event` reconstructs the display prompt and inserts a callback-free `PendingUserQueryBlock`.
4. Setup commands, if any, continue through `maybe_insert_setup_command_blocks`.
5. When the real shared-session response appends an Oz exchange with a renderable user query, `handle_ai_history_model_event` removes the queued-query rich content and inserts the normal `AIBlock`.
6. `AIBlock::render` renders the real query/header because the optimistic-query hiding gate is gone.
7. For follow-up prompts, `try_submit_pending_cloud_followup` calls `submit_cloud_followup`, `FollowupDispatched` inserts the same queued-query block, and the next appended exchange that contains the actual follow-up user query replaces it the same way.
## Testing and validation
Unit tests should cover the product invariants without depending on pixel rendering:
- Add or update a terminal view test that enables `CloudModeSetupV2`, dispatches an Oz initial cloud run, and asserts that `pending_user_query_view_id` is set and the corresponding rich content metadata is `PendingUserQuery`. This covers Behavior 1, 3, 5, 18, 20, and 25.
- Add the same assertion for a non-Oz harness to ensure the existing third-party path is unchanged. This covers Behavior 3 and 26.
- Add a follow-up test around `FollowupDispatched` or `try_submit_pending_cloud_followup` that asserts a queued-query block is inserted instead of an optimistic follow-up view. This covers Behavior 2, 6, 19, 20, and 25.
- Add a history-model append test for an ambient cloud conversation that starts with a queued-query block, appends a visible exchange containing a renderable user query, and asserts the pending block is removed while an AI block remains. This covers Behavior 9-14 and 24.
- Add a negative history/session test that attaches a session, emits progress/setup-command/generic output, or appends an exchange without a renderable user query, and asserts the queued-query block remains visible. This covers Behavior 10.
- Remove or rewrite `app/src/ai/blocklist/block/view_impl/cloud_mode_setup_tests.rs (1-37)`. The old assertions should no longer hold; if a lightweight replacement is useful, assert that normal AI block query/header rendering is not suppressed for cloud exchanges.
- Keep existing setup-command tests or add a regression assertion that `maybe_insert_setup_command_blocks` still inserts setup text and command blocks while a queued-query block exists and does not remove that queued item. This covers Behavior 7-8, 10, and 23.
- Keep existing follow-up route tests in `app/src/terminal/view_test.rs (552-576)` and `app/src/terminal/view/shared_session/view_impl_test.rs (638-805)` passing; extend them only if useful to assert queued-query insertion after `submit_cloud_followup`.
Targeted validation commands:
- `cargo test -p warp cloud_mode_setup_tests`
- `cargo test -p warp pending_cloud_followup`
- `cargo test -p warp append_followup`
- `cargo check -p warp --features handoff_cloud_cloud`
Before opening or updating a PR, follow repository rules for formatting and clippy. Do not use `cargo fmt --all` or file-specific `cargo fmt`.
Manual validation:
- With CloudModeSetupV2 enabled, submit an initial Oz cloud prompt and verify the queued-query item appears immediately, remains visible after session attach and while setup-command rich content runs, and is replaced only when the real transcript user query appears.
- Submit a follow-up from an eligible cloud tombstone or owned follow-up input and verify the same queued-query item appears and is replaced only by the real follow-up transcript query.
- Repeat the same initial run with a third-party harness to confirm its queued-query behavior is unchanged.
- Test failure/auth/cancel during startup and confirm the queued item follows the existing third-party lifecycle and no bespoke `Failed` optimistic-query block remains.
## Risks and mitigations
### Removing the queued item too early
`SessionReady`, setup-command output, progress events, or an exchange without a renderable user query can arrive before the real user-query transcript is rendered. Removing the queued item there would create a visible gap. Mitigate by removing only when a visible exchange contains the actual user query from the shared-session transcript.
### Removing unrelated `/queue` prompts
`pending_user_query_view_id` is shared by normal queued prompts and cloud lifecycle prompts. Mitigate by only using the new `AppendedExchange` removal inside ambient cloud-session handling, and only after a visible cloud exchange with a renderable user query is about to render.
### Prompt display mismatch for `/plan` and `/orchestrate`
The spawn request stores the stripped prompt and separate mode. Mitigate by keeping the existing `display_user_query_with_mode` reconstruction already used by third-party cloud runs.
### Setup-command regressions
Setup-command insertion is adjacent to the query UI in the same event flow. Mitigate by not touching `maybe_insert_setup_command_blocks` or startup-command flags, and by adding a regression check that setup blocks still render alongside the queued item.
### Stale optimistic-query code paths
Leaving optimistic tracking in place could continue hiding real transcript queries. Mitigate by deleting the model fields/methods and the AI block hiding helper in the same change that switches insertion to queued-query UI.
## Parallelization
Parallel implementation is not beneficial for this change. The work is tightly coupled across one event handler, one model cleanup, one AI-block render gate, and a small set of targeted tests; splitting it across agents would increase merge conflicts more than it would reduce wall-clock time.