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
+26
View File
@@ -0,0 +1,26 @@
# Product Spec: Attachments on Queued Prompts
Linear: [APP-4617](https://linear.app/warpdotdev/issue/APP-4617)
Figma: none provided
## Summary
Queued agent prompts retain the image and file attachments staged in the input at the time they were queued. The attachments move off the input onto the queued row, are sent with that prompt when it fires, and are restored to the input if the row is brought back for editing.
## Problem
With queued prompts, staged attachments lived only in the live input. Queuing a prompt either left them in the input (where they would be re-sent with the user's next prompt) or cleared them (losing them entirely). A queued prompt could never carry its own attachments, and a fired queued prompt could steal attachments meant for the next message.
## Behavior
1. When a prompt is queued while one or more attachments (images, files) are staged in the input, those attachments are captured onto the queued row and removed from the live input. The input returns to empty — no text and no attachment chips — ready for the next prompt.
2. Each queued row owns its own attachment set. Queuing prompt A with attachment X and then prompt B with attachment Y produces two rows that each carry only their own attachments; firing or removing one row never alters the other's attachments.
3. When a queued row fires (the conversation it was queued on finishes), the prompt is sent with that row's stored attachments: images are sent inline as image context, and files are sent as file references. Files that share a basename are disambiguated with `(1)`, `(2)`, … suffixes.
4. A fired queued row is always submitted into the conversation it was queued on, even if the user has since navigated to a different conversation. Its attachments resolve from the row, never from whatever is currently staged in the input.
5. Attachments staged for the user's next prompt are never consumed by a firing queued row. If the user stages new attachments after queuing, those stay in the input and are sent with the user's next prompt, independent of any row that fires in the meantime.
6. Restoring a queued row to the input re-stages its attachments (the chips reappear):
- If the head row was in edit mode when auto-fire reached it and the input is empty, the row's last-committed text and its attachments are placed back into the input. Uncommitted live-editor text is not used.
- When the user manually restores a row to the input for editing, its attachments are re-staged so a manual re-submit keeps them.
7. Removing a queued row — whether deleted by the user or removed after it fires — drops its attachments. They are not left behind in the input or any other row.
8. Queued slash commands and queued skill invocations carry attachments the same way: a queued skill invocation with staged images/files sends them with the skill when it fires. A direct (non-queued) skill invocation continues to consume the live input staging as before.
9. The `/queue` command, when the agent is not currently in progress, submits immediately as a regular prompt — consuming and clearing the live staging — rather than being treated as a queued-row fire.
10. **Known limitation — cloud follow-up:** when a queued row fires into a cloud follow-up prompt, which does not support attachments, the prompt text is still sent but the row's attachments are dropped (a warning is logged). No attachment chips or files reach the cloud run.
11. **Shared-session viewer:** when a queued row fires in a shared-session viewer, its stored images/files are uploaded and sent with the prompt (when the cloud pane supports image context), via the same upload-then-send path as an immediate viewer submission.
12. Empty-queue and locked-row behavior is unchanged: draining an empty queue does nothing, and the locked initial Cloud Mode row never auto-fires. Each conversation keeps an independent queue, so attachments on one conversation's rows never appear on another's.
+81
View File
@@ -0,0 +1,81 @@
# Tech Spec: Attachments on Queued Prompts
See `specs/APP-4617/PRODUCT.md` for user-visible behavior.
## Context
Queued prompts (V2) store per-conversation rows in `QueuedQueryModel` (`app/src/ai/blocklist/queued_query.rs`). Before this change a `QueuedQuery` held only `text` + `origin`; staged attachments lived solely in the live input staging on `BlocklistAIContextModel.pending_attachments`. Two things coupled attachments to the live input rather than the queued row:
- At enqueue time the input attachments were left in place or cleared, so a row never carried them.
- The send path always sourced pending attachments from the context model: `input_for_query` built image context from `vec![]` and `input_context_for_request` (`app/src/ai/blocklist/controller/input_context.rs`) appended `context_model.pending_files()` as `FilePathReference`s. A fired queued row therefore picked up whatever was currently staged, and a direct send after queuing re-sent the previous attachments.
Relevant code (current branch):
- `app/src/ai/blocklist/queued_query.rs``QueuedQuery` (text/origin), `AutofireAction`, `pop_for_autofire` (removed the head row and returned its action)
- `app/src/ai/blocklist/context_model.rs``pending_attachments` staging, `clear_pending_attachments`
- `app/src/ai/blocklist/controller.rs:3132``input_for_query`; send path in `send_query` (~758)
- `app/src/ai/blocklist/controller/input_context.rs (230-)` — pending-file → `FilePathReference` conversion
- `app/src/ai/blocklist/controller/slash_command.rs:68``SlashCommandRequest::send_request`
- `app/src/terminal/input.rs:13179` `submit_queued_prompt`, `:13277` `submit_queued_prompt_for_active_pane`, `:5242` `execute_skill_command`, viewer send (~13767)
- `app/src/terminal/input/slash_commands/mod.rs (1069-)``/queue` handling
- `app/src/terminal/view.rs:5199` enqueue, `:5227` `drain_queued_prompts`
- `app/src/terminal/view/pending_user_query.rs` — legacy pending-query submission
## Proposed changes
### 1. Rows own their attachments (`queued_query.rs`)
`QueuedQuery` gains `attachments: Vec<PendingAttachment>`, a `new_with_attachments(text, origin, attachments)` constructor (`:59`; `new` delegates to it), and an `attachments()` accessor (`:100`). `attachments_for(conversation_id, query_id)` (`:374`) returns a row's attachments by id without removing it; it returns `&[]` when the row is absent.
### 2. Capture-and-clear at every enqueue site
Each enqueue site drains the live input via a new `BlocklistAIContextModel::take_pending_attachments(ctx)` (`context_model.rs:987`) and stores the drained set on the row with `new_with_attachments`. `take_pending_attachments` emits the same `UpdatedPendingContext` event as `clear_pending_attachments` so the input's attachment chips disappear. Sites: `terminal/view.rs:5199`, the two auto-queue-toggle paths in `terminal/input.rs` (~13433, ~13490), and the `/queue` in-progress branch in `slash_commands/mod.rs`.
### 3. Auto-fire becomes peek + remove (`queued_query.rs`, `view.rs`)
`pop_for_autofire` (which mutated the queue) is replaced by:
- `peek_autofire(conversation_id) -> Option<AutofireAction>` (`:326`) — read-only; returns the head row's action while leaving the row in the queue so the send path can resolve its attachments by id.
- `remove_fired_row(conversation_id, query_id, ctx)` (`:349`) — removes the row after dispatch/restore and clears edit state if it pointed at that row.
Both `AutofireAction` variants now carry `query_id`; `PopFromEditMode` additionally carries `attachments`. `drain_queued_prompts` (`view.rs:5227`) peeks, dispatches or restores, then calls `remove_fired_row`. This peek-then-remove ordering is required: the row must stay addressable during the synchronous send so attachments can be read by id.
### 4. Send path resolves attachments by source (`controller.rs`, `input_context.rs`)
`InputQuery` gains `queued_query_id: Option<QueuedQueryId>`. In `send_query`, the attachment set for a `UserSubmittedQueryFromInput` is resolved once:
- `Some(id)``QueuedQueryModel::attachments_for(conversation_id, id)` (the fired row)
- `None``context_model.pending_attachments()` (live staging)
`input_for_query` (`:3132`) now takes `prompt_attachments: Vec<PendingAttachment>`, splits them into image context (sent inline) and file references, and no longer relies on `input_context_for_request` for pending files. The pending-file → `FilePathReference` conversion (with duplicate-basename suffixing) moves out of `input_context.rs` into a shared `add_pending_file_attachments` (`controller.rs:3190`); `input_context.rs` no longer sources pending files.
### 5. Conversation routing for fired rows (`controller.rs`, `slash_command.rs`, `input.rs`)
A fired row routes into the conversation it was queued on rather than re-deriving from the current UI selection. `send_queued_slash_command_request` and `send_queued_user_query_in_conversation` thread `queued_query_id` plus a `conversation_id` override; `SlashCommandRequest::send_request` (`slash_command.rs:68`) replaces its `is_queued_prompt: bool` with `queued_query_id: Option<QueuedQueryId>` + `conversation_id_override: Option<AIConversationId>` and derives `is_queued_prompt` from the id. Queued skill invocations resolve `prompt_attachments` from the row (or `vec![]` if the conversation is unknown) and feed them through `add_pending_file_attachments` into `InvokeSkillUserQuery`.
### 6. Preserve next-prompt staging (`controller.rs`, `slash_command.rs`)
The context reset after a send is skipped when `is_queued_prompt` is true (the fired row's attachments came from the row, so the live `pending_attachments` belong to the user's next prompt). The same guard applies to queued skill invocations so they don't clear a new draft's staged attachments; direct skills still reset.
### 7. Split immediate vs. queued submission (`input.rs`, `pending_user_query.rs`, `slash_commands/mod.rs`)
- `submit_queued_prompt` (`input.rs:13179`) now takes `conversation_id` + `query_id` and submits the fired row into that conversation.
- New `submit_user_query_now` (`input.rs:13248`) is the immediate (non-queued) path that resets live staging; used by the `/queue` not-in-progress fallback and the legacy pending-user-query paths in `pending_user_query.rs`.
- `submit_queued_prompt_for_active_pane` (`input.rs:13277`) takes `conversation_id` + `query_id` and branches: cloud follow-up (drop attachments, log a warning), shared-session viewer (upload via the shared path below), local agent (`submit_queued_prompt`).
- `execute_skill_command` (`input.rs:5242`) replaces `is_queued_prompt: bool` with `queued_query_id` + `conversation_id_override`.
### 8. Shared viewer upload path (`input.rs`)
A new `upload_and_send_viewer_prompt` is extracted from the immediate viewer-submit path and shared with the queued viewer drain, so both go through the identical upload-then-send (`Event::SendAgentPrompt`) flow. The queued viewer drain reads the firing row's images/files from `attachments_for` and passes them in.
### 9. Re-stage on restore (`view.rs`)
`drain_queued_prompts`' `PopFromEditMode` branch and the manual edit/restore path call `context_model.append_pending_attachments(row attachments)` after restoring the row's text, so the chips reappear and a manual re-submit keeps them.
## Testing and validation
Unit tests added alongside the changed modules; each maps to PRODUCT.md invariants:
- `context_model_tests.rs``take_pending_attachments` drains and returns all staged attachments and clears the input (inv. 1); enqueue moves staged attachments onto the row and leaves the input empty (inv. 1, 7).
- `queued_query_tests.rs``peek_autofire` leaves the row until `remove_fired_row` drops it (inv. 3); `PopFromEditMode` carries committed text + attachments and peek is non-mutating (inv. 6).
- `controller_tests.rs``input_for_query` builds image/file context purely from the provided attachment set, ignoring live staging, including duplicate-basename suffixing (inv. 3, 5).
- `queued_prompts_tests.rs` — multi-cycle queue keeps each row's attachments independent and draining one leaves the other intact (inv. 2); shared `drain_one` helper mirrors peek + `remove_fired_row`.
Manual verification:
- Stage an image + file, queue while the agent is busy → chips clear from input; on fire the prompt arrives with the image inline and the file referenced (inv. 1, 3).
- Queue two prompts with different attachments → each fires with only its own (inv. 2).
- Stage attachments after queuing → they ride the next manual prompt, not the fired row (inv. 5).
- Edit-mode auto-fire pop and manual restore → attachments re-appear in the input (inv. 6).
- Cloud follow-up fire → text sent, attachments dropped, warning logged (inv. 10).
- Shared-session viewer fire → attachments uploaded and sent (inv. 11).
## Risks and mitigations
- **Double-fire / leaked rows:** peek no longer removes the row, so `remove_fired_row` must run after every dispatch and every restore. `drain_queued_prompts` removes in both `Submit` and `PopFromEditMode` arms immediately after the synchronous dispatch.
- **Attachment lifetime:** attachments are cloned when resolved by id during send and dropped when the row is removed; there is no shared ownership between the row and the live input, which is what keeps inv. 2 and inv. 5 independent.
## Parallelization
Not beneficial. The change is a single tightly-coupled thread through the queued-prompt enqueue, drain, and send paths (`queued_query.rs``view.rs`/`input.rs``controller.rs`/`slash_command.rs`); the signature changes ripple across these files and must land together. Best done sequentially in one PR.