From f0b47173ceeac8d2541877ca69a7e2d5ae92caa6 Mon Sep 17 00:00:00 2001 From: lonexreb Date: Tue, 5 May 2026 23:23:51 -0500 Subject: [PATCH 1/5] spec: editable prompt suggestions (#9842) --- specs/GH9842/SPEC.md | 77 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 specs/GH9842/SPEC.md diff --git a/specs/GH9842/SPEC.md b/specs/GH9842/SPEC.md new file mode 100644 index 0000000000..89eb33a523 --- /dev/null +++ b/specs/GH9842/SPEC.md @@ -0,0 +1,77 @@ +# Spec: Editable prompt suggestions (GH-9842) + +## Problem + +Prompt suggestions in Warp are all-or-nothing: clicking one +sends the suggested prompt as-is. There's no way to insert it +into the input as a *draft* the user can edit, refine, and then +send themselves. This makes suggestions feel rigid for the +common case where the suggestion is "almost right." + +## Goal + +Add an alternate affordance on each prompt-suggestion entry that +inserts the suggestion into the agent input as editable text +without dispatching it. The user edits, then submits manually. + +## Behavior contract + +- B1. Each prompt-suggestion entry exposes two affordances: + - **Primary click (existing):** sends the suggestion as-is. + Pixel-equivalent to today. + - **New: insert-as-draft.** Discoverable via either: + - A small "edit" icon next to the suggestion (preferred, low + friction). + - Modifier-click on the suggestion (Cmd-click / Ctrl-click). +- B2. Insert-as-draft replaces the current agent-input contents + with the suggestion text. If the input already has user text, + show a one-time confirmation tooltip ("Replace existing + draft?") OR insert at the current caret position — TECH spec + picks one. +- B3. Cursor lands at the end of the inserted text. +- B4. The insert dispatches NO send action. The user must + manually send via Enter / Cmd-Enter as usual. +- B5. The existing inline-banner prompt-suggestion model + (`app/src/terminal/view/inline_banner/prompt_suggestions.rs`) + is the data source; no schema change. +- B6. Telemetry: emit a single `prompt_suggestion_inserted_as_draft` + event when the user takes the new affordance. No payload beyond + the suggestion category (e.g., "zero_state", "follow_up") — + never the suggestion text or the user's edited result. + +## Acceptance criteria + +- A1. Click the suggestion → it sends (existing behavior preserved). +- A2. Modifier-click OR click the edit icon → text inserts into + the agent input, no send fires, cursor at end. +- A3. With existing draft text in the input, the chosen behavior + from B2 fires (replace-with-confirmation OR insert-at-caret). +- A4. The new event fires exactly once per insert; respects + global telemetry opt-out. + +## Implementation pointers + +- Suggestion render is in + `app/src/terminal/view/inline_banner/prompt_suggestions.rs`. +- `TerminalAction::ResolvePromptSuggestion(...)` (search: + `app/src/terminal/view/init.rs`) is today's "send-it-now" path. + Add a sibling `InsertPromptSuggestionAsDraft(String)` action + that targets the agent input editor's set-text path. +- The agent-input editor lives in + `app/src/ai/blocklist/agent_view/agent_input_footer/...`. + +## Test plan + +- T1. Modifier-click on a suggestion fixture inserts the text + into the agent-input editor model, no send action fires. +- T2. Plain click still dispatches the existing send action. +- T3. With pre-existing input, the chosen B2 behavior is honored. +- T4. New telemetry event fires exactly once per insert. + +## Out of scope + +- Multi-suggestion compose (insert two suggestions in sequence + to build a longer prompt). +- Drag-and-drop of suggestions into the input. +- A "save this edited suggestion" path (turning it into a + reusable rule). From d6fa0543aa9473d726d79975e7a8fd248c9a8bf5 Mon Sep 17 00:00:00 2001 From: lonexreb Date: Thu, 7 May 2026 11:24:24 -0500 Subject: [PATCH 2/5] spec: clarify editable prompt suggestion behavior Co-Authored-By: Warp --- specs/GH9842/SPEC.md | 57 ++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/specs/GH9842/SPEC.md b/specs/GH9842/SPEC.md index 89eb33a523..da39704d63 100644 --- a/specs/GH9842/SPEC.md +++ b/specs/GH9842/SPEC.md @@ -19,16 +19,18 @@ without dispatching it. The user edits, then submits manually. - B1. Each prompt-suggestion entry exposes two affordances: - **Primary click (existing):** sends the suggestion as-is. Pixel-equivalent to today. - - **New: insert-as-draft.** Discoverable via either: - - A small "edit" icon next to the suggestion (preferred, low - friction). - - Modifier-click on the suggestion (Cmd-click / Ctrl-click). -- B2. Insert-as-draft replaces the current agent-input contents - with the suggestion text. If the input already has user text, - show a one-time confirmation tooltip ("Replace existing - draft?") OR insert at the current caret position — TECH spec - picks one. -- B3. Cursor lands at the end of the inserted text. + - **New: insert-as-draft.** Both entry points are required and + dispatch the same action: + - A small edit icon next to the suggestion. + - Modifier-click on the suggestion (Cmd-click on macOS, + Ctrl-click on Linux/Windows). +- B2. Insert-as-draft inserts the suggestion text at the current + agent-input caret position without dispatching it. If the input + is empty, this is equivalent to setting the input to the + suggestion text. If the input already has user text, preserve + that text and insert at the caret; V1 does not show a replace + confirmation and does not overwrite the existing draft. +- B3. Cursor lands immediately after the inserted suggestion text. - B4. The insert dispatches NO send action. The user must manually send via Enter / Cmd-Enter as usual. - B5. The existing inline-banner prompt-suggestion model @@ -36,16 +38,23 @@ without dispatching it. The user edits, then submits manually. is the data source; no schema change. - B6. Telemetry: emit a single `prompt_suggestion_inserted_as_draft` event when the user takes the new affordance. No payload beyond - the suggestion category (e.g., "zero_state", "follow_up") — - never the suggestion text or the user's edited result. + the suggestion category. Derive the category only from existing + `PromptSuggestion` fields, in this order: + `static_prompt_suggestion_name = Some(name)` maps to + `static:{name}`; otherwise `coding_query_context.is_some()` maps + to `coding_query`; otherwise `should_start_new_conversation` + maps to `new_conversation` when true and `follow_up` when false. + Never derive the category from `prompt`, `label`, `id`, or the + user's edited result. ## Acceptance criteria - A1. Click the suggestion → it sends (existing behavior preserved). -- A2. Modifier-click OR click the edit icon → text inserts into - the agent input, no send fires, cursor at end. -- A3. With existing draft text in the input, the chosen behavior - from B2 fires (replace-with-confirmation OR insert-at-caret). +- A2. Modifier-click and click the edit icon both insert text into + the agent input, no send fires, cursor lands after the inserted + text. +- A3. With existing draft text in the input, the suggestion inserts + at the caret and preserves the surrounding text. - A4. The new event fires exactly once per insert; respects global telemetry opt-out. @@ -55,8 +64,10 @@ without dispatching it. The user edits, then submits manually. `app/src/terminal/view/inline_banner/prompt_suggestions.rs`. - `TerminalAction::ResolvePromptSuggestion(...)` (search: `app/src/terminal/view/init.rs`) is today's "send-it-now" path. - Add a sibling `InsertPromptSuggestionAsDraft(String)` action - that targets the agent input editor's set-text path. + Add a sibling `InsertPromptSuggestionAsDraft(PromptSuggestion)` + action that targets the agent input editor's insert-at-caret + path. Keep the full `PromptSuggestion` available until telemetry + is emitted so the category can be derived from existing fields. - The agent-input editor lives in `app/src/ai/blocklist/agent_view/agent_input_footer/...`. @@ -64,9 +75,13 @@ without dispatching it. The user edits, then submits manually. - T1. Modifier-click on a suggestion fixture inserts the text into the agent-input editor model, no send action fires. -- T2. Plain click still dispatches the existing send action. -- T3. With pre-existing input, the chosen B2 behavior is honored. -- T4. New telemetry event fires exactly once per insert. +- T2. Clicking the edit icon on the same fixture inserts the text + through the same action path. +- T3. Plain click still dispatches the existing send action. +- T4. With pre-existing input, insert-as-draft inserts at the + caret and preserves existing text. +- T5. New telemetry event fires exactly once per insert and uses + the B6 category mapping without suggestion text. ## Out of scope From cf468c2d95a595d7b2f2f851f24ede3f6ae384c6 Mon Sep 17 00:00:00 2001 From: lonexreb Date: Fri, 8 May 2026 03:26:29 -0500 Subject: [PATCH 3/5] spec: address review (#9842) Round-1 fixes for oz-for-oss bot review: - Affordance contract now explicitly requires BOTH edit icon AND Alt/Ctrl-click insert path with separate AC items - Define V1 existing-draft behavior as replace-with-confirm; choice is not persisted across inserts - Telemetry uses existing prompt_suggestion.action event with new action_type=insert_as_draft alongside send; no new fields, reuses existing category and source - Sequential composition is SUPPORTED in V1 with banner persistence until send/close/context-loss - Add UI/accessibility details: 16x16 pencil glyph, tabindex roving focus, aria-label, modifier-click tooltip; no pixel-parity claim --- specs/GH9842/SPEC.md | 161 ++++++++++++++++++++++++++++++++----------- 1 file changed, 119 insertions(+), 42 deletions(-) diff --git a/specs/GH9842/SPEC.md b/specs/GH9842/SPEC.md index da39704d63..7bba8b46b3 100644 --- a/specs/GH9842/SPEC.md +++ b/specs/GH9842/SPEC.md @@ -16,47 +16,107 @@ without dispatching it. The user edits, then submits manually. ## Behavior contract -- B1. Each prompt-suggestion entry exposes two affordances: +- B1. Each prompt-suggestion entry exposes two affordances. **Both + entry points are required in V1** — shipping only one is + insufficient. They dispatch the same `InsertPromptSuggestionAsDraft` + action: - **Primary click (existing):** sends the suggestion as-is. - Pixel-equivalent to today. - - **New: insert-as-draft.** Both entry points are required and - dispatch the same action: - - A small edit icon next to the suggestion. - - Modifier-click on the suggestion (Cmd-click on macOS, - Ctrl-click on Linux/Windows). -- B2. Insert-as-draft inserts the suggestion text at the current - agent-input caret position without dispatching it. If the input - is empty, this is equivalent to setting the input to the - suggestion text. If the input already has user text, preserve - that text and insert at the caret; V1 does not show a replace - confirmation and does not overwrite the existing draft. + Matches today's interaction; no pixel-parity requirement, but + the existing card padding and icon-button base style are + preserved. + - **New: insert-as-draft entry points (V1 ships BOTH):** + - **B1a. Edit icon.** A small pencil glyph rendered at the + trailing edge of each suggestion card. See B7 for affordance + details. + - **B1b. Modifier-click.** Alt-click on macOS and Linux, + Ctrl-click on Windows. Plain (no modifier) click continues + to send. +- B2. Existing-draft handling. Insert-as-draft REPLACES any existing + draft in the agent input. If the input is empty, the suggestion + text becomes the input. If the input has user text (length > 0), + show a confirm dialog: *"Replace your current draft with this + suggestion?"* with **[Replace]** and **[Cancel]** buttons. + - Replace clears the existing draft and inserts the suggestion + text. The caret lands at the end of the inserted text. + - Cancel makes no change to the input or draft. + - The choice is NOT persisted across inserts ("don't ask again" + is intentionally absent in V1). Each insert with non-empty + draft prompts again. + - A future TECH may relax to caret-insert or append-with-newline; + V1 is replace-with-confirm only. - B3. Cursor lands immediately after the inserted suggestion text. - B4. The insert dispatches NO send action. The user must manually send via Enter / Cmd-Enter as usual. - B5. The existing inline-banner prompt-suggestion model (`app/src/terminal/view/inline_banner/prompt_suggestions.rs`) is the data source; no schema change. -- B6. Telemetry: emit a single `prompt_suggestion_inserted_as_draft` - event when the user takes the new affordance. No payload beyond - the suggestion category. Derive the category only from existing - `PromptSuggestion` fields, in this order: - `static_prompt_suggestion_name = Some(name)` maps to - `static:{name}`; otherwise `coding_query_context.is_some()` maps - to `coding_query`; otherwise `should_start_new_conversation` - maps to `new_conversation` when true and `follow_up` when false. - Never derive the category from `prompt`, `label`, `id`, or the - user's edited result. +- B6. Telemetry. Insert-as-draft reuses the existing + `prompt_suggestion.action` event (same category, same surface + payload, same private-field rules) with a new `action_type` + enum value `insert_as_draft` alongside the existing `send`. + - `category = "suggestion"` (existing). + - `source` = the existing source field already populated by the + suggestion's origin (banner, autocomplete, etc.) — no new field. + - The suggestion category is derived only from existing + `PromptSuggestion` fields, in this order: + `static_prompt_suggestion_name = Some(name)` maps to + `static:{name}`; otherwise `coding_query_context.is_some()` maps + to `coding_query`; otherwise `should_start_new_conversation` + maps to `new_conversation` when true and `follow_up` when false. + Never derive the category from `prompt`, `label`, `id`, or the + user's edited result. + - No new event type, no new private payload fields. +- B7. Edit-icon affordance & accessibility. + - Rendered as a 16×16 pencil glyph at the trailing edge of each + suggestion card. + - Keyboard-focusable; participates in the suggestion list's + existing roving-focus / `tabindex` model. + - `aria-label="Edit suggestion before sending"`. + - On focus or hover, surfaces a tooltip: *"Click to send, + [⌥/Ctrl]-click or this icon to edit first"* (with the modifier + matching the host OS). + - Activating via Enter / Space while focused dispatches + `InsertPromptSuggestionAsDraft`, identical to mouse click. + - Pixel parity with the existing card layout is NOT required; the + icon uses the existing icon-button base style and the card's + existing padding. +- B8. Sequential composition (NOT out of scope in V1). Users may + insert multiple suggestions in a row. Each insert applies the + B2 replace-with-confirm rule against the current draft. The + suggestion banner remains visible after insert-as-draft; it + does NOT auto-dismiss. The banner dismisses only on: + - The user manually sending the prompt (Enter / Cmd-Enter), OR + - The user manually closing the banner, OR + - Context loss (conversation switch, tab close, model change). ## Acceptance criteria -- A1. Click the suggestion → it sends (existing behavior preserved). -- A2. Modifier-click and click the edit icon both insert text into - the agent input, no send fires, cursor lands after the inserted - text. -- A3. With existing draft text in the input, the suggestion inserts - at the caret and preserves the surrounding text. -- A4. The new event fires exactly once per insert; respects - global telemetry opt-out. +- A1. Plain click on a suggestion → sends (existing behavior). +- A2. **Edit icon affordance.** Clicking the pencil icon on a + suggestion card inserts the suggestion text into the agent + input, fires no send action, and lands the caret at the end of + the inserted text. The icon is keyboard-focusable, has + `aria-label="Edit suggestion before sending"`, and Enter/Space + while focused dispatches the same insert action. +- A3. **Modifier-click affordance.** Alt-click on macOS/Linux and + Ctrl-click on Windows on a suggestion card inserts the + suggestion text identically to A2. +- A4. **Both affordances ship together.** A V1 build with only one + affordance present fails this spec. +- A5. Existing-draft replace flow. With a non-empty draft in the + input, insert-as-draft shows the replace-confirm dialog. Choosing + Replace clears the draft and inserts the suggestion at the end; + choosing Cancel makes no change. +- A6. Empty-draft insert. With an empty input, insert-as-draft + inserts directly with no confirm dialog. +- A7. **Sequential composition.** After insert-as-draft, the + suggestion banner remains visible; clicking another suggestion + prompts the replace-confirm dialog again. Banner dismisses only + on send, manual close, or context loss. +- A8. Telemetry. The `prompt_suggestion.action` event fires exactly + once per insert with `action_type = "insert_as_draft"`. Existing + `category` and `source` fields are reused; no new payload field + is added. Respects global telemetry opt-out. ## Implementation pointers @@ -73,20 +133,37 @@ without dispatching it. The user edits, then submits manually. ## Test plan -- T1. Modifier-click on a suggestion fixture inserts the text - into the agent-input editor model, no send action fires. -- T2. Clicking the edit icon on the same fixture inserts the text - through the same action path. -- T3. Plain click still dispatches the existing send action. -- T4. With pre-existing input, insert-as-draft inserts at the - caret and preserves existing text. -- T5. New telemetry event fires exactly once per insert and uses - the B6 category mapping without suggestion text. +- T1. Modifier-click (Alt/Ctrl per OS) on a suggestion fixture + dispatches `InsertPromptSuggestionAsDraft`; no send action fires. +- T2. Clicking the edit icon on the same fixture dispatches the + same action through the same path. +- T3. Edit icon: keyboard activation via Enter and Space when the + icon is focused dispatches the insert action; `tabindex` and + `aria-label` present. +- T4. Plain click still dispatches the existing send action. +- T5. Empty-draft path: insert-as-draft against an empty input + fills the input with the suggestion text, caret at end, no + confirm dialog shown. +- T6. Non-empty-draft path: insert-as-draft against a non-empty + input shows the replace-confirm dialog. Replace clears the + draft and inserts; Cancel preserves the existing draft. +- T7. Sequential composition: two consecutive insert-as-draft + actions each prompt confirm (when draft non-empty); banner + remains visible across both. +- T8. Telemetry: `prompt_suggestion.action` event fires exactly + once per insert with `action_type = "insert_as_draft"`, reusing + existing `category` and `source` fields and using the B6 + category derivation without suggestion text. Respects global + telemetry opt-out. ## Out of scope -- Multi-suggestion compose (insert two suggestions in sequence - to build a longer prompt). +- Caret-insert / append-with-newline behavior for non-empty drafts + (V1 is replace-with-confirm; future TECH may relax). +- A "don't ask again" persistence option for the replace-confirm + dialog. - Drag-and-drop of suggestions into the input. - A "save this edited suggestion" path (turning it into a reusable rule). +- Pixel parity with any specific design mock — the icon and card + use the existing icon-button base style and card padding. From df853e65ed4ea0b0cbfd67978b23b627bc503907 Mon Sep 17 00:00:00 2001 From: lonexreb Date: Fri, 8 May 2026 12:49:34 -0500 Subject: [PATCH 4/5] spec: address oz-for-oss round-2 review (#9842) --- specs/GH9842/SPEC.md | 108 +++++++++++++++++++++++++++++++------------ 1 file changed, 79 insertions(+), 29 deletions(-) diff --git a/specs/GH9842/SPEC.md b/specs/GH9842/SPEC.md index 7bba8b46b3..2af5f1df02 100644 --- a/specs/GH9842/SPEC.md +++ b/specs/GH9842/SPEC.md @@ -50,22 +50,43 @@ without dispatching it. The user edits, then submits manually. - B5. The existing inline-banner prompt-suggestion model (`app/src/terminal/view/inline_banner/prompt_suggestions.rs`) is the data source; no schema change. -- B6. Telemetry. Insert-as-draft reuses the existing - `prompt_suggestion.action` event (same category, same surface - payload, same private-field rules) with a new `action_type` - enum value `insert_as_draft` alongside the existing `send`. - - `category = "suggestion"` (existing). - - `source` = the existing source field already populated by the - suggestion's origin (banner, autocomplete, etc.) — no new field. - - The suggestion category is derived only from existing - `PromptSuggestion` fields, in this order: - `static_prompt_suggestion_name = Some(name)` maps to - `static:{name}`; otherwise `coding_query_context.is_some()` maps - to `coding_query`; otherwise `should_start_new_conversation` - maps to `new_conversation` when true and `follow_up` when false. - Never derive the category from `prompt`, `label`, `id`, or the - user's edited result. - - No new event type, no new private payload fields. +- B6. Telemetry. V1 adds a NEW telemetry event, + `TelemetryEvent::PromptSuggestionInsertedAsDraft`, that mirrors + the payload shape of the existing + `TelemetryEvent::PromptSuggestionAccepted` event (verified in + `app/src/server/telemetry/events.rs`). The Warp client follows + an event-per-action pattern (separate `PromptSuggestionAccepted`, + `StaticPromptSuggestionAccepted`, `PromptSuggestionShown`, etc.); + V1 stays consistent with that pattern instead of introducing a + unified `prompt_suggestion.action` event with an `action_type` + field. + - **Telemetry events (verified from + `app/src/server/telemetry/events.rs`):** + - `PromptSuggestionAccepted { id: String, + view: PromptSuggestionViewType, + interaction_source: InteractionSource }` — existing. + - `StaticPromptSuggestionAccepted { id: String, + view: PromptSuggestionViewType, + interaction_source: InteractionSource }` — existing. + - `PromptSuggestionShown { ... }` — existing. + - `StaticPromptSuggestionsBannerShown { ... }` — existing. + - `ZeroStatePromptSuggestionUsed { ... }` — existing. + - `PromptSuggestionInsertedAsDraft { id: String, + view: PromptSuggestionViewType, + interaction_source: InteractionSource }` — **new in V1**. + - The new event reuses the same three fields (`id`, + `view`, `interaction_source`) as `PromptSuggestionAccepted`. No + additional payload fields. `interaction_source` is `Button` for + edit-icon click and `Keybinding` for modifier-click. + - For static prompt suggestions inserted as draft, V1 emits + `PromptSuggestionInsertedAsDraft` (single new event covers both + static and dynamic suggestions; the suggestion `id` is enough to + join with prior `*Shown` events on the server side). + - The fictional `prompt_suggestion.action` event with `category`, + `source`, and `action_type` is NOT introduced — it conflicts + with the verified event-per-action shape currently in code. + - No private payload fields are added; opt-out behavior matches + the existing `PromptSuggestionAccepted` event exactly. - B7. Edit-icon affordance & accessibility. - Rendered as a 16×16 pencil glyph at the trailing edge of each suggestion card. @@ -113,10 +134,12 @@ without dispatching it. The user edits, then submits manually. suggestion banner remains visible; clicking another suggestion prompts the replace-confirm dialog again. Banner dismisses only on send, manual close, or context loss. -- A8. Telemetry. The `prompt_suggestion.action` event fires exactly - once per insert with `action_type = "insert_as_draft"`. Existing - `category` and `source` fields are reused; no new payload field - is added. Respects global telemetry opt-out. +- A8. Telemetry. The new `PromptSuggestionInsertedAsDraft` event + fires exactly once per insert with `id`, `view`, and + `interaction_source` populated identically to the existing + `PromptSuggestionAccepted` event. No new payload fields are + introduced. Respects global telemetry opt-out (matching + `PromptSuggestionAccepted` opt-out behavior). ## Implementation pointers @@ -125,11 +148,33 @@ without dispatching it. The user edits, then submits manually. - `TerminalAction::ResolvePromptSuggestion(...)` (search: `app/src/terminal/view/init.rs`) is today's "send-it-now" path. Add a sibling `InsertPromptSuggestionAsDraft(PromptSuggestion)` - action that targets the agent input editor's insert-at-caret - path. Keep the full `PromptSuggestion` available until telemetry - is emitted so the category can be derived from existing fields. + action. Keep the full `PromptSuggestion` available until + telemetry is emitted so identifiers can be derived from existing + fields. +- **V1 input behavior — replace, NOT insert-at-caret.** V1 sets the + agent input's text via the existing `replace_buffer_content` API + (verified: see `Input::replace_buffer_content` used in + `app/src/workspace/view.rs` and `app/src/pane_group/mod.rs`). + Concretely: + - When the input buffer is empty, V1 calls + `input.replace_buffer_content(&suggestion.prompt, ctx)`. The + suggestion text becomes the entire draft content. + - When the input buffer has user text (length > 0), V1 first + shows the B2 replace-confirm dialog. On `Replace`, V1 calls + `input.replace_buffer_content(&suggestion.prompt, ctx)`, + replacing the entire prior draft. On `Cancel`, no buffer + mutation occurs. + - In both branches the caret lands at the end of the inserted + text (existing behavior of `replace_buffer_content`). + - The earlier "insert at caret" wording is removed from V1 — V1 + is replace-only. Insert-at-caret (preserve surrounding draft, + insert at current cursor position) is **deferred to V1.5** as + an additional mode toggled by a future setting; out of scope + here. - The agent-input editor lives in - `app/src/ai/blocklist/agent_view/agent_input_footer/...`. + `app/src/ai/blocklist/agent_view/agent_input_footer/...`. The + `Input::replace_buffer_content(text: &str, ctx)` entry point is + the single API V1 uses; no new editor APIs are introduced. ## Test plan @@ -150,14 +195,19 @@ without dispatching it. The user edits, then submits manually. - T7. Sequential composition: two consecutive insert-as-draft actions each prompt confirm (when draft non-empty); banner remains visible across both. -- T8. Telemetry: `prompt_suggestion.action` event fires exactly - once per insert with `action_type = "insert_as_draft"`, reusing - existing `category` and `source` fields and using the B6 - category derivation without suggestion text. Respects global - telemetry opt-out. +- T8. Telemetry: `PromptSuggestionInsertedAsDraft` event fires + exactly once per insert. Payload (`id`, `view`, + `interaction_source`) matches the existing + `PromptSuggestionAccepted` shape. `interaction_source = Button` + for edit-icon click; `interaction_source = Keybinding` for + modifier-click. Respects global telemetry opt-out. ## Out of scope +- **Insert-at-caret (V1.5).** Inserting suggestion text at the + current caret position while preserving the surrounding draft. + Deferred from V1; tracked as a follow-up mode togglable from + settings. - Caret-insert / append-with-newline behavior for non-empty drafts (V1 is replace-with-confirm; future TECH may relax). - A "don't ask again" persistence option for the replace-confirm From 9a2a04b3ca1c338c85cc2cbbda345b507cb97773 Mon Sep 17 00:00:00 2001 From: lonexreb Date: Fri, 8 May 2026 13:21:03 -0500 Subject: [PATCH 5/5] spec: address oz-for-oss round-3 review (#9842) --- specs/GH9842/SPEC.md | 88 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/specs/GH9842/SPEC.md b/specs/GH9842/SPEC.md index 2af5f1df02..2e035be879 100644 --- a/specs/GH9842/SPEC.md +++ b/specs/GH9842/SPEC.md @@ -91,8 +91,7 @@ without dispatching it. The user edits, then submits manually. - Rendered as a 16×16 pencil glyph at the trailing edge of each suggestion card. - Keyboard-focusable; participates in the suggestion list's - existing roving-focus / `tabindex` model. - - `aria-label="Edit suggestion before sending"`. + existing roving-focus model. - On focus or hover, surfaces a tooltip: *"Click to send, [⌥/Ctrl]-click or this icon to edit first"* (with the modifier matching the host OS). @@ -101,6 +100,60 @@ without dispatching it. The user edits, then submits manually. - Pixel parity with the existing card layout is NOT required; the icon uses the existing icon-button base style and the card's existing padding. + +#### Accessibility contract — native Warp UI (NOT browser DOM) + +The previous draft referenced web-only accessibility primitives +(`tabindex`, `aria-label`). Warp's UI is native (GPUI-rendered) +and does not have a browser DOM to attach those attributes to. +This section names the equivalent native primitives the +implementation MUST use, verified against the existing codebase: + +- **Accessibility label.** The edit icon implements + `accessibility_label()` (the trait method already used by + Warp search items, slash-command items, profile pickers, + etc. — see `app/src/terminal/input/{prompts,profiles, + slash_commands,...}/search_item.rs::accessibility_label`, + `app/src/search/data_source.rs`). The value MUST be the + literal string `"Edit suggestion before sending"`. This is + the assistive-tech-readable name (replaces the prior + `aria-label` reference; it is NOT a DOM attribute, it is + the native `accessibility_label` value surfaced through + Warp's AT bridge — UIA on Windows, NSAccessibility on + macOS, AT-SPI / `accessibility_content_text` on Linux). +- **Focus / tab order.** The edit icon participates in Warp's + native focus system (`FocusHandle` / `PaneFocusHandle`), + not browser tab order. Concretely, the suggestion list's + existing roving-focus state is extended to treat the edit + icon as a focusable peer of the suggestion card such that + Tab moves focus suggestion-card → edit-icon → next- + suggestion-card → next-edit-icon, and Shift+Tab reverses. + No `tabindex` attribute is set (Warp has no DOM); the + effect is implemented through Warp's existing focus-handle + registration on the icon element. Activation via the + existing `KeyboardAction::Confirm` (Enter) and a Space + binding routed through the same handler dispatches + `InsertPromptSuggestionAsDraft`. +- **Validation target.** Accessibility behavior is validated + against the **native AT bridge**, not against DOM-snapshot + tools. Acceptable validation: + - macOS: Accessibility Inspector reports the icon with + role = button, label = `"Edit suggestion before + sending"`, and that VoiceOver announces the label on + focus. + - Windows: Inspect.exe / Accessibility Insights reports + the equivalent UIA `Name` property and a button-like + `LocalizedControlType`. + - Linux: Accerciser / `dogtail` reports the equivalent + AT-SPI `accessible-name`. + Automated coverage in CI uses Warp's existing accessibility + testing harness (the same one exercised by the + `accessibility_content_text` and search-bar accessibility + tests, e.g. `app/src/search/search_bar.rs` and + `app/src/search/command_search/searcher_test.rs:: + accessibility_label`). Browser-DOM tools (Lighthouse, + axe-core, Cypress a11y) are NOT applicable and MUST NOT + be used as the validation target. - B8. Sequential composition (NOT out of scope in V1). Users may insert multiple suggestions in a row. Each insert applies the B2 replace-with-confirm rule against the current draft. The @@ -116,9 +169,17 @@ without dispatching it. The user edits, then submits manually. - A2. **Edit icon affordance.** Clicking the pencil icon on a suggestion card inserts the suggestion text into the agent input, fires no send action, and lands the caret at the end of - the inserted text. The icon is keyboard-focusable, has - `aria-label="Edit suggestion before sending"`, and Enter/Space - while focused dispatches the same insert action. + the inserted text. The icon is keyboard-focusable through + Warp's native focus system (it acquires a `FocusHandle` and + participates in the suggestion list's roving-focus order), + exposes the native accessibility label + `"Edit suggestion before sending"` via the existing + `accessibility_label()` trait method (verified against + `app/src/search/data_source.rs` and friends), and Enter/Space + while focused dispatches the same insert action. Validation + happens through the native AT bridge (Accessibility Inspector + on macOS, Inspect / Accessibility Insights on Windows, + Accerciser on Linux), NOT through DOM-based tooling. - A3. **Modifier-click affordance.** Alt-click on macOS/Linux and Ctrl-click on Windows on a suggestion card inserts the suggestion text identically to A2. @@ -183,8 +244,21 @@ without dispatching it. The user edits, then submits manually. - T2. Clicking the edit icon on the same fixture dispatches the same action through the same path. - T3. Edit icon: keyboard activation via Enter and Space when the - icon is focused dispatches the insert action; `tabindex` and - `aria-label` present. + icon is focused dispatches the insert action. Native + accessibility assertions (NOT DOM): + - The icon's `accessibility_label()` returns the literal + string `"Edit suggestion before sending"` (verified by + direct call against the same trait used by + `app/src/search/data_source.rs`, + `app/src/terminal/input/{prompts,profiles, + slash_commands,...}/search_item.rs::accessibility_label`). + - The icon registers a `FocusHandle` and is reachable in the + suggestion list's roving-focus order without injecting a + `tabindex` attribute (Warp has no DOM). + - Native AT-bridge validation: macOS Accessibility Inspector, + Windows Inspect / Accessibility Insights, and Linux + Accerciser report the icon with role = button and the + expected accessible name. - T4. Plain click still dispatches the existing send action. - T5. Empty-draft path: insert-as-draft against an empty input fills the input with the suggestion text, caret at end, no