12 KiB
Queued Prompts V2 for /compact-and and /fork-and-compact — Tech Spec
Builds on the regular Agent Mode queued-prompts panel from specs/REMOTE-1543/ and the Cloud Mode extension from specs/APP-4562/. This spec is the third (and final) step in moving every user-facing queued prompt surface onto the new panel UI behind QueuedPromptsV2.
Context
Today /compact-and <prompt> and /fork-and-compact <prompt> still file their follow-up prompts through the legacy PendingUserQueryBlock path while a summarize (or fork-then-summarize) runs. The follow-up appears as a rich-content placeholder in the blocklist instead of as a row in the new queued-prompts panel.
Both flows funnel into one helper:
TerminalView::send_user_query_after_next_conversation_finished(app/src/terminal/view/pending_user_query.rs:185) inserts aPendingUserQueryKind::QueuedPromptblock viainsert_pending_user_query_blockand stashes aqueued_prompt_callbackonTerminalView(app/src/terminal/view.rs:4218).TerminalView::handle_finished_conversation(app/src/terminal/view.rs:4788) drains both the new queue (drain_queued_prompts) and the legacy callback. The callback submits viaInput::submit_queued_promptonFinishReason::Completeand restores the text into the input on the error/cancel reasons. The two callsites that reach this helper are:/compact-and: dispatched asWorkspaceAction::SummarizeAIConversation { initial_prompt, .. }, which callsWorkspace::summarize_active_ai_conversation(app/src/workspace/view.rs:12173-12200). The active terminal view sendsSlashCommandRequest::Summarizeand, if aninitial_promptis present, callssend_user_query_after_next_conversation_finished(prompt, /*show_close*/ true, /*show_send_now*/ false, ctx)./fork-and-compact: dispatched asWorkspaceAction::ForkAIConversation { summarize_after_fork: true, initial_prompt, .. }. After the fork is created and restored into the new pane,Workspace::handle_forked_conversation_prompts(app/src/workspace/view.rs:12071-12117) sendsSlashCommandRequest::Summarizeon the forked terminal view, then calls the samesend_user_query_after_next_conversation_finishedhelper on that new terminal view. The forked conversation has already been restored viarestore_conversation_after_view_creation, soselected_conversation_idon the new terminal view resolves to the forked id by the time the helper runs. Otheron_next_conversation_finishedcallers (app/src/terminal/view.rs:6744, 13597, 13656) are internal init/project sequencing — they do not render user-visible queued prompts and stay on the existing path. A grep forsend_user_query_after_next_conversation_finishedconfirms/compact-andand/fork-and-compactare the only remaining queued-prompt surfaces still on the legacy block. The new queued-prompts panel (app/src/ai/blocklist/queued_prompts_panel.rs) and its modelQueuedQueryModel(app/src/ai/blocklist/queued_query.rs) already support exactly the surface this spec needs: append a row per origin, drain onFinishReason::CompleteviaTerminalView::drain_queued_prompts(app/src/terminal/view.rs:5106), and restore text to the input on error/cancel. For local Agent Mode,drain_queued_promptssubmits viaInput::submit_queued_prompt_for_active_pane(app/src/terminal/input.rs:13145), which falls back toInput::submit_queued_prompt(app/src/terminal/input.rs:13062) — the exact path the legacy callback already uses. No new submission plumbing is required.
Proposed changes
1. Two new QueuedQueryOrigin variants for telemetry
Extend QueuedQueryOrigin (app/src/ai/blocklist/queued_query.rs:22) with two variants matching the existing per-origin pattern:
CompactAndSlashCommandForkAndCompactSlashCommandNeither variant is locked (QueuedQuery::is_lockedkeepsInitialCloudModeas the only locked origin atapp/src/ai/blocklist/queued_query.rs:63). These rows are user-managed: interactive drag, edit, and delete, exactly likeQueueSlashCommandandAutoQueueTogglerows. The follow-up has not been sent anywhere yet at queue time; only the summarize is running. Mirror the new variants intoTelemetryQueuedQueryOrigin(app/src/server/telemetry/events.rs:1187) and itsFrom<QueuedQueryOrigin>impl. No new telemetry events are needed — the existingQueuedPrompt.Edited/QueuedPrompt.Deleted/QueuedPrompt.Reordered/QueuedPrompt.PanelCollapseToggledevents already carryoriginand gate onFeatureFlag::QueueSlashCommand, which is transitively enabled byQueuedPromptsV2perapp/Cargo.toml:942.
2. Single TerminalView helper hides the V2 gate from callers
Add TerminalView::enqueue_followup_prompt (next to enqueue_prompt at app/src/terminal/view.rs:5051):
pub fn enqueue_followup_prompt(
&mut self,
prompt: String,
origin: QueuedQueryOrigin,
conversation_id: AIConversationId,
ctx: &mut ViewContext<Self>,
) {
if FeatureFlag::QueuedPromptsV2.is_enabled() {
self.queued_query_model.update(ctx, |model, ctx| {
model.append(conversation_id, QueuedQuery::new(prompt, origin), ctx);
});
} else {
self.send_user_query_after_next_conversation_finished(
prompt,
/* show_close_button */ true,
/* show_send_now_button */ false,
ctx,
);
}
}
The helper takes an explicit conversation_id so the /fork-and-compact caller can pass forked_conversation_id directly without depending on selected_conversation_id post-restoration ordering. The legacy branch ignores it, matching the existing helper which uses the input's selected conversation lazily inside the callback.
The helper is the only place that names FeatureFlag::QueuedPromptsV2 for this rollout; subsequent cleanup deletes the legacy branch and the helper collapses to a one-liner.
3. Route both slash-command paths through the helper
Replace the two send_user_query_after_next_conversation_finished callsites in Workspace so they delegate to the helper instead:
Workspace::summarize_active_ai_conversation(app/src/workspace/view.rs:12194): after sendingSlashCommandRequest::Summarize, resolve the active conversation id viaterminal.ai_context_model().as_ref(ctx).selected_conversation_id(ctx)and callterminal.enqueue_followup_prompt(prompt, QueuedQueryOrigin::CompactAndSlashCommand, conversation_id, ctx). Ifselected_conversation_idisNone, fall through with no follow-up (matches the legacy semantics — the slash-command handler atapp/src/terminal/input/slash_commands/mod.rs:1030-1048already requires an active conversation to dispatch/compact-and).Workspace::handle_forked_conversation_prompts(app/src/workspace/view.rs:12097): pass the already-knownforked_conversation_idand useQueuedQueryOrigin::ForkAndCompactSlashCommand. Both callsites continue to sendSlashCommandRequest::Summarizeviaai_controllerbefore enqueueing the follow-up, so the conversation transitions toInProgressbefore any drain hook can observe it.
4. Legacy path is preserved when the flag is off
When QueuedPromptsV2 is off, the helper's else branch is the exact code that runs today: send_user_query_after_next_conversation_finished inserts a PendingUserQueryKind::QueuedPrompt block, sets queued_prompt_callback, and handle_finished_conversation drains it on completion. No other code paths change.
PendingUserQueryIndicator continues to gate the block's visibility inside send_user_query_after_next_conversation_finished (app/src/terminal/view/pending_user_query.rs:192); this spec does not modify that gate.
Testing and validation
Behavior maps to the existing regular-queue panel invariants in specs/REMOTE-1543/PRODUCT.md (12-30, 31-37). The new code is the helper plus two callsites; testing focuses on the helper's branching and on the routing changes:
- Helper branching: Add unit tests next to the existing terminal view tests in
app/src/terminal/view/queued_prompts_test.rsproving (a) with V2 on,enqueue_followup_promptappends a row with the supplied origin and conversation id toQueuedQueryModel, and (b) with V2 off, it callssend_user_query_after_next_conversation_finished, which setspending_user_query_view_idandqueued_prompt_callback. Use the sameApp::test-style harness already used by sibling tests in that file. /compact-andintegration: DispatchWorkspaceAction::SummarizeAIConversation { initial_prompt: Some(...) }on a terminal view with an active conversation. With V2 on, assert the queued-prompts panel contains exactly one row with originCompactAndSlashCommand. With V2 off, assert aPendingUserQueryKind::QueuedPromptrich content was inserted and the legacy callback is set./fork-and-compactintegration: DriveWorkspace::handle_forked_conversation_promptswithsummarize_after_fork: trueand aninitial_prompt, then assert the forked terminal view'sQueuedQueryModelcontains exactly one row with originForkAndCompactSlashCommandkeyed by the forked conversation id. Verify the source terminal view's queue is untouched.- Drain semantics: With V2 on, simulate
FinishReason::Completeon the conversation and assert the row drains throughsubmit_queued_prompt_for_active_pane→ local fallbacksubmit_queued_prompt. SimulateFinishReason::Errorwith an empty input and assert the row's text lands in the input editor (matches §35 ofspecs/REMOTE-1543/PRODUCT.md). - Telemetry: Add a serialization assertion for both new
TelemetryQueuedQueryOriginvariants in the existing telemetry test pattern atapp/src/server/telemetry/events.rs. - Compile gating:
cargo check -p warpandcargo check -p warp --features queued_prompts_v2both pass;cargo fmtandcargo clippy --workspace --all-targets --all-features --tests -- -D warningsper the WARP.md PR workflow. Do not run the app to test.
Parallelization
Single workstream. The helper, two callsite edits, origin enum changes, and telemetry mirror all sit in a tight ownership boundary (TerminalView, Workspace, QueuedQueryModel, telemetry events). Sub-agents would create merge churn on the same files without reducing wall-clock time.
Risks and mitigations
selected_conversation_idnot yet resolving to the forked id onCurrentPaneforks: the helper takes an explicitconversation_idfrom the caller, and the/fork-and-compactpath already hasforked_conversation_idin scope athandle_forked_conversation_prompts. The/compact-andpath uses the selected conversation id resolved at dispatch time on the active terminal view, which is guaranteed to be set because the slash-command handler short-circuits when none exists.- Double drain on the legacy path:
handle_finished_conversationcalls bothdrain_queued_promptsand the legacyqueued_prompt_callback. With V2 off, only the legacy callback fires (no row was appended). With V2 on, only the V2 row is present (no callback was set). The helper's branching guarantees the two paths are mutually exclusive. - Telemetry origin drift between core and telemetry enums: extending
QueuedQueryOriginwithout mirroringTelemetryQueuedQueryOriginwould compile but ship payloads without telemetry for the new origins. The exhaustivematchin theFromimpl catches that at compile time, per the WARP.md "Exhaustive Matching" guideline. - Removing the legacy block UI prematurely: this spec deliberately keeps
send_user_query_after_next_conversation_finishedandPendingUserQueryBlockintact for the V2-off case. Cleanup happens whenQueuedPromptsV2is removed in a later pass.